JavaScript 模块化编程(一):模块的写法
JavaScript 模块化编程(二):规范
JavaScript 模块化编程(三):实现一个RequireJS
JavaScript 模块化编程(四):结合Node源码分析CommonJs规范
常见的JavaScript 模块化规范有3种,CommonJS、AMD(异步模块定义)、CMD(公共模块定义)
其中
服务端 :NodeJS 服务:CommonJS规范,新版本的Node也可以启用ES6 Module功能
浏览器端:主要使用的是AMD规范和CMD规范,现在已经逐步被ES6 Module取代
当ES2015标准的出现后,ES6 在语言标准的层面上,实现了模块功能。这也让AMD、CMD逐渐被淘汰。相信ES6模块最终会一统天下
1.CommonJS规范
(1) 每一个文件都是一个模块,每一个模块都有一个独立的作用域,文件内的变量,函数都是私有的,其他文件不可使用(除非赋值到 global上)
(2)每个模块内部,module变量代表当前模块
(3)每个文件对外的接口是 module.exports 属性
(4) require用于引用其他模块,实际获得的是其他模块的module.exports这个属性
例子
//module_a.js
var x = 5;
var addX = function(value) {
return value + x;
};
module.exports.x = x;
module.exports.addX = addX;
//index.js
var example = require('./module_a.js');
console.log(example.x); // 5
console.log(example.addX(1)); // 6
CommonJS模块的特点
(1)所有代码都运行在模块作用域,不会污染全局作用域
(2)模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存
(3)模块加载的顺序,按照其在代码中出现的顺序(即是同步加载)
require内部处理流程
require 实际是 指向当前模块的 module.require, module.require 又调用Node的 Module._load(此Module非彼module)
Module._load = function(request, parent, isMain) {
// 1. 检查 Module._cache,是否缓存之中有指定模块
// 2. 如果缓存之中没有,就创建一个新的Module实例
// 3. 将它保存到缓存
// 4. 使用 module.load() 加载指定的模块文件,
// 读取文件内容之后,使用 module.compile() 执行文件代码
// 5. 如果加载/解析过程报错,就从缓存删除该模块
// 6. 返回该模块的 module.exports
};
其中 module.compile()执行如下:
Module.prototype._compile = function(content, filename) {
// 1. 生成一个require函数,指向module.require
// 2. 加载其他辅助方法到require
// 3. 将文件内容放到一个函数之中,该函数可调用 require
// 4. 执行该函数
};
2.AMD(Asynchromous Module Definition - 异步模块定义)
AMD 是 RequireJS 在推广过程中对模块定义的规范化产出
CommonJS 采用的是同步加载
机制,如果用于客户端,必定受到网络的限制。所以,CommonJS不适用于客户端。
而 AMD 采用的是模块异步加载
方式,在需要执行到模块文件的时候,实现异步加载,回调执行。
require.js
首先下载最新require.js ,然后引入,data-main用于指定网页程序的主模块:
使用
定义模块
define(id?, dependencies?, factory)
加载模块
require([module], callback)
例子
//math.js
define(function() {
var add = function(x, y) {
return x + y
}
return {
add: add
}
})
//main.js
require(['math'], function (math) {
math.add(2, 3);
});
当执行到这一段代码的时候, 浏览器会先 加载 math模块,在math模块加载成功后, 再执行后面的回调函数
3.CMD(Common Module Definition - 公共模块定义)
CMD 是 SeaJS 在推广过程中对模块定义的规范化产出
SeaJS
使用
定义模块
define(factory)
加载模块
require(id)
区别
- 对于依赖的模块,AMD 是
提前执行
,CMD 是延迟执行
。不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible - CMD 推崇
依赖就近
,AMD 推崇依赖前置
,例:
//CMD
define (function (require, exports, module) {
var a = require('./a') // 模块加载
a.doSomething();
var b = require('./b') // 依赖可以就近书写
b.doSomething();
// 通过 exports 对外提供接口
exports.doSomething = ...
// 或者通过 module.exports 提供整个接口
module.exports = ...
})
代码在运行时,首先是不知道依赖的,需要遍历所有的require关键字,找出后面的依赖。具体做法是将function toString后,用正则匹配出require关键字后面的依赖。显然,这是一种牺牲性能来换取更多开发便利的方法。而AMD是依赖前置的,换句话说,在解析和执行当前模块之前,模块作者必须指明当前模块所依赖的模块,表现在require函数的调用结构上为:
//AMD
define(['./a','./b'],function(a,b){
a.doSomething()
b.doSomething()
})
代码在一旦运行到此处,能立即知晓依赖。而无需遍历整个函数体找到它的依赖,因此性能有所提升,缺点就是开发者必须显式得指明依赖——这会使得开发工作量变大,比如:当你写到函数体内部几百上千行的时候,忽然发现需要增加一个依赖,你不得不回到函数顶端来将这个依赖添加进数组
4.UMD(Universal Module Definition - 通用模块定义)
Universal Module Definition。可以看成是AMD和CommonJS的一个合并方案。解决跨平台的解决方案。
步骤
1.先判断是否支持Node.js模块格式(CommonJS),存在则使用Node.js模块格式
2.再判断是否支持AMD(define是否存在),存在则使用AMD方式加载模块
3.前两个都不存在,则将模块公开到全局(window或global)
以一个calculator模块为例:
// if the module(calculator) has no dependencies, the above pattern can be simplified to
(function (name, context, definition) {
if (typeof module != 'undefined' && module.exports){ //CommonJs
module.exports = definition();
}else if (typeof define == 'function' && define.amd){ //AMD
define(name, definition);
} else{ // Browser globals (context is window)
context[name] = definition();
}
}('calculator', this, function () {
// your module here!
return {
sum: function(a, b) { return a + b; }
};
});
5.ES6 Module
export命令用于规定模块的对外接口
//module_a.js
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};
使用export命令定义了模块的对外接口以后,其他 JS 文件就可以通过import命令加载这个模块
// main.js
import {firstName, lastName, year} from './profile.js';
function setName(element) {
element.textContent = firstName + ' ' + lastName;
}
export default命令,为模块指定默认输出。其他模块加载该模块时,import命令可以为该输出指定任意名字
详细见ES6 Module
6.回顾总结
1.CommonJS和AMD区别?
(1)CommonJS是适用于服务器端
,Node就是采用的CommonJS模式。它是同步加载
不同模块文件。之所以采用同步,是因为模块文件都存放在服务器的各个硬盘上,实际的加载时间就是硬盘的文件读取时间
(2)AMD是适用于浏览器端
的一种模块加载方式。从名字可知,AMD采用的是异步加载
方式。浏览器需要使用的js文件(忽略缓存)都存放在服务器端,从服务器端加载文件到浏览器受网速等各种因素的影响,如果采用同步加载方式,一旦js文件加载受阻,页面将处于阻塞状态
2.CommonJS和ES6 模块区别?
(1)CommonJS 模块是运行时
加载,ES6 模块是编译时
输出接口
这是因为CommonJS 的输出接口是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成,使得编译时就能确定模块的依赖关系(“静态优化”)。
(2)CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用
CommonJS 模块输出的是module.exports这个对象,我们读取的也是这个对象
,而不是模块内部某个变量。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值(除非引用类型)。ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用
。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
参考
CommonJS规范
RequireJS和AMD规范
AMD 和 CMD 的区别有哪些?
Javascript 模块化管理的来世今生