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之后被请求的。

前端模块化-LMLPHP

此时再看看我们的页面,发现多了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文件加载到运行环境中。

我们再来看看输出:

前端模块化-LMLPHP

可以很明显的发现,依赖模块的加载没有阻塞后面代码的执行,并且模块会在使用前加载好

而且模块加载是异步的。

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会帮我们把需要的依赖模块动态的加载进来,这里就不截图了。

同样的,我们先看输出结果:

前端模块化-LMLPHP

有没有发现,虽然写法上依赖就近,但实际上依赖的模块还是被前置加载了

最新版本中模块加载也是异步的了。

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,会阻塞后面的代码执行。

前端模块化-LMLPHP

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 };

输出结果:

前端模块化-LMLPHP

可以发现依赖模块还是会被提前加载,再看看第二种方式:

<!-- 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");

结果如下:

前端模块化-LMLPHP

可以发现,模块是异步加载进来的。

6. Webpack中的模块化

可能有人有疑问,我们在Webpack中好像既可以使用require和module.exports的CommonJS语法,也可以使用export和import的ES6语法。那Webpack又是怎么处理的?

而且,前面列出的几个模块化方案中基本都是一个js文件作为一个模块,但是好像Webpack没有输出那么多的文件啊?

其实Webpack有自己的模块化实现,兼容了这二种标准,而且还有一个编译的过程将多文件bundle到一起。详细的可以参考:https://segmentfault.com/a/1190000010349749

其核心还是模块化设计的几个要点:

  • 模块加载
  • 模块隔离
  • 模块缓存控制
  • 模块依赖维护

总结

其实从个人观点来看,前端的模块化经历了:

  1. 野蛮发展阶段:每个团队和公司有自己的方案,好苦逼
  2. 到AMD/CMD阶段:行业领头人推广,大家围观
  3. 再到原生ES6支持阶段:建立浏览器标准,大家围观
  4. 和编译支持阶段:在前端越来越复杂,引入预编译模式,大家膜拜

这么几个以上的阶段后,现阶段基本比较稳定在预编译模式,结合预编译工具的其他功能和带来的便利,前端模块化不再是一个主要关注的技术点。取而代之的是更加关注:代码分割、按需加载、Tree Shaking、模块合并、模块缓存等等问题。

01-08 14:59