ES6系列之模块化开发总结(十二)
Opened this issue · 0 comments
前言
模块化规范可分为四种
- AMD
- CMD
- CommonJS
- ES6 import
下面我们来以下顺序聊聊模块化加载规范
AMD规范
在 ES6 之前,社区制定了一些模块加载方案,最主要的有 CommonJS 和 AMD 两种。前者用于服务器,后者用于浏览器。
// CommonJS模块
let { start, readFile } = require('fs');
// fs.js
exports.start = fn
exports.readFile = fn
当然这是在 node 中的写法,在浏览器中又是另一种写法。
// fs.js
define(['./start','./readFile'], function() {
console.log('加载了 start ,readFile 模块')
return {
readFile: function() {
return fn
},
start: function() {
return fn
},
};
});
// 依然按照AMD规范引用
let { start, readFile } = require('fs');
这样浏览器也可以按照AMD规范来实现模块化了
CMD规范
与 AMD 一样,CMD 其实就是 SeaJS 在推广过程中对模块定义的规范化产出。
// fs.js
define(function(require, exports, module) {
var addModule = require('./start');
var squareModule = require('./readFile');
});
// start.js
define(function(require, exports, module) {
console.log('加载了 start 模块')
module.exports = {
start: function() {
return fn
}
};
});
CMD 更像是在 AMD 之上的扩展,使其粒度更细。
AMD 与 CMD 的区别
- CMD 推崇依赖就近,AMD 推崇依赖前置。
// require.js 例子中的 fs.js
// 依赖必须一开始就写好
require(['./start', './readFile'], function(start, readFile) {
console.log(startModule)
console.log(readFileModule)
});
// sea.js 例子中的 fs.js
define(function(require, exports, module) {
var readFileModule = require('./start');
// 依赖可以就近书写
var readFileModule = require('./readFile');
});
- AMD 是将需要使用的模块先加载完再执行代码,而 CMD 是在 require 的时候才去加载模块文件,加载完再接着执行。
CommonJS
AMD 和 CMD 都是用于浏览器端的模块规范,而在服务器端比如 node,采用的则是 CommonJS 规范。
导出模块的方式:
var add = function(x, y) {
return x + y;
};
module.exports.add = add;
引入模块的方式:
var add = require('./add.js');
console.log(add.add(1, 1));
我们将之前的例子改成 CommonJS 规范:
// main.js
var add = require('./add.js');
console.log(add.add(1, 1))
var square = require('./square.js');
console.log(square.square(3));
// add.js
console.log('加载了 add 模块')
var add = function(x, y) {
return x + y;
};
module.exports.add = add;
// multiply.js
console.log('加载了 multiply 模块')
var multiply = function(x, y) {
return x * y;
};
module.exports.multiply = multiply;
// square.js
console.log('加载了 square 模块')
var multiply = require('./multiply.js');
var square = function(num) {
return multiply.multiply(num, num);
};
module.exports.square = square;
如果我们执行 node main.js
,打印的顺序为:
加载了 add 模块
2
加载了 square 模块
加载了 multiply 模块
9
我们发现 CommonJS 是先执行再去加载之后的文件,和 seaJS 一致。
ES6的import
导出模块的方式:
/// profile
var firstName = 'Michael';
var lastName = 'Jackson';
var year = 1958;
export {firstName, lastName, year};
引入模块的方式:
import {firstName, lastName, year} from './profile';
我们再将上面的例子改成 ES6 规范:
// main.js
import {add} from './add.js';
console.log(add(1, 1))
import {square} from './square.js';
console.log(square(3));
// add.js
console.log('加载了 add 模块')
var add = function(x, y) {
return x + y;
};
export {add}
// multiply.js
console.log('加载了 multiply 模块')
var multiply = function(x, y) {
return x * y;
};
export {multiply}
// square.js
console.log('加载了 square 模块')
import {multiply} from './multiply.js';
var square = function(num) {
return multiply(num, num);
};
export {square}
打印的顺序为:
加载了 add 模块
加载了 multiply 模块
加载了 square 模块
2
9
我们可以发现是模块先加载完再执行代码,更加符合 AMD 规范。
ES6 与 CommonJS
引用阮一峰老师的 《ECMAScript 6 入门》:
它们有两个重大差异。
- CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
- CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
第二个差异可以从两个项目的打印结果看出,导致这种差别的原因是:
因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
重点解释第一个差异。
CommonJS 模块输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。
举个例子:
// 输出模块 counter.js
var counter = 3;
function incCounter() {
counter++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// 引入模块 main.js
var mod = require('./counter');
console.log(mod.counter); // 3
mod.incCounter();
console.log(mod.counter); // 3
counter.js 模块加载以后,它的内部变化就影响不到输出的 mod.counter 了。这是因为 mod.counter 是一个原始类型的值,会被缓存。
但是如果修改 counter 为一个引用类型的话:
// 输出模块 counter.js
var counter = {
value: 3
};
function incCounter() {
counter.value++;
}
module.exports = {
counter: counter,
incCounter: incCounter,
};
// 引入模块 main.js
var mod = require('./counter.js');
console.log(mod.counter.value); // 3
mod.incCounter();
console.log(mod.counter.value); // 4
value 是会发生改变的。不过也可以说这是 "值的拷贝",只是对于引用类型而言,值指的其实是引用。
而如果我们将这个例子改成 ES6:
// counter.js
export let counter = 3;
export function incCounter() {
counter++;
}
// main.js
import { counter, incCounter } from './counter';
console.log(counter); // 3
incCounter();
console.log(counter); // 4
这是因为:
ES6 模块的运行机制与 CommonJS 不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令 import,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。
webpack
webpack 其实是遵循 CommonJS 规范的。
我们以 CommonJS 项目中的 square.js 为例,它依赖了 multiply 模块:
console.log('加载了 square 模块')
var multiply = require('./multiply.js');
var square = function(num) {
return multiply.multiply(num, num);
};
module.exports.square = square;
webpack 会将其包裹一层,注入这些变量:
function(module, exports, require) {
console.log('加载了 square 模块');
var multiply = require("./multiply");
module.exports = {
square: function(num) {
return multiply.multiply(num, num);
}
};
}
那 webpack 又会将 CommonJS 项目的代码打包成什么样呢?我写了一个精简的例子,你可以直接复制到浏览器中查看效果:
// 自执行函数
(function(modules) {
// 用于储存已经加载过的模块
var installedModules = {};
function require(moduleName) {
if (installedModules[moduleName]) {
return installedModules[moduleName].exports;
}
var module = installedModules[moduleName] = {
exports: {}
};
modules[moduleName](module, module.exports, require);
return module.exports;
}
// 加载主模块
return require("main");
})({
"main": function(module, exports, require) {
var addModule = require("./add");
console.log(addModule.add(1, 1))
var squareModule = require("./square");
console.log(squareModule.square(3));
},
"./add": function(module, exports, require) {
console.log('加载了 add 模块');
module.exports = {
add: function(x, y) {
return x + y;
}
};
},
"./square": function(module, exports, require) {
console.log('加载了 square 模块');
var multiply = require("./multiply");
module.exports = {
square: function(num) {
return multiply.multiply(num, num);
}
};
},
"./multiply": function(module, exports, require) {
console.log('加载了 multiply 模块');
module.exports = {
multiply: function(x, y) {
return x * y;
}
};
}
})
最终的执行结果为:
加载了 add 模块
2
加载了 square 模块
加载了 multiply 模块
9
总结
- AMD 是先加载完后再执行,CMD是边执行边加载,CMD 结果与 CommonJS 结果一致。
- CommonJS 模块输出的是一个值的拷贝,而 ES6 模块输出的是值的引用。
- 在 node 中模块加载和 webpack 打包遵循的都是 CommonJS 规范。