前端模块化是指将一个大型的前端程序分解为小的、独立的模块,每个模块都有自己的功能和接口,可以被其他模块使用。

1. IIFE 闭包

古早时期的 IIFE 模式,通过自执行函数创建闭包。数据是私有的,外部只能通过暴露的方法操作。通过传入依赖的方式来解决模块间引用的问题。这种方式确实可以实现模块化开发了,但是模块挂载在window下,代码阅读困难,无法支持大规模模块化开发。

1
2
3
4
5
6
7
function(global, otherModule) {
var data = 1
function fetchData() {}
function handleData() {}

window.myModule = {fetchData, handleData, otherApi: otherModule.api}
}(window, window.other_module)

2. ES6 的 import, export

在 ES6 之前,JavaScript 并没有原生支持模块化。

ES Module 模块化允许开发者将代码分割成小的、独立的模块,每个模块都有自己的作用域。它是基于文件的,每个文件都是一个独立的模块。在浏览器中,可以使用<script type="module">标签来加载 ESModule 模块。在 Node.js 中,可以使用 import 关键字来加载 ESModule 模块。

1
2
<!-- 在浏览器中加载ESModule模块 -->
<script type="module" src="./module.js"></script>
1
2
// 在Node.js中加载ESModule模块
import { name } from './module';

特点:

  • ES6 Module是静态的,代码发生在编译时,import 和 export 不能放在块级作用域(函数)内。
  • ES6 Module输出的是值的引用,如果一个模块修改了另一个模块导出的值,那么这个修改会影响到原始模块。代码是在模块作用域中运行,而不是在全局作用域运行。
    1
    2
    3
    4
    5
    6
    7
    8
    // module.js
    export const name = '张三';
    export function sayHello() {
    console.log('Hello');
    }

    // app.js
    import { name, sayHello } from './module';

影响:
JS模块加载、包依赖自动处理、JS自动编译,Web应用的加载性能将获得划时代的提升。因为ES6 Module,前端的构建部署发生革命性的变化——单独的构建环节将逐渐弱化乃至消失,构建所包含的实质性内容即脚本、样式等资源的编译转译等将实时化。Vite崛起,Gulp/Webpack将退出主流。

2. CommonJS(require/exports)

CommonJS 是 NodeJS 的默认模块化规范。在 CommonJS 规范中,模块的加载方式是同步的。也就是说,当一个模块被引入时,会立即执行该模块内部的代码,并将该模块导出的内容返回给引入该模块的代码。

模块可以多次加载,第一次加载时会运行模块,模块输出结果会被缓存,再次加载时,会从缓存结果中直接读取模块输出结果。模块加载的顺序,按照其在代码中出现的顺序。

  • 定义模块

    1
    2
    3
    4
    5
    6
    7
    8
    9
    var basicNum = 0; 
    function add(a, b) {
    return a + b;
    }

    module.exports = {
    add: add,
    basicNum: basicNum
    }
  • 引用模块

    1
    2
    3
    4
    5
    6
    7
    // 引用自定义模块,参数为文件路径,可省略.js
    var math = require('./math');
    math.add(2,5)

    // 引用核心模块,不需要带路径
    var http = require('http');
    http.createService(...).listen(3000);

3. AMD(define, require)

它是由 RequireJS 的作者 James Burke 提出的一种模块化规范。AMD 规范的主要特点是:异步加载、提前执行。

1
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
// 定义math.js模块
define(function(){
var basicNum = 0;
var add = function(x, y) {
return x + y;
}
return {
add: add,
basicNum: basicNum,
}
});

// 定义一个依赖underscore.js的模块
define(['underscore'],function(_){
var classify = function(list){
_.countBy(list,function(num){
return num > 30 ? 'old' : 'young';
})
};
return {
classify :classify
};
})

// 引用模块,将模块放在[]内
require(['jquery', 'math'], function($, math){
var sum = math.add(10,20);
})

4. CMD

CMD 规范整合了 CommonJS 和 AMD 规范的特点。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//定义没有依赖的模块
define(function(require, exports, module){
exports.xxx = value
module.exports = value
})
//定义有依赖的模块
define(function(require, exports, module){
//引入依赖模块(同步)
var module2 = require('./module2')
//引入依赖模块(异步)
require.async('./module3', function (m3) {
})
//暴露模块
exports.xxx = value
})

// 引入该模块
define(function (require) {
var m1 = require('./module1')
var m4 = require('./module4')
m1.show()
m4.show()
})

5. 区别

5.1 值拷贝 / 值引用

  • commonJS输出的是值的拷贝。一旦输出一个值,模块内部的变化就影响不到这个值。
  • ES6 Module输出的是值的引用。JS引擎在遇到ES6模块import命令时,会生成一个只读引用。等脚本真正执行的时候,根据引用,到被加载的模块里面去取值。如果一个模块修改了另一个模块导出的值,那么这个修改会影响到原始模块。
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    // lib.js
    export let counter = 3;
    export function incCounter() {
    counter ++
    }

    // main.js
    // ES6模块输出的变量counter是活的,完全反应其在模块lib.js内部的变化
    import { counter, incCounter } from './lib';
    console.log(counter); // 3
    incCounter();
    console.log(counter); // 4

5.2 运行时加载 / 编译时加载

  • commonJS 是运行时加载,只有运行时才能得到模块export的对象。
  • ES6 module 是编译时加载,编译时就要确定模块的依赖关系。

参考资料