JS模块化的发展历程
CommonJS、AMD、CMD、UMD、ES6模块化
模块化之前的引用方式
最开始的样子
这样带来的问题
- 代码复用率低
- 全局作用域污染:无法保证不与其他模块发生变量名冲突,而且模块成员之间没什么关系。
- 可维护性差
命名空间和IIEF
为了解决以上问题,就出现了命名空间、IIEF
命名空间
var namespace = {}
namespace.add = function(a, b){ return a + b }
namespace.add(1, 2)
这样书写解决了以上问题,但是还有一些问题没有解决,那就是需要注意
缺点:
文件依赖的顺序
// 如果 utils.js 依赖于 jquery.js, 那么引用顺序就必须是 jquery 在前面,否则就会报错
外部可以随意修改内部成员
// 例如外部调用
utils.add = 100
// 其他地方在调用 utils.add(1,2) 就会报错
IIEF: 立即执行函数
可以通过立即函数可以达到隐藏细节的目的,这样在模块外部无法修改我们暴露的变量、函数
// IIEF
var utils = (function(){
var module = {}
module.add = function(a,b){ return a + b }
return module
})()
utils.add(1, 2)
再增强一点:引入依赖
这就是模块模式,也是现代模块实现的基石
var Module = (function($){
var _$body = $('body')
var foo = function(){
console.log(_$body)
}
return {
foo: foo
}
})(jQuery)
Module.foo()
为什么要模块化
- 网页变为单页面应用
- 复杂度增加
- 解耦性越来越被需要
- 部署希望得到优化,提高性能
模块化希望带来的好处
- 避免命名冲突,减少命名空间污染
- 更好的文件分离,按需加载
- 更高的复用性
- 更高的维护性
模块化以后带来的问题
页面由引用一个js文件变为引用多个js文件
意味着请求数量变多,同时可能存在依赖顺序的问题
带来的缺点
- 请求多
- 依赖模糊
- 难以维护
模块化规范
CommonJs 或者叫做CJS:用在服务器端
网页端没有模块化编程时候只是页面 JS 逻辑复杂,但还是可以工作下去,在服务端却一定要有模块,所以JS 发展这么多年,第一个流行的模块化规范却是由服务端的 JS 应用带来的,CommonJS 规范是由 nodejs 发扬光大,这标志着 JS 模块化正式登上舞台
-
定义模块
根绝CommonJS 规范,一个单独的文件就是一个模块。每一个模块都是一个单独的作用域,也就是说,在改模块内部定义的变量,无法被其他模块读取,除非定义的变量为 global 对象的属性
-
模块输出
模块只有一个输出,module.exports 对象,我们需要把模块希望输出的内容放入该对象
-
暴露模块方式(暴露的模块本质都是是 exports 对象)
- exports.xxx = value
- module.exports = xxxx
-
加载模块:
加载模块使用 require 方法,该方法读取一个文件并执行,返回文件内部的 module.exports 对象
-
实现
服务器端实现: Node.js
-
浏览器端实现:Browserify (Browserify是一个node.js模块,主要用于改写现有的CommonJS模块,使得浏览器端也可以使用这些模块。)
注意:浏览器不识别require 方法,需要提前编译打包处理
// utils.js文件
function add(a,b){
return a + b
}
module.exports = { add }
// main.js
var nameModule = require('./utils.js')
nameModule.add(1,2)
缺点:加载模块是同步的,只有加载完后才能执行后面的操作;现加载现用,在服务器端编程,加载的模块一般存在本地硬盘里,加载起来比较快,不用考虑异步加载的问题,因为 CommonJS 规范比较适用。然后,并不适用于浏览器环境,同步意味着阻塞线程,浏览器资源的加载方式是异步的。
解决方式之一:开发一个服务端组件,对模块化代码最静态分析,将模块与它的依赖列表一起返回浏览器。这确实很好使,不过需要服务端加载额外的组件,需要调整底层架构,不太友好
另一个思路,用一套标准模板来封装定义,但是对于模块怎么定义和怎么加载,产生了分歧?
AMD
AMD是
"Asynchronous Module Definition"
的缩写,意思就是"异步模块定义"。
它采用异步加载方式加载模块,模块的加载不影响它后面的语句的运行,所有依赖这个模块的语句,都定义在一个回调函数中,等到加载完毕之后,这个回调函数才会执行。
由于不是 JS 原生支持,使用AMD 规范进行页面开发需要用到对应的库函数,也就是大名鼎鼎的 RequireJ,实际上AMD是 requireJS 在推广过程中对模块定义的规范化的产生。
Requirejs 也是采用require() 加载模块,但是不同于 CommonJS,它要求两个参数
require([module], callback)
第一个参数 module 是一个数组,里面的成员就是要加载的模块,第二个参数是callback 则是加载成功之后的回调函数。
Requirejs它也定义了一个函数 define, 他是全局变量,用来定义模块:
define(id?, dependencies?, factory)
参数说明
id: 指定义中模块的名字,可选,如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字。如果提供了该参数,模块名必须是“顶级的”和绝对的(不允许相对名字)
-
dependencies: 是一个当前模块依赖的,已经被模块定义的模块标识的数组字面量。依赖参数是可选的,如果忽略此参数,他应该默认为 ["require", "exports", "module" ]. 然而,如果工厂方法的长度属性小于3,加载器会选择以函数的长度属性指定的参数个数调用工厂方法
- define(name,[] , callback); 这个name可以省掉,默认是文件名称;当然也可以自定义,一旦我们定义了name,根据源代码我们可以发现define函数内部其实就是把这个name以及依赖模块、回调函数作为一个对象存储在全局的数组当中,也就是 defQueue.push([name,deps,callback]);那么这个name就是这个组件注册的的ID
- require([name , name2],callback); 系统首先会在全文检索path中是否对应的路径,如果没有自然把他作为路径拼接在baseUrl上去异步加载这个js文件,加载时从源代码中可以看到 ,var data = getScriptData(evt);返回的 data.id 其实就是name,然后执行contex.completeLoad(node.id),其内部就很清楚了,把define中注册的name和这里得到的name进行比较如果相等就执行
- 所以道理就是:require 和 define 的 name 必须保证一致!
-
factory: 工厂方法,模块初始化要执行的函数或者对象。如果为函数,他应该只执行一次,如果是对象,此对象应该为模块的输出值。
举个例子
require(['foo', 'bar'], function ( foo, bar ) { foo.doSomething(); bar.doSomething(); }, function(err){ console.log(err) } );
CMD
CMD 即
Common Module Definition
通用模块定义
CMD规范是国内发展起来的,就像AMD有一个 requirejs, CMD 有个浏览器的实现 SeaJS. SeaJS 要解决的问题和 requirejs一样,只不过在模块定义方式和模块加载(可以说运行、解析) 时机上有所不同。
在CMD 中,一个模块就是一个文件,代码的书写格式如下:
// 定义有依赖的模块
define(function(require, exports, module){
// 模块代码
// 引入依赖模块(同步)
var module2 = require('./module2')
// 引入依赖模块(异步)
require.sync("./module3", function(m3){
// 模块代码
})
// 暴露模块
export.xxx = value
})
- require 是可以把其他模块导入进来的一个参数
- exports 是可以把模块内的一些属性和方法导出,
- module 是一个对象,上面存储了与当前模块相关联的一些属性和方法
注意区别AMD和CMD:
AMD是依赖关系前置,在定义模块的时候就要声明其依赖的模块
CMD是按需加载依赖就近,只有在用到某个模块的时候再去 require
// CMD
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
// 此处略去 100 行
var b = require('./b') // 依赖可以就近书写
b.doSomething()
// ...
})
// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
a.doSomething()
// 此处略去 100 行
b.doSomething()
...
})
tips: RequireJS其实也是支持CMD这种写法的
UMD
UMD (Universal Module Definition)
,就是一种javascript
通用模块定义规范,让你的模块能在javascript
所有运行环境中发挥作用。
UMD先判断是否支持Node.js的模块(exports)是否存在,存在则使用Node.js模块模式, 在判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块。
(function(root, factory) {
if (typeof module === 'object' && typeof module.exports === 'object') {
console.log('是commonjs模块规范,nodejs环境')
module.exports = factory();
} else if (typeof define === 'function' && define.amd) {
console.log('是AMD模块规范,如require.js')
define(factory())
} else if (typeof define === 'function' && define.cmd) {
console.log('是CMD模块规范,如sea.js')
define(function(require, exports, module) {
module.exports = factory()
})
} else {
console.log('没有模块环境,直接挂载在全局对象上')
root.umdModule = factory();
}
}(this, function() {
return {
name: '我是一个umd模块'
}
}))
ES6 modules
也需要对依赖模块进行编译打包, 使用 Babel
// a.js
import { age } from './b.js';
console.log(age);
setTimeout(() => {
console.log(age);
import('./b.js').then(({ age }) => {
console.log(age);
})
}, 100);
// b.js
export let age = 1;
setTimeout(() => {
age = 2;
}, 10);
// 打开 index.html 引用的是 a.js
// 执行结果:
// 1
// 2
// 2
CommonJS 与ES6的区别
-
CommonJS 是运行时候加载,ES6 模块是编译时候输出接口
原因:因为 CommonJS 加载的时候是一个对象 (即 module.exports 属性),该对象只有在脚本运行完才会生成;而 ES6 模块不是对象,它的对外接口只是一个静态定义,在代码静态解析阶段就会生成。
ES6 模块的设计思想是尽量放入静态化,使得在编译时候就确定依赖关系
而CommonJS 就只能在运行时候确定这些输入和输出的变量
CommonJS 模块输出的是一个值的复制,ES6 模块输出的值是值的引用
CommonJS 加载的是整个模块,即将所有的方法全部加载出来,ES6可以单独加载其中某个方法
CommonJS 中的 this 指向当前模块,ES6 中的 this 指向 undefined
CommonJS 默认非严格模式,ES6的模块自动采用严格模式
举个例子
// CommonJS 模块
let { stat, exists, readFile } = require('fs');
// 等同于
let _fs = require('fs');
let stat = _fs.stat;
let exists = _fs.exists;
let readfile = _fs.readfile;
// 上面代码的实质就是整体加载 fs 模块,生成一个对象 _fs,然后再从这个对象上面读取需要的三个方法,这种加载方式称为“运行时加载”,因为只有运行时候才能得到这个对象,导致完全无法在编译时候做到 "静态优化"
// ES6模块
import { stat, exists, readFile } from 'fs';
// 上面代码的实质就是从 fs 模块加载三个方法,其他方法不加载。这种加载方式成为 "编译时加载"或者 "静态加载",即ES6 可以在编译时候就完成模块加载,效率要比 CommonJS 模块的加载方式高。
参考文献
前端模块化
理解CommonJS、AMD、CMD三种规范
阮一峰:Javascript模块化编程(三):require.js的用法
RequireJS用法解析
前端模块化(CommonJs,AMD和CMD)
从 CommonJS 到 Sea.js #269
可能是最详细的UMD模块入门指南
推荐文章
深入学习CommonJS和ES6模块化规范
js当中CommonJS 和es6的模块化引入方案以及比较
阮一峰:Node.js 如何处理 ES6 模块
视频链接
WEB前端-javascript&JS模块化规范&AMD规范&自定义模块