模块化简史
IIFE
如使用IIFE模式,工具库创建模块时通常会暴露一个绑定到window对象上的变量,然后重用该变量,以尽量减少对全局命名空间的污染。
<script>
void function() {
window.mathlib = window.mathlib || {}
window.mathlib.sum = sum
function sum(...values) {
return values.reduce((a, b) => a + b, 0)
}
}()
const sum = mathlib.sum(1,2,3)
console.log('sum', sum)
</script>
这种模式也无意之中促使了JavaScript工具快速发展,让开发者首次将所有IIFE模块整合到一个文件中。这种工具提供了原始打包的解决方案,该方案可以在不破坏应用程序逻辑的基础上,解决自动插入分号和缩小文件体积的问题,减轻了网络传输的负担。
IIFE 方法的问题在于没有显式的依赖树。 这意味着开发者必须以精确的顺序生成组件文件列表,来实现在加载依赖的模块之前递归加载该模块的依赖。
RequireJS、AngularJS以及依赖注入
随着像RequireJS这样的模块系统和AngularJS中依赖注入机制的出现,我们几乎不用再考虑上一节末尾所说的IIFE方法没有显示依赖树的问题,因为这两种机制都允许我们显式地命名每个模块的依赖项。
使用RequireJS的define函数定义mathlib/sum.js程序库,并把该函数添加到全局作用域中。来自define函数回调的返回值被作为该模块的公开接口:
Node.js和CommonJS的出现
Node.js衍生的若干创新,CommonJS模块系统算得上一个,它被简称为CJS。由于利用了Node.js程序访问文件系统的能力,因此CommonJS的规范更符合传统的模块加载机制。在CommonJS中,每个文件都是一个拥有自己的作用域和上下文的模块,它们使用同步的require函数加载其依赖项,并且可以在模块生命周期的任何阶段动态调用该函数,如下面的代码片段所示:
const mathlib = require('./mathlib')
和RequireJS、AngularJS类似的是,CommonJS也通过路径名引用对应的依赖项。 它们之间的主要区别在于,CommonJS不再需要样板函数和依赖数组,而是将模块中的接口指派给一个绑定的变量,或者在任何可以使用JavaScript表达式的地方使用该接口。
与RequireJS和AngularJS不同,CommonJS更加严格。在RequireJS和AngularJS中,每个文件可以包含若干动态定义的模块,而在CommonJS中,文件和模块的关系是一对一的。同时,RequireJS有多种声明模块的方式,而AngularJS则有不同种类的工厂、服务、服务提供商等,而且AngularJS的依赖注入机制与该框架本身紧密耦合。相比之下,CommonJS只有一种声明模块的方式。任何JavaScript文件皆是一个模块,调用require会加载依赖项,并且其接口就是赋值给module.exports的内容。因此,CommonJS更好用,它还可以做代码自检(code introspection),使工具更容易发现CommonJS组件系统的层次结构。
最终,Browserify被发明出来,弥合了Node.js服务器的CommonJS模块和浏览器之间的鸿沟,只要使用browserify命令行界面程序(CLI)并向其传递入口模块的路径,就可以将数量难以想象的模块打包成一个适用于浏览器的文件。CommonJS 的杀手级特性是npm包管理仓库,对其接管模块加载生态系统起到了决定性作用。
当然,npm的使用并不局限于CommonJS模块,甚至也不限于JavaScript软件包,但总的来说,这仍然是它的主要用例。只需点击几次鼠标,数以千计的包(现在已经有50多万个,并且仍在稳定增长)在你的Web应用中就是可用的了,而且还能在 Node.js Web 服务器和每个客户端Web浏览器中重用系统的大部分内容,这种优势让其他的系统难以望其项背。
ES6、import、Babel和Webpack
随着ES6在 2015 年 6 月标准化,加上在此很久之前就可以用Babel将ES6转换成ES5,很快一场新的革命就此开启。ES6规范包含了JavaScript原生的模块语法,它们通常也被称为 ECMAScript模块(ESM)。
ESM在很大程度上受到CJS及其前辈们的影响,它提供了静态声明式API和基于promise的动态可编程API,如下所示:
import mathlib from './mathlib'
import('./mathlib').then(mathlib => {
// ...
})
ESM和CJS一样,每个文件都是拥有自己的作用域和上下文的单独模块。ESM相对于CJS的一个主要优势是ESM可以静态导入依赖项(并且鼓励这样使用)。静态导入极大地提升了模块系统的代码自检能力,因为模块系统可以被静态地分析,还能从系统各个模块的抽象语法树(Abstract Syntax Tree,AST)中提取词法元素。ESM中的静态导入只限制在模块的顶层,这进一步简化了解析和代码自检。ESM相对于CommonJS require()的另一个优点是,ESM指定了一种异步模块加载方式,这意味着应用程序的依赖关系图可以根据特定事件并发加载,也可以根据需要延迟加载。
作为Browserify的接班人,Webpack在很大程度上承担了通用模块打包器的角色,这归功于其具备的大量新特性。与Babel和ES6的情况一样,Webpack一直通过它的import和export静态声明语句,以及类似import()动态函数的表达式来支持ESM。Webpack采用ESM带来的效益极高,这在很大程度上要归功于它采用的“代码分割(code-splitting)”机制,这种机制能够将一个应用分成不同的包,以提升首次加载时的性能[2]。