1. 没有模块化的时代
在JS没有模块化标准的时代,如果存在以下依赖关系:
main.js -> b.js -> a.js
那么我们必须把js文件的顺序按照模块的依赖关系顺序放到页面中(简单的举例,不考虑循环依赖等复杂情况)
<!-- NoModule.html -->
<head>
<link rel="icon" href="data:image/ico;base64,aWNv">
<script src="./a.js"></script>
<script src="./b.js"></script>
<script src="./main.js"></script>
</head>
<body></body>
我们需要提前加载好所有的依赖。
//main.js
(function(){
moduleB.logb();
})()
//b.js
var moduleB = (function () {
function logb() {
moduleA.loga();
console.log("logb");
}
return { logb: logb }
})()
//a.js
var moduleA = (function () {
function loga() {
console.log("loga");
}
return { loga: loga }
})()
//输出结果
//loga
//logb
这种方式相当简单粗暴啊,当然造成的问题也很多:依赖关系无法显式维护,全局命名空间污染冲突等等
2. AMD
首先:AMD是一种规范,全称Asynchronous Module Definition 异步模块定义
其次:RequireJS(2.3.6)是AMD的一个实现,我们可以使用RequireJS来实际看看这种规范到底怎么回事
依赖关系:main.js -> b.js -> a.js
我们来看看js文件的在页面中的结构:
<!-- AMD.html -->
<head>
<link rel="icon" href="data:image/ico;base64,aWNv">
<script src="./require.js"></script>
<script src="./main.js"></script>
</head>
<body></body>
然后是各个文件的代码:
//main.js
console.log("load main.js");
require(['./b.js'], function (b) {
console.log("call b.logb()");
b.logb();
return {};
})
console.log("end main.js");
//b.js
define(['./a.js'], function (a) {
console.log("load b.js");
function sleep(d) {
for (var t = Date.now(); Date.now() - t <= d;);
}
function logb() {
a.loga();
//注意,这里暂停了5秒
var startTime = new Date().getMinutes() + ":" + new Date().getSeconds();
console.log(startTime);
sleep(5000);
var endTime = new Date().getMinutes() + ":" + new Date().getSeconds();
console.log(endTime);
console.log("logb");
}
return {
logb: logb
};
})
//a.js
define([], function () {
console.log("load a.js")
function loga() {
console.log("loga");
}
return {
loga: loga
};
})
从上面可以看出来,我们初始页面并不需要引入依赖的模块js文件。Chrome中打开AMD.html,我们可以观察到网络时序图如下,可以明显的发现b.js和a.js是在main.js之后被请求的。
此时再看看我们的页面,发现多了2个script标签把b.js和a.js给引入进来了。
<!-- AMD.html -->
<html>
<head>
<link rel="icon" href="data:image/ico;base64,aWNv">
<script src="./require.js"></script>
<script src="./main.js"></script>
<script type="text/javascript" charset="utf-8" async="" data-requirecontext="_" data-requiremodule="b.js"
src="b.js"></script>
<script type="text/javascript" charset="utf-8" async="" data-requirecontext="_" data-requiremodule="a.js"
src="a.js"></script>
</head>
<body></body>
</html>
这就是RequireJS帮我们做的事情了,根据我们指定的依赖,在代码运行时动态的将依赖的模块js文件加载到运行环境中。
我们再来看看输出:
可以很明显的发现,依赖模块的加载没有阻塞后面代码的执行,并且模块会在使用前加载好。
而且模块加载是异步的。
3. CMD
首先:CMD是一种规范,全称Common Module Definition 通用模块定义
其次:Sea.js(3.0.0)是CMD的一个实现,我们可以使用Sea.js来实际看看这种规范到底怎么回事
<!-- CMD.html -->
<head>
<link rel="icon" href="data:image/ico;base64,aWNv">
<script src="./sea.js"></script>
<script>
seajs.use("./main.js");
</script>
</head>
<body></body>
//main.js
console.log("load main.js");
define(function (require, exports, module) {
console.log("call b.logb()");
var b = require('./b.js');
b.logb();
});
console.log("end main.js");
//b.js
console.log("load b.js");
define(function (require, exports, module) {
function sleep(d) {
for (var t = Date.now(); Date.now() - t <= d;);
}
function logb() {
var a = require('./a.js');
a.loga();
//注意,这里暂停了5秒
var startTime = new Date().getMinutes() + ":" + new Date().getSeconds();
console.log(startTime);
sleep(5000);
var endTime = new Date().getMinutes() + ":" + new Date().getSeconds();
console.log(endTime);
console.log("logb");
}
exports.logb = logb;
})
//a.js
console.log("load a.js");
define(function (require, exports, module) {
function loga() {
console.log("loga");
}
exports.loga = loga;
})
同样的,sea.js会帮我们把需要的依赖模块动态的加载进来,这里就不截图了。
同样的,我们先看输出结果:
有没有发现,虽然写法上依赖就近,但实际上依赖的模块还是被前置加载了。
最新版本中模块加载也是异步的了。
4. CommonJS
NodeJS运行环境下的模块规范
//main.js
console.log("load main.js");
const a = require('./a.js');
const b = require('./b.js');
a.loga();
b.logb();
console.log("end main.js");
//a.js
console.log("load a.js");
function loga() {
console.log("loga");
}
module.exports.loga = loga;
//b.js
console.log("load b.js");
function sleep(d) {
for (var t = Date.now(); Date.now() - t <= d;);
}
function logb() {
//注意,这里暂停了5秒
var startTime = new Date().getMinutes() + ":" + new Date().getSeconds();
console.log(startTime);
sleep(5000);
var endTime = new Date().getMinutes() + ":" + new Date().getSeconds();
console.log(endTime);
console.log("logb");
}
exports.logb = logb;
不同于最新的requireJS和sea.js,CommonJS在node环境中是同步IO,会阻塞后面的代码执行。
5. ES6 模块
ES6也有自己的模块化方案,现在我们即使不使用AMD或者CMD的js实现库,也能在浏览器中直接使用模块化的方案了。浏览器的支持率可以参考: https://caniuse.com/?search=import
<!-- ES6.html -->
<head>
<link rel="icon" href="data:image/ico;base64,aWNv">
<script type="module" src="./main.js"></script>
<!-- <script src="./main2.js"></script> -->
</head>
<body>ES6.html</body>
ES6支持二种方式的模块使用,第一种是在script上使用type=module
//main.js
console.log("load main.js");
import { loga } from './a.js';
import logb from './b.js';
loga();
logb();
console.log("end main.js");
//a.js
console.log("load a.js");
export function loga() {
console.log("loga");
}
//b.js
console.log("load b.js");
function sleep(d) {
for (var t = Date.now(); Date.now() - t <= d;);
}
function logb() {
//注意,这里暂停了5秒
var startTime = new Date().getMinutes() + ":" + new Date().getSeconds();
console.log(startTime);
sleep(5000);
var endTime = new Date().getMinutes() + ":" + new Date().getSeconds();
console.log(endTime);
console.log("logb");
}
export default { logb };
输出结果:
可以发现依赖模块还是会被提前加载,再看看第二种方式:
<!-- ES6.html -->
<head>
<link rel="icon" href="data:image/ico;base64,aWNv">
<!-- <script type="module" src="./main.js"></script> -->
<script src="./main2.js"></script>
</head>
<body>ES6.html</body>
console.log("load main.js");
import('./a.js').then(a => {
a.loga();
})
import('./b.js').then(b => {
console.log(b.default());
})
console.log("end main.js");
结果如下:
可以发现,模块是异步加载进来的。
6. Webpack中的模块化
可能有人有疑问,我们在Webpack中好像既可以使用require和module.exports的CommonJS语法,也可以使用export和import的ES6语法。那Webpack又是怎么处理的?
而且,前面列出的几个模块化方案中基本都是一个js文件作为一个模块,但是好像Webpack没有输出那么多的文件啊?
其实Webpack有自己的模块化实现,兼容了这二种标准,而且还有一个编译的过程将多文件bundle到一起。详细的可以参考:https://segmentfault.com/a/1190000010349749
其核心还是模块化设计的几个要点:
- 模块加载
- 模块隔离
- 模块缓存控制
- 模块依赖维护
总结
其实从个人观点来看,前端的模块化经历了:
- 野蛮发展阶段:每个团队和公司有自己的方案,好苦逼
- 到AMD/CMD阶段:行业领头人推广,大家围观
- 再到原生ES6支持阶段:建立浏览器标准,大家围观
- 和编译支持阶段:在前端越来越复杂,引入预编译模式,大家膜拜
这么几个以上的阶段后,现阶段基本比较稳定在预编译模式,结合预编译工具的其他功能和带来的便利,前端模块化不再是一个主要关注的技术点。取而代之的是更加关注:代码分割、按需加载、Tree Shaking、模块合并、模块缓存等等问题。