JS模块化
本文系统梳理JavaScript模块化演进历程,从IIFE、命名空间到现代模块规范,重点解析CommonJS、AMD和ES Module的核心特性与差异。通过对比同步加载与异步加载机制、值拷贝与引用绑定的区别,揭示不同模块规范在前端工程化中的适用场景,帮助开发者深入理解模块化设计原理并做出合理的技术选型。
本文是这个系列的第一篇文章,在这里为大家做个介绍。
这个系列名叫十分钟,顾名思义,每篇文章的阅读时间控制在十分钟内,内容不会太深入,主要是或者抛出一个点,帮助回忆,亦或者是查漏补缺。对本文方向感兴趣的同学,通过文章内相关内容推荐,可以自行深入学习。
为什么需要模块化
JavaScript 程序本来很小——在早期,它们大多被用来执行独立的脚本任务,在你的 web 页面需要的地方提供一定交互,如一些简单的表单校验等等,代码直接写在<script>
标签中,所以一般不需要多大的脚本。过了几年,我们现在有了运行大量 JavaScript 脚本的复杂程序,还有一些被用在其他环境(例如 Node.js)。开发者们便把JS代码放在独立的文件中,与html解耦,通过<script>
标签引入。
随着项目越来越庞大,参与的开发者越来越多,难免会出现命名重复的问题,这些变量都存在全局作用域中,造成全局污染,如下所示:
// html
<script src="./a.js"></script>
<script src="./b.js"></script>
// a.js
var title = 'titleA'
// b.js
var title = 'titleB'
为了解决这个问题,开发者采取命名空间的方式,如下所示:
// html
<script src="./a.js"></script>
<script src="./b.js"></script>
// a.js
xxx.moduleA = {}
xxx.moduleA.title = 'titleA'
// b.js
xxx.moduleB = {}
xxx.moduleB.title = 'titleB'
这样写在一定程度上解决了全局变量污染的问题,在b.js
文件中,通过xxx.moduleA.title
可以取到a.js
中定义的title
,此时已经有了模块化的概念。但是在b.js
中,也可以使用xxx.moduleA.title = 'titleNew'
,去修改模块A中的变量,这种方式存在隐患。
聪明的开发者们又想出了一个方案,使用闭包,将变量放在函数作用域中
// html
<script src="./a.js"></script>
<script src="./b.js"></script>
// a.js
xxx.moduleA = (function(){
var title = 'titleA'
return {
getTitle: function(){
return title
}
}
})
// b.js
xxx.moduleB = (function(){
var title = 'titleB'
return {
getTitle: function(){
return title
}
}
})
现在b.js
中,可以通过xxx.moduleA.getTitle()
来取到模块A的title,每个模块的变量都保存在各自的函数内,不会被其他模块修改。模块只维护内部私有的东西,提供接口函数给其他模块使用,但依然存在问题。比如上面的例子中,模块B可以取到模块A的东西,但模块A取不到模块B的,这是因为模块的加载有先后顺序,当项目庞大时,这种依赖关系便难以维护了。
CommonJS
在 ES6 之前,ECMAScript 并没有提供代码组织的方式,那时候通常是基于 IIFE(立即调用函数表达式) 来实现“模块化”,随着 JavaScript 在前端大规模的应用,以及服务端 JavaScript 的推动,原先浏览器端的模块规范不利于大规模应用。
于是早期便有了CommonJS 规范,其目标是为了定义模块,提供通用的模块组织方式。
特点如下
- 在 commonjs 中每一个 js 文件都是一个单独的模块,我们可以称之为 module,有自己的作用域
- 该模块中,包含 CommonJS 规范的核心变量: exports、module.exports、require
- 每个模块内部都有一个
module
变量,代表当前模块,这个变量是一个对象 - exports 和 module.exports 负责对模块中的内容进行导出
- require 函数可以帮助我们导入其他模块(自定义模块、系统模块、第三方库模块)中的内容
- 同步加载模块
AMD
AMD
是Asynchronous Module Definition
的缩写,意思就是"异步模块定义"。
基于CommonJS
规范的nodeJS
出来以后,服务端的模块概念已经形成,很自然地,大家就想要客户端模块。而且最好两者能够兼容,一个模块不用修改,在服务器和浏览器都可以运行。
但是,由于一个重大的局限,使得CommonJS
规范不适用于浏览器环境。如果将上面的代码运行在客户端浏览器,就会报错。
上文提到,CommonJS
是同步加载模块。这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。
因此,浏览器端的模块,不能采用"同步加载",只能采用"异步加载"。所以AMD
规范就诞生了。
CommonJS与AMD的区别
CommonJS规范加载模块是同步的,也就是说,只有加载完成,才能执行后面的操作。
AMD规范则是非同步加载模块,允许指定回调函数。
由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。
如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。
ES Module
我们所说的esModule
其实就是es6
推出的javaScript
模块规范。在这之前由于没有规范所以社区推出了CommonJS
规范、require.js
等。esModule
的语法是静态的、导出是绑定的
静态的语法意味着可以在编译时确定导入和导出,更加快速的查找依赖,可以使用lint工具对模块依赖进行检查,可以对导入导出加上类型信息进行静态的类型检查
由于使用import
导入的模块是运行在严格模式下的,且均为只读的(即无法被赋值。但是可以更改属性),且均为引用传递,无关类型,均是与原变量的引用。
基本特性
- ESM 自动采用严格模式,忽略
use strict
- 每个 ES Module 都是运行在单独的私有作用域中
- ESM 是通过 CORS 的方式请求外部 JS 模块的
- ESM 的 script 标签会延迟执行脚本(浏览器页面渲染后执行)
CommonJS和ES Module的区别
CommonJS
- CommonJS可以动态加载语句,代码发生在运行时
- CommonJS混合导出,还是一种语法,只不过不用声明前面对象而已,当我导出引用对象时之前的导出就被覆盖了
- CommonJS导出值是拷贝,可以修改导出的值,这在代码出错时,不好排查
ES Module
- ES Module是静态的,不可以动态加载语句,只能声明在该文件的最顶部,代码发生在编译时
- ES Module混合导出,单个导出,默认导出,完全互不影响
- ES Module导出是引用值之前都存在映射关系,并且值都是可读的,不能修改
CommonJS、ES Module、AMD比较
ES Module | CommonJS | AMD | |
---|---|---|---|
环境 | 服务端和浏览器 | 服务端 | 浏览器 |
何时加载 | 编译时 | 运行时 | 运行时 |
设计思想 | 尽量静态化 | ||
模块是否为对象 | 不是 | 是 | |
可以部分加载 | 是 | 否 | |
动态更新 | 是。输出的是值的引用 | 否。输出的是值的拷贝 | |
变量是否只读 | 是 | ||
如何使用 | ES6+ | 使用node | 引入require.js |
相关文章
JavaScript 模块:https://developer.mozilla.org/zh-CN/docs/Web/JavaScript/Guide/Modules
IIFE:https://developer.mozilla.org/zh-CN/docs/Glossary/IIFE
深入浅出 CommonJS 和 ES Module:https://juejin.cn/post/6994224541312483336#heading-4