❝前沿:有玩过qiankun的童鞋都知道,若想保证qiankun父应用能够成功正常加载子应用。我们需要“包装好”子应用,在官方文档中可以看到对子应用的打包方式中,就是通过将
❞webpack
的输出模式定位为UMD
。你可以能好奇,这个UMD
是个什么玩意?
1. UMD
❝UMD 叫做通用模块定义规范,也是前端模块化演变出的一种模块化定义,是模块定义的跨平台解决方案。它支持运行时让同一个代码的模块,在使用 Commonjs、AMD等其他模块化规范项目中运行,换句话说,UMD可以让你的代码兼容基于其他多种模块化规范写的模块,统一浏览器端以及非浏览器端的模块化方案的规范,通用性很强,但本质上他没有自己的“规范”,他其实就是个集合,将CommonJs、AMD等规范汇聚于一体,就可以同时支持import、require和script直接引用,简单理解可以看下图👇
❞
1.1 使用场景
假设你现在有一个场景:你需要开发一个工具库,一个util的方法,与此同时你想让这个代码(开发的类库)既能在nodejs环境直接使用,又能在浏览器中使用,那么我们就使用umd的输出模式,只需在 webpack
的 output
中添加libraryTarget: 'UMD'
即可,就可以更好地解决跨平台、跨环境等问题,有兴趣的同学可以看看之前写的工具库 kdutil,就是基于umd模式下打包
1.2 qiankun为什么需要将子应用输出为umd
❝qiankun架构下的子应用通过 webpack 的 umd 输出格式来做,让父应用在执行子应用的 js 资源时可以通过 eval,将 window 绑定到一个 Proxy 对象上,以此来防止污染全局变量,方便对脚本的 window 相关操作做劫持处理,达到子应用之间的脚本隔离。
❞
1.3 运行机制 🚗
❝通过webpack配置
❞libraryTarget:umd
来配置如何暴露 library,将你的library暴露为所有的模块定义下都可运行的方式,平时开发中童鞋应该很少配置webpack的output.libraryTarget的配置,一般在开发插件的场景中使用得比较多。常见的配置除了umd还有:var、this、window、global、commonjs、commonjs2、amd、amd-require等,不过umd兼容性最好 umd方式输出配置如下
// webpack.config.js module.exports = { output: { filename: `kdutil.min.js`, path: path.resolve(rootPath, 'dist'), library: `kdutil`, libraryTarget: "umd" }, } // index.js export default { name: 'hello 🌲酱', };
- 先判断是否支持CommonJS2规范(exports是否存在以及module是否存在),存在则使用的是CommonJS2方式加载。
- 再判断是否支持AMD(define是否存在,因为AMD规范是通过define来定义模块的),存在则使用AMD方式加载模块,我们看下下面这段webpack输出的结果
- 其次判断是否支持CommonJS规范(exports是否存在),存在则使用的是CommonJS方式加载模块。
- 前两个都不存在,则将模块公开到全局(window 或 global);
我们尝试直接以<script>
的引用方式引入kdutil这个全局访问的变量名,通过直接引用这个用umd输出的模块,在浏览器直接访问全局的kdutil,发现输出的内容跟我们导出的不一样,如下所示
<script src="../dist/kdutil.min.js"></script> <script type="text/javascript"> console.log(kdutil); </script>
❝👨🎓 啊轩同学:怎么多一层default,这样的话使用变量还需要以kdutil.default这样的写法,有没有办法简洁些?
❞
答::使用export default导出的全局变量会多一个default属性,可以在webpack.out输出添加一个配置:libraryExport: "default"
来解决
❝👨🎓 啊乐同学:CommonJS是Node.js的模块规范,那上面那个CommonJS2又是什么鬼?
❞
答:其实本质上就是在区别输出,使用的是exports还是module.exports,我们都知道CommonJS规范定义了exports才是亲生的,而module.exports顶多算是个“养子”,CommonJS2规范是规定用module.exports来输出的
我们借助webpack分别通过这两种规范来打包来对比就一目了然
区别在于,如果定义的是CommonJS需要定义一个output.library的名称,可以用来exports导出使用,反而如果使用CommonJS2,你则可以不用定义output.library,直接导出使用
🌲 拓展阅读:
2.前端模块化
❝👨🎓 啊呆同学:那你说umd是CommonJS、AMD 的结合,那CommonJS、CMD、AMD这几个分别又有什么区分呢?
❞
答:前端模块化是指,通过将前端代码根据一定的规则解耦封装成几个代码文件(模块),并对外暴露特定的接口或方法,然后在项目开发中根据具体情况进化合理的组合的方法,本质上有助于开发效率的提升、提高代码复用率、方便依赖关系管理。
2.1 前端模块化的演变
回顾前端模块化的发展,从早期的简单函数封装、对象封装、到立即执行函数表达式(IIFE)、script标签按js依赖执行顺序加载等的简单模块化使用,再到后面形成模块化规范演变,如下图所示
2.2 模块化规范的区分
CommonJS
: Nodejs采用的Commonjs(也叫cjs)这个规范,但因为Nodejs加载模块是同步的。一个模块的加载需要加载完成后(同步加载),才能执行后面的操作。我们知道服务器加载模块文件一般都已经放在服务器自身的硬盘,加载速度快。使用方式是通过exports或modules.exports导出,require引入
AMD
: 全称叫异步模块定义,与前者Commonjs不同的是,它是异步加载的,因为浏览器需要加载,且模块都放在服务器端,有等待时间,需要请求下来,导致浏览器会有一段"假死"的状态。于是就有个AMD规范诞生,让浏览器端的模块支持异步加载,使用方式是通过define方法定义模块(将所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完成之后,这个回调函数才会运行), 需要依赖RequireJS
CMD
: 与AMD一样,都是通过异步加载,不同的是,AMD是提前执行,而CMD是延迟执行,且CMD推崇的是依赖就近加载,AMD 推崇的是依赖前置加载, 需要依赖seajs
ES6模块化
:自从后来出了ES6模块化,AMD及CMD基本不用了,而是用ES6中自带的模块化,但是部分浏览器还不支持ES6语法,则需要借助Babel来“翻译”,对于Babel翻译可以看之前树酱的Babel傻傻看不懂,使用方式可以使用export default命令为模块指定默认输出及用import导入
运行环境 | 加载方式 | 依赖第三方库 | 是否需要babel编译 | |
CommonJS | 服务器 | 同步加载 | 不需要 | 否 |
AMD | 浏览器 | 异步加载 | requireJs | 否 |
CMD | 浏览器 | 异步加载(按需加载) | seajs | 否 |
UMD | 浏览器 + 服务器 | 异步加载 | 不需要 | 否 |
ES6 Module(esm) | 浏览器 + 服务器 | 同步加载 | 不需要 | 是 |
🌲 拓展阅读:
2.3 ES6 Module 和 CommonJs的区别
❝👨 啊雪同学: CommonJs的用法和ES6 Module的用法好容易混淆,两者有什么区分吗?
❞
答:有,看下面这个区分表格
CommonJS | ES6 Module | |
输出方式 | 输出的是一个值的拷贝 | 输出的是值的引用,模块可以实时变化 |
运行 | 运行时加载 | 编译时输出接口 |
是否支持 treeshake | 不支持 | 支持(因为支持静态分析) |
关于输出方式,前者是拷贝值,后者是引用值
前者当lib.js模块加载后,它的内部变化就影响不到输出的lib.counter
了,因为本质上lib.counter
是一个原始类型的值,会被缓存。而后者ES6 模块的运行机制与 前者CommonJS 不一样。当JS引擎对模块进行静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值,不会被缓存
本质上是因为前者对模块依赖的解决是动态的(模块依赖关系的建立是发生在代码运行阶段),而后者是静态的(模块依赖关系的建立是发生在代码编译阶段)
🌲 拓展阅读:
2.4 import能替代require,进行动态加载吗?
我们先看上面这段代码,有没有错。上一节讲过,import在js引擎处理时,是在发生编译的阶段。而这个阶段是不会去分析或执行这个if语句,所以import语句放在if中是没有任何意义的,会报语法错误,也就是说import和export命令只能在模块的顶层。
就好比我要加载一个动态的模块,用require我可以这样写。也就是说require此时加载的是哪一个模块,我们只有等到运行的时候才知道
❝啊凌同学:那除了require就没有其他方式进行动态加载模块了吗?
❞
答:有,ES2020提案引入了import()函数,可以用来支持动态加载模块,两者区别在于,前者require是同步加载,后者是异步加载,具体用法请看使用文档 🔗
2.5 如何在Node环境使用ES6模块加载
❝啊斌同学:树酱,那我如果想在node环境使用es6模块加载,可以吗?
❞
答:可以,但是不推荐混用。node从v13.2
版本开始就默认打开了ES6 module支持。如果想在node环境使用es6模块加载,则需要.mjs后缀文件名。也就是Node.js 遇到.mjs文件,则会认为它是ES6模块。如果不希望将后缀名改成.mjs,可以在项目的package.json文件中,指定type字段为module。但这个时候也就意味着,该目录下js只能解释ES6 模块,如果这时还要使用 CommonJS 模块,那么需要将 CommonJS 脚本的后缀名都改成.cjs。
❝简单说: .mjs文件总是以 ES6 模块加载,.cjs文件总是以 CommonJS 模块加载,.js文件的加载取决于package.json里面type字段的设置
❞