深入浅出前端模块化原理

前言

什么是模块化

模块化简单说就是为了解决代码没有自己的“域”,很容易造成命名冲突,依赖混乱。我们的代码通过模块化隔离开来,更有利于后期维护。

模块化解决方案

从最开始的通过立即执行函数(IIFE)到 CJS、AMD、CMD、UMD、再到 ES6 引入模块化 ESM,本文主要写 CJS 和 ESM。

CommonJs

实现原理

1
2
3
// src/index.js
const name = require("./name")
console.log(name)
1
2
3
4
5
// src/name.js
var name = "john";
module.exports = {
name: name,
}

我们把以上代码打包,通过产物看一下CJS到底是如何进行模块化的。产物代码经过简化

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
29
30
31
32
(() => {
// 定义一个 modules 存放每个文件的源码,[key] 是文件路径
var __webpack_modules__ = ({
"./src/index.js": ((__unused_webpack_module, __unused_webpack_exports, __webpack_require__) => {
// ...
}),
"./src/name.js": ((module) => {
// ...
})
});
// 缓存
var __webpack_module_cache__ = {};

// 获取源码的方法
function __webpack_require__(moduleId) {
// 读取缓存
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
// 有缓存就不执行了,直接返回缓存
return cachedModule.exports;
}
var module = __webpack_module_cache__[moduleId] = {
exports: {}
};
// 没缓存就执行然后缓存,
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
return module.exports;
}

// 执行入口函数
var __webpack_exports__ = __webpack_require__("./src/index.js");
})();

从产物中不难看出 CJS 把每个路径的源码都存在一起,然后通过 require 来实现读取并缓存,实现模块化的功能,其实说到底还是使用了立即执行函数(IIFE)来实现的。
CJS原理

EsModule

我们在 name.js 加上默认导出。

1
2
3
// src/name.js
export var name = "john";
export default "this is default"

ESM 和 CJS 的区别在于,ESM 给 exports 做了一层代理。

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
var __webpack_modules__ = ({
// ...
"./src/name.js": ((__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
// 扩展里会写到
__webpack_require__.r(__webpack_exports__);
// 进行代理
__webpack_require__.d(__webpack_exports__, {
"default": () => (__WEBPACK_DEFAULT_EXPORT__),
name: () => (name)
});
var name = "john";
const __WEBPACK_DEFAULT_EXPORT__ = ("this is default");
})
});

(() => {
// 代理
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
if (__webpack_require__.o(definition, key) && !__webpack_require__.o(exports, key)) {
Object.defineProperty(exports, key, {enumerable: true, get: definition[key]});
}
}
};
})();

当我们使用 ESM 导入的值,实际上都是代理值,我们每次访问这个值其实都是通过要这个代理,这也就是为什么导出后改变这个值,导入的文件也能拿到更新后的值。

扩展

设置值类型

在 EsModule 的 __webpack_modules__ 中有一个方法 __webpack_require__.r 这个是干什么用的呢?他其实就是把 exports标记成 Module 类型。

1
2
3
4
5
6
7
8
(() => {
__webpack_require__.r = (exports) => {
if (typeof Symbol !== 'undefined' && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, {value: 'Module'});
}
Object.defineProperty(exports, '__esModule', {value: true});
};
})();

我们判断一个值的属性,通过 Object.prototype.toString.call()方法,那么这个方法是根据什么判断出类型的?其实就是根据值的 Symbol.toStringTag 来识别。

举个例子:

1
2
3
const obj = {};
Object.defineProperty(obj, Symbol.toStringTag, {value: "Module"});
console.log(Object.prototype.toString.call(obj)); // [object Module]

CJS 和 ESM 区别

CJS ESM
语法类型 动态 静态
加载方式 运行时加载 编译时加载
加载行为 同步加载 异步加载
this指向 当前模块 undefined
能否修改 可以修改引用的值 引入的值是只读的
引用 基本类型复制
引用类型浅拷贝
动态只读引用
书写位置 任意位置 顶层

运行时加载和编译时加载

运行时加载会生成一个对象全部加载,运行时获得该对象。编译时加载直接从模块中加载,能做到按需加载,效率更高。


深入浅出前端模块化原理
https://l1ushun.github.io/2023/12/18/module/
作者
liu shun
发布于
2023年12月18日