1、JavaScript 模块发展史
1.1 Vanilla JS(1995~2009)
JavaScript 被开发出来的时候,是没有模块标准的,因为 JavaScript 的设计初衷就是作为一个 toy script,在浏览器中做一些简单的交互。但是随着互联网的高速发展,人们已经不再满足于简单的交互,而代码的复杂度也日益增长,维护难度也越来越高。
那么维护指的是维护什么呢?指的是维护变量。因为随着项目不断迭代,多人协同开发是不可避免的。在 JS 初期所有变量都写在全局作用域上,那么很可能出现的问题是什么呢?变量的覆盖、篡改和删除,这是一个很头疼的问题。很可能突然有一天你的功能报错了,就是因为你的某个变量被另一位开发者所删除了。
所以对于模块的引入初衷是为了解对变量的控制。当然还有其他的好处,例如对代码的封装、复用等等。
那么初期在没有模块标准的支持下,开发者们是如何实现类似模块的效果呢?有 2 种方式。
1.1.1 Object Literal Pattern(对象字面量)
使用 JS 内置的对象对变量进行控制:
function Person(name) {
this.name = name;
}
Person.prototype.talk = function () {
console.log("my name is", this.name);
};
const p = new Person("anson");
p.talk();
复制代码
这样就可以通过 new Person 的方式把变量都控制在对象内部。
1.1.2 IIFE(Immediately Invoked Function Expression)
我们知道在 JavaScript 中有作用域(Scope)的概念,在作用域内的变量,只在作用域内可见。在 ES6 之前,作用域只有 2 种,分别是:
全局作用域(Global Scope)
函数作用域(Function Scope)
上面提到了对变量的控制,那么肯定是把变量的作用范围控制的越小越好,所以毫无疑问把变量写在函数内是最好的办法。但是,这又引发了另一个问题,函数中的变量要如何提供给外部使用呢?
这个问题在初期并没有很好的解决方法,你必须把变量暴露到全局作用域中,例如经典的 jQuery。
而开发者们通常会使用 IIFE 去实现:
// lib.js
(function() {
const base = 10;
this.sumDOM = function(id) {
// 依赖 jQuery
return base + +$(id).text();
}
})();
复制代码
在 HTML 中引入 lib.js:
// index.html
window.sumDOM(20);
复制代码
但是 IIFE 有几个问题:
至少一个变量污染全局作用域;
模块之间的依赖关系模糊,不明确(lib.js 不能直观看出依赖 jquery.js);
加载顺序无法保证,不好维护(必须确保 jquery.js 必须在 lib.js 前加载完成,否则会报错)。
所以,JavaScript 非常需要一个模块标准来解决上述问题。
1.2 Non-Native Module Format & Module Loader(2009~2015)
由于模块能为我们解决上述问题,所以开发者尝试着自己去设计一些非原生模块标准如 CommonJS、AMD (Asynchronous Module Definition)、UMD (Universal Module Definition),然后搭配对应的 Module Loader 如 cjs-loader、RequireJS、SystemJS 可以实现模块的效果,我们下面过一下几个流行的非原生模块标准。
1.2.1 CommonJS (CJS)
2009 年,来自 Mozilla 的工程师 Kevin 提出了为运行在浏览器以外的 JavaScript 建立一个模块标准 CommonJS,主要应用在服务端如 Node.js。因为使用效果不错,随后也被用在浏览器的模块开发中,但由于浏览器并不支持 CommonJS,所以代码需要通过 Babel 等 transpiler 转换为 ES5 才能在浏览器上运行。
CommonJS 的特征是使用 require 来导入依赖,exports 来导出接口。
// lib.js
module.exports.add = function add() {};
// main.js
const { add } = require("./lib.js");
add();
复制代码
1.2.2 AMD
因为 CommonJS 设计初衷是应用在服务端的,所以模块的加载执行也都是同步的(因为本地文件的 IO 很快)。但是同步的方式运用到浏览器就不友好了,因为在浏览器中模块文件都是通过网络加载的,单线程阻塞在模块加载上,这是不可接受的。所以在 2011 年有人提出了 AMD,对 CommonJS 兼容的同时支持异步加载。
AMD 的特征是使用 define(deps, callback) 来异步加载模块。
// Calling define with a dependency array and a factory function
define(['dep1', 'dep2'], function (dep1, dep2) {
//Define the module value by returning a value.
return function () {};
});
复制代码
1.2.3 UMD
因为 CommonJS 和 AMD 的流行,随后又有人提出了 UMD 的模块标准,UMD 通过对不同的环境特性进行检测,对 AMD、CommonJS 和 Global Variable 三种格式兼容。
// UMD
(function (root, factory) {
if (typeof define === 'function' && define.amd) {
// AMD
define(['jquery', 'underscore'], factory);
} else if (typeof exports === 'object') {
// Node, CommonJS-like
module.exports = factory(require('jquery'), require('underscore'));
} else {
// Browser globals (root is window)
root.returnExports = factory(root.jQuery, root._);
}
}(this, function ($, _) {
// methods
function a(){}; // private because it's not returned (see below)
function b(){}; // public because it's returned
function c(){}; // public because it's returned
// exposed public methods
return {
b: b,
c: c
}
}));
复制代码
因为 UMD 的兼容性好,不少库都会提供 UMD 的版本。
1.3 ESM(2015~now)
随着 ECMAScript 的逐渐规范化、标准化,终于在 2015 年发布了 ES6(ES 2015),在这次版本更新中,制定了 JS 模块标准即 ES Modules,ES Modules 使用 import 声明依赖,export 声明接口。
// lib.mjs
const lib = function() {};
export default lib;
// main.js
import lib from './lib.mjs';
复制代码
截止到 2018 年,大部分主流浏览器都已经支持 ES Modules,在 HTML 中通过为