浅谈前端模块化_异步加载

前沿:有玩过qiankun的童鞋都知道,若想保证qiankun父应用能够成功正常加载子应用。我们需要“包装好”子应用,在官方文档中可以看到对子应用的打包方式中,就是通过将​​webpack​​​的输出模式定位为 ​​UMD​​​ 。你可以能好奇,这个​​UMD​​是个什么玩意?

浅谈前端模块化_异步加载_02

1. UMD

UMD 叫做通用模块定义规范,也是前端模块化演变出的一种模块化定义,是模块定义的跨平台解决方案。它支持运行时让同一个代码的模块,在使用 Commonjs、AMD等其他模块化规范项目中运行,换句话说,UMD可以让你的代码兼容基于其他多种模块化规范写的模块,统一浏览器端以及非浏览器端的模块化方案的规范,通用性很强,但本质上他没有自己的“规范”,他其实就是个集合,将CommonJs、AMD等规范汇聚于一体,就可以同时支持import、require和script直接引用,简单理解可以看下图????

浅谈前端模块化_前端_03

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 ????酱',
};

浅谈前端模块化_模块化_04

  • 先判断是否支持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>

浅谈前端模块化_前端_05

❝ 啊轩同学:怎么多一层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分别通过这两种规范来打包来对比就一目了然

浅谈前端模块化_异步加载_06

区别在于,如果定义的是CommonJS需要定义一个output.library的名称,可以用来exports导出使用,反而如果使用CommonJS2,你则可以不用定义output.library,直接导出使用

???? 拓展阅读:

2.前端模块化

????‍???? 啊呆同学:那你说umd是CommonJS、AMD 的结合,那CommonJS、CMD、AMD这几个分别又有什么区分呢?

答:前端模块化是指,通过将前端代码根据一定的规则解耦封装成几个代码文件(模块),并对外暴露特定的接口或方法,然后在项目开发中根据具体情况进化合理的组合的方法,本质上有助于开发效率的提升、提高代码复用率、方便依赖关系管理。

2.1 前端模块化的演变

回顾前端模块化的发展,从早期的简单函数封装、对象封装、到立即执行函数表达式(IIFE)、script标签按js依赖执行顺序加载等的简单模块化使用,再到后面形成模块化规范演变,如下图所示

浅谈前端模块化_加载_07

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

不支持

支持(因为支持静态分析)

关于输出方式,前者是拷贝值,后者是引用值

浅谈前端模块化_加载_08

前者当lib.js模块加载后,它的内部变化就影响不到输出的​​lib.counter​​​了,因为本质上​​lib.counter​​是一个原始类型的值,会被缓存。而后者ES6 模块的运行机制与 前者CommonJS 不一样。当JS引擎对模块进行静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值,不会被缓存

本质上是因为前者对模块依赖的解决是动态的(模块依赖关系的建立是发生在代码运行阶段),而后者是静态的(模块依赖关系的建立是发生在代码编译阶段)

???? 拓展阅读:

2.4 import能替代require,进行动态加载吗?

浅谈前端模块化_异步加载_09

我们先看上面这段代码,有没有错。上一节讲过,import在js引擎处理时,是在发生编译的阶段。而这个阶段是不会去分析或执行这个if语句,所以import语句放在if中是没有任何意义的,会报语法错误,也就是说import和export命令只能在模块的顶层。

就好比我要加载一个动态的模块,用require我可以这样写。也就是说require此时加载的是哪一个模块,我们只有等到运行的时候才知道

浅谈前端模块化_加载_10

啊凌同学:那除了require就没有其他方式进行动态加载模块了吗?

答:有,ES2020提案引入了import()函数,可以用来支持动态加载模块,两者区别在于,前者require是同步加载,后者是异步加载,具体用法请看​​使用文档 ????​

浅谈前端模块化_模块化_11

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字段的设置