Skip to content

JS模块化

本文系统梳理JavaScript模块化演进历程,从IIFE、命名空间到现代模块规范,重点解析CommonJS、AMD和ES Module的核心特性与差异。通过对比同步加载与异步加载机制、值拷贝与引用绑定的区别,揭示不同模块规范在前端工程化中的适用场景,帮助开发者深入理解模块化设计原理并做出合理的技术选型。

本文是这个系列的第一篇文章,在这里为大家做个介绍。

这个系列名叫十分钟,顾名思义,每篇文章的阅读时间控制在十分钟内,内容不会太深入,主要是或者抛出一个点,帮助回忆,亦或者是查漏补缺。对本文方向感兴趣的同学,通过文章内相关内容推荐,可以自行深入学习。

为什么需要模块化

JavaScript 程序本来很小——在早期,它们大多被用来执行独立的脚本任务,在你的 web 页面需要的地方提供一定交互,如一些简单的表单校验等等,代码直接写在<script>标签中,所以一般不需要多大的脚本。过了几年,我们现在有了运行大量 JavaScript 脚本的复杂程序,还有一些被用在其他环境(例如 Node.js)。开发者们便把JS代码放在独立的文件中,与html解耦,通过<script>标签引入。

随着项目越来越庞大,参与的开发者越来越多,难免会出现命名重复的问题,这些变量都存在全局作用域中,造成全局污染,如下所示:

html
// html
<script src="./a.js"></script>
<script src="./b.js"></script>

// a.js
var title = 'titleA'

// b.js
var title = 'titleB'

为了解决这个问题,开发者采取命名空间的方式,如下所示:

html
// 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
// 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

AMDAsynchronous 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 ModuleCommonJSAMD
环境服务端和浏览器服务端浏览器
何时加载编译时运行时运行时
设计思想尽量静态化
模块是否为对象不是
可以部分加载
动态更新是。输出的是值的引用否。输出的是值的拷贝
变量是否只读
如何使用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

AMD:https://github.com/amdjs/amdjs-api/blob/master/AMD.md

Last updated: