前端模块化规范演进
2025年4年14日 · 1639 字
前端模块化规范演进
前端模块化的目的是为了更好地组织代码、提高可维护性与复用性。随着前端工程的不断发展,模块化方案也经历了多个阶段的演进。本文将依次介绍:
- 单例模式
- AMD(require.js)
- CommonJS
- CMD(sea.js)
- ES6 Module
前景
最开始的模块化开发:把每个模块代码写在不同的文件中,最后在页面中分别导入
- 需要自己“手动分析出相关的依赖”,规划出导入的先后顺序「麻烦」;即便基于grunt/gulp/webpack等处理,也需要知道依赖无系,按照依赖顺序打包,
- 如果不基于闭包把每个模块中的代码私有化处理,最后合并在一起的时候,容易引发"全局变量污染”
- ...
解决私有化:自执行函数执行,产生闭包即可 解决模块之间的相互访问:
- 把需要供外面访问的内容,暴露到全局上 window.xxx=xxx,但这种方式在需要暴露更多方法的时候,也可能会导致全局变量的冲突!!
- 把模块中需要暴露的属性方法放在一个对象中管理,最后基于模块名存储这个对象即可
let xModule=(function(){
...
return {
// 包含了需要暴露给外面用的属性方法
fn,
};
})();
访问: xModule.fn()
总结:这种处理方案,即保证了模块代码间的私有化,也支持模块间的相互访问,而且避免了全局变量的污染... 我们把这种代码设计方法称之为"单例设计模式";所有设计模式其实都是一种思想,这种思想解决了某一类问题!!
1. 单例模式(Namespace)
核心思想:
使用全局对象(如 window
)来定义命名空间,将模块封装成一个对象,避免变量污染。
示例:
// 定义
let xxxModule = (function () {
let time = new Date();
const query = function query() {
// ...
};
const handle = function handle() {
// ...
};
// 把供其它板块调用的方法,暴露到全局对象上
//「局限:暴露的内容比较多,则还会引发全局变量冲突」
// window.query = query;
return {
query,
handle
};
})();
// 使用
xxxModule.query();
特点:
- 所有模块都挂在全局命名空间,容易冲突。
- 无法处理依赖关系。
- 无法实现模块隔离或按需加载。
2. AMD(require.js)
核心思想:
使用异步方式加载模块,适用于浏览器环境。
示例:
// 定义模块 define.js
define(['dep1', 'dep2'], function (dep1, dep2) {
return {
method: function () {
// ...
}
};
});
// 使用模块
require(['define'], function (module) {
module.method();
});
特点:
- AMD设计思想:在单例设计模式基础上,有效的解决了模块之间的依赖问题,告别之前“手动一点点分析依赖,按照顺序依次导入”的问题了。
- 而且可以结合gulp/grunt等,最后把各个模块代码合并压缩打包在一起。
3. CommonJS(Node.js)
核心思想:
模块通过 require
引入,通过 module.exports
导出。
示例:
// math.js
const add = (a, b) => a + b;
module.exports = { add };
// 使用
const math = require('./math');
console.log(math.add(1, 2));
特点:
- 同步加载,适合服务器端。
- 加载模块时会立刻执行。
4. CMD(sea.js)
核心思想:
推迟执行,按需引入模块,支持依赖就近。
示例:
// define module
define(function (require, exports, module) {
var $ = require('jquery');
var msg = 'Hello CMD';
exports.msg = msg;
});
// 使用
seajs.use(['moduleA'], function (moduleA) {
console.log(moduleA.msg);
});
特点:
- 灵活,依赖可在需要时再引入。
- 适合浏览器端。
- 现在已不再维护。
5. ES6 Module(ESM)
标准化模块系统(浏览器和 Node.js 都支持)
核心语法:
// math.js
export const name = "小王";
export default { x:10 }
// 使用
import { name } from './math.js';
import A from './math.js';
import * as modules from "./a.js";
console.log(name); // 小王
console.log(A); // { x:10 }
console.log(modules); // { "default": { "x": 10 }, "name": "小王" }
特点:
- 在浏览器端开启ES6Module规范必须遵循以下两点
type="module"
- 基于标准的http/https协议的web服务预览页面
- 静态分析(编译时可确定依赖关系)。
- 老旧浏览器不支持。
- 支持 tree-shaking(摇树优化)。
语法:
-
导出模块:
- export 声明变量且赋值。
- export default 值; 在一个模块中只能使用一次。
- 每个模块导出一个Module对象
{ num:10, ..., default:sum }
-
导入模块:
import x from '模块地址'
- 浏览器端直接使用,地址中模块的后缀不能省略
- 只能接收到基于 export default 导出的这个值
- 原理:找到导出Module对象中的default属性值,把属性值赋值给x变量
- 但是不能在这直接给x解构赋值 ,例如:
import {n,m} from'模块地址'
; 这样是不能给default后面的值解构赋值;需要解构赋值,则先基于x接收,然后再给x解构赋值即可;例如:const{n,m}=x
;
import * as x from'模块地址'
- 把当前模块导出的所有内容获取到;赋值给x变量,后期基于.xxx访问即可「含:x,default 获取export default导出的值!
import { num, obj } from'模块地址'
- 直接结构赋值,是把模块导出的Module中所有内容(不含default)进行解构赋值
结语
单例设计模式是“最早期的模块规范",在没有CommonJS/ES6Module模块规范的时代,帮助我们实现了模块化开发!AMD(require.js)是在单例设计模式的基础上,实现了模块和模块之间的依赖管理!
----- 但是上述操作都是过去时了
当代前端开发,都是基于模块化进行开发,而模块化方案以CommonJS/ES6Module 为主
- 他们都是按照创建一个JS就是创建一个模块来管理的「每个JS文件中的代码都是私有的」
- CommonIS: require && module.exports
- ES6Module: export && import
我们编写的JS代码,可以运行的环境
-
@1 浏览器
<script src='...'>
「和其类似的还有webview」- 直接支持ES6Module,但是不支持CommonJS
- 全局对象 window
-
@2 NODE
- 支持CommonJS,但是不支持ES6Module
- 全局对象 global
-
@3 webpack「基于node实现代码的合并压缩打包、最后把打包的结果导入到浏览器中运行」
- CommonJS & ES6Module都支持,而且支持相互之间的"混用"(原理:webpack把两种模块规范都实现了一遍)
- 支持 window & global
-
@4 vite「新的工程化打包工具」
- 不是像webpack一样编译打包的,它本质就是基于ES6Module规范,实现模块之间的相互引用