模块化解析
# 什么是模块
随着程序规模的扩大,以及引入各种第三方库,共享全局作用域会带来很多问题。首先是命名冲突问题,为了解决命名冲突问题,主流编程语言都提供了语言层面的方案,JavaScript社区选择了模块方案。一个合格的模块方案需要满足以下特性:
- 独立性——能够独立完成某个功能,隔绝外部环境的影响
- 完整性——能够完成某个特定功能
- 可依赖——可以依赖其他模块
- 被依赖——可以被其他模块依赖
# 原始模块
如果仅从定义层面来看,一个函数即可称为一个模块,而我们早就开始使用这种模块了:
// 最简单的函数,可以称作一个模块
function add(x,y) {
return x + y
}
2
3
4
在ECMAScript2015
(即es6
)之前,只有函数能够创建作用域。下面是JavaScript社区中原始模块的定义代码:
(function (mod, $) {
function clone(source) {
//此处省略代码
}
mod.clone = clone
})((window.clone = window.clone || {}), jQuery)
2
3
4
5
6
上面的mod
模块不会被重复定义,依赖通过函数参数注入。这种实现其实并不完美,仍需要手动维护依赖的顺序,典型的场景就是其中的jQuery
必须先于代码被引用,否则会报引用错误。随着模块数量的增加,这种问题很快变得不可维护,这显然不是我们想要的。
一般的库都会提供对这种模块的支持,因为这种模块可以直接通过script
标签引入,使用script
标签引入库的方式依然存在使用场景,如古老的前端系统、简单的活动页面、简单的测试页面等。
# AMD
AMD
是一种异步模块加载规范,专为浏览器端设计,其全称是Asynchronous Module Definition
,中文名称是异步模块定义。AMD
规范定义模块的方式如下:
define(id?, dependencies?, factory)
浏览器并不支持AMD模块
,在浏览器端,需要借助RequireJs
才能加载AMD模块。RequireJS
是使用最广泛的AMD模块加载器,但目前的新系统基本不再使用RequireJS
,因为大部分库都会提供对AMD模块的支持。
// 匿名,无依赖模块,文件名就是模块名
define(function() {
function clone(source) {
//此处省略代码
}
return clone
})
2
3
4
5
6
7
8
上面的代码定义了一个匿名AMD模块,假设代码位于clone.js
文件中,那么在index.js
文件中可以像下面这样使用上面定义的模块
define(['clone'], function(clone) {
const a = {a:1}
const b = clone(a)
})
2
3
4
# CommonJS
CommonJS
是一种同步模块加载规范,目前主要用于Node.js
环境中(Sea.js
使用的也是CommonJS
规范)。CommonJS
规范中定义模块的方式如下:
define(function(require, exports, module) {
//此处省略代码
})
2
3
在Node.js
中,外面的define
包裹函数是系统自动生成的,不需要开发者自己书写。
// 匿名,无依赖模块,文件名就是模块名
function clone(source) {
//此处省略代码
}
module.exports = clone
2
3
4
5
6
在Node.js
环境下,假设上面的代码位于clone.js
文件中,那么在index.js
文件中可以像下面代码这样使用上面代码定义的模块
const clone = require('./clone.js')
const a = {a:1}
const b = clone(a)
2
3
# UMD
UMD
是一种通用模块加载规范,其全称是Universal Module Definition
,中文名称是通用模块定义。UMD想要解决的问题和其名称所传递的意思是一致的,它并不是一种新的规范,而是对前面介绍的3种模块规范(原始模块、AMD,CommonJS)的整合,支持UMD规范的库可以在任何模块环境中工作。
(function (root, factory) {
var clone = factory(root)
if(typeof define === 'function' && define.amd) {
//AMD
define('clone', function() {
return clone
})
} else if (typeof exports === 'object') {
//CommonJS
module.exports = clone
} else {
// 原始模块
var _clone = root.clone
clone.noConflict = function() {
if(root.clone === clone) {
root.clone = _clone
}
return clone;
}
}
})(this, function(root) {
function clone(source) {
// 此处省略代码
}
return clone
})
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
可以看到,UMD
规范只是对不同模块规范的简单整合,稍微不同的是,代码中给原始模块增加了noConflict
方法,使用noConflict
方法可以解决全局名称冲突的问题。
# ES Module
ECMAScript 2015带来了原生的模块系统——ES Module
。目前,部分浏览器已经支持直接使用ES Module,而不兼容的浏览器则可以通过构建工具来使用。
ES Module
的语法更加简单,只需要在函数前面加上关键字export
即可。
export function clone(source) {
//此处省略代码
}
2
3
假设上面的代码位于clone.js
文件中,那么在index.js
文件中可以像下面代码这样引用clone.js
文件中的clone
函数
import {clone} from './clone.js'
const a = {a:1}
const b = clone(a)
2
3
# 总结
对于开源库来说,为了满足各种模块使用者的需求,需要对每种模块提供支持。开源库可以提供两个入口文件,这两个入口文件及其支持的模块如下:
入口文件 | 支持的模块 |
---|---|
index.js | 原始模块,AMD模块,CommonJS模块,UMD模块 |
index.esm.js | ES Module |