Webpack入门教程(三):打包文件分析
前言
在 webpack
官网上有这么一句话:webpack
对 import
和 export
语句提供了开箱即用般的支持,事实上 webpack
还能够很好地支持多种其他模块语法。不知道大家有没有对 webpack
在背后是如何 转译
的好奇?它是如何提供开箱即用的支持的?
本章我们就从 webpcak
打包文件后的文件开始分析,来深入的理解一下 webpack
的模块化原理。
1. 纯 CJS 模块
1.1 测试代码
// index.js
const { sayHello } = require("./utils");
sayHello();
/// utils.js
const sayHello = () => {
console.log("hello world");
};
module.exports = {
sayHello,
};
纯 CommonJS
模块的代码打包后的文件比较简单,我们直接用下面一张图来演示:
1.2 代码分析
我们将上图的代码分为五块。因为上面四块都是对象函数的定义,所以直接来到第五块,
- 这是一个自执行的函数,里面会调用
__webpack_require__
函数,而这个__webpack_require__
函数实际上就是require
函数的实现了。 - 进入
__webpack_require__
,首先会去判断当前require
的模块是否已经缓存过,缓存就直接获取并返回,__webpack_module_chche__
这个变量就是用于缓存已经加载过的模块。 - 没缓存过时就会去
__webpack_modules__
中获取,而__webpack_modules__
以key为模块的路径名称,value为模块这样键值对的形式存储了所有的模块。 - 最终获取到了
./src/utils.js
模块中的sayHello
方法并成功调用
1.3 总结
webpack
会将运行的入口模块index.js
中的require
关键词转化成__webpack_require__
放到最后自执行函数中执行- 在
__webpack_require__
函数中有两个很重要的全局变量就是__webpack_module_cache__
和__webpack_modules__
,__webpack_module_cache__
:用于缓存调用过得模块。__webpack_modules__
用于存储所有引入的模块,存储的key
为模块文件的路径,value
为拥有一个参数为module
的函数,这个函数最终的导出都会赋值到module.exports
对象上,在__webpack_require__
函数中最终返回的也就是这个对象。
2. 纯 ESM 模块
2.1 测试代码
// index.js
import { sayHello } from "./utils";
sayHello();
// utils.js
const sayHello = () => {
console.log("hello world");
};
export {
sayHello
}
打包后的结果
图示如下:
代码如下:
(() => {
"use strict";
var __webpack_modules__ = {
"./src/utils.js": (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
sayHello: () => sayHello,
});
const sayHello = () => {
console.log("hello world");
};
},
};
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 });
};
})();
var __webpack_exports__ = {};
(() => {
__webpack_require__.r(__webpack_exports__);
var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(/*! ./utils */ "./src/utils.js");
(0, _utils__WEBPACK_IMPORTED_MODULE_0__.sayHello)();
})();
})();
//# sourceMappingURL=main.bundle.js.map
2.1 代码分析
- 首先会执行
__webpack_require__.r(__webpack_exports__)
,这个函数是在第44
行定义的
(() => {
__webpack_require__.r = (exports) => {
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
}
Object.defineProperty(exports, "__esModule", { value: true });
};
})();
他的作用是在 __webpack_exports__
对象上定义了两个属性 Symbol.toStringTag
和 __esModule
,最终的 __webpack_exports__
对象如下图所示:
- 之后就会调用
__webpack_require
函数去加载utils
模块,__webpack_require
函数与Commonjs
里的是一模一样的,只不过__webpack_modules__
这个对象有些不同:
在 __webpack_modules__
对象中,或先后调用 r
函数和 d
函数, r
函数的作用上面已经讲过了不再复述,d
函数的作用其实就是将第二个参数对象的属性拷贝到第一个参数上。
- 最终返回获取到的
utils
模块执行:
(0, _utils__WEBPACK_IMPORTED_MODULE_0__.sayHello)();
这里的代码其实就是调用的 sayHello
函数
2.3 总结
- 整体的流程和纯 CJS 模块类似,都是通过
__webpack_require__
请求模块,通过__webpack_module_cache__
作缓存, 通过__webpack_modules__
存储所有导入模块,不同的是新增了一些辅助函数,比如__webpack_require__.r
:标记为纯ES模块__webpack_require__.d
:将模块中导出的变量、函数定义到module.exports
上__webpack_require__.o
:封装了hasOwnProperty
方法,判断一个属性是否是对象的自有属性(不是继承来的)
3. require 导入 ESM 模块
3.1 测试代码
// index.js
const { sayHello } = require("./utils");
sayHello();
// utils.js
const sayHello = () => {
console.log("hello world");
};
export { sayHello };
将以上测试代码打包后,对比纯 ESM 模块的打包文件,发现只有下图所示的区别。
代码如下:
(() => {
var __webpack_modules__ = {
"./src/utils.js": (__unused_webpack_module, __webpack_exports__, __webpack_require__) => {
"use strict";
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
sayHello: () => sayHello,
});
const sayHello = () => {
console.log("hello world");
};
},
};
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 });
};
})();
var __webpack_exports__ = {};
(() => {
const { sayHello } = __webpack_require__(/*! ./utils */ "./src/utils.js");
sayHello();
})();
})();
//# sourceMappingURL=main.bundle.js.map
3.2 代码分析
因为这里的代码整体和 纯 ESM 模块
打包后的代码相同,就不做分析了。
3.3 总结
require 导入 ESM 模块
的方式打包后的代码整体与 纯 ESM 模块
打包后的打包相同,仅一处不同,纯 ESM 模块
会将当前模块(index.js)的导出 __webpack_exports__
标记为 es模块
4. import 导入 CJS 模块
4.1 测试代码
// index.js
const sayHello = () => {
console.log("hello world");
};
module.exports = {
sayHello
}
// utils.js
const sayHello = () => {
console.log("hello world");
};
module.exports = {
sayHello
}
打包后的文件
(() => {
var __webpack_modules__ = {
"./src/utils.js": (module) => {
const sayHello = () => {
console.log("hello world");
};
module.exports = { sayHello };
},
};
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__.n = (module) => {
var getter = module && module.__esModule ? () => module["default"] : () => module;
__webpack_require__.d(getter, { a: getter });
return getter;
};
})();
(() => {
__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 });
};
})();
var __webpack_exports__ = {};
(() => {
"use strict";
__webpack_require__.r(__webpack_exports__);
var _utils__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__("./src/utils.js");
var _utils__WEBPACK_IMPORTED_MODULE_0___default = __webpack_require__.n(_utils__WEBPACK_IMPORTED_MODULE_0__);
(0, _utils__WEBPACK_IMPORTED_MODULE_0__.sayHello)();
})();
})();
//# sourceMappingURL=main.bundle.js.map
4.2 代码分析
- 执行代码
61
行将__webpack_exports__
变量标记为 es模块 - 执行代码
62
行__webpack_require__
方法获取对应模块 - 执行代码
63
行,这里有一个新的方法__webpack_require__.n
这个函数接受一个 module
作为参数,他会首先判断module是否是es模块,如果是,就会导出module['default']
,如果不是则导出 module
。__webpack_require__.d
函数负责使用 getter函数的值定义到 getter 对象上的属性 a 上。最后返回 getter 函数。
- 最终执行
sayHello
函数
4.3 总结
import 导入 CJS 模块
相比 纯 ESM 模块
,多了一个 __webpack_require__.d
函数,这个函数是用于获取es模块的默认导出的,因为在我们这个例子中utils.js
模块没有默认导出的对象。所以最终的 _utils__WEBPACK_IMPORTED_MODULE_0___default
就没有被使用到了。
5. 总结
我们一共分析了
- 纯 CJS 模块的打包文件
- 纯 ESM 模块的打包文件
- require 导入 ESM 模块的打包文件
- import 导入 CJS 模块的打包文件
四种情况,无论是哪一种方法,webpack 转译
的整体逻辑都是一样的。即:
- 通过
__webpack_require__
方法函数请求模块 - 通过
__webpack_module_cache__
全局对象缓存模块 - 通过
__webpack_modules__
全局对象存放模块 - 另外会通过一些辅助函数边缘情况的处理,比如
r
标识 es模块,d
函数赋值属性,o
函数。因为我们的场景比较简单,实际这些辅助函数还会更多更复杂,比如存在加载图片、css、异步加载的情况。
转载自:https://juejin.cn/post/7239925906067570747