前言
什么是模块化
模块化简单说就是为了解决代码没有自己的“域”,很容易造成命名冲突,依赖混乱。我们的代码通过模块化隔离开来,更有利于后期维护。
模块化解决方案
从最开始的通过立即执行函数(IIFE)到 CJS、AMD、CMD、UMD、再到 ES6 引入模块化 ESM,本文主要写 CJS 和 ESM。
CommonJs
实现原理
1 2 3
| const name = require("./name") console.log(name)
|
1 2 3 4 5
| 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
| (() => { 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)来实现的。

EsModule
我们在 name.js 加上默认导出。
1 2 3
| 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));
|
CJS 和 ESM 区别
|
CJS |
ESM |
语法类型 |
动态 |
静态 |
加载方式 |
运行时加载 |
编译时加载 |
加载行为 |
同步加载 |
异步加载 |
this指向 |
当前模块 |
undefined |
能否修改 |
可以修改引用的值 |
引入的值是只读的 |
引用 |
基本类型复制 引用类型浅拷贝 |
动态只读引用 |
书写位置 |
任意位置 |
顶层 |
运行时加载和编译时加载
运行时加载会生成一个对象全部加载,运行时获得该对象。编译时加载直接从模块中加载,能做到按需加载,效率更高。