一、思考
Webpack是何如实现 ESM(Es Module) 与 CMS(Commonjs) 相互导出引用呢?带着这些问题,咱们通过简单文件的打包来分析不同类型的模块是如何处理的
二、极简单的代码
webpack.config.js
const path = require("path"); const HtmlWebpackPlugin = require("html-webpack-plugin"); module.exports = { devtool: "none", mode: "development", entry: "./src/index.js", output: { filename: "bundle.js", path: path.resolve(__dirname, "dist"), }, plugins: [ new HtmlWebpackPlugin({ template: "./src/index.html", }), ], };
index.js
const home = require("./home"); console.log("Hello World"); console.log(home);
home.js
module.exports = "我是主页面";
index.html
<!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8" /> <meta http-equiv="X-UA-Compatible" content="IE=edge" /> <meta name="viewport" content="width=device-width, initial-scale=1.0" /> <title>跟着webpack产物来学习webpack是如何做模块处理</title> </head> <body> <h1>跟着webpack产物来学习webpack是如何做模块处理</h1> </body> </html>
三、分析产物的公共方法和属性
1. 完整的产物
(function (modules) { // webpackBootstrap // The module cache var installedModules = {}; // The require function function __webpack_require__(moduleId) { // Check if module is in cache if (installedModules[moduleId]) { return installedModules[moduleId].exports; } // Create a new module (and put it into the cache) var module = (installedModules[moduleId] = { i: moduleId, l: false, exports: {}, }); // Execute the module function modules[moduleId].call( module.exports, module, module.exports, __webpack_require__ ); // Flag the module as loaded module.l = true; // Return the exports of the module return module.exports; } // expose the modules object (__webpack_modules__) const home = require("./home"); console.log("Hello World"); console.log(home);0 // expose the module cache const home = require("./home"); console.log("Hello World"); console.log(home);1 // define getter function for harmony exports __webpack_require__.d = function (exports, name, getter) { if (!__webpack_require__.o(exports, name)) { Object.defineProperty(exports, name, { enumerable: true, get: getter }); } }; // define __esModule on exports __webpack_require__.r = function (exports) { if (typeof Symbol !== "undefined" && Symbol.toStringTag) { Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); } Object.defineProperty(exports, "__esModule", { value: true }); }; // create a fake namespace object // mode & 1: value is a module id, require it // mode & 2: merge all properties of value into the ns // mode & 4: return value when already ns object // mode & 8|1: behave like require __webpack_require__.t = function (value, mode) { if (mode & 1) value = __webpack_require__(value); if (mode & 8) return value; if (mode & 4 && typeof value === "object" && value && value.__esModule) return value; var ns = Object.create(null); __webpack_require__.r(ns); Object.defineProperty(ns, "default", { enumerable: true, value: value }); if (mode & 2 && typeof value != "string") for (var key in value) __webpack_require__.d( ns, key, function (key) { return value[key]; }.bind(null, key) ); return ns; }; // getDefaultExport function for compatibility with non-harmony modules __webpack_require__.n = function (module) { var getter = module && module.__esModule ? function getDefault() { return module["default"]; } : function getModuleExports() { return module; }; __webpack_require__.d(getter, "a", getter); return getter; }; // Object.prototype.hasOwnProperty.call __webpack_require__.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property); }; // __webpack_public_path__ const home = require("./home"); console.log("Hello World"); console.log(home);2 // Load entry module and return exports return __webpack_require__((__webpack_require__.s = "./src/index.js")); })({ "./src/home.js": /*! no static exports found */ function (module, exports) { module.exports = "我是主页面"; }, "./src/index.js": /*! no static exports found */ function (module, exports, __webpack_require__) { const home = __webpack_require__(/*! ./home */ "./src/home.js"); console.log("Hello World"); console.log(home); }, });
是一个IIFE模块,实参是moduleId 和 对应的模块(函数)
2. 公共方法分析
a. 模块导入方法:__webpack_require__
// 模块导入方法:缓存模块信息,执行模块函数,将模块的导出return出去 function __webpack_require__(moduleId) { // 缓存已经加载过的模块,比如:key是 ./src/index.js,value 是模块信息 if (installedModules[moduleId]) { // 将模块导出的内容 return 出去 return installedModules[moduleId].exports; } // 创建一个新的模块,并将其放入缓存中 var module = (installedModules[moduleId] = { i: moduleId, // 模块的id 比如:./src/index.js l: false, // 标记当前模块是否被加载 当时是未被加载 exports: {}, // 存储模块导出的内容 }); // Execute the module function // 执行模块函数 modules[moduleId].call( module.exports, // 模块内的this执行的是模块导出的内容 module, // 当前的模块 module.exports, // 存储模块导出的内容 __webpack_require__ // // 导入方法 ); // 标记当前模块已经被加载 module.l = true; // 返回模块导出的内容 return module.exports; }
b. 判断对象上是否有 xxx 属性
// 判断对象上是否有 xxx 属性 __webpack_require__.o = function (object, property) { return Object.prototype.hasOwnProperty.call(object, property); };
c. 向exports对象上自定义属性和属性的getter方法
剧透:向ESM的exports添加导出内容,后面有具体的分析
// 向exports对象上自定义属性和属性的getter方法 __webpack_require__.d = function (exports, name, getter) { // 当前 导出对象中没有有 xxx 属性时候 if (!__webpack_require__.o(exports, name)) { // 向 exports 对象添加 xxx 属性,并设置值为 getter 方法的返回值,可枚举的 Object.defineProperty(exports, name, { enumerable: true, get: getter }); } };
d.标记 exports 是 esModule 模块方法
// 标记 exports 是 esModule 模块 __webpack_require__.r = function (exports) { // 自定义 exports.toString 为 [object Module],标记当前 exports 是 esModule 模块 if (typeof Symbol !== "undefined" && Symbol.toStringTag) { Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" }); } // 在exports添加属性 __esModule 值为 true Object.defineProperty(exports, "__esModule", { value: true }); };
e.兼容 ESM 和 CMS 默认导出
__webpack_require__.n = function (module) { // 如果是 esModule 模块,创建 getter 获取 模块 default 值 // 非 esModule 模块,创建 getter 获取 模块 exports 值 var getter = module && module.__esModule ? function getDefault() { return module["default"]; } : function getModuleExports() { return module; }; // 在 getter 函数添加 a 为当前模块 默认 导出的值 __webpack_require__.d(getter, "a", getter); // 获取 当前模块 默认 导出的值 的 getter return getter; };
f.当所有的modules 挂在到 __webpack_require__
方法上
const home = require("./home"); console.log("Hello World"); console.log(home);0
g. 模块的缓存,挂在到 __webpack_require__
方法上
const home = require("./home"); console.log("Hello World"); console.log(home);1
h. 配置的public path 挂在到 __webpack_require__
方法上
const home = require("./home"); console.log("Hello World"); console.log(home);2
以上属于挂在到 __webpack_require__
方法上 主要是为了后面方便获取对应的值
四、分析ESM(Es Module) 与 CMS(Commonjs)的相互导出引用
1. 导出:CMS, 导入:CMS
a.源码
// index.js -- 导入 const home = require("./home"); console.log("Hello World"); console.log(home);
// home.js -- 导出 module.exports = "我是主页面";
b. 产物分析
实参
{ "./src/home.js": function (module, exports) { module.exports = "我是主页面"; }, "./src/index.js": function (module, exports, __webpack_require__) { const home = __webpack_require__(/*! ./home */ "./src/home.js"); console.log("Hello World"); console.log(home); }, }
由上图可看出将 ./src/index.js
作为moduleId传给 __webpack_require__
这个函数作用:模块导入方法:缓存模块信息,执行模块函数,将模块的导出return出去 具体这个函数坐上,到【公共方法分析】查看,这里注重分析模块执行 上面执行加载 home 模块,并将home导出的内容答应出来,下面看看加载home的时候,怎样将内容导出去
由上图可以值,“index.js” 中home 值是 “我是主页面”
2. 导出:CMS, 导入:ESM
a.源码
const home = require("./home"); console.log("Hello World"); console.log(home);6
// home.js -- 导出 module.exports = "我是主页面";
b. 产物分析
实参
{ "./src/home.js": function (module, exports) { module.exports = "我是主页面"; }, "./src/index.js": function ( module, __webpack_exports__, __webpack_require__ ) { "use strict"; __webpack_require__.r(__webpack_exports__); var _home__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__( /*! ./home */ "./src/home.js" ); var _home__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n( _home__WEBPACK_IMPORTED_MODULE_0__ ); console.log(_home__WEBPACK_IMPORTED_MODULE_0___default.a); }, }
执行顺序和上面的一样,咱们直接分析 ./src/index.js
模块的执行
首先执行
__webpack_require__.r(__webpack_exports__);
,这个函数就是标记 当前模块exports 对象 为 esModules 接着加载home模块
const home = require("./home"); console.log("Hello World"); console.log(home);9
此时 _homeWEBPACK_IMPORTED_MODULE_0 值 为 “我是主页面” 接着 做一波 兼容 ESM 和 CMS 默认导出 `webpack_require.n` 的操作
module.exports = "我是主页面";0
可以看出,
_home__WEBPACK_IMPORTED_MODULE_0___default
就是一个模块导出的的getter,获取home的值。
最后 console.log(_home__WEBPACK_IMPORTED_MODULE_0___default.a);
打印的就是 ‘我是主页面’
3. 导出:ESM, 导入:CMS
a.源码
module.exports = "我是主页面";1
module.exports = "我是主页面";2
b. 产物分析
实参
module.exports = "我是主页面";3
执行顺序和上面的一样,咱们直接分析 ./src/index.js
模块的执行 这块,上面其实已经分析过了,加载 home模块,并将home导出的内容打印出来,核心关注 home是做模块导出的
从这里就能看到
./src/index.js
打印的default 和 name 就是 home模块 的 "Hello World" 和 "我是主页面"
4. 导出:ESM, 导入:ESM
a.源码
module.exports = "我是主页面";1
module.exports = "我是主页面";2
b. 产物分析
实参
module.exports = "我是主页面";6
执行顺序和上面的一样,咱们直接分析 ./src/index.js
模块的执行 那么 我们去看看 home 模块做了哪些
从这里就能看到
./src/index.js
打印的default 和 name 就是 home模块 的 "Hello World" 和 "我是主页面"
五、总结
-
ESM导出做了三步:
a. 标记当前导出对象为 ES Module
b. 如果导出 有default,直接将值挂在到 导出对象的 default属性上
c. 如果导出具名的内容,首先将当前属性通过defineProperty
定义在 导出对象上,getter就是获取当前作用于下的该属性,并在当前模块定声明该属性并赋值 -
CMS导出:直接将内容挂在到 导出的对象上
-
ESM导入做了四步:
a. 标记当前导出对象为 ES Module
b. 导入被导入的模块
c. 对导入的内容 default 做 ESM 和 CMS的 getter 兼容
d. 在getter的a属性获取被导入的值 -
CMS导入:直接将导入的内容打印出来即可