Skip to main content

模块化规范

模块化历史

1、文件划分

  • 每个文件是一个独立的模块,通过 script 标签引入不同模块
  • 缺点:模块之间缺少依赖关系、维护困难、没有私有空间、变量污染等
<script src="a.js"></script>
<script src="b.js"></script>

2、命名空间

  • 规定每个模块只暴露一个全局对象,然后模块的内容都挂载到这个对象中
  • 缺点:外部可以更改模块内的值
window.moduleA = {
name: 'zgh',
f1: function () {}
};

modeluA.name = 'js';

3、立即执行函数

为模块提供私有空间,通过参数的形式作为依赖声明。本质是匿名函数的自调用。

  • 用 script 标签在页面引入模块,模块的加载不受控,维护困难
  • script 标签的加载顺序不能乱
(function ($) {
var name = 'zgh';

function foo() {}

window.moduleA = {
foo: foo
};
})(jQuery);

例子中引入 Jquery 必须要在前面。只将 foo 方法暴露出去,外部无法修改内部值,如 name 的值。

理想方式:在页面中引入一个 JS 入口文件,其余的模块可以通过代码控制,按需加载进来

除了模块加载的问题以外,还需要规定模块化的规范,当前主流是 CommonJS 、ES Modules

模块化概述

什么是模块化?

将一个复杂的程序依据一定的规范封装成几个模块,并组合在一起。

模块的内部数据、方法是私有的,只是向外部暴露一些接口方法与外部模块通信。

为什么要有模块化?

  • 数据、方法都是私有的,避免命名冲突,减少命名空间污染
  • 降低耦合性,模块拆分,按需加载
  • 高复用性,独立的功能模块便于多处复用
  • 高可维护性,维护单独的小模块更方便,如果维护一个有很多功能放在一起的大文件会很困难

为什么要引入模块化规范?

如果引入模块化,可能就是在一个文件中引入多个 js 文件,如:

<script src="a.js"></script>
<script src="b.js"></script>
<script src="c.js"></script>

这样做会带来很多问题:

  • 请求过多:引入 n 个 js 文件,就有 n 次 http 请求
  • 依赖模糊:不同的 js 文件可能会相互依赖,如果改一个文件,其他文件可能会报错

最终可能难以维护,所以引入了模块化规范

CommonJS 规范

CommonJS 规范是一套约定标准,主要内容是模块通过 module.exports 导出对外的变量或接口,通过 require() 来导入其他模块的输出到当前模块作用域中。

特性:

  • 每个文件就是一个模块,有自己的作用域
  • 在一个文件里面定义的变量、函数、类,都是私有的,对其他文件不可见
  • 可以从 node_modules 中引入一个库或者从本地目录引入一个文件
  • 所有代码都运行在模块作用域,不会污染全局作用域
  • 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了
  • 同步加载

Node.js 早期遵循的就是CommonJS规范。从v13.2.0之后也引入了规范的ES Modules机制,同时兼容早期的CommonJS

模块的导入导出

引入模块的方式:

const module1 = require('模块名');

暴露的模块本质是 exports 对象

const axios = require('axios');

function getData() {}

function postData() {}

// 方式一、exports
exports.getData = getData;

// 方式二、module.exports
// 导出单个
module.exports.getData = getData;

// 导出所有
module.exports = { getData, postData };

模块的初始化

一个模块中的 JS 代码仅在模块第一次被使用时执行一次,并且在使用的过程中进行初始化,然后会被缓存起来,便于后续继续使用

示例:add.js

let a = 1;

function add() {
return ++a;
}

exports.add = add;

在 main.js 中引入 add.js 模块

let addModule1 = require('./add');
let addModule2 = require('./add');

console.log(addModule1.add()); // 2
console.log(addModule2.add()); // 3

在终端执行 node main.js 运行程序,可以看出 add.js 这个模块虽然被引用了两次,但只初始化了一次

tip

在浏览器端可以通过 browserify 中转使用 CommonJS 规范,属于历史产物。

AMD

AMD (Asynchronous Module Definition) 是 js 中一种模块定义的规范,可以在浏览器端异步加载模块。

AMD 规范主要解决的问题是浏览器中模块化开发的时候,如何保证模块的依赖能够被正确地加载。在 AMD 规范中,模块是以函数的形式组织,并且需要通过 define 函数进行定义

define(id, dependencies, factory);
  • id 是可选参数,表示模块标识符
  • dependencies 是可选参数,表示依赖的模块列表
  • factory 是一个函数,在模块加载完成后执行。这个函数返回模块的接口
define('moduleA', ['moduleB', 'moduleC'], function (moduleB, moduleC) {
// ... do something ...
return {};
});

这里定义了一个名为 moduleA 的模块,它依赖于 moduleB 和 moduleC 两个模块。在 factory 函数中可以使用这些依赖模块,并返回该模块对外暴露的接口。

使用 require 函数来获取一个模块的接口:

require([dependencies], callback);
  • dependencies 需要加载的模块列表
  • callback 是一个函数,在所有依赖模块都被加载完成后执行。在该函数中,可以使用依赖模块的接口。

例如加载上面定义的 moduleA 模块

require(['moduleA'], function (moduleA) {
// ... do something ...
});

通常情况下会使用 requirejs 库来实现 AMD 规范的模块加载和管理。Why AMD

CMD

CMD(Common Module Definition) 规范和 AMD 很相似,尽量保持简单,并与 CommonJS 规范保持了很大的兼容性。

  • 优点是依赖就近,延迟执行,容易在 Node.js 中运行
  • 缺点是依赖 SPM 打包,模块的加载逻辑偏重
  • 代表实现有 Sea.js

依赖就近:执行到这一部分的时候,再去加载对应的文件。而 AMD 是依赖前置。

define(function (require, exports, module) {
const dep1 = require('dep1');
const dep2 = require('dep2');
exports.doSomething = {};
module.exports = {};
});

UMD

UMD(Universal Module Definition)规范类似于兼容 CommonJS 和 AMD 的语法糖,是模块定义的跨平台解决方案

ESM

ESM(ES Modules)表示 ES6 的模块规范,支持异步特性,是最常用的一种规范

// a.js
const a = () => {};
const b = 'are you ok';
export { a, b };

// 导入
import { a, b } from 'a.js';

export default默认导出,一个模块内只能有一个,在 import 时可以用任意名字引入

// default.js
const a = () => {};
export default a;

// 其他文件导入,可以任意命名
import foo from 'default.js';

还可直接在定义变量时就导出

export const a = 100;
export const b = () => {};

如果想在导入导出时重新命名

// 导出时将a重新命名为test
export { a as test, b };

// 导入时将b重新命名为mm
import { a, b as mm } from 'a.js';

在导入时还可以将导出的变量声明成一个任意名字的对象的属性

import * as obj from 'a.js';

console.log(obj.a);
console.log(obj.b);

构建工具

有了模块化和模块化规范,那具体怎么落地实现呢?

开发方式从 JSP、PHP、原生 JavaScript、jQuery,再到 Angular、React、 Vue 框架。从 ES5、ES6+,再到 TypeScript,以及 less、scss 等。前端开发变的复杂,会遇到一些问题:

  • 需要模块化开发,逻辑复用
  • 使用高级特性来加快开发效率,如 ES6+、sass、less
  • 监听文件的变化并反映到浏览器上(热更新)
  • 静态资源需要模块化
  • 代码压缩、合并以及其他相关的优化

所以 webpack、vite、rollop、gulp、turbopack 等构建工具就产生了