模块化规范
模块化历史
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 的值。
虽然 IIFE 解决了命名冲突和作用域问题,但是并没有解决模块加载的问题。如果模块间存在依赖关系,那么 script 标签的加载顺序就需要受到严格的控制,一旦顺序不对,则可能会产生运行时 Bug。
理想方式:在页面中引入一个 JS 入口文件,其余的模块可以通过代码控制,按需加载进来。
除了模块加载的问题以外,还需要规定模块化的规范,当前主流是 CommonJS 、ES Module
模块化概述
什么是模块化?
将一个复杂的程序依据一定的规范封装成几个模块,并组合在一起。
模块的内部数据、方法是私有的,只是向外部暴露一些接口方法与外部模块通信。
为什么要有模块化?
- 数据、方法都是私有的,避免命名冲突,减少命名空间污染
- 降低耦合性,模块拆分,按需加载
- 高 复用性,独立的功能模块便于多处复用
- 高可维护性,维护单独的小模块更方便,如果维护一个有很多功能放在一起的大文件会很困难
为什么要引入模块化规范?
如果引入模块化,可能就是在一个文件中引入多个 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
中引入一个库或者从本地目录引入一个文件 - 所有代码都运行在模块作用域,不会污染全局作用域
- 模块可以多次加载,但是只会在第一次加载时运行一次,然后运行结果就被缓存了
- 同步加载。意味着在 浏览器端的模块请求会造成浏览器 JS 解析过程的阻塞
Node.js 早期遵循的就是
CommonJS
规范。从v13.2.0
之后也引入了规范的ES Module
机制,同时兼容早期的CommonJS
模块的导入导出
导出的方式:
const name = 'zgh';
function getData() {}
// 方式一、exports
exports.getData = getData;
// 方式二、module.exports
// 导出单个
module.exports.getData = getData;
// 导出所有
module.exports = { getData, name };
引入模块的方式:
const module1 = require('./module1');
// 解构赋值
const { getData, name } = require('./module1');
模块的初始化
一个模块中的 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 这个模块虽然被引用了两次,但只初始化了一次
CommonJS 模块直接放到浏览器中是无法执行的。在浏览器端可以通过 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 Module)表示 ES6 的模块规范,支持异步特性,是最常用的一种 规范。是由 ECMAScript 官方提出的模块化规范,已经得到了现代浏览器的内置支持。如果在 HTML 中加入含有 type="module"
属性的 script 标签,那么浏览器会按照 ES Module 规范来进行依赖加载和模块解析。
ESM 能够同时在浏览器与 Node.js 环境中执行,拥有跨平台能力。
// 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 等构建工具就产生了