模块化的目的
为了代码的可组织重复性、隔离性、可维护性、版本管理、依赖管理等
模块化发展阶段
阶段一:语法层面的约定封装
利用 JavaScript 的语言特性和浏览特性,使用 Script 标签、目录文件的组织、闭包、IIFE、对象模拟命名空间等方法
一些典型的示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14
15// app.js
var app = {}
// hello.js
app.hello = {
sayHi: function () {
console.log('Hi')
},
sayHello: function () {
console.log('Hello')
}
}
// main.js
app.hello.sayHi()
1 | var hello = (function(module1){ |
1 | // hello.js |
这一阶段,解决了一些问题,但对日渐复杂的前端代码和浏览器异步加载的特性,很多总是并没有解决
阶段二:规范的制定和预编译
这一阶段的发展,开始了对模块化规范的制定,以 CommonJS 社区为触发点,发展出了不同的规范如 CommonJS(Modules/***)、AMD、CMD、UMD 等和不同的模块加载库如 RequireJS、Sea.js、Browserify 等。
解决了浏览器端 JavaScript 依赖管理、执行顺序等在前一阶段未被解决的问题,随着 browserify 和 webpack 工具的出现,让写法上也可以完全和服务端 Node.js 的模块写法一样,通过抽象语法树(AST)转为在浏览器端可运行的代码,虽然多了一层预编译的过程,但对开发来说是很友好的,预编辑的过程完全可以由工具自动化。
一些典型示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14// hello.js
var hello = {
sayHi: function() {
console.log('Hi')
},
sayHello: function() {
console.log('Hello')
}
}
module.exports.hello = hello
// main.js
var sayHello = require('./hello.js').sayHello
sayHello()
1 | // hello.js |
1 | // hello.js |
阶段三:原生语言层面模块化的支持
ECMAScript 标准对原生语言层面提出了声明式语法的模块化规范标准 ES Modules。各大浏览器对 ES Modules 逐渐实现,在未实现的浏览器上也可通过 Babel 等工具预编译来兼容,ES Modules 逐渐在前端成了公认的编写模块化的标准。
在 Node.js 端,虽然最新的 Node.js 版本(v13.0.1)的 ES Modules 还处于 Stability:1-Experimental 阶段,需要增加后缀 .mjs,而且还需要增加 --experimental-modules
参数来开启,不过相信完全稳定版本不会太远。而且还一定程度支持 CommonJS 和 ES Modules 之间互相引用,通过 Babel 或 Rollup 等工具则完全可以兼容。
示例:1
2
3
4
5
6
7
8
9
10
11
12
13
14// hello.js
var words = ['Hi', 'Hello']
export const hello = {
sayHi: function() {
console.log(words[0])
},
sayHello: function() {
console.log(words[1])
}
}
// main.js
import { hello } from './lib/greeting'
hello.sayHello()
import 命令会提升到文件顶部执行,将 JavaScript 引擎静态分析,并且不能使用在非顶部的作用域里。所以缺少在运行时动态加载的方法,之后出来的 import() 提案,大部分浏览器也已经支持。
虽然大部分浏览器都已实现,大部分开发人员还是会使用打包构建工具。除了浏览器的兼容性问题,这也是一个 trade-off 问题。在 HTTP/1.1下,虽然 keep-alive 一段时间内不会断开 TCP 连接,但是 HTTP 的开销还是不能忽略,必要时需要合并请求、减少开销;HTTP/2.0 的多路复用,在一定程度上可以让开发者直接在浏览器上使用 ES Modules 而不必担心加载的文件过多。两种方法其实会有一个最优比例,这时配合打包构建工具可以做到平衡优化。
总结
以史为鉴,可以知兴替