# 前端模块化
主要介绍CommonJS模块 和 ES6模块的区别,不感兴趣可以跳过本节内容。
## 1、ES6模块
ES6 模块的设计思想是尽量的静态化,使得编译时就能确定模块的依赖关系,以及输入和输出的变量。
**1.1 CommonJs模块运行时加载(对象),ES6模块编译时输出接口**
引用[阮一峰老师的例子][1]来进行说明:
CommonJS的输出缓存机制,使得模块被加载时,会被缓存起来,将整个模块当作一个对象输出。
```js
// 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的所有方法),生成一个对象(_fs),然后再从这个对象上面读取 3 个方法。这种加载称为“运行时加载”,因为只有运行时才能得到这个对象,导致完全没办法在编译时做“静态优化”。
并且,由于缓存机制,重复加载同一模块,都只会在第一次加载时运行一次,以后再加载,就返回第一次运行的结果,除非手动清除系统缓存。
ES6 模块不是对象,而是通过export命令显式指定输出的代码,再通过import命令输入。
```js
// ES6模块
import { stat, exists, readFile } from 'fs';
```
上面代码的实质是从fs模块加载3个方法,其他方法不加载。这种加载称为“编译时加载”或者静态加载,即 ES6 可以在编译时就完成模块加载,效率要比CommonJS模块的加载方式高。当然,这也导致了没法引用 ES6 模块本身,因为它不是对象。
**1.2 CommonJs 模块输出的是一个值的复制,ES6模块编译时输出接口(动态只读引用)**
CommonJs 模块:
对于基本数据类型,属于复制。即会被模块缓存。同时,在另一个模块可以对该模块输出的变量重新赋值,而不会影响其他模块。
对于复杂数据类型,属于浅拷贝。由于两个模块引用的对象指向同一个内存空间,因此对该模块的值做修改时会影响另一个模块。
ES6 模块:
ES6 模块的运行机制与 CommonJS不一样。JS引擎对脚本静态分析的时候,遇到模块加载命令import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
**1.3 对于循环引用的处理**
CommonJS 模块:
其重要特性是加载时执行,即脚本代码在require的时候,就会全部执行。一旦出现某个模块被"循环加载",就只输出已经执行的部分,还未执行的部分不会输出。
例如:
```js
// a.js
console.log('a start');
var b = require('./b.js');
console.log('a doing');
console.log(b);
exports.a = 'a模块的变量';
// b.js
console.log('b start');
var a = require('./b');
console.log('b doing')
console.log(a);
exports.b = 'b模块的变量';
// 要理解CommonJS模块是同步执行
// 当执行 node a.js时,输出为:
// a start
// b start
// b doing
// {}
// a doing
// {b: 'b模块的变量'}
```
ES6 模块:
ES6 模块是动态引用,如果使用import从一个模块加载变量,那些变量不会被缓存,而是成为一个指向被加载模块的引用,需要开发者自己保证,真正取值的时候能够取到值。
上面例子的ES6改写:
```js
$ node –experimental-modules a.mjs
// a.js
console.log('a start');
import {b} from ./b.mjs';
console.log('a doing');
console.log(b);
exports.a = 'a模块的变量';
// b.js
console.log('b start');
import {a} from ./a.mjs';
console.log('b doing')
console.log(a);
exports.b = 'b模块的变量';
// 输出
// b start
// b ding
// ReferenceError: a is not defined
```
分析:执行a.mjs以后,引擎发现它加载了b.mjs,因此会优先执行b.mjs,然后再执行a.mjs。接着,执行b.mjs的时候,已知它从a.mjs输入了foo接口,这时不会去执行a.mjs,而是认为这个接口已经存在了,继续往下执行。执行到第三行console.log(foo)的时候,才发现这个接口根本没定义,因此报错。
解决方法:
```js
// a.js
// exports.a = 'a模块的变量'; 改为
export function a () {console.log('a模块的函数')}
// b.js
// exports.b = 'b模块的变量';
export function b () {console.log('b模块的函数')}
// 输出
// b start
// b ding
// [Function: a]
// a start
// b doing
// [Funcion: b]
```
分析:function a(){...}具有提升作用。因此,在import之前,已经存在函数a。
下面简单介绍前端的其它模块化规范。
## 2、CommonJS规范
1、核心思想:
> 把一个文件当做一个模块,通过require方法同步加载模块。
2、应用场景
> Node.js的服务端编程加载的模块主要存在于服务器磁盘,所以加载速度较快,对node这种单线程,影响较小。因此,CommonJS同步加载模块方案主要应用于**node**服务端。
3、不适用于浏览器环境
> 浏览器加载方式与服务端完全不同。如果浏览器请求服务器资源时,会由于某个请求加载的时间过长导致浏览器阻塞,不会往下执行,所以就会出现网页打开缓慢的现象。并且,浏览器端是以插入<script>标签的形式来加载资源(ajax方式不行,有跨域问题),没办法让代码同步执行,使用commonJS同步写法会直接报错。
4、示例
```js
// 1.js
module.export = function() {
say: function() {
alert("你好");
}
}
// main.js
var a = require('./a.js');
a.say();
```
浏览器环境下必须采用异步模式。所以就有了ES6模块、 AMD,CMD 解决方案。
## 3、AMD规范
AMD(异步模块定义)是RequireJS在推广过程中对模块化定义的规范化产出。
1、核心思想:
> 异步加载所需的模块,然后在回调函数中执行主逻辑。
**可以并行加载多个模块,等所有模块都加载并且解释执行完成后**,才会执行接下来的代码,
2、吐槽点
> “提前执行”:所有依赖模块会被预先下载,并且提前执行(不管该模块的调用时机)。“提前执行”的性能消耗是不容忽视。
3、懒加载
> AMD保留了commonjs中的require、exprots、module这三个功能。因此,为了解决“提前执行”的性能消耗,依赖的模块不写在dependencies数组中,
4、示例
```js
// a.js
define(function(){
return {
say: function(){
console.log('hello, a.js');
}
}
});
// b.js
define(function(){
return {
say: function(){
console.log('hello, a.js');
}
}
});
// main.js
require(['a', 'b'], function(a, b){
a.say();
$('#b').click(function(){
b.say();
});
})
// AMD懒加载方式
// main.js
require(['a'], function(a){
a.say();
$('#b').click(function(){
require(['b'], function(b){
b.say();
});
});
})
```
这种懒加载减轻了初始化操作。**但是,在需要执行b.say\(\)时,需要实时下载代码然后在回调中才能执行,用户的操作会有明显的延迟卡顿。**
## 4、CMD规范
CMD\(同步模块定义\)是SeaJS\(淘宝团队\)在推广过程中对模块化定义的规范化产。
1、示例
```js
// a.js
define(function(require, exports, module){
return {
say: function(){
console.log('hello, a.js');
}
}
});
// b.js
define(function(require, exports, module){
return {
say: function(){
console.log('hello, b.js');
}
}
});
// main.js
define(function(require, exports, module){
var a = require('a');
a.say();
$('#b').click(function(){
var a = require('b');
b.say();
});
});
})
```
a.js和b.js都会预先下载,但是,**b.js不会预先执行(即用即返回)。**
[1]: http://es6.ruanyifeng.com/#docs/module-loader