本文主体部分 翻译 + 搬运 自外网著名技术博客网站 medium.com 的一篇点赞数 2.7k 的文章 (文章链接在结尾处)
# 什么是 JavaScript 模块
JavaScript 模块指的是一段可复用的,独立的代码。他们通常都有特定的功能,能在整个代码系统里被引进或删除 —— 这个概念多少类似于 Java 或者 Python 中的类。
模块通常是独立的 —— 与其他代码解耦,因此方便修改。这也提高了代码的可读性和可维护性。模块化在使部分代码保持私有,仅暴露公共部分的的同时,还解决了命名空间模糊性的问题。
# 史前时代
在标准的 JavaScript 模块化方案还没被提出之前,显示模块化模式(**Revealing Module Pattern)** 被用来模拟模块化。
var revealingModule = (function () { | |
var privateVar = "Ben Thomas"; // 私有变量 | |
function setNameFn( strName ) { | |
privateVar = strName; | |
} | |
return { | |
setName: setNameFn, // 暴露的公用方法 | |
}; | |
})(); // 自执行函数 + 闭包 | |
revealingModule.setName( "Paul Adams" ); |
利用自执行函数和闭包的特性将部分代码封装私有化,仅暴露部分公有的方法。
使用这种模式的优点是可以在一个 js 文件中定义多个模块,但缺点是无法异步导入模块,也无法动态地导入。
注:
我认为这种模式也一定程度地损害了代码的可读性和可维护性:
- 想象有多个 js 文件被同时使用,后面的 js 代码里要保证模块在前面的 js 里被声明了才能安心使用该模块;
- 当后面的 js 文件中使用到某个模块时,IDE 也无法帮你快速找到这个模块的定义是什么、在哪个文件;
- 一旦模块的命名重复了,后面声明的模块就会覆盖掉前面的,造成 bug (命名空间问题)
- 代码规模到达一定程度后,这种模块化就会失控,无法维护
# CommonJS
后来,CommonJS 规范被提出来了。
采用 CommonJS 规范需要使用两个关键字 require 和 exports ,其中 require 用来声明导入某个 模块,而 exports 声明当前 js 文件要导出的内容
//------ store/customer.js 文件 ------ | |
exports = function(){ | |
return customers.get('store); | |
} |
//------ payments.js 文件 ------ | |
var customerStore = require('store/customer'); // 导入模块 |
Nodejs 模块化的实现基本遵循了 CommonJS 规范,稍有不同的是 Nodejs 使用了 module.exports 而不是 exports
//store/customer.js 文件 | |
function customerStore(){ | |
return customers.get('store); | |
} | |
modules.exports = customerStore; |
特点:
Nodejs 模块化的实现是同步的,因为 Nodejs 通常在服务端使用,所有 js 文件都在文件系统上,基本没有异步的问题。
require 即可以传入具体的模块路径也可以传入模块名。当 require 某个模块名时,Nodejs 就会在 node_modules 文件夹中查找对应的模块。
缺点:
一个文件一个模块的形式有点僵硬。
只有对象能被导出(函数也是特殊的对象),即无法导出变量 / 常量。
CommonJS 规范不能直接在浏览器的 js 环境下使用 ( 必须使用 Webpack 等工具转译处理)
# Asynchronous Module Definition (AMD)
AMD 规范的出现就是因为 CommonJS 无法直接在浏览器 js 环境下使用,并且正如它的名字所表达的意思 —— AMD 天然支持异步的模块加载
AMD 使用 define 函数。define 函数有多种重载版本
define 函数最多接收 3 个参数:模块 id, 依赖的模块构成的数组,回调函数
define("alpha", ["require", "exports", "beta"], function (require, exports, beta) { | |
exports.verb = function() { | |
return beta.verb(); | |
//Or: | |
return require("beta").verb(); | |
} | |
}); |
一个匿名的模块
define(["alpha"], function (alpha) { | |
return { | |
verb: function(){ | |
return alpha.verb() + 2; | |
} | |
}; | |
}); |
直接导出对象字面量
define({ | |
add: function(x, y){ | |
return x + y; | |
} | |
}); |
还能兼容 CommonJS 的 require 和 exports
define(function (require, exports, module) { | |
var a = require('a'), | |
b = require('b'); | |
exports.action = function () {}; | |
}); |
特点:
AMD 的设计初衷就是给浏览器环境使用的 —— 可以加快页面启动时间,而且这些模块导出内容可以是对象、函数、构造器、字符串、JSON 等等。支持多模块多文件。
实现:
RequireJS 是 AMD API 完整的实现者,有兴趣地可以去官方文档看看 RequireJS
# ECMAScript 6 模块化 (Native JavaScript)
ECMAScript 6 又名 ES6 又名 ES2015 ,终于推出了 JavaScript 原生的模块化方案
主要使用了两个关键字 imprort 和 export (注意和 CommonJS 不同,没有 s 后缀 )
// lib/math.js | |
export function sum (x, y) { return x + y } | |
export var pi = 3.141593 | |
// someApp.js | |
import * as math from "lib/math" | |
console.log("2π = " + math.sum(math.pi, math.pi)) | |
// otherApp.js | |
import { sum, pi } from "lib/math" | |
console.log("2π = " + sum(pi, pi)) |
还可以使用 通配符 和 default 关键字
// lib/mathplusplus.js | |
export * from "lib/math" // 把 lib/math 里的内容一起导出 | |
export var e = 2.71828182846 | |
export default (x) => Math.exp(x) // 默认导出幂函数 | |
// someApp.js | |
import exp, { pi, e } from "lib/mathplusplus" //exp 是默认导出的 幂函数; 而 pi 其实是来源于 lib/math | |
console.log("e^{π} = " + exp(pi)) |
特点:
不同于 require 和 define , 在 ES6 import 语句是静态的,只能放在 js 文件的头部(前几行)—— 在之后的 ES 版本中,将支持动态 / 异步的 import 参考官方文档
export 语句用来导出模块内容,支持导出对象,函数,变量等等。
由于 import 和 export 都是静态语句,方便代码依赖分析,比如 IDE 就能轻松地帮我们跳转、查询依赖模块。
原生 JavaScript 的方案,目前已被大多数现代框架使用 如 React,Vue, Angular 等等。
缺点:
目前浏览器环境还没有普遍支持 es6 的模块化语法,还是需要借助 Babel 等工具转译代码。
# 如果你现在从零开始写一个 js 项目的话, 强烈推荐使用 ES6 的模块化方案,因为这是原生 JavaScript 的实现而且必定是未来的趋势。
# 总结
本文回顾了 JavaScript 模块化的历史发展,从 显示模块化模式 到 CommonJS 到 AMD 最后到 ES6,JavaScript 模块化方案不断在发展,改良和进化。
# Bonus
问:我想在 现有的 Nodejs 项目中使用 ES6 模块化方案怎么办?
答:由于 Nodejs 直到 13.2 版本才支持 ES6 ,很多旧项目还是只能使用传统的 CommonJS 规范。但是有个神奇的库叫 esm ( Github 链接),只需要几行代码或者一个配置就能帮我们在 Nodejs 中愉快地使用 import /export 了,我在个人项目中已经用上了,真的香!
相关文章和链接: