一、思考
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__)
__webpack_require__.m = modules;
// expose the module cache
__webpack_require__.c = installedModules;
// 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__
__webpack_require__.p = "";
// 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__ 方法上
__webpack_require__.m = modules;
g. 模块的缓存,挂在到 __webpack_require__ 方法上
__webpack_require__.c = installedModules;
h. 配置的public path 挂在到 __webpack_require__ 方法上
__webpack_require__.p = "";
以上属于挂在到
__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.源码
// index.js -- 导入
import home from "./home";
console.log(home);
// 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模块
var _home__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(
/*! ./home */ "./src/home.js"
);
此时 home__WEBPACK_IMPORTED_MODULE_0_ 值 为 “我是主页面”
接着 做一波 兼容 ESM 和 CMS 默认导出
__webpack_require__.n 的操作
var _home__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(
_home__WEBPACK_IMPORTED_MODULE_0__
);
可以看出,
_home__WEBPACK_IMPORTED_MODULE_0___default就是一个模块导出的的getter,获取home的值。
最后 console.log(_home__WEBPACK_IMPORTED_MODULE_0___default.a);
打印的就是 ‘我是主页面’
3. 导出:ESM, 导入:CMS
a.源码
// index.js -- 导入
const home = require("./home");
console.log("default", home.default);
console.log("name", home.name);
// home.js -- 导出
export const name = "Hello Home";
export default "我是主页面";
b. 产物分析
实参
{
"./src/home.js":
/*! exports provided: name, default */
function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(
__webpack_exports__,
"name",
function () {
return name;
}
);
const name = "Hello Home";
/* harmony default export */ __webpack_exports__["default"] =
"我是主页面";
},
"./src/index.js":
/*! no static exports found */
function (module, exports, __webpack_require__) {
const home = __webpack_require__(/*! ./home */ "./src/home.js");
console.log("default", home.default);
console.log("name", home.name);
},
}
执行顺序和上面的一样,咱们直接分析 ./src/index.js 模块的执行
这块,上面其实已经分析过了,加载 home模块,并将home导出的内容打印出来,核心关注 home是做模块导出的
从这里就能看到
./src/index.js 打印的default 和 name 就是 home模块 的 "Hello World" 和 "我是主页面"
4. 导出:ESM, 导入:ESM
a.源码
// index.js -- 导入
const home = require("./home");
console.log("default", home.default);
console.log("name", home.name);
// home.js -- 导出
export const name = "Hello Home";
export default "我是主页面";
b. 产物分析
实参
{
"./src/home.js":
/*! exports provided: name, default */
function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony export (binding) */ __webpack_require__.d(
__webpack_exports__,
"name",
function () {
return name;
}
);
const name = "Hello Home";
/* harmony default export */ __webpack_exports__["default"] =
"我是主页面";
},
"./src/index.js":
/*! no exports provided */
function (module, __webpack_exports__, __webpack_require__) {
"use strict";
__webpack_require__.r(__webpack_exports__);
/* harmony import */ var _home__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__(/*! ./home */ "./src/home.js");
console.log("home", _home__WEBPACK_IMPORTED_MODULE_0__["default"]);
console.log("name", _home__WEBPACK_IMPORTED_MODULE_0__["name"]);
},
});
执行顺序和上面的一样,咱们直接分析 ./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导入:直接将导入的内容打印出来即可