前端模块化:CommonJS,AMD,CMD,ES6
# 0.前言
本篇博客介绍的是前端模块化语法规范,通过模块化开发的方式可以极大的提高代码的复用率,以及结构化项目的管理。在模块化语法规范中,一个文件就是一个模块,每个模块拥有各自的作用域(模块作用域),只向外暴露特定的变量和函数。
目前流行的 js
模块化规范有 CommonJS
、AMD
、CMD
以及 ES6
的模块系统。
# 1.CommonJS (Node.js)
Node.js 是 commonJS 规范的主要实践者,它有四个重要的环境变量为模块化的实现提供支持:module
、exports
、require
、global
。实际使用时,用 module.exports 定义当前模块对外输出的接口(不推荐直接用 exports
),用 require
加载模块。
// 定义模块math.js
var basicNum = 0;
function add(a, b) {
return a + b;
}
// 导出模块(export带s)
module.exports = { //在这里写上需要向外暴露的函数、变量
add: add,
basicNum: basicNum
}
// 或者 module.export.add = add
// 导入模块时可省略.js
var math = require('./math');
math.add(2, 5);
// 当导入的是Node的核心模块时,不需要带路径
var http = require('http');
http.createService(...).listen(3000);
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
commonJS 的特点:同步
、服务器端
- 在Node服务器端,模块文件都是存在本地磁盘,因此 commonJS 使用的是
同步
的方式加载模块。 - 在浏览器端,限于网络原因,更合理的方式使用异步加载。因此commonJS更使用于服务端的加载。
# 2.AMD (require.js)
AMD规范采用异步
方式加载模块,模块的加载不影响它后面语句的运行。
此规范暂时用不到,省略,用时可查:https://juejin.cn/post/6844903576309858318 (opens new window)
# 3.CMD (sea.js)
CMD是另一种js模块化方案,它与AMD很类似,不同点在于:AMD 推崇依赖前置、提前执行,CMD推崇依赖就近、延迟执行。此规范其实是在sea.js推广过程中产生的。
此规范暂时用不到,省略,用时可查:https://juejin.cn/post/6844903576309858318 (opens new window)
# 4.ES6 (浏览器和服务器通用模块解决方案)
ES6 在语言标准的层面上,实现了模块功能,而且实现得相当简单,旨在成为浏览器和服务器通用的模块解决方案。其模块功能主要由两个命令构成:export
和import
。export
命令用于规定模块的对外接口,import
命令用于输入其他模块提供的功能。
/** 定义模块 math.js **/
var basicNum = 0;
var add = function (a, b) {
return a + b;
};
export { basicNum, add };
// 或者直接 export let num = 1
// 或者直接 export function setNUm(){}
/** 引用模块(import作为关键字) **/
import { basicNum, add } from './math';
function test(ele) {
ele.textContent = add(99 + basicNum);
}
2
3
4
5
6
7
8
9
10
11
12
13
14
除上例所示,使用import
命令时,直接导出的已知的变量名或函数名。使用export default
命令,可以 module
的方式导出,对应的import
语句不需要使用大括号。
/** 导出模块(default) **/
export default { basicNum, add }; // 就多加了 default
/** 导入模块(default) **/
import math from './math';
console.log(math.add(1,2)); // 读取函数
console.log(math.basicNum); // 读取变量名
function test(ele) {
ele.textContent = math.add(99 + math.basicNum);
}
2
3
4
5
6
7
8
9
10
11
# ✡️ 5.重点:ES6 和 CommonJS 模块的差异
讨论 Node.js 加载 ES6 模块之前,必须了解 ES6 模块与 CommonJS 模块完全不同。
它们有三个重大差异:
CommonJS 模块输出的是一个值的拷贝;ES6 模块输出的是值的引用。
// es6.mjs (注意这里是mjs,因为当在node中使用es6必须要告诉node) let num = 1; function setNum(newNum) { num = newNum } export { num, setNum } // es6导出语法 module.export.num = num // commonJs导出语法 module.export.setNum = setNum // commonJs导出语法 // main.mjs (同理,在导入时也需要告诉node当前的js是ES6 module) import {num,setNum} from './es6.mjs' console.log(num) // 打印:1 setNum(2) console.log(num) // 打印:2 (说明ES6是值索引,因为它修改了内部的num变量) // commonJS 会缓存 _module.num let _module = require('./demo01-es6-and-CommonJS-cmj.js') console.log(_module.num) // 打印:1 _module.setNum(2) console.log(_module.num) // 打印:1 (_module.num 是一个原始类型的值,会被缓存)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20结论:
ES6 模块不会缓存运行结果,而是动态地去被加载的模块取值,并且变量总是绑定其所在的模块(就好像
module.mjs
的文件被直接写在了main.mjs
文件中,专业的说法:JS 引擎对脚本静态分析的时候,遇到模块加载命令import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。换句话说,ES6 的import
有点像 Unix 系统的“符号连接”,原始值变了,import
加载的值也会跟着变。因此,ES6 模块是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。)CommonJS 则是把原先的module又给拷贝了一份(且是一种浅拷贝)。
CommonJS若也想达到ES6的值引用效果,可以将num变为一个函数(取值器函数getter)
let num = 1; module.exports = { get num(){ return num } setNum: setNum }
1
2
3
4
5
6
7CommonJS 模块是运行时加载;ES6 模块是编译时输出接口。
这一特性使得ES6模块支持
静态代码分析
,如果代码写错,可以直接提示问题;而CommonJS 模块的输出接口是module.exports
,是一个对象,无法被静态分析,所以只能整体加载。CommonJS 模块的
require()
是同步加载模块;ES6 模块的import
命令是异步加载,有一个独立的模块依赖的解析阶段。这一特性决定了:在
cjs
文件中无法使用require()
函数加载mjs
模块,只能使用import()
函数。
# 6.Node中使用ES6模块
JavaScript 现在有两种模块。一种是 ES6 模块,简称 ESM;另一种是 CommonJS 模块,简称 CJS。
CommonJS 模块是 Node.js 专用的,与 ES6 模块不兼容。语法上面,两者最明显的差异是,CommonJS 模块使用require()
和module.exports
,ES6 模块使用import
和export
。
有两种方式:
- 更改后缀名:
.mjs
文件总是以 ES6 模块加载,.cjs
文件总是以 CommonJS 模块加载 - 若
.js
文件,可在项目的package.json
文件中,指定type
字段为module
。
# 7.同时兼容两种模块的方式
以下是.cjs
即为CommonJS模块中使用ES6模块,以import
关键字的方式会报错。
// commonJS 会缓存 _module.num
let _module = require('./demo01-es6-and-CommonJS-cmj.cjs') //cjs加载cjs合理
import './demo01-es6-and-CommonJS-es6.mjs' // 错误
import('./demo01-es6-and-CommonJS-es6.mjs').then((module)=>{
console.log('es6',module.num) // 1
module.setNum(3)
console.log('es6',module.num) // 3
})
console.log(_module.num) // 1
_module.setNum(2)
console.log(_module.num) // 2
2
3
4
5
6
7
8
9
10
11
第二种方式是修改package.json
的exports
字段:
"exports":{
"require": "./index.js",
"import": "./esm/wrapper.js"
}
2
3
4
# 8.动态 import () (opens new window)
ES6 模块在编译时就会静态分析,优先于模块内的其他内容执行,所以导致了我们无法写出像下面这样的代码:
if(some condition) {
import a from './a';
}else {
import b from './b';
}
2
3
4
5
因为编译时静态分析,导致了我们无法在条件语句或者拼接字符串模块,因为这些都是需要在运行时才能确定的结果在 ES6 模块是不被允许的,所以 动态引入import()
应运而生。
动态 import
的基本使用上面已经用过了,这里主要是使用Promise.all
进行并行异步加载。
Promise.all([
import('./a.js'),
import('./b.js'),
import('./c.js'),
]).then(([a, {default: b}, {c}]) => {
console.log('a.js is loaded dynamically');
console.log('b.js is loaded dynamically');
console.log('c.js is loaded dynamically');
});
2
3
4
5
6
7
8
9
还有 Promise.race 方法,它检查哪个 Promise 被首先 resolved 或 reject。我们可以使用import()来检查哪个CDN速度更快:
const CDNs = [
{
name: 'jQuery.com',
url: 'https://code.jquery.com/jquery-3.1.1.min.js'
},
{
name: 'googleapis.com',
url: 'https://ajax.googleapis.com/ajax/libs/jquery/3.1.1/jquery.min.js'
}
];
console.log(`------`);
console.log(`jQuery is: ${window.jQuery}`);
Promise.race([
import(CDNs[0].url).then(()=>console.log(CDNs[0].name, 'loaded')),
import(CDNs[1].url).then(()=>console.log(CDNs[1].name, 'loaded'))
]).then(()=> {
console.log(`jQuery version: ${window.jQuery.fn.jquery}`);
});
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
当然,如果你觉得这样写还不够优雅,也可以结合 async/await 语法糖来使用。
async function main() {
const myModule = await import('./myModule.js');
const {export1, export2} = await import('./myModule.js');
const [module1, module2, module3] =
await Promise.all([
import('./module1.js'),
import('./module2.js'),
import('./module3.js'),
]);
}
2
3
4
5
6
7
8
9
10
动态 import() 为我们提供了以异步方式使用 ES 模块的额外功能。 根据我们的需求动态或有条件地加载它们,这使我们能够更快,更好地创建更多优势应用程序。
# 本篇blog未理解知识点
- CommonJS 和 ES6 模块是如何解决循环依赖的。
- 看了方应杭的webpack,才发现import的支持分为两种,一是以
type="module"
的方式,同时运行多个文件的方式。但是这种方式在浏览器
中是不可靠的,因为需要下载的文件太多,一般是通过静态分析的方式,将多个文件合并成一个文件,并且转译为require