学习笔记
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, FlagDependencyUsagePlugin
, FlagIncludedChunksPlugin
, ModuleConcatenationPlugin
, NoEmitOnErrorsPlugin
and TerserPlugin
.
none
Opts out of any default optimization options
可以看到mode
可以设置为development
, production
, none
, 并且development
和production
存在 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 (() => { var __webpack_modules__ = { "./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__ = {}; 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 ; } 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__ = {}; 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 _util_math__WEBPACK_IMPORTED_MODULE_0__ = __webpack_require__ ("./util/math.js" ); 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 最大的区别是
增加了 esmodule 属性标识
通过Object.defineProperty
代理 module.exports 属性代替直接赋值
多了 default
混用 CommonJs 和 ESModule
同时在项目中使用 CommonJS 和 ESModule 打包结果和上述大同小异, 可以直接示例代码中 esModule 文件夹下的打包产物, 这里不过多赘述.
配置 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 链接
以下是 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 配置, 但是这样的代码不方便阅读
inline-source-map
不会再 source-map 文件, 而是以 base64 编码 inline 在打包产物 js 中
eval-source-map
顾名思义, 会生成 eval 链接和 sourcep-map 链接
1 //# sourceMappingURL=data:application/json;charset=utf-8;base64 ,eyJ2ZXJzaW9uIjozLCJmaWxlIjoiLi9zcmMvaW5kZXguanMuanMiLCJtYXBwaW5ncyI6IkFBQUE7QUFDQTtBQUNBOztBQUVBO0FBQ0E7QUFDQTtBQUNBOztBQUVBLG1EQUFtRDs7QUFFbkQ7O0FBRUEiLCJzb3VyY2VzIjpbIndlYnBhY2s6Ly8vLi9zcmMvaW5kZXguanM/YjYzNSJdLCJzb3VyY2VzQ29udGVudCI6WyIvKiogbmVjZXNzYXJ5IHdoaWxlIHVzZUJ1aWx0SW5zOiAnZW50cnknICovXG4vLyBpbXBvcnQgJ2NvcmUtanMvc3RhYmxlJztcbi8vIGltcG9ydCAncmVnZW5lcmF0b3ItcnVudGltZS9ydW50aW1lJztcblxuY29uc3QgbWVzc2FnZSA9ICdIZWxsbyB3b3JsZCc7XG5jb25zdCBmb28gPSAobmFtZSkgPT4ge1xuICBjb25zb2xlLmxvZyhuYW1lKTtcbn07XG5cbmNvbnN0IHByb21pc2UgPSBuZXcgUHJvbWlzZSgocmVzb2x2ZSwgcmVqZWN0KSA9PiB7fSk7XG5cbmZvbyhtZXNzYWdlKTtcblxuY29uc29sZS5sb2coYWJjKTsiXSwibmFtZXMiOltdLCJzb3VyY2VSb290IjoiIn0=\n//
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