前言
说到前端模块化,你第一时间能想到的是什么?Webpack?ES6 Module?还有吗?咱们一块儿来看一下下图。 javascript

1、千丝万缕
为了更贴合咱们的平常开发场景(先后端分离),咱们尝试先从不一样平台的维度区分,做为本文的切入点。php
1. 根据平台划分
平台 | 规范 | 特性 |
---|---|---|
浏览器 | AMD、CMD | 存在网络瓶颈,使用异步加载 |
非浏览器 | CommonJS | 直接操做 IO,同步加载 |
能够看到咱们很是暴力的以是否是浏览器做为划分标准。仔细分析一下,他们之间最大的差别在于其特性上,是否存在瓶颈。 例如说网络性能瓶颈,每一个模块的请求都须要发起一次网络请求,并等待资源下载完成后再进行下一步操做,那整个用户体验是很是糟糕的。 根据该场景,咱们简化一下,以同步加载和异步加载两个维度进行区分。前端
特性 | 规范 |
---|---|
同步加载 | CommonJS |
异步加载 | AMD、CMD |
2. AMD、CMD 两大规范
先忽略 CommonJS,咱们先介绍下,曾经一度盛行的 AMD、CMD 两大规范。java
规范 | 约束条件 | 表明做 |
---|---|---|
AMD | 依赖前置 | requirejs |
CMD | 就近依赖 | seajs |
AMD、CMD 提供了封装模块的方法,实现语法上相近,甚至于 requirejs 在后期也默默支持了 CMD 的写法。咱们用一个例子,来说清楚这两个规范之间最大的差别:依赖前置和就近依赖。node
AMD:webpack
web
// hello.js
define(function() {
console.log('hello init');
return {
getMessage: function() {
return 'hello';
}
};
});
// world.js
define(function() {
console.log('world init');
});
// main define(['./hello.js', './world.js'], function(hello) { return { sayHello: function() { console.log(hello.getMessage()); } }; });es6
复制代码// 输出 // hello init // world init 复制代码复制代码
CMD:后端
// hello.js
define(function(require, exports) {
console.log('hello init');
exports.getMessage = function() {
return 'hello';
};
});
// world.js
define(function(require, exports) {
console.log('world init');
exports.getMessage = function() {
return 'world';
};
});
// main
define(function(require) {
var message;
if (true) {
message = require('./hello').getMessage();
} else {
message = require('./world').getMessage();
}
});
// 输出
// hello init
复制代码复制代码
结论: CMD 的输出结果中,没有打印"world init"。可是,须要注意的是,CMD 没有打印"world init"并是不 world.js 文件没有加载。AMD 与 CMD 都是在页面初始化时加载完成全部模块,惟一的区别就是就近依赖是当模块被 require 时才会触发执行。浏览器
requirejs 和 seajs 的具体实如今这里就不展开阐述了,有兴趣的同窗能够到官网了解一波,毕竟如今使用 requirejs 和 seajs 的应该不多了吧。
3. CommonJS
回到 CommonJS,写过 NodeJS 的同窗对它确定不会陌生。CommonJS 定义了,一个文件就是一个模块。在 node.js 的实现中,也给每一个文件赋予了一个 module 对象,这个对象包括了描述当前模块的全部信息,咱们尝试打印 module 对象。
// index.js
console.log(module);
复制代码// 输出 { id: '/Users/x/Documents/code/demo/index.js', exports: {}, parent: { module }, // 调用该模块的模块,能够根据该属性查找调用链 filename: '/Users/x/Documents/code/demo/index.js', loaded: false, children: [...], paths: [...] } 复制代码复制代码
也就是说,在 CommonJS 里面,模块是用对象来表示。咱们经过“循环加载”的例子进行来加深了解。
// a.js
exports.x = 'a1';
console.log('a.js ', require('./b.js').x);
exports.x = 'a2';
//b.js
exports.x = 'b1';
console.log('b.js ', require('./a.js').x);
exports.x = 'b2';
//main
console.log('index.js', require('./a.js').x);
// 输出
b.js a1
a.js b2
index.js a2
复制代码复制代码
咱们的理论依据是模块对象,根据该依据咱们进行以下分析。
一、 a.js准备加载,在内存中生成module对象moduleA 二、 a.js执行exports.x = 'a1'; 在moduleA的exports属性中添加x 三、 a.js执行console.log('a.js', require('./b.js').x); 检测到require关键字,开始加载b.js,a.js执行暂停 四、 b.js准备加载,在内存中生成module对象moduleB 五、 b.js执行exports.x = 'b1'; 在moduleB的exports属性中添加x 六、 b.js执行console.log('b.js', require('./a.js').x); 检测到require关键字,开始加载a.js,b.js执行暂停 七、 检测到内存中存在a.js的module对象moduleA,因而能够将第6步当作console.log('b.js', moduleA.x); 在第二步中moduleA.x赋值为a1,因而输出b.js, a1 八、 b.js继续执行,exports.x = 'b2',改写moduleBexports的x属性 九、 b.js执行完成,回到a.js,此时同理能够将第3步当作console.log('a.js', modulerB.x); 输出了a.js, b2 十、 a.js继续执行,改写exports.x = 'a2' 十一、 输出index.js a2 复制代码复制代码
至此,“CommonJS 的模块,是一个对象。”这个概念大伙儿应该能理解吧?
回到这个例子,例子里面还出现了一个保留字 exports。其实 exports 是指向 module.exports 的一个引用。举个例子能够说明他们两个之间的关系。
const myFuns = { a: 1 };
let moduleExports = myFuns;
let myExports = moduleExports;
// moduleExports 从新指向 moduleExports = { b: 2 }; console.log(myExports); // 输出 {a : 1}
复制代码// 也就是说在module.exports被从新复制时,exports与它的关系就gg了。解决方法就是从新指向 myExports = modulerExports; console.log(myExports); // 输出 { b: 2 } 复制代码复制代码
4. ES6 module
对 ES6 有所了解的同志们应该都清楚,web 前端模块化在 ES6 以前,并非语言规范,不像是其余语言 java、php 等存在命名空间或者包的概念。上文说起的 AMD、CMD、CommonJS 规范,都是为了基于规范实现的模块化,并不是 JavaScript 语法上的支持。 咱们先简单的看一个 ES6 模块化写法的例子:
// a.js
export const a = 1;
// b.js export const b = 2;
复制代码// main import { a } from './a.js'; import { b } from './b.js'; console.log(a, b); //输出 1 2 复制代码复制代码
emmmm,没错,export 保留字看起来是否是和 CommonJS 的 exports 有点像?咱们尝试 下从保留字对比 ES6 和 CommonJS。
保留字 | CommonJS | ES6 |
---|---|---|
require | 支持 | 支持 |
export / import | 不支持 | 支持 |
exports / module.exports | 支持 | 不支持 |
好吧,除了 require 两个均可以用以外,其余实际上仍是有明显差异的。那么问题来了,既然 require 两个均可以用,那这两个在 require 使用上,有差别吗?
咱们先对比下 ES6 module 和 CommonJS 之间的差别。
模块输出 | 加载方式 | |
---|---|---|
CommonJS | 值拷贝 | 对象 |
ES6 | 引用(符号连接) | 静态解析 |
又多了几个新颖的词汇,咱们先经过例子来介绍一下值拷贝和引用的区别。
// 值拷贝 vs 引用
// CommonJS let a = 1; exports.a = a; exports.add = () => { a++; };
const { add, a } = require('./a.js'); add(); console.log(a); // 1
// ES6 export const a = 1; export const add = () => { a++; };
复制代码import { a, add } from './a.js'; add(); console.log(a); // 2 // 显而易见CommonJS和ES6之间,值拷贝和引用的区别吧。 复制代码复制代码
静态解析,什么是的静态解析呢?区别于 CommonJS 的模块实现,ES6 的模块并非一个对象,而只是代码集合。也就是说,ES6 不须要和 CommonJS 同样,须要把整个文件加载进去,造成一个对象以后,才能知道本身有什么,而是在编写代码的过程当中,代码是什么,它就是什么。
PS:
- 目前各个浏览器、node.js 端对 ES6 的模块化支持实际上并不友好,更多实践同志们有兴趣能够本身搞一波。
- 在 ES6 中使用 require 字样,静态解析的能力将会丢失!
5. UMD
模块化规范中还有一个 UMD 也不得不说起一下。什么是 UMD 呢?
UMD = AMD + CommonJS 复制代码复制代码
没错,UMD 就是这么简单。经常使用的场景就是当你封装的模块须要适配不一样平台(浏览器、node.js),例如你写了一个基于 Date 对象二次封装的,对于时间的处理工具类,你想推广给负责前端页面开发的 A 同窗和后台 Node.js 开发的 B 同窗使用,你是否是就须要考虑你封装的模块,既能适配 Node.js 的 CommonJS 协议,也能适配前端同窗使用的 AMD 协议?
2、工具时代
1. webpack
webpack 兴起以后,什么 AMD、CMD、CommonJS、UMD,彷佛都变得不重要了。由于 webpack 的模块化能力真的强。
webpack 在定义模块上,能够支持 CommonJS、AMD 和 ES6 的模块声明方式,换句话说,就是你的模块若是是使用 CommonJS、AMD 或 ES6 的语法写的,webpack 都支持!咱们看下例子:
//say-amd.js
define(function() {
'use strict';
return {
sayHello: () => {
console.log('say hello by AMD');
}
};
});
//say-commonjs.js
exports.sayHello = () => {
console.log('say hello by commonjs');
};
//say-es6.js
export const sayHello = () => {
console.log('say hello in es6');
};
//main import { sayHello as sayInAMD } from './say-amd'; import { sayHello as sayInCommonJS } from './say-commonjs'; import { sayHello as sayInES6 } from './say-es6';
复制代码sayInAMD(); sayInCommonJS(); sayInES6(); 复制代码复制代码
不只如此,webpack 识别了你的模块以后,能够将其打包成 UMD、AMD 等等规范的模块从新输出。例如上文说起到的你须要把 Date 模块封装成 UMD 格式。只须要在 webpack 的 output 中添加 libraryTarget: 'UMD'便可。
2. more...
总结
回到开始咱们提出的问题,咱们尝试使用一张图汇总上文说起到的一溜模块化相关词汇。
