home

前端模块化规范演进

2025年4年14日 · 1639

前端模块化规范演进

前端模块化的目的是为了更好地组织代码、提高可维护性与复用性。随着前端工程的不断发展,模块化方案也经历了多个阶段的演进。本文将依次介绍:

  1. 单例模式
  2. AMD(require.js)
  3. CommonJS
  4. CMD(sea.js)
  5. 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规范,实现模块之间的相互引用