js模块规范
# 前言
# JS之AMD、CMD、CommonJS、ES6、UMD的使用
# 图示对比
# 总括
- AMD规范在浏览器环境中异步加载模块,而且可以并行加载多个模块。不过,AMD规范开发成本高,代码的阅读和书写比较困难,模块定义方式的语义不顺畅;
- CMD规范与AMD规范很相似,都用于浏览器编程,依赖就近,延迟执行,可以很容易在Node.js中运行。不过,依赖SPM 打包,模块的加载逻辑偏重;
- CommonJS规范主要用于服务端编程**,加载模块是同步的**,这并不适合在浏览器环境,因为同步意味着阻塞加载,浏览器资源是异步加载的,因此有了AMD CMD解决方案;
- ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,完全可以取代 CommonJS 和 AMD 规范,成为浏览器和服务器通用的模块解决方案;
# AMD 和 CMD 的区别
- 对于依赖的模块,**AMD 是提前执行,CMD 是延迟执行。**不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.
- AMD 推崇依赖前置, CMD 推崇依赖就近;
模块化 | 代表应用 | 特点 |
---|---|---|
AMD | require.js | 1、AMD的api默认一个当多个用 2、依赖前置,异步执行 |
CMD | sea.js | 1、CMD的api严格区分,推崇职责单一 2、依赖就近,按需加载,异步执行 |
# Commonjs 和 ES6 Module的区别
取自阿里巴巴淘系技术前端团队
的回答:
- Commonjs是拷贝输出,ES6模块化是引用输出;
- Commonjs是运行时加载,ES6模块化是编译时输出接口;
- Commonjs是动态语法可写在函数体中,ES6模块化静态语法只能写在顶层;
- Commonjs是单个值导出,ES6模块化可以多个值导出
- Commonjs的this是当前模块化,ES6模块化的this是undefined
# 为什么Commonjs不适用于浏览器
var math = require('math');
math.add(2, 3);
2
第二行math.add(2, 3),在第一行require('math')之后运行,因此必须等math.js加载完成。也就是说,如果加载时间很长,整个应用就会停在那里等。
这对服务器端不是一个问题,因为所有的模块都存放在本地硬盘,可以同步加载完成,等待时间就是硬盘的读取时间。但是,对于浏览器,这却是一个大问题,因为模块都放在服务器端,等待时间取决于网速的快慢,可能要等很长时间,浏览器处于"假死"状态。
因此,浏览器端的模块,不能采用"同步加载"(synchronous),只能采用"异步加载"(asynchronous)。这就是AMD规范诞生的背景。
# AMD(requirejs)
AMD规范可参考地址:https://github.com/amdjs/amdjs-api/wiki/AMD
AMD 是 RequireJS 在推广过程中对模块定义的规范化产出。
AMD规范则是非同步加载模块,允许指定回调函数。由于Node.js主要用于服务器编程,模块文件一般都已经存在于本地硬盘,所以加载起来比较快,不用考虑非同步加载的方式,所以CommonJS规范比较适用。但是,如果是浏览器环境,要从服务器端加载模块,这时就必须采用非同步模式,因此浏览器端一般采用AMD规范。此外AMD规范比CommonJS规范在浏览器端实现要来着早。
根据AMD规范,我们可以使用define定义模块,使用require调用模块,语法:
define(id?, dependencies?, factory);
- id: 定义中模块的名字;可选;如果没有提供该参数,模块的名字应该默认为模块加载器请求的指定脚本的名字;
- dependencies:依赖的模块;
- factory:工厂方法,返回定义模块的输出值。
总结一段话:声明模块的时候指定所有的依赖dependencies,并且还要当做形参传到factory中,对于依赖的模块提前执行,依赖前置
例子1:
define("alpha", ["require", "exports", "beta"], function (require, exports, beta) {
exports.verb = function() {
return beta.verb();
//Or:
return require("beta").verb();
}
});
2
3
4
5
6
7
例子2:
define(["alpha"], function (alpha) {
return {
verb: function(){
return alpha.verb() + 2;
}
};
});
2
3
4
5
6
7
例子3:
define({
add: function(x, y){
return x + y;
}
});
2
3
4
5
例子4:
define(function (require, exports, module) {
var a = require('a'),
b = require('b');
exports.action = function () {};
});
2
3
4
5
使用require函数加载模块:
require([dependencies],function(){});
- 第一个参数是一个数组,表示所依赖的模块
- 第二个参数是一个回调函数,当前面指定的模块都加载成功后,它将被调用.加载的模块会以参数形式传入该函数,从而在回调函数内部就可以使用这些模块
require()函数在加载依赖的函数的时候是异步加载的,这样浏览器不会失去响应,它指定的回调函数,只有前面的模块都加载成功后,才会运行,解决了依赖性的问题
require(['alpha'],function(alpha){
alpha.verb ();
})
2
3
# CMD(seajs)
CMD规范参考地址:https://github.com/seajs/seajs/issues/242 CMD 是 SeaJS 在推广过程中对模块定义的规范化产出。
CMD规范专门用于浏览器端,模块的加载是异步的,模块使用时才会加载执行。**CMD规范整合了AMD和CommonJS规范的特点。**在 Sea.js 中,所有 JavaScript 模块都遵循 CMD模块定义规范。
# 语法
# define Function
define
是一个全局函数,用来定义模块。
define define(factory)
define
接受 factory
参数,factory
可以是一个函数,也可以是一个对象或字符串。
factory
为对象、字符串时,表示模块的接口就是该对象、字符串。比如可以如下定义一个 JSON 数据模块:
define({ "foo": "bar" });
也可以通过字符串定义模板模块:
define('I am a template. My name is {{name}}.');
factory
为函数时,表示是模块的构造方法。执行该构造方法,可以得到模块向外提供的接口。factory
方法在执行时,默认会传入三个参数:require
、exports
和 module
:
define(function(require, exports, module) {
// 模块代码
});
2
3
define define(id?, deps?, factory)
define
也可以接受两个以上参数。字符串 id
表示模块标识,数组 deps
是模块依赖。比如:
define('hello', ['jquery'], function(require, exports, module) {
// 模块代码
});
2
3
id
和 deps
参数可以省略。省略时,可以通过构建工具自动生成。
注意:带 id
和 deps
参数的 define
用法不属于 CMD 规范,而属于 Modules/Transport (opens new window) 规范。
define.cmd Object
一个空对象,可用来判定当前页面是否有 CMD 模块加载器:
if (typeof define === "function" && define.cmd) {
// 有 Sea.js 等 CMD 模块加载器存在
}
2
3
# require Function
require
是 factory
函数的第一个参数。
require require(id)
require
是一个方法,接受 模块标识 (opens new window) 作为唯一参数,用来获取其他模块提供的接口。
define(function(require, exports) {
var a = require('./a');// 获取模块 a 的接口
a.doSomething();// 调用模块 a 的方法
});
2
3
4
require.async require.async(id, callback?)
require.async
方法用来在模块内部异步加载模块,并在加载完成后执行指定回调。callback
参数可选。
define(function(require, exports, module) {
require.async('./b', function(b) {// 异步加载一个模块,在加载完成时,执行回调
b.doSomething();
});
require.async(['./c', './d'], function(c, d) {// 异步加载多个模块,在加载完成时,执行回调
c.doSomething();
d.doSomething();
});
});
2
3
4
5
6
7
8
9
注意:require
是同步往下执行,require.async
则是异步回调执行。require.async
一般用来加载可延迟异步加载的模块。
# exports Object
exports
是一个对象,用来向外提供模块接口。
define(function(require, exports) {
// 对外提供 foo 属性
exports.foo = 'bar';
// 对外提供 doSomething 方法
exports.doSomething = function() {};
});
2
3
4
5
6
除了给 exports
对象增加成员,还可以使用 return
直接向外提供接口。
define(function(require) {
// 通过 return 直接提供接口
return {
foo: 'bar',
doSomething: function() {}
};
});
2
3
4
5
6
7
# module Object
module
是一个对象,上面存储了与当前模块相关联的一些属性和方法。
module.id String
模块的唯一标识。
define('id', [], function(require, exports, module) {
// 模块代码
});
2
3
上面代码中,define
的第一个参数就是模块标识。
module.exports Object
当前模块对外提供的接口。
传给 factory
构造方法的 exports
参数是 module.exports
对象的一个引用。只通过 exports
参数来提供接口,有时无法满足开发者的所有需求。 比如当模块的接口是某个类的实例时,需要通过 module.exports
来实现:
define(function(require, exports, module) {
// exports 是 module.exports 的一个引用
console.log(module.exports === exports); // true
// 重新给 module.exports 赋值
module.exports = new SomeClass();
// exports 不再等于 module.exports
console.log(module.exports === exports); // false
});
2
3
4
5
6
7
8
# AMD与CMD的主要区别
- 对于依赖的模块,**AMD 是提前执行,CMD 是延迟执行。**不过 RequireJS 从 2.0 开始,也改成可以延迟执行(根据写法不同,处理方式不同)。CMD 推崇 as lazy as possible.
- AMD 推崇依赖前置, CMD 推崇依赖就近;
// AMD 默认推荐的是
define(['./a', './b'], function(a, b) { // 依赖必须一开始就写好
a.doSomething()
b.doSomething()
})
// CMD
define(function(require, exports, module) {
var a = require('./a')
a.doSomething()
var b = require('./b')
b.doSomething()
})
2
3
4
5
6
7
8
9
10
11
12
AMD与CMD的其它区别可参考地址:https://www.zhihu.com/question/20351507
# CommonJS 【要点】
CommonJS是服务器端模块的规范,Node.js采用了这个规范.Node.JS首先采用了js模块化的概念.
CommonJS定义的模块分为:模块引用(require)/ 模块定义(exports)/模块标识(module)
所有代码都运行在模块作用域,不会污染全局作用域。
模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了,以后再加载,就直接读取缓存结果。要想让模块再次运行,必须清除缓存。
模块加载的顺序,按照其在代码中出现的顺序。
//common.js
module.exports = function(a, b) {
return a-b
}
let minus = require('./common.js') //文件相对路径
console.log(minus(5,4)) // 结果: 1
2
3
4
5
6
# module对象
Node中提供了一个Module构建函数,所有模块都是Module的实例
# Node 9下import/export使用简单须知
- Node 环境必须在 9.0以上
- 不加loader时候,使用
import/export
的文件后缀名必须为*.mjs
(下面会讲利用Loader Hooks兼容*.js
后缀文件) - 启动必须加上flag
--experimental-modules
- 文件的
import
和export
必须严格按照ECMAScript Modules
语法 ECMAScript Modules
和require()
的cache机制不一样
# exports 与 module.exports
根据AMD规范:
- 每个文件就是一个模块,有自己的作用域。在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见。
- 每个模块内部,module变量代表当前模块。这个变量是一个对象,它的exports属性(即module.exports)是对外的接口。加载某个模块,其实是加载该模块的module.exports属性。
- exports是module.exports一种简写形式,不能直接给exports赋值
- 当直接给module.exports赋值时,exports会失效
为了方便,Node为每个模块提供一个exports变量,指向module.exports。这等同在每个模块头部,有一行这样的命令。
const exports = module.exports;
于是我们可以直接在 exports 对象上添加方法,表示对外输出的接口,如同在module.exports上添加一样。注意,不能直接将exports变量指向一个值,因为这样等于切断了exports与module.exports的联系。
// a.js
exports = function a() {};
// b.js
const a = require('./a.js') // a 是一个空对象
2
3
4
# 示例
//加载示例:commonjs与e6模块加载 test.js
var x = 5;
var add = function(a, b){return a + b;}
module.exports = {x, add}
//module.exports = {
// msg: 'module1',
// foo() { console.log(this.msg) }
//}
var test = require('./test.js');
console.log(test.x); // 5
console.log(test.add(1, 2)); // 3
//使用import语法加载commonjs模块
import { x, add } from './test'
console.log(x); // 5
console.log(add(1, 2)); // 3
//导出的两种不同方式
exports.bcompare = (str, hash) => {
return bcrypt.compareSync(str, hash)
}
module.exports = {
bcompare
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
# ES6 module【要点】
es6 module内部自动采用严格模式
import和export只能出现在模块的最外层(顶层)结构中,否则报错
由于es6模块是静态加载的,因此import和export不能出现在判断等动态语句中
采用import获取的是模块接口的引用,当模块内部发生改变是,import出的接口也会对应改变
【与commonjs规范不同,commonjs中获得的是接口运行结果的缓存】
# 导出
ES6两种导出方式:
- 命名导出
- 默认导出
# 命名导出
// 写法1
export const name = 'calculator';
export const add = function (a,b) {
return a + b;
}
// 写法2
const name = 'calculator';
const add = function (a,b) {
return a + b;
}
export {name, add};
2
3
4
5
6
7
8
9
10
11
在使用命名导出时,可以通过as关键字对变量重命名。如:
const name = 'calculator';
const add = function (a,b) {
return a + b;
}
export {name, add as getSum}; // 在导入时即为name 和 getSum
2
3
4
5
# 默认导出
export default {
name: 'calculator';,
add: function (a,b) {
return a + b;
}
};
2
3
4
5
6
# 导入
针对命名导出的模块,导入方式如下:
// calculator.js
const name = 'calculator';
const add = function (a,b) {
return a + b;
}
export {name, add};
// 一般导入方式
import {name, add} from './calculator.js'
add(2,3);
// 通过as关键字对导入的变量重命名
import {name, add as calculateSum} from './calculator.js'
calculateSum(2,3);
// 使用import * as <myModule>可以把所有导入的变量作为属性值添加到<myModule>对象中,从而减少了对当前作用域的影响
import * as calculateObj from './calculator.js'
calculateObj.add(2,3);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
对于默认导出来说,import后面直接跟变量名,并且这个名字可以自由指定,它指代了calculator.js中默认导出的值。从原理上可以这样理解:
import {default as myCalculator} from './calculator.js'
还有两种导入方式混合起来使用的例子,如下:
import React, {Component} from 'react';
注:这里的React必须写在大括号前面,而不能顺序颠倒,否则会提示语法错误。
# 复合写法
在工程中,有时候需要把一个模块导入后立即导出,比如专门用来集合所有页面或组件的入口文件。此时可以采用复合形式的写法:
export {name, add } from './calculator.js'
复合写法只支持通过命名导出的方式暴露出来的变量,默认导出则没有对应的复合形式,只能将导入和导出拆分开。
import calculator from './calculator.js'
export default calculator;
2
跨文件引入,比如组件库导出;
export * from 'react-router-dom' // 和下面两个一样的意思;
// import * as router from 'react-router-dom';
// export router
2
3
# exoprt和export default
将exoprt和export default放在一起,因为它们关联性很大
简单说:export是导出,export default是默认导出
一个模块可以有多个export,但是只能有一个export default,export default可以和多个export共存
export default 为默认导出,导出的是用{}包裹的一个对象,以键值对的形式存在
导出的方式不同,导入的方式也就不同, export default解构以后就是export
所以建议同一个项目下使用同一的导入导出方式,方便开发
# ES6 模块与 CommonJS 模块的差异
它们有两个重大差异:
1:CommonJS 模块输出的是一个值的拷贝,ES6 模块输出的是值的引用。
2:CommonJS 模块是运行时加载,ES6 模块是编译时输出接口。
第二个差异是因为 CommonJS 加载的是一个对象(即module.exports属性),该对象只有在脚本运行完才会生成。而 ES6 模块不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
**CommonJS模块的加载机制是,输入的是被输出的值的拷贝。也就是说,一旦输出一个值,模块内部的变化就影响不到这个值;**这是因为counter是一个原始类型的值,会被缓存。除非写成一个函数,才能得到内部变动后的值
ES6 模块的运行机制与 CommonJS 不一样。ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
// 另外,export语句输出的接口,与其对应的值是动态绑定关系; 即通过该接口,可以取到模块内部实时的值
export var a = 1;
setTimeout(() => a = 2, 1000); // 1s之后a变为2
2
3
# 示例
/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {return a + b;};
export { basicNum, add };// 导出多个
//注意:export命令规定的是对外的接口,必须与模块内部的变量建立一一对应关系
export var m = 1; // 等价于 var m = 1;export { m }// 导出一个
var a = 1;export a;// 报错,因为通过变量a,直接输出的是1,1只是一个值,不是接口
export const student = {// 导出对象
name: 'Megan',
age: 18
}
export function add(a, b) {// 导出函数
return a + b;
}
// 使用as关键字重命名: 导出接口别名,在导出时重命名,导入时也可以;
export { person as boy }
import { year as y } from './lib';// 将输入的变量重命名
import * as lib from './lib'; //整体加载模块 全局总重命名
/** 引用模块 **/
import { basicNum, add } from './math';
function test(ele) {
ele.textContent = add(99 + basicNum);
}
//一个文件即模块中只能存在一个export default语句,导出一个当前模块的默认对外接口
//使用import命令的时候,为了给用户提供方便,让他们不用阅读文档就能加载模块,就要用到export default命令,为模块指定默认输出。
export default var i = 0;
// export-default.js
export default function () {
console.log('foo');
}
// import-default.js
import customName from './export-default';
customName(); // 'foo'
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
# UMD【要点】
UMD并不能说是一种模块标准,不如说它是一组模块形式的集合更准确。UMD的全称是Universal Module Definition,也就是通用模块标准。它的目标是使一个模块能运行在各种环境下,不论是CommonJS、AMD,还是非模块化的环境(当时ES6 Module还未被提出)
(function(root, factory) {
if (typeof define === 'function' && define.amd) {// AMD
define(['jquery'], factory);
} else if (typeof exports === 'object') {// Node, CommonJS之类的
module.exports = factory(require('jquery'));
} else {// 浏览器全局变量(root 即 window)
root.returnExports = factory(root.jQuery);
}
}(this, function($) {
function myFunc() {};// 方法
return myFunc;// 暴露公共方法
}));
;(function(){
function MyModule() {
// ...
}
var moduleName = MyModule;
if (typeof module !== 'undefined' && typeof exports === 'object') {
module.exports = moduleName;
} else if (typeof define === 'function' && (define.amd || define.cmd)) {
define(function() { return moduleName; });
} else {
this.moduleName = moduleName;
}
}).call(function() {
return this || (typeof window !== 'undefined' ? window : global);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
# 引入UMD
# babel中设置
babel中设置modules 该参数的含义是:启用将ES6模块语法转换为另一种模块类型。将该设置为false就不会转换模块。默认为 'commonjs'. 该值可以有如下: 'amd' | 'umd' | 'systemjs' | 'commonjs' | false
我们在项目中一般会看到如下配置,设置modules: false, 如下代码配置:
"presets": [
'env',
{
'modules': false
}
]
2
3
4
5
6
这样做的目的是:
- 以前我们需要使用babel来将ES6的模块语法转换为AMD, CommonJS,UMD之类的模块化标准语法,
- 但是现在webpack都帮我做了这件事了,所以我们不需要babel来做
- 因此需要在babel配置项中设置modules为false,因为它默认值是commonjs, 否则的话,会产生冲突。
# webpack设置
umd方式,他支持浏览器端和node端(require方式),webpack同样也是支持该打包方式的。 首先修改webpack打包脚本,在webpack.config.js中,增加output下的参数:
{
libraryExport: "default",
library: "Test",
libraryTarget: "umd"
}
2
3
4
5
告诉webpack导出的方式和导出库名,详细见官方文档output (opens new window)和library (opens new window)。
外部 library 可能是以下任何一种形式:
- root:可以通过一个全局变量访问 library(例如,通过 script 标签)。
- commonjs:可以将 library 作为一个 CommonJS 模块访问。
- commonjs2:和上面的类似,但导出的是
module.exports.default
. - amd:类似于
commonjs
,但使用 AMD 模块系统。
externals : {
lodash : {
commonjs: "lodash",
amd: "lodash",
root: "_" // 指向全局变量
}
}
2
3
4
5
6
7
# rollup的设置
import babel from "rollup-plugin-babel";
import serve from "rollup-plugin-serve";
export default {
input: "./src/index.js",
output: {
format: "umd", // amd commonjs规范 默认将打包后的结果挂载到window上
file: "dist/vue.js", // 打包出的vue.js 文件 new Vue
name: "Vue", // 打包后的全局变量的名字
sourcemap: true,
},
plugins: [
// 解析es6 -》 es5
babel({
exclude: "node_modules/**", // 排除文件的操作 glob
}),
// 开启本地服务
serve({
open: true,
openPage: "/public/index.html",
port: 3000,
contentBase: "",
}),
],
};
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25