随着各种脚手架和编译工具的流行,我们习惯一键初始化,照葫芦画瓢地粘贴飘逸的代码,而可能忽略了一些精妙的设计和有趣的发展历史,其中就包括模块化。
- window
- module bundlers
- module loaders
- webpack, browserify, requirejs
- AMD vs CommonJS vs ES6 Modules
- define, require, module.exports, import, export
如果这些词让你眼花缭乱,那不妨读读这篇文章吧。
什么是模块化
当你打开一本书时,你会看到目录里清晰地划分着章节,同样地,一个庞大的代码库,也需要通过拆分成一个个“模块”,来增强维护性和复用度。而一个优秀的模块,必须是高度自治的,独立的,可以被轻松地的添加,移除而丝毫不影响整个项目。
模块化的发展史
“类”是模块化的一种具体形式,而我们知道,javascript 并不天生支持类,但我们可以利用函数,因为函数是 javacript 中唯一可以创建作用域的途径。
土办法
1. 匿名函数闭包
(function() {
var number = [1, 2, 3];
var min = function () {
return number.sort()[0];
};
})();
我们可以把一段代码包装到一个匿名函数中,这个函数中有它自己的运行环境,然后我们立即执行它。但是我们发现,外部无法使用其中的方法,同时这个函数里也可能误改外部的变量。
2. 挂在在全局空间上
像jQuery这样的库就是将 $
挂载在了 window
。和上面的实现方式类似。
(function (global) {
var jQuery = function() {};
window.jQuery = window.$ = jQuery;
})(window);
window 作为“桥梁”,让挂在在上面的各个属性(命名空间)可以相互使用了。
3. 对象接口
我们也可以通过创建一个对象的方式,同时暴露多个方法。
var Utils = (function() {
var min = function() {};
var max = function() {};
return {
min,
max
}
}());
Utils.min();
上述的3种方式都是通过创建匿名函数获得了私有的作用域空间,然后再将要暴露的挂载在一个全局空间上。
但是它们有一些弊端。例如你开发了一个模块,需要使用其他模块,你必须非常清楚模块之间的调用关系,并且将他们按正确的顺序在你的代码之前加载执行。另外,很有可能发成命名冲突,比如2个模块用了同一个名字。
那么是否有什么方法不通过全局空间呢?答案是,CommonJS 和 AMD。
CommonJS 规范
CommonJS 是一种模块化规范,并非官方提出,它最初是由 Mozilla 工程师提出的,后来被 Node.js 采用。
一个 CommonJS 模块,通过module.exports
暴露接口,以require
的方式被引用。
a.js 定义一个模块
function myModule() {}
module.exports = myModule;
b.js 调用一个模块
var myModule = require('a.js');
值得注意的是,CommonJS 是一种规范,而 Node.js "支持了"这种规范,因此上面的代码可以运行在 Node.js 之中,因为那有 require
和 module.exports
,而浏览器端,并不存在。
为什么这个模块化规范没有浏览器端支持呢?因为它的设计就是针对服务端的,并且它是同步
加载模块的。当你require
多个模块时,它们一个一个的被加载。这样在服务端,从磁盘上读取是可以的,但是在浏览器中,模块需要通过网络加载,而加载过程将会阻塞JS线程的执行。这就不合理了。
尽管如此,你还是可以通过 Browserify 这样的工具将 CommonJs 格式的代码转换成浏览器端可运行的代码,模块的挂在原理是
var module = {
exports: {}
};
(function(module, exports) {
exports.myModule = function () { ... };
}(module, module.exports))
var myModule = module.exports.myModule;
除此之外 Browserify 还通过解析语法树,找到 require
的文件,将所有依赖按顺序打包进一个 bundle.js
,最终在浏览器端运行。
AMD 规范
既然 CommonJS 是同步的,那么如果你想异步加载一个模块,该怎么办呢?答案是,通过 AMD,全称是 Asynchronous Module Definition。
a.js 定义一个模块
define([], function () {
return {
...
}
}
b.js 调用一个模块
define(['myModule'], funciton (myModule) {
...
});
AMD 规范是针对浏览器端进行设计的。并不支持 io, 文件系统,和服务端的一些特性。尽管 AMD 规范是针对浏览器端设计的,但浏览器端并没有原生“支持”它,因此你还是需要依赖像RequireJs这样的模块化工具。
UMD 规范
UMD(Universal Module Definition) 规范是一种“包装”,将上述的3种都包装在一起了,先判断是否支持AMD(define 是否存在),再判断是否支持 CommonJS(exports是否存在);前两个都不存在,则将模块挂载到全局(window 或 global),常见的是
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
//AMD
define(['myModule'], factory);
} else if (typeof exports === 'object') {
//CommonJS
module.exports = factory(require('myModule');
} else {
root.returnExports = factory(root.myModule);
}
})(this, (myModule) => {
...
return {}
});
ES6 Module
终于,ECMAScript 制定了正统的模块化方案,并在 ES6 中引入。这就是现在大家熟悉的 export
和 import
。
ES6 模块化既有简明的语法,又能解决异步加载模块问题,同时还对循环依赖有更好的支持。另外和 CommonJS 规范不同的是:
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
什么意思,2段代码解释一下:
差异1
// lib.js
var counter = 1;
function increment() {
counter++;
}
module.exports = {
counter,
increment,
}
// main.js
var counter = require('./lib.js');
counter.increment();
console.log(counter); // 1
// lib.js
export let counter = 1;
export function increment() {}
// main.js
import * as Counter from './lib.js'
Counter.increment();
console.log(Counter.counter); // 2
差异2 将和编译工具一起阐述。
虽然 ES6 Module 是官方出品,但是它的可用场景还不是很“普遍”,因为 Node 端有 CommonJS 规范,和 ES6 Module 并不兼容,在 node v13.1.0 版本下,只有开启 --experimental-modules
才能运行,同时对模块的定义有一定的要求。而在浏览器端,import 语句只能在声明了 type="module" 的 script 的标签中使用。真实的场景下,也需要通过编译工具的优化。
在了解了有哪些模块化规范之后,下一篇,我们再来聊聊“治理”模块的工具吧。
今天先到这里。