前端模块化:IIFE、ES6 Module、CommonJS
前端模块化是指将一个大型的前端程序分解为小的、独立的模块,每个模块都有自己的功能和接口,可以被其他模块使用。
1. IIFE 闭包
古早时期的 IIFE 模式,通过自执行函数创建闭包。数据是私有的,外部只能通过暴露的方法操作。通过传入依赖的方式来解决模块间引用的问题。这种方式确实可以实现模块化开发了,但是模块挂载在window下,代码阅读困难,无法支持大规模模块化开发。1
2
3
4
5
6
7function(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 | <!-- 在浏览器中加载ESModule模块 --> |
1 | // 在Node.js中加载ESModule模块 |
特点:
- 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
9var 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 是编译时加载,编译时就要确定模块的依赖关系。