JS 模块化简史
模块化的历史进程
模块模式
起初,我们写代码比较随意:
function foo() {
//...
}
function bar() {
//...
}
这种写法容易和全局变量发生命名冲突问题,于是我们有了改进的写法:
var Utils = {
foo: function() {},
bar: function() {}
}
Utils.foo()
这种写法看似解决了命名冲突问题,其实也暗含了安全问题。我们可以在外部随意访问 Utils.foo 并对其进行修改。
来看 jQuery,jQuery 通过 IIFE,使用引入全局变量
的方式解决了命名冲突及模块安全问题。
;(function(window) {
const _privateFn = function() {
console.log('Aha, this is a private Fn')
}
})(window)
_privateFn // undefined
这篇博客提到了如何解决状态私有问题:
var MODULE = (function(my) {
const _private = (my._private = my._private || {})
const _seal = (my._seal =
my._seal ||
function() {
delete my._private
delete my._seal
delete my._unseal
})
const _unseal = (my._unseal =
my._unseal ||
function() {
my._private = _private
my._seal = _seal
my._unseal = _unseal
})
return my
})(MODULE || {})
模块加载
起初,我们这样组织代码:
<script type="text/javascript" src="module1.js"></script>
<script type="text/javascript" src="module2.js"></script>
<script type="text/javascript" src="module3.js"></script>
<script type="text/javascript" src="module4.js"></script>
YUI3、KISSY 主要是通过配置的方式来解决文件依赖的问题。
YUI.add(
'module1',
function(Y) {
// ...
},
'0.0.1',
{
requires: ['node', 'event']
}
)
但是也带来了命名空间冲突的问题。
YUI().use('a', 'b', function(Y) {
Y.foo()
// foo 方法究竟是模块 a 还是 b 提供的?
// 如果模块 a 和 b 都提供 foo 方法,如何避免冲突?
})
为什么我们需要模块化?
当下来看,页面内应用正在变得复杂,我们需要使用模块解耦各个 JS 文件,使开发过程变得可维护,在部署过程中优化以提高页面加载性能。
- 如何安全的把模块的 API 暴漏出去?
- 如何唯一标识一个模块?
- 如何方便的使用所依赖的模块?
模块化规范演变
目前有三种主流的模块加载方式,
- CommonJS
- AMD(Asynchronous Module Definition)/ CMD / UMD
- ES Module
CommonJS 原名为 ServerJS,推出 Modules/1.0 规范后,在 Node.js 环境下取得了很不错的实践。
// Define in math.js
exports.sum = function(...numbers) {
return numbers.reduce((result, num) => result + num, 0)
}
// Use in other file
var math = require('math')
math.sum(1, 3)
ServerJS 想进一步推广到浏览器端,于是将社区改名叫 CommonJS。激烈争论 Modules 的下一版规范时,分歧和冲突由此诞生。
James Burke 推荐直接改良 CommonJS 的模块格式以适应浏览器端开发,但是 CommonJS 的发起者并不同意,这也就催生了 RequireJS。James Burke 制定了 AMD 规范,并在 2010 年实现了遵循 AMD 规范的模块加载器 RequireJS。
require(['module/module1.js', 'module/module2.js'], function(module1, module2) {
module1.printModule1FileName()
module2.printModule2FileName()
})
玉伯认为 RequireJS 不够完善,并从头开始实现模块加载程序 SeaJS。CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。
define(function(require, exports, module) {
// AMD推崇依赖前置,而CMD推崇依赖就近
const a = require('./a')
a.test()
// 软依赖
if (status) {
const b = require('./b')
b.test()
}
})
UMD 统一 CommonJS 和 AMD,所以 UMD 定义的模块可以同时在客户端和服务端使用:
;(function(global, factory) {
typeof exports === 'object' && typeof module !== 'undefined'
? (module.exports = factory())
: typeof define === 'function' && define.amd
? define(factory)
: ((global = global || self), (global.myModule = factory()))
})(this, function() {
return myModule
})
现代模块规范 ES Module
2015 年 6 月, ECMAScript6 标准正式发布,其中 ES 模块化规范的提出目标是整合 CommonJS、AMD 等已有模块方案,在语言标准层面实现模块化,成为浏览器和服务器通用的模块解决方案。
import { foo, bar } from '/modules/my-module.js'
export const foo = Math.sqrt(2)
为什么 ES 模块比 CommonJS 更好?我引用 RollupJS 文档中的一段加以解释:
ES Module 是官方标准,也是 JavaScript 语言明确的发展方向,而 CommonJS 模块是一种特殊的传统格式,在 ES 模块被提出之前做为暂时的解决方案。 ES 模块允许进行静态分析,从而实现像 tree-shaking 的优化,并提供诸如循环引用和动态绑定等高级功能。
在浏览器中,需要使用特定属性的脚本标签,以支持 ES Module 规范( <script type="module">
)。
兼容性
截止 2020 年 5 月 18 日,各大浏览器兼容 ES Module 情况如下:
在 NodeJS 中(Version >= 8.5),需要打开一个开关(--experimental-modules
),以支持 ES Module 规范,但是需要模块的文件名为 .mjs
。
动态 Import
动态 Import 如今已经被大部分浏览器所支持。
CJS VS ESM
尽管 ES Module 规范发布已久,但并没有得到广泛的兼容。由于 ES Module 继承了现在来说仍属主流的 CommonJS 规范的诸多优点,所以两者之间有许多共性。我们来看看它们之间的区别。
最大的不同之处在于,CommonJS 是一种约定,而 ES Module 是语言规范。CommonJS 定义了一套使用“module.epoxrts 和 require”等代码进行模块导入导出的约定,通过扩充 JS 编译器外层代码,只要开发人员遵守约定,就可以使用此规范。而 ES Module 的实现依赖于编译器。这也导致:
- Require 导入值的拷贝,模块内部变化不能直接反应到外部;ES Module 导入值的映射,就算是模块内部的字符串有修改,同样会反映到模块外部。
- Require 本质是一个函数,所以容易实现动态导入的功能;ES Module 依赖于编译器的静态分析,所以动态导入功能难以被完善实现。
要看看 Require 到底是个啥玩意儿,请看这篇:Require。
如何在 esm 中引入 cjs 中的命名导出?
// .mjs
import { namedExport } from './lib.cjs'
// .cjs
exports.namedExport = 'yes'
// .cjs wrong example
// exports = { namedExport: 'yes' }
见:How to write CommonJS exports that can be name-imported from ESM,就算在 mjs 中导入了 cjs 的导出,为了兼容性考虑,导入的并不是映射。
打包工具之争
TODO
为什么需要打包工具
- 代码分析(合并、压缩、混淆)
- 兼容各种规范
应用程序使用 WebpackJS,库文件使用 RollupJS