获取未经压缩的 Webpack 打包产出
为了获取 webpack 的原始产出,我们要对 webpack 配置进行如下操作:
- 移除 babel-loader;
optimization.minimize
设置为 false 以关闭 teser 对代码的压缩;
optimization.concatenateModules
设置为 false,避免 ESM 模块被提升到主 IIFE 中,不便于我们观察;
Webpack Runtime
在查看 webpack 导出内容前,我们需要先了解一下 webpack 的运行时方法、属性。
__webpack_require__.g
表示全局对象,这段代码的作用是确保在各种不同的 JavaScript 运行环境中,能够准确地获取到全局对象,从而保证模块化代码在不同环境中的兼容性:
1 2 3 4 5 6 7 8 9 10
| !(function () { __webpack_require__.g = (function () { if (typeof globalThis === 'object') return globalThis; try { return this || new Function('return this')(); } catch (e) { if (typeof window === 'object') return window; } })(); })();
|
__webpack_require__.p
表示脚本的公共路径,如果在 webpack 构建文件中设置了 output.publicPath
,则会被赋值为设置的路径:
1 2 3 4
| !(function () { __webpack_require__.p = '/'; })();
|
如果没有指定 publicPath,webpack 将会按照下面的方法,在不同的脚本运行环境来自动获取基础路径,如果无法正常获取则会报错:
1 2 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
| !(function () { var scriptUrl; if (__webpack_require__.g.importScripts) scriptUrl = __webpack_require__.g.location + ''; var document = __webpack_require__.g.document; if (!scriptUrl && document) { if (document.currentScript) scriptUrl = document.currentScript.src; if (!scriptUrl) { var scripts = document.getElementsByTagName('script'); if (scripts.length) { var i = scripts.length - 1; while (i > -1 && !scriptUrl) scriptUrl = scripts[i--].src; } } } if (!scriptUrl) throw new Error('Automatic publicPath is not supported in this browser'); scriptUrl = scriptUrl .replace(/#.*$/, '') .replace(/\?.*$/, '') .replace(/\/[^\/]+$/, '/'); __webpack_require__.p = scriptUrl; })();
|
在代码运行时,我们可以使用 __webpack_public_path__
来指定模块引用的基础路径,如:
1 2 3 4
| setTimeout(() => { __webpack_public_path__ = '/woo'; }, 1000);
|
编译后的代码实际上就是将 __webpack_require__.p
进行了重新赋值:
1 2 3
| setTimeout(() => { __webpack_require__.p = '/woo'; }, 1000);
|
__webpack_require__.o
这个方法是 Webpack 用于检测对象是否具有指定名称的属性,但不会检查原型链上的属性。
1 2 3 4 5 6
| !(function () { __webpack_require__.o = function (obj, prop) { return Object.prototype.hasOwnProperty.call(obj, prop); }; })();
|
__webpack_modules__
webpack 将编写代码时使用 cjs、esm 导入导出的模块进行转换后存放在该变量下,其格式为:
1 2 3
| type WebpackModules = { [moduleId: string]: (module, exports, __webpack_require__) => void }
|
__webpack_module_cache__
webpack 的模块缓存,默认为一个空对象,用于存放模块的注册结果,避免重复注册。
__webpack_require__
如果要使用 Webpack 处理过的模块,就需要使用该方法进行导入,该方法创建了一个 module
对象,并从 __webpack_modules__
拿到对应的模块注册方法,执行模块并将模块导出的内容挂载在 module
对象上,让后将 module
对象缓存在 __webpack_module_cache__
中:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| function __webpack_require__(moduleId) { var cachedModule = __webpack_module_cache__[moduleId]; if (cachedModule !== undefined) { return cachedModule.exports; } var module = (__webpack_module_cache__[moduleId] = { id: moduleId, loaded: false, exports: {}, });
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
module.loaded = true;
return module.exports; }
|
Webpack 打包 CJS 模块
创建一个 CJS 规范的模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14
| function add(a, b) { return a + b; }
function reduce(a, b) { return a - b; }
module.exports = { add, reduce, };
exports.CJS_CONSTANCE = 'constance';
|
webpack 会将代码打包为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
| var __webpack_modules__ = { 834: function ( module, exports, ) { function add(a, b) { return a + b; }
function reduce(a, b) { return a - b; }
module.exports = { add, reduce, }; exports.CONSTANCE = 'constance'; } }
|
当执行 __webpack_require__(834)
时,会将模块导出的对象挂载在 module
上,并返回 module.exports
。
Webpack 打包 ESM 模块
创建一个 ESM 规范的模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| export function cloneDeep(obj, hash = new WeakMap()) { return cloneObj; }
export function esmAdd(a, b) { return a + b; }
export function unusedFunc() { console.log('this is a unused function'); }
export const ESM_CONSTANCE = 'constance';
export default function () { console.log('this is a default export'); }
|
webpack 对于使用 ESM 规范引入的模块,并不是将其赋值到 module.exports
上,而是使用 __webpack_require__.d
:
1 2 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 33 34 35 36 37 38 39 40 41 42 43 44
| var __webpack_modules__ = { 624: function ( __unused_webpack_module, __webpack_exports__, __webpack_require__, ) { 'use strict'; __webpack_require__.d(__webpack_exports__, { K1: function () { return ESM_CONSTANCE; }, Xh: function () { return cloneDeep; }, ZP: function () { return __WEBPACK_DEFAULT_EXPORT__; }, }); function cloneDeep(obj, hash = new WeakMap()) { return cloneObj; }
function esmAdd(a, b) { return a + b; }
function unusedFunc() { console.log('this is a unused function'); }
const ESM_CONSTANCE = 'constance';
function __WEBPACK_DEFAULT_EXPORT__() { console.log('this is a default export'); } } };
|
__webpack_require__.d
接受一个 __webpack_require__
中创建的 exports
对象,以及一个导出声明 definition
。definition
是一个对象,其 key 为一个随机字符,value 为一个函数,函数执行后返回对应 ESM 模块导出的某个方法,__webpack_require__.d
就是将 definition
定义的各个方法挂载到 exports
对象上:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| !(function () { __webpack_require__.d = function (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__.d
对 ESM 导出的内容通过属性注册的方式注册到 module.exports
上而不是直接赋值,这是因为 ESM 和 CJS 的特性不同决定的:ESM 导出的内容是只读的,所以 exports
上的属性只有 getter 没有 setter;ESM 模块导出的是对值的引用,因此需要返回存放值的变量,而 CJS 返回的是对值的拷贝。
Webpack 打包混用模块
CJS 中使用 require 引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| const { esmAdd } = require('./index');
function add(a, b) { return esmAdd(a, b); }
function reduce(a, b) { return a - b; }
module.exports = { add, reduce, };
exports.CJS_CONSTANCE = 'constance';
|
打包后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20
| var __webpack_modules__ = { 834: function (module, exports, __webpack_require__) { const { esmAdd } = __webpack_require__(624);
function add(a, b) { return esmAdd(a, b); }
function reduce(a, b) { return a - b; }
module.exports = { add, reduce, };
exports.CJS_CONSTANCE = 'constance'; } }
|
使用 __webpack_require__
来引入其他模块,没啥特别的。
CJS 中使用 import 引入
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
| import { esmAdd } from './index';
function add(a, b) { return esmAdd(a, b); }
function reduce(a, b) { return a - b; }
module.exports = { add, reduce, };
exports.CJS_CONSTANCE = 'constance';
|
打包后:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26
| var __webpack_modules__ = { 834: function ( module, __unused_webpack___webpack_exports__, __webpack_require__, ) { var _index__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__(624); module = __webpack_require__.hmd(module);
function add(a, b) { return (0, _index__WEBPACK_IMPORTED_MODULE_0__ .bO)(a, b); }
function reduce(a, b) { return a - b; }
module.exports = { add, reduce, };
exports.CJS_CONSTANCE = 'constance'; } }
|
这样的产出在浏览器中是无法正常运行的,控制台会报错:
这是因为 __webpack_require__.hmd
对 module
对象进行了一层包裹,让 module 在执行 set 时产生报错,后续往 module.exports
上赋值自然就会报错了,模块无法执行,__webpack_require__.hmd
的实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17
| !(function () { __webpack_require__.hmd = function (module) { module = Object.create(module); if (!module.children) module.children = []; Object.defineProperty(module, 'exports', { enumerable: true, set: function () { throw new Error( 'ES Modules may not assign module.exports or exports.*, Use ESM export syntax, instead: ' + module.id, ); }, }); return module; }; })();
|
由此可见,在 CJS 中是无法使用 import
语法的,同样的使用 export
也会报错。
ESM 中使用 import 引入
1 2 3 4 5 6 7 8 9
| import cjsDefault from './utils/cjs.js';
const addResult = cjsDefault.add(1, 2);
console.log( 'App ready!', addResult, cjsDefault.CJS_CONSTANCE, );
|
编译为:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19
| var _utils_cjs_js__WEBPACK_IMPORTED_MODULE_1__ = __webpack_require__(834); var _utils_cjs_js__WEBPACK_IMPORTED_MODULE_1___default = __webpack_require__.n( _utils_cjs_js__WEBPACK_IMPORTED_MODULE_1__, );
const addResult = _utils_cjs_js__WEBPACK_IMPORTED_MODULE_1___default().add( 1, 2, );
console.log( 'App ready!', addResult, _utils_cjs_js__WEBPACK_IMPORTED_MODULE_1___default().CJS_CONSTANCE, );
(0, _utils__WEBPACK_IMPORTED_MODULE_2__ .ZP)();
|
可以看到编译后的代码使用 __webpack_require__.n
来包裹 CJS 模块的产出,这是因为 CJS 是没有默认导出,而该方法就是将一个 CJS 模块添加默认导出,默认导出值即为 module.exports
出的对象,用于兼容在 ESM 场景下的使用,具体实现如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| !(function () { __webpack_require__.n = function (module) { var getter = module && module.__esModule ? function () { return module['default']; } : function () { return module; }; __webpack_require__.d(getter, { a: getter }); return getter; }; })();
|
ESM 中使用 require 引入
在 ESM 模块中使用 require 是被允许的:
1
| import { add, CJS_CONSTANCE } from './utils/cjs.js';
|
会被转换为:
1
| const { cloneDeep, ESM_CONSTANCE } = __webpack_require__(624);
|
但需要注意的是,如果使用了 require 来引入 ESM 模块,及时模块中未使用的方法也是会被 __webpack_require__.d
注册导出的,这就会使模块失去 tree-shaking 的特性,因此谨慎使用 require 导入模块:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18
| __webpack_require__.d(__webpack_exports__, { ESM_CONSTANCE: function () { return ESM_CONSTANCE; }, cloneDeep: function () { return cloneDeep; }, default: function () { return __WEBPACK_DEFAULT_EXPORT__; }, esmAdd: function () { return esmAdd; }, unusedFunc: function () { return unusedFunc; }, });
|
此外,如果 ESM 中有 export default
的默认导出,转为 require
引入后会被挂载 default
属性下。
参考