前言
什么是模块化
模块化简单说就是为了解决代码没有自己的“域”,很容易造成命名冲突,依赖混乱。我们的代码通过模块化隔离开来,更有利于后期维护。
模块化解决方案
从最开始的通过立即执行函数(IIFE)到 CJS、AMD、CMD、UMD、再到 ES6 引入模块化 ESM,本文主要写 CJS 和 ESM。
CommonJs
实现原理
| 12
 3
 
 | const name = require("./name")
 console.log(name)
 
 | 
| 12
 3
 4
 5
 
 | var name = "john";
 module.exports = {
 name: name,
 }
 
 | 
我们把以上代码打包,通过产物看一下CJS到底是如何进行模块化的。产物代码经过简化。
| 12
 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 加上默认导出。
| 12
 3
 
 | export var name = "john";
 export default "this is default"
 
 | 
ESM 和 CJS 的区别在于,ESM 给 exports 做了一层代理。
| 12
 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 类型。
| 12
 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 来识别。
举个例子:
| 12
 3
 
 | const obj = {};Object.defineProperty(obj, Symbol.toStringTag, {value: "Module"});
 console.log(Object.prototype.toString.call(obj));
 
 | 
CJS 和 ESM 区别
|  | CJS | ESM | 
| 语法类型 | 动态 | 静态 | 
| 加载方式 | 运行时加载 | 编译时加载 | 
| 加载行为 | 同步加载 | 异步加载 | 
| this指向 | 当前模块 | undefined | 
| 能否修改 | 可以修改引用的值 | 引入的值是只读的 | 
| 引用 | 基本类型复制 引用类型浅拷贝
 | 动态只读引用 | 
| 书写位置 | 任意位置 | 顶层 | 
运行时加载和编译时加载
运行时加载会生成一个对象全部加载,运行时获得该对象。编译时加载直接从模块中加载,能做到按需加载,效率更高。