webpack 模块化原理解析
前言
本文将探究 CommonJs
、ES Module
两种规范的区别,并从打包后的代码
对 webpack 是如何处理两种模块化方式做出解析。
CommonJs 和 ES Module
CommonJs 规范
对于 CommonJs 规范,Node 是其具有代表性的一个实现,而它具备以下特点:
- 在 Node 中,
每一个 JS 文件都被看作是一个模块
- CommonJs 使用
module.exports、exports
导出模块,使用require
引入模块
比如:
//a.js
const name = 'Jolyne'
const age = 22
module.exports = { name, age }
//或者
exports.name = 'Jolyne'
exports.age = 22
//main.js
const { name, age } = require("./a.js")
console.log(name, age) // Jolyne 22
ES Module 规范
对于 ES Module 规范,它借鉴了 CommonJs,它具有以下特点:
- ES Module 使用
export
导出模块,使用import
引入模块 - import() 引入模块,可以实现
懒加载
- ES Module 静态导入的方式,可以实现
tree shaking
比如:
//a.js
export const name = 'Joylne'
export default function getName() { return name }
//main.js
import getName, { name } from "./a.js"
console.log(getName(), name) // Jolyne Jolyne
//或者
import * as module from "./a.js"
console.log(module.getName(), module.name) // Jolyne Jolyne
//或者动态导入
const promise = import("./a.js")
webpack 模块化原理
在 webpack 里面使用的模块化基本上是 ES Module
、CommonJs
,接下来我们来看看 webpack 是如何处理的
webpack 中使用 CommonJs
//a.js
const name = "Joylne";
module.exports = { name };
//main.js
const {name} = require("./a")
console.log(name) //Jolyne
我们直接打包,根据打包后的文件我们来分析 webpack 是如何处理 CommonJs 模块化的(在这里,我把打包后的代码的注释去掉,加上了我自己的注释)
//打包后的代码
// webpack 打包后,会将编译后的结果放到这个立即执行函数中
(() => {
//定义了一个模块导出对象,以模块的路径为 key,value 是一个函数,函数里面就是对应模块编写的源代码
var __webpack_modules__ = {
"./src/a.js": (module) => {
const name = "Joylne";
module.exports = { name };
},
};
//定义了一个模块缓存对象
var __webpack_module_cache__ = {};
//定义了一个加载模块的函数,这里的参数 moduleId 其实就是 某个模块的路径,比如下面调用的 './src/a.js'
function __webpack_require__(moduleId) {
// 1、判断当前模块有没有缓存
var cachedModule = __webpack_module_cache__[moduleId];
// 2、如果缓存了,直接返回缓存的内容
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 3、如果没有缓存当前模块,那就创建一个 { exports: {} } 对象,把这个对象的地址 赋值给 module 变量 以及 缓存对象
// 也就是说: var module = cache = { exports: {} }, module 和 cache 的值是一样的,存的都是 { exports: {} } 这个对象的地址
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
// 4、从模块导出对象里面,找到对应模块,然后执行对应的函数,然后就会得到:{ exports: { 模块里面的数据 }}
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// 5、最后,将 { exports: { 模块中的数据 }} 返回回去
return module.exports;
}
var __webpack_exports__ = {};
(() => {
// 这里调用 require 函数,就能拿到模块导出的值了
const { name } = __webpack_require__("./src/a.js");
console.log(name);
})();
})();
从打包后的代码分析,我们可以知道:
-
首先,webpack 打包后,会将打包的内容放入一个
立即执行函数
中,目的是防止命名冲突
-
定义了一个
__webpack_modules__
模块对象,这个对象以 JS 模块的路径作为 key
,函数作为 value
,函数体的内容是模块的源代码
-
定义了一个
__webpack_module_cache__
缓存对象,用于缓存模块内容(比如对 webpack 开启缓存后,打包构建的速度会变快) -
定义了一个
__webpack_require__
的执行函数(重点)
,以模块路径
作为参数- 判断当前模块有没有缓存(也就是从
__webpack_module_cache__
中看拿不拿得到缓存数据) - 如果缓存了,直接返回缓存的内容
- 如果没有缓存,则会创建一个
{ exports: {} }
对象,这个对象其实就是后面处理过后 return 的结果,然后在定义一个module
变量,将{ exports: {} } 的地址
赋值给module
和__webpack_module_cache__[模块路径]
(这样就既能将最终的结果返回出去,也可以缓存结果) - 然后通过模块路径,通过
__webpack_modules__[模块路径]
拿到对应的函数,执行函数,最后将 { exports: { 模块数据 } } 返回
- 判断当前模块有没有缓存(也就是从
-
调用
__webpack_require__
函数,得到结果,最终打印出来
从上面的流程中可以知道:
webpack 对于 CommonJs 的处理,
实际上就是将模块里面的数据,最终给了 module.exports 这个对象
,然后将 module.exports 这个对象返回,我们就可以通过结构拿到里面的 name 了
图解如下:
webpack 中使用 ES Module
// a.js
export const name = "Joylne";
const age = 23;
export default age;
//main.js
import age, { name } from "./a.js";
console.log(age, name); // 23 Joylne
打包后,得到的结果如下:
(() => {
"use strict";
//1、定义模块变量 __webpack_modules__,同样以模块路径为 key,值是函数,函数体内部对模块源码进行了代理
var __webpack_modules__ = {
"./src/a.js": (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
// 2、这个方法其实就是标识 当前模块 .src/a.js 是一个 ES Module 模块
__webpack_require__.r(__webpack_exports__);
// 3、这个方法其实就是对传进来的 { exports: {} } 这个对象进行了代理,即: { exports: { naem: () => name, default: () => __WEBPACK_DEFAULT_EXPORT__ }}
__webpack_require__.d(__webpack_exports__, {
default: () => __WEBPACK_DEFAULT_EXPORT__,
name: () => name,
});
//4、这里是将 export default 默认导出的数据,挂载到了 __WEBPACK_DEFAULT_EXPORT__ 这个变量而已
const name = "Joylne";
const age = 23;
const __WEBPACK_DEFAULT_EXPORT__ = age;
},
};
//5、同样的,定义一个缓存对象
var __webpack_module_cache__ = {};
//6、这里和 commonJS 一样
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
//7、先判断模块有没有缓存,有就直接 return
if (cachedModule !== undefined) {
return cachedModule.exports;
}
//8、否则就将 { exports: {} } 的地址赋值给 module、__webpack_module_cache__[moduleId]
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
//9、根据模块路径,执行对应的函数,得到结果
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
//10、return module.exports,在这里也就是: { naem: () => name, default: () => __WEBPACK_DEFAULT_EXPORT__ }
return module.exports;
}
(() => {
// 这个方法就是对 exports 对象做代理,通过 Object.defineProperty 往 exports 对象身上添加属性
__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],
});
}
}
};
})();
(() => {
// 这个方法就是判断 obj 对象本身身上有没有某个 prop
__webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);
})();
(() => {
// 这个方法就是标识当前模块是 ES Module 模块,通过 Symbol.toStringTag 自定义了类型
__webpack_require__.r = (exports) => {
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
}
Object.defineProperty(exports, "__esModule", { value: true });
};
})();
var __webpack_exports__ = {};
(() => {
__webpack_require__.r(__webpack_exports__);
//拿到结果
var _a_js__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/a.js");
console.log(
_a_js__WEBPACK_IMPORTED_MODULE_0__["default"], // 23
_a_js__WEBPACK_IMPORTED_MODULE_0__.name // Jolyne
);
})();
})();
其实 webpack 对于 ES Module 的处理和对 CommonJS 的处理大同小异,区别在于:
- 处理 CommonJS 时,因为导出语法是 module.exports = XXX 或 exports = XXX,所以是通过 require 函数将模块的数据
赋值给了 exports 对象
再返回 - 处理 ES Module 时,因为导出语法是 export 或 export default,并没有 exports 对象给我们赋值,所以 require 函数对我们创建的 { exports: {} } 对象进行了代理,将模块的数据通过 {
name: () => name
} 的形式,挂载到 exports 对象
身上再返回
图解如下:
注意:当访问 export default
属性时,其实访问的是 DEFAULT_EXPORT
,访问 export const
属性时,其实访问的是 () => xxx
webpack 中使用 CommonJS 加载 ES Module
//a.js
export const name = "Joylne";
const age = 23;
export default age;
//main.js
const obj = require('./a');
console.log(obj); // { default: [Getter], name: [Getter]}
打包后的代码如下:
(() => {
var __webpack_modules__ = {
"./src/a.js": (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
"use strict";
// 同样,对 exports 对象做了代理
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
default: () => __WEBPACK_DEFAULT_EXPORT__,
name: () => name,
});
const name = "Joylne";
const age = 23;
const __WEBPACK_DEFAULT_EXPORT__ = age;
},
};
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;
}
(() => {
__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],
});
}
}
};
})();
(() => {
__webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);
})();
(() => {
__webpack_require__.r = (exports) => {
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
}
Object.defineProperty(exports, "__esModule", { value: true });
};
})();
(() => {
const obj = __webpack_require__("./src/a.js");
console.log(obj);
})();
})();
其实当 CommonJS 引用 ES Module 导出的模块时,和 webpack 处理 ES Module 的情况基本相同,都是对 { exports: {} } 做了代理,将模块数据挂载到 exports 对象上然后返回
。唯一的区别就是:
(() => {
// 也就是引用的时候,这里和处理 CommonJS 是一样的
const obj = __webpack_require__("./src/a.js");
console.log(obj);
})();
webpack 中使用 ES Module 加载 CommonJS
(() => {
var __webpack_modules__ = {
// 同样,这里是直接赋值给 exports 对象,而不会去做代理
"./src/a.js": (module) => {
const name = "Jolyne";
module.exports = name;
},
};
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;
}
(() => {
// 这个函数是用来返回 默认导出的内容,最终会挂载到 exports 对象上,key 是 "default"
__webpack_require__.n = (module) => {
var getter =
module && module.__esModule ? () => module["default"] : () => module;
__webpack_require__.d(getter, { a: getter });
return getter;
};
})();
(() => {
// 这个方法是通过 Object.defineProperty 对 exports 对象做代理
__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],
});
}
}
};
})();
(() => {
__webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);
})();
(() => {
// 标识当前模块是 ES Module 导出的模块
__webpack_require__.r = (exports) => {
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
}
Object.defineProperty(exports, "__esModule", { value: true });
};
})();
var __webpack_exports__ = {};
(() => {
"use strict";
__webpack_require__.r(__webpack_exports__);
var _a__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/a.js");
var _a__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(
_a__WEBPACK_IMPORTED_MODULE_0__
);
console.log(_a__WEBPACK_IMPORTED_MODULE_0__.name);
})();
})();
这种情况就多了一个 __webpack_require__.n
函数,他做的事情就是把默认导出的内容挂载到 exports
对象上,核心就是把模块数据赋值给 exports 对象
总结
-
当模块通过
CommonJS
的方式导出时,会直接将模块的数据赋值给 exports
对象然后返回 -
当模块通过
ES Module
的方式导出时,会创建一个 { exports: {} }对象,然后通过Object.defineProperty
的方式为 exports 对象做个代理
,以{ key: () => data }
的形式赋值给 exports 对象然后导出 -
当模块通过
CommonJS 导出、ES Module 导入
时,同样直接将模块数据赋值给 exports 对象并返回,再通过__webpack_require__.n
函数将默认导出的部分再赋值给 exports 对象 -
当模块通过
ES Module 导出、CommonJS 导入
时,也是会创建 { exports: {} }对象,通过Object.defineProperty
的方式为 exports 对象做个代理
,以{ key: () => data }
的形式赋值给 exports 对象然后导出
再简单暴力点总结就是:
- CommonJS 方式导出模块时,直接将模块数据赋值个 exports 对象并返回
- ES Module 方式导出模块时,创建 exports 对象并对其做代理,挂载导出的数据(正常导出和默认导出)并返回
结语
以上内容如有错误,欢迎留言指出,一起进步💪,也欢迎大家一起讨论。
转载自:https://juejin.cn/post/7234361839689515069