webpack学习笔记(5)

学习笔记

mode in webpack config

使用:

1
2
3
module.exports = {
mode: "development",
};

简单看下 webpack 官网对mode的描述:

option description
development Sets process.env.NODE_ENV on DefinePlugin to value development.
Enables useful names for modules and chunks.
production Sets process.env.NODE_ENV on DefinePlugin to value production.
Enables deterministic mangled names for modules and chunks,
 FlagDependencyUsagePluginFlagIncludedChunksPlugin,
 ModuleConcatenationPluginNoEmitOnErrorsPlugin and TerserPlugin.
none Opts out of any default optimization options

可以看到mode可以设置为development, production, none, 并且developmentproduction 存在 webpack 预设的一些默认配置. 后面例子为特殊说明mode都为development

webpack 模块化原理

之前提到浏览器目前只能解析 ESM 模块(某些浏览器甚至无法解析模块), 需要添加<script type="module">, 但是通过 webpack 打包的代码, 允许 使用各种各样的模块化, AMD, CMD, CommonJS, ESModule 等, 它是如何帮助我们实现代码中支持模块化的呢? 这里以最常用的 CommonJS 和 ESModule 举例, 依次介绍 webpack 中:

  • CommonJS 模块化实现原理
  • ES Module 模块化实现原理
  • CommonJS 加载 ES Module 原理
  • ES Module 加载 CommonJS 原理

每种情况都会以简单案例讲解, 开始之前webpack.config.js先配置devtool: "source-map", 因为development模式下打包默认使用eval, 不方便阅读打包产物代码, 具体含义后续介绍.

IIFE 立即执行函数

产物中会出现大量立即执行函数, 存在多种写法, 可以先阅读JavaScript 中的立即执行函数

CommonJS 模块化实现原理

入口文件index.js:

1
2
3
4
const { dateFormat, prizeFormat } = require("./util/format");

console.log(dateFormat(new Date()));
console.log(prizeFormat(100));

引入模块文件:

1
2
3
4
5
6
7
8
9
10
11
12
function dateFormat(date) {
return "2022-09-14";
}

function prizeFormat(prize) {
return "100.00";
}

module.exports = {
dateFormat,
prizeFormat,
};

打包后的文件bundle.js:

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
45
46
47
48
49
50
51
52
// 立即执行函数建立作用域, 避免变量污染
(() => {
// 定义当前作用域下全局对象, 包含所有require引用的模块
var __webpack_modules__ = {
// key为模块路径, 值为包含模块内容的函数
"./util/format.js": (module) => {
function dateFormat(date) {
return "2022-09-14";
}

function prizeFormat(prize) {
return "100.00";
}

module.exports = {
dateFormat,
prizeFormat,
};
},
};
// 创建对象用于缓存
var __webpack_module_cache__ = {};
// commonjs中require函数的polyfill实现
function __webpack_require__(moduleId) {
// 根据模块路径获取缓存中的模块内容
var cachedModule = __webpack_module_cache__[moduleId];
// 如果存在直接返回
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 缓存中不存在, 将module变量和在缓存中都初始化一个对象, 属性为exports
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
// 将模块内容赋值给module, 从而得到module.exports, 后续能够从缓存中获取
// 实例中还没用到模块中引用其他模块的情况, 暂不讲解后两个参数
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);
// 返回模块内容
return module.exports;
}

var __webpack_exports__ = {};

// 立即执行函数输出结果
(() => {
// 下面引用模块逻辑和打包前源码相同, 不做解释
const { dateFormat, prizeFormat } = __webpack_require__("./util/format.js");

console.log(dateFormat(new Date()));
console.log(prizeFormat(100));
})();
})();

打包后的所有逻辑都有对应的注释, 可以看到 webpack 通过__webpack_modules__对象存储模块路径和内容, __webpack_module_cache__对象缓存所有module.exports, __webpack_require__函数简单实现了 CommonJS 中require

ESM 模块化实现原理

入口文件esm_index.js:

1
2
3
4
import math from "./util/math";

console.log(math.sum(10, 20));
console.log(math.mul(10, 20));

引入模块文件:

1
2
3
4
5
6
7
8
9
10
11
12
function sum(a, b) {
return a + b;
}

function mul(a, b) {
return a * b;
}

export default {
sum,
mul,
};

打包产物:

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
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
(() => {
"use strict";
var __webpack_modules__ = {
"./util/math.js": (
__unused_webpack_module,
__webpack_exports__,
__webpack_require__
) => {
__webpack_require__.r(__webpack_exports__);
__webpack_require__.d(__webpack_exports__, {
default: () => __WEBPACK_DEFAULT_EXPORT__,
});
function sum(a, b) {
return a + b;
}

function mul(a, b) {
return a * b;
}

const __WEBPACK_DEFAULT_EXPORT__ = {
sum,
mul,
};
},
};

var __webpack_module_cache__ = {};
// 和commonjs打包结果类似, exports赋值, 写入缓存, 返回module.exports
function __webpack_require__(moduleId) {
var cachedModule = __webpack_module_cache__[moduleId];
if (cachedModule !== undefined) {
return cachedModule.exports;
}
// 缓存
var module = (__webpack_module_cache__[moduleId] = {
exports: {},
});
// exports复制
__webpack_modules__[moduleId](module, module.exports, __webpack_require__);

return module.exports;
}

(() => {
// define 这里
__webpack_require__.d = (exports, definition) => {
for (var key in definition) {
// 如果definition hasOwnProperty, module.exports没有该属性, 这里是default熟悉
if (
__webpack_require__.o(definition, key) &&
!__webpack_require__.o(exports, key)
) {
// 将__webpack_modules__中的属性代理到module.exports中, 和commonjs不同, 不是简单的复制
Object.defineProperty(exports, key, {
enumerable: true,
get: definition[key],
});
}
}
};
})();

(() => {
__webpack_require__.o = (obj, prop) =>
Object.prototype.hasOwnProperty.call(obj, prop);
})();

(() => {
// 给当前module.exports添加标识ESM属性
__webpack_require__.r = (exports) => {
// 如果浏览器支持Symbol数据类型, 还会添加一个
if (typeof Symbol !== "undefined" && Symbol.toStringTag) {
Object.defineProperty(exports, Symbol.toStringTag, { value: "Module" });
}
// 添加__esModule属性
Object.defineProperty(exports, "__esModule", { value: true });
};
})();

// 缓存对象
var __webpack_exports__ = {};

(() => {
__webpack_require__.r(__webpack_exports__);
var _util_math__WEBPACK_IMPORTED_MODULE_0__ =
__webpack_require__("./util/math.js");
// 使用exports映射default对象的方法
console.log(_util_math__WEBPACK_IMPORTED_MODULE_0__["default"].sum(10, 20));
console.log(_util_math__WEBPACK_IMPORTED_MODULE_0__["default"].mul(10, 20));
})();
})();

可以看到其实和 commonjs 最大的区别是

  1. 增加了 esmodule 属性标识
  2. 通过Object.defineProperty代理 module.exports 属性代替直接赋值
  3. 多了 default

混用 CommonJs 和 ESModule

同时在项目中使用 CommonJS 和 ESModule 打包结果和上述大同小异, 可以直接示例代码中 esModule 文件夹下的打包产物, 这里不过多赘述.

devtool in webpack

配置 devtool 能够帮助我们在程序报错更好地定位问题, webpack 中提供了 26 种 devtool 的值, 下面详细介绍.

mode 为development, devtool 默认值为eval;
mode 为production, devtool 默认值为(nond), 这里的 none 不是字符串, 表示没有该项配置;

source-map

devtool 设置为source-map后, 打包产物会多出来一个bundle.js.map文件

并且在bundle.js最后会添加 source-map 链接

1
//# sourceMappingURL=bundle.js.map

以下是 mode 为production, devtool 设置source-map生成的bundle.js.map内容:

1
2
3
4
5
6
7
8
9
10
11
{
"version": 3,
"file": "bundle.js",
"mappings": "AASgB,IAAIA,SAAQ,CAACC,EAASC,KAAV,IAH1BC,QAAQC,IAFM,eAShBD,QAAQC,IAAIC",
"sources": ["webpack:///./src/index.js"],
"sourcesContent": [
/*源代码字符串*/
],
"names": ["Promise", "resolve", "reject", "console", "log", "abc"],
"sourceRoot": ""
}

每个字段含义如下:

  • version:Source map 的版本,目前为 3。
  • file:转换后的文件名。
  • sourceRoot:转换前的文件所在的目录。如果与转换前的文件在同一目录,该项为空。
  • sources:转换前的文件。该项是一个数组,表示可能存在多个文件合并。有时也会包含 webpack 库中的文件
  • names:转换前的所有变量名和属性名。mode 为development时代码并没有进行混淆, 变量名和属性名都不变, names 为空数组
  • mappings:记录位置信息的字符串,记录原文件到打包文件的所有映射, 详情可以查阅阮一峰博客

默认浏览器会开启 source-map, 配置如图:

在浏览器中可以看到 source-map 映射的源文件

eval

JavaScript 原生支持的 eval 函数可以在结尾添加类似 source-map 的 sourceURL 用于打包前的源文件, 从而实现映射, 这也是mode: "development"的默认 devtool 配置, 但是这样的代码不方便阅读

1
//# sourceURL=webpack:///./src/index.js?

inline-source-map

不会再 source-map 文件, 而是以 base64 编码 inline 在打包产物 js 中

1
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiYnVuZGxlLmpzIiwibWFwcGluZ3MiOiI7Ozs7O0FBQUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBOztBQUVBLG1EQUFtRDs7QUFFbkQ7O0FBRUEsaUIiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9zcmMvaW5kZXguanMiXSwic291cmNlc0NvbnRlbnQiOlsiLyoqIG5lY2Vzc2FyeSB3aGlsZSB1c2VCdWlsdEluczogJ2VudHJ5JyAqL1xuLy8gaW1wb3J0ICdjb3JlLWpzL3N0YWJsZSc7XG4vLyBpbXBvcnQgJ3JlZ2VuZXJhdG9yLXJ1bnRpbWUvcnVudGltZSc7XG5cbmNvbnN0IG1lc3NhZ2UgPSAnSGVsbG8gd29ybGQnO1xuY29uc3QgZm9vID0gKG5hbWUpID0+IHtcbiAgY29uc29sZS5sb2cobmFtZSk7XG59O1xuXG5jb25zdCBwcm9taXNlID0gbmV3IFByb21pc2UoKHJlc29sdmUsIHJlamVjdCkgPT4ge30pO1xuXG5mb28obWVzc2FnZSk7XG5cbmNvbnNvbGUubG9nKGFiYyk7Il0sIm5hbWVzIjpbXSwic291cmNlUm9vdCI6IiJ9

eval-source-map

顾名思义, 会生成 eval 链接和 sourcep-map 链接

1
//# sourceMappingURL=data:application/json;charset=utf-8;base64,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9zcmMvaW5kZXguanMuanMiLCJtYXBwaW5ncyI6IkFBQUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBOztBQUVBLG1EQUFtRDs7QUFFbkQ7O0FBRUEiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9zcmMvaW5kZXguanM/YjYzNSJdLCJzb3VyY2VzQ29udGVudCI6WyIvKiogbmVjZXNzYXJ5IHdoaWxlIHVzZUJ1aWx0SW5zOiAnZW50cnknICovXG4vLyBpbXBvcnQgJ2NvcmUtanMvc3RhYmxlJztcbi8vIGltcG9ydCAncmVnZW5lcmF0b3ItcnVudGltZS9ydW50aW1lJztcblxuY29uc3QgbWVzc2FnZSA9ICdIZWxsbyB3b3JsZCc7XG5jb25zdCBmb28gPSAobmFtZSkgPT4ge1xuICBjb25zb2xlLmxvZyhuYW1lKTtcbn07XG5cbmNvbnN0IHByb21pc2UgPSBuZXcgUHJvbWlzZSgocmVzb2x2ZSwgcmVqZWN0KSA9PiB7fSk7XG5cbmZvbyhtZXNzYWdlKTtcblxuY29uc29sZS5sb2coYWJjKTsiXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//# sourceURL=webpack-internal:///./src/index.js

cheap-source-map

source-map 对于报错信息会详细到某行某列, cheap-source-map 只会精细到行**(网上是这么说的, 虽然我验证的时候设置 source-map 或 cheap-source-map 好像没什么区别)**

cheap-module-source-map

如果源码经过 loader 例如 babel-loader 处理后, 使用 cheap-source-map 会链接到 loader 处理过的文件, 和源码还是有点区别的, 使用 cheap-module-source-map 就会正常指向源文件了.
这也是 react 官方脚手架本地环境下默认的 devtool 配置.

cheap-source-map:

cheap-module-source-map:

hidden-source-map

会生成 source-map 文件, 但不会在 javascript 文件中链接, 一般用于前端错误信息上报, 后端通过错误中的行列信息还原出源文件的报错位置.

nosources-source-map

使用 nosources 关键字生成的 source-map 文件中不包含 sourcesContent 内容, 因此调试时只能看到源文件的行列错误信息, 无法看到源码

上面介绍了比较典型的几个 devtool 配置, 理解了每个关键字的含义也就知道如何配置了. 所有的 devtool 配置都是以下几个关键字的排列组合

[inline-|hidden-|eval][nosources-][cheap-[module-]][source-map]

下面给出不同环境下的最佳配置:

  • 开发/测试环境: source-map or cheap-module-source-map
  • 线上环境: false or 根据上报需求使用 hidden, nosources

示例代码

https://github.com/Mariana-Yui/fe-learn-code/tree/main/learn-webpack/day5

reference

JavaScript 中的立即执行函数
一文搞懂 SourceMap 以及 webpack devtool


webpack学习笔记(5)
https://mariana-yui.github.io/2022/09/15/2022-09-15-study-webpack-day5/
作者
Mariana
发布于
2022年9月15日
许可协议