基于 CommonJS
谈谈前端模块化
前端模块化的内容贯穿我们平时的开发工作,最早的
AMD
等模块化组织方式现在已经很少使用,本课程基于目前最常见的CommonJS
(以下简称为CMJ
) 出发,(ESM
可能目前更常见,但是其是底层支持的)谈谈模块化的那些事儿
课程大纲
1、基于 CMJ
理清楚前端模块化
- 模块化是解决了哪些问题(找痛点)
- 设计思路与解决方案
2、展望未来的模块化机制(ESM
)
- 与
CMJ
的对比 - 社区的动态
谈谈 CMJ
CMJ
被熟知得益于 Node
社区的炸裂兴起,不过目前 Node
已经转移向了 ESM
。不过有兴趣的依然可以研究研究,Node
中的 CMJ
实现
本次课程会更多的从前端(浏览器层面)去探寻
CMJ
的一些特点
1、模块如何切分?
不管是 CMJ
还是 ESM
,都不约而同的选择了以「文件」为基本单位来划分模块。
文件是组织代码结构的基本单位,在模块加载器自身的处理上来说,也是一个相对来说更容易的粒度~
2、如何使用?
// a.js
exports.action = function () {
console.log('Action!');
}
// b.js
const a = require('./a.js');
a.action();
其实模块机制也是在设计 API
,在设计层面,要注重考虑简洁、易用,CMJ
统一使用 require
关键字来引入模块,暴露模块的方式是挂载到 exports
对象引用上(当然也可以重写这个对象引用),这样的设计使得用户使用、学习起来没有过多的心智负担,想想一个模块加载器暴露给你的基本 API
就有十几个,你会觉得好用吗?
3、同步还是异步?
这也是设计之初就需要考虑到的问题,对模块的加载解析过程如果是异步,那必然和同步的处理方式有极大的区别,CMJ
是 之所以被 Node
采用,也是因为其设计之初考虑的就不是浏览器层面的,更偏向 Server
, Node
本身在 IO
上就有足够的底气和实力,同步的方式契合了需求,只是目前推崇的 ESM
是官方的,未来的,Node
必须妥协于大流。
4、一个一个扣细节,一步一步实现
- 4-1、文件为粒度
如何拿文件内容?
Node
自不用说,本身就是底层可操作文件,但是浏览器层面的话是不支持也不认识 CMJ
的模块加载器,更不用说拿文件了,所以得依赖第三方工具,比如 webpack
,当然,本质也是通过 Node
暴露的能力去拿文件内容的
我们今天的 DEMO
打算从纯浏览器层面去模拟,所以可以用最简单的方式,就直接写文件字符串了(假设我们通过各种三方能力拿到了文件字符串内容)
- 4-2、关键的接口设计
第一,引入的方法 require
第二,导出的方式 exports
(module.exports
)
- 4-3、如何实现隔离,封闭作用域?
闭包,IIFE
- 4-4、如何绑定
require
的能力?
IIFE
有的能力,可以注入依赖,这个能力基本就是模块化能力的基石了
(function (global, exports) {
console.log(exports.test);
})(window, { test: '测试' })
官方、未来 -- ESM
与 CMJ
对比看看:
1、使用方式不同(略)
2、对基本类型,CMJ
是值拷贝,ESM
则是引用
3、动态运行时,静态编译 (import
语句都是静态执行,export
则是动态绑定的)
4、ESM
提升特性
5、ESM
支持 Top-level await
,this-undefined
6、ESM
天然支持 dynamic import
,CMJ
本身则是基于运行时
7、ESM
现在被大多数现代浏览器原生支持,通过 type="module"
进行标识
8、同步,异步
ESM
将流程拆分为了三个步骤进行,首先是【构建阶段】解析模块,创建底层数据结构Module Record
(可以看成是AST
结构节点),然后【实例化阶段】解析import
,export
存入内存(这个时候代码并没执行),【执行阶段】最后才是执行
然后将执行得到的结果放进对应的内存中,这样的过程拆分为了三个主要步骤,意味着ESM
拥有了CMJ
不具备的异步的能力!
为啥要拆成这么几个步骤?
前端常常面临的场景是多chunk
渲染,通过入口文件<script src='index.js' type='module'/>
进来,可能需要加载很多js 模块
,这个时候如果ESM
机制本身是多过程且可分离的,
就可以最大限度的压榨浏览器并行下载能力,快速加载依赖(当然底层支持按需更yyds
),这是ES modules
规范将算法分为多个阶段的原因之一
多阶段算法也有弊端,比如不能
import { foo } from "${fooPath}/a.js"
这样使用,因为构建依赖图是在第一阶段,这个时候路径信息是没有的
为了解决这个问题提出了dynamic import
,底层其实单独给这种情况创建了Module Record
,然后通过module map
的方式管理起来(module map
就是一种管理Module Record
的数据结构)
JS
引擎会深度优先后序遍历模块树,完成实例化过程,采用动态绑定的方式来联系export
import
值,这是和CMJ
非常不同的地方
三阶段设计,天生支持循环引用
没有完成三阶段的时候,会标记为Fetching
状态,循环引用的时候,看到是Fetching
状态就先不管这里了,继续执行,等完成执行阶段,就会把对应的import
和export
链接到一个内存地址
这样就可以访问到了