模块化
模块化是指自上而下把一个复杂问题(功能)划分成若干模块的过程,在编程中就是指通过某种规则对程序(代码)进行分割、组织、打包,每个模块完成一个特定的子功能,再把所有的模块按照某种规则进行组装,合并成一个整体,最终完成整个系统的所有功能。
从基于 Node.js 的服务端 CommonJS
模块化,到前端基于浏览器的 AMD
、CMD
模块化,再到 ECMAScript 6
开始原生内置的模块化, JavaScript 的模块化方案和系统日趋成熟。
模块化历程
- CommonJS
- AMD
- UMD
- ESM
无论是那种模块化规范,都着重于保证模块独立性 的同时又能很好的 与其它模块进行交互:
- 如何定义一个模块与模块内部私有作用域
- 通过何种方式导出模块内部数据
- 通过何种方式导入其它外部模块数据
基于服务端、桌面端的模块化
CommonJS
在早期,对于运行在浏览器端的 JavaScript 代码,模块化的需求并不那么的强烈,反而是偏向 服务端、桌面端 的应用对模块化有迫切的需求(相对来说,服务端、桌面端程序的代码和需求要复杂一些)。CommonJS
规范就是一套偏向服务端的模块化规范,它为非浏览器端的模块化实现制定了一些的方案和标准,NodeJS 就采用了这个规范。
因为
NodeJS
就是CommonJS
的实现,所以使用node
就行,不用引入其他包。
- 独立模块作用域
一个文件就是模块,拥有独立的作用域。
- 导出模块内部数据
通过 module.exports
或 exports
对象导出模块内部数据:
// ./a.js
let a = 1;
let b = 2;
module.exports = {
x: a,
y: b
}
// or
exports.x = a;
exports.y = b;
复制代码
exports
和 module.export
的区别
exports
:对于本身来讲是一个变量(对象),它不是module
的引用,它是{}
的引用,它指向module.exports
的{}
模块。 只能使用.
语法 向外暴露变量。
module.exports
:module
是一个变量,指向一块内存。exports
是module
中的一个属性,存储在内存中,exports
属性指向{}
模块。 既可以使用.
语法,也可以使用=
直接赋值。
require
引用模块后,返回给调用者的是module.exports
而不是exports
。
- 导入外部模块数据
require
通过 require
函数导入外部模块数据,必须加 ./
路径,不加的话会去 node_modules
文件找。
导入自定义的模块时,参数包含路径,可省略后缀:
// ./b.js
let a = require('./a'); // 导入自定义模块,可省略后缀
const Koa = require('koa'); // 不加 `./` ,导入 node_modules 内的模块
const fs = require('fs'); // 引用核心模块,不需要带路径
a.x;
a.y;
复制代码
基于浏览器的模块化
AMD
CommonJS
用同步的方式加载模块(基于文件系统)。在服务端,模块文件都存放在本地磁盘,读取非常快,所以这样做不会有问题。
但是在浏览器端,限于网络原因,更合理的方案是使用异步加载,所以另外定义了适用于浏览器端的规范:AMD(Asynchronous Module Definition)
,github。
AMD
是 requireJS
在推广过程中对模块定义的规范化产出,它是一个概念,RequireJS
是对这个概念的实现。
requireJS
requireJS ,依赖前置、异步定义。
- 引用
requireJS
<!-- ./index.html -->
<!-- cdn -->
<script src="https://cdn.bootcss.com/require.js/2.3.6/require.min.js" data-main="./js/a.js"></script>
<!-- or -->
<!-- 本地引入 -->
<script src="./libs/require.min.js" data-main="./js/a.js"></script>
复制代码
- 独立模块作用域
define
通过一个 define
方法来定义一个模块,在该方法内部模拟模块独立作用域:
// ./js/b.js
define(function() {
// 模块内部代码
})
复制代码
- 导出模块内部数据
return
通过 return
导出模块内部数据:
// ./js/b.js
define(function() {
// 模块内部代码
let a = 1;
let b = 2;
// 通过 return 导出
return {
x: a,
y: b
}
})
复制代码
- 导入外部模块数据
如果我们定义的模块本身依赖其他模块,那就需要将它们放在 []
中作为 define()
的第一参数。通过前置依赖列表导入外部模块数据。
// ./js/a.js
// 定义一个模块,并导入 ./b 模块
define(['./b'], function(b) {
console.log('define', b);
})
// 引用 b 模块
require(['./b'], function(b) {
console.log('require', b);
});
复制代码
requireJS
的 config 配置
首先我们需要引入 require.js 文件和一个入口文件 main.js。main.js 中配置 require.config()
并规定项目中用到的基础模块。
/** 网页中引入 requireJS 及 main.js **/
<script src="./libs/require.min.js" data-main="./js/main.js"></script>
/** main.js 入口文件/主模块 **/
// 首先用 config() 指定各模块路径和引用名
require.config({
baseUrl: "js/lib",
paths: {
"jquery": "jquery.min", // 实际路径为 js/lib/jquery.min.js
}
});
复制代码
引用模块的时候,我们将模块名放在 []
中作为 reqiure()
的第一参数
// ./js/math.js
// 定义 math.js 模块
define(function () {
var basicNum = 0;
var add = function (x, y) {
return x + y;
};
return {
add: add,
basicNum :basicNum
};
});
// 引用模块,将模块放在[]内
require(['jquery', 'math'],function($, math){
var sum = math.add(10,20);
$("#sum").html(sum);
});
复制代码
requireJS
的 CommonJS
风格
require.js
也支持 CommonJS
风格的语法:
- 导出模块内部数据
// ./js/b.js
define(function(require, exports, module) {
let a = 1;
let b = 2;
// 通过 module.exports 对象导出
// exports 与 module.exports 是一样的
module.exports = {
x: a,
y: b
}
})
复制代码
- 导入外部模块数据
// ./js/a.js
define(function(require, exports, module) {
// require 与 前置列表 相似
let b = require('./b')
console.log(b);
})
复制代码
CMD
AMD的实现者require.js在申明依赖的模块时,会在第一时间加载并执行模块内的代码:
define(["a", "b", "c", "d", "e", "f"], function(a, b, c, d, e, f) {
// 等于在最前面声明并初始化了要用到的所有模块
if (false) {
// 即便没用到某个模块 b,但 b 还是提前执行了。**这就CMD要优化的地方**
b.foo()
}
});
复制代码
CMD
是另一种 js 模块化方案,它与 AMD
很类似,不同点在于:AMD
推崇依赖前置、提前执行,CMD
推崇依赖就近、延迟执行。
通过 define()
定义,没有依赖前置,通过 require
导入。
define(function(require, exports, module) {
var a = require('./a'); // 在需要时申明
a.doSomething();
if (false) {
var b = require('./b');
b.doSomething();
}
});
复制代码
CMD
是 SeaJS
在推广过程中对模块定义的规范化产出,是一个同步模块定义。SeaJS
是 CMD
概念的一个实现, SeaJS
是淘宝团队提供的一个模块开发的 js 框架。
SeaJS
/** sea.js **/
// 定义模块 math.js
define(function(require, exports, module) {
var $ = require('jquery.js');
var add = function(a,b){
return a+b;
}
exports.add = add;
});
// 加载模块
seajs.use(['math.js'], function(math){
var sum = math.add(1+2);
});
复制代码
AMD
和 CMD
的优缺点
AMD
和 CMD
的优缺点,一个的优点就是另一个的缺点。
-
AMD
- 优点:加载快速,尤其遇到多个大文件,因为并行解析,所以同一时间可以解析多个文件。
- 缺点:并行加载,异步处理,加载顺序不一定,可能会造成一些困扰,甚至为程序埋下大坑。
-
CMD
- 优点:只有在使用的时候才会解析执行js文件,执行顺序在代码中是有体现的,是可控的。
- 缺点:使用时执行,没法利用空闲时间,执行等待时间会叠加。
UMD
严格来说,UMD
并不属于一套模块规范,它主要用来处理 CommonJS
、AMD
、CMD
的差异兼容,使模块代码能在前面不同的模块环境下都能正常运行。
随着 Node.js 的流行,前端和后端都可以基于 JavaScript 来进行开发,这个时候或多或少的会出现前后端使用相同代码的可能,特别是一些不依赖宿主环境(浏览器、服务器)的偏低层的代码。我们能实现一套代码多端适用(同构),其中在不同的模块化标准下使用也是需要解决的问题,UMD
就是一种解决方式。
// root 解决不同环境下全局变量不同的问题,如浏览器是 window,node.js 是 global
(function (root, factory) {
if (typeof module === "object" && typeof module.exports === "object") {
// Node, CommonJS-like
module.exports = factory();
}
else if (typeof define === "function" && define.amd) {
// AMD 模块环境下
define(factory);
} else {
// 不使用任何模块系统,直接挂载到全局
root.kkb = factory();
}
}(this, function () {
let a = 1;
let b = 2;
// 模块导出数据
return {
x: a,
y: b
}
}));
复制代码
JavaScript 原生模块化
ESM
从 ECMAScript 6
开始,JavaScript 原生引入了模块概念,而且现在主流浏览器也都有了很好的支持,同时在 Node.js 也有了支持,所以未来基于 JavaScript 的程序无论是在前端浏览器还是在后端 Node.js 中,都会逐渐的被统一。
- 独立模块作用域
一个文件就是模块,拥有独立的作用域,且导出的模块都自动处于 严格模式
下,即:'use strict'
。
script
标签需要声明 type="module"
:
<script type="module"></script>
复制代码
- 导出模块内部数据
使用 export
语句导出模块内部数据。
export
可以导出多个;export default
只能导出一个。
export
导出的,导入时命名要保持一致;export default
导出的,导入时命名可以自定义。
// 1. 直接导出
export let name1 = …, name2 = …, …, nameN;
export function FunctionName(){...}
export class ClassName {...}
// 2. 导出列表
export { name1, name2, …, nameN };
// 3. 重命名导出
// 关键字 "as" 重命名。as 前是原名,as 后是新名。注意不是解构赋值哦。
export { variable1 as name1, variable2 as name2, …, variableN as nameN };
// 4. 默认导出,只能导出一个
export default expression;
export default function (…) { … }
export default function name1(…) { … }
// 5. 模块重定向导出 ???
export * from …; // 通配符 "*" 导出
export { name1, name2, …, nameN } from …;
export { import1 as name1, import2 as name2, …, nameN } from …;
export { default } from …;
复制代码
-
导入外部模块数据
- 静态导入
import
使用
import
语句导入模块,这种方式称为:静态导入
。静态导入方式不支持延迟加载,
import
必须在模块的最开始。// 直接导入,里面的内容直接用 import "module-name"; // export default 导出的,导入时命名可以自定义 import defaultExport from "module-name"; // 通配符 "*" 导入 import * from "module-name"; import * as name from "module-name"; // export 导出的,导入时命名要保持一致 import { export1 } from "module-name"; import { export1 as alias } from "module-name"; import { export1 , export2 } from "module-name"; import { export1 , export2 as alias2 , [...] } from "module-name"; // 前面是默认导出的,后面是其余导出的 import defaultExport, { export1 , export2 } from "module-name"; import defaultExport, * as name from "module-name"; 复制代码
函数内的静态导入,
import
也要在函数内部最开始的位置document.onclick = function () { // import 必须放置在当前模块的最开始加载 import m from './m.js' console.log(m); } 复制代码
- 动态导入/按需导入
import()
此外,还有一个类似函数的动态
import()
,它不需要依赖type="module"
的script
标签。关键字
import
可以像调用函数一样来动态的导入模块。以这种方式调用,将返回一个promise
。let res = import("./m.js"); console.log(res); import('./m.js').then(m => { console.log(m) }); // 也支持 await let m = await import('./m.js'); 复制代码
通过
import()
方法导入返回的数据会被包装在一个对象中,即使是default
也是如此。 - 静态导入
导出导入对应使用
/* 1. 直接导入 */
// ./a.js 导出
let a = 10;
// ./index.html 导入
import './a.js';
console.log("a"); // 10
/* 2. 导出列表 */
// ./a.js 导出
export let a = 10;
export let b = 20;
// index.html 导入
// export 导出的,命名要保持一致,可以重命名。
import {a as c, b} from './a.js';
console.log(b); // 20
console.log(c); // 10
/* 3. 默认导出 */
// ./a.js 导出
// export 导出:导出多个
export let a = 10;
export let b = 20;
// export default :导出一个
let obj = {
fn(){
console.log("fn");
}
}
export default obj;
// 等同于
// export {obj as default};
// ./index.html 导入
// export default 导出的,命名可以自定义
import myfn, {a, b} from './a.js';
myfn.fn(); // 'fn'
/* 4. 通配符 "*" 导入 */
// ./a.js 导出
export let obj = {
x: 1
}
// ./index.html 导入
import * as m1 from './a.js'
console.log(m1.obj.x) // 1
复制代码
ESM
与 CommonJS
的差异
-
CommonJS
输出的是一个值的拷贝,ESM
输出的是值的引用-
CommonJS
输出的是值的拷贝,也就是说,一旦输出一个值,模块内部的变化就影响不到这个值。 -
ESM
的运行机制与CommonJS
不一样。JS 引擎对脚本静态分析的时候,遇到模块加载命令import
,就会生成一个只读引用。等到脚本真正执行时,再根据这个只读引用,到被加载的那个模块里面去取值。因此,ESM
是动态引用,并且不会缓存值,模块里面的变量绑定其所在的模块。
-
-
CommonJS
是运行时加载,ES6 是编译时输出接口-
运行时加载:
CommonJS
就是对象;即在输入时是先加载整个模块,生成一个对象,然后再从这个对象上面读取方法,这种加载称为“运行时加载”。 -
编译时加载:
ESM
不是对象,而是通过export
命令显式指定输出的代码,import
时采用静态命令的形式。即在import
时可以指定加载某个输出值,而不是加载整个模块,这种加载称为“编译时加载”。模块内部引用的变化,会反应在外部。
-
CommonJS
加载的是一个对象(即 module.exports
属性),该对象只有在脚本运行完才会生成。而 ESM
不是对象,它的对外接口只是一种静态定义,在代码静态解析阶段就会生成。
首先看个 CommonJS
输出拷贝的例子:
/* CommonJS */
// a.js
let a = 1;
let b = { num: 1 }
setTimeout(() => {
a = 2;
b = { num: 2 };
}, 200);
module.exports = { a, b };
// main.js
let {a, b} = require('./a');
console.log(a); // 1
console.log(b); // { num: 1 }
setTimeout(() => {
console.log(a); // 1
console.log(b); // { num: 1 }
}, 500);
复制代码
所谓输出拷贝,如果了解过 NodeJS 或者 webpack 对 CommonJS
的实现(不了解可以看这篇文章),就会知道:exports
对象是模块内外的唯一关联, CommonJS
输出的内容,就是 exports
对象的属性,模块运行结束,属性就确定了。
再看 ESM
输出的例子:
/* ESM */
// a.js
let a = 1;
let b = { num: 1 }
setTimeout(() => {
a = 2;
b = { num: 2 };
}, 200);
export { a, b };
// main.js
import {a, b} from './a';
console.log(a); // 1
console.log(b); // { num: 1 }
setTimeout(() => {
console.log(a); // 2
console.log(b); // { num: 2 }
}, 500);
复制代码
ESM
模块内部引用的变化,会反应在外部。
总结
-
AMD
/CMD
/CommonJs
是 js 模块化开发的规范,对应的实现是require.js
/sea.js
/Node.js
。 -
UMD
并不属于一套模块规范,它主要用来处理CommonJS
、AMD
、CMD
的差异兼容,使模块代码能在前面不同的模块环境下都能正常运行。 -
CommonJs
主要针对服务端,AMD
/CMD
/ESM
主要针对浏览器端。 -
AMD
推崇依赖前置、提前执行,CMD
推崇依赖就近、延迟执行。 -
CommonJs
和ESM
区别:CommonJs
模块输出的是一个值的拷贝,ESM
输出的是值的引用。
针对服务器端和针对浏览器端有什么区别?
服务器端
一般采用 同步加载文件,也就是说需要某个模块,服务器端便停下来,等待它加载再执行。
浏览器端
要保证效率,需要采用 异步加载,这就需要一个预处理,提前将所需要的模块文件并行加载好。