在上一课时中我们聊了开发时的热更新机制和其中的技术细节,热更新能帮助我们在开发时快速预览代码效果,免去了手动执行编译和刷新浏览器的操作。课后留的思考题是找一个实现了 HMR 的 Loader,查看其中用到哪些热替换的 API,希望通过这个题目能让你加深对相关知识点的印象。
那么除了热更新以外,项目的开发环境还有哪些在影响着我们的开发效率呢?在过去的工作中,公司同事就曾问过我一个问题:为什么我的项目在开发环境下每次构建还是很卡?每次保存完代码都要过 1~2 秒才能看到效果,这是怎么回事呢?其实这里面的原因主要是这位同事在开发时选择的 Source Map 设定不对。今天我们就来具体讨论下这个问题。首先,什么是 Source Map 呢?
在前端开发过程中,通常我们编写的源代码会经过多重处理(编译、封装、压缩等),最后形成产物代码。于是在浏览器中调试产物代码时,我们往往会发现代码变得面目全非,例如:
因此,我们需要一种在调试时将产物代码显示回源代码的功能,source map 就是实现这一目标的工具。
source-map 的基本原理是,在编译处理的过程中,在生成产物代码的同时生成产物代码中被转换的部分与源代码中相应部分的映射关系表。有了这样一张完整的映射表,我们就可以通过 Chrome 控制台中的"Enable Javascript source map"来实现调试时的显示与定位源代码功能。
对于同一个源文件,根据不同的目标,可以生成不同效果的 source map。它们在构建速度、质量(反解代码与源代码的接近程度以及调试时行号列号等辅助信息的对应情况)、访问方式(在产物文件中或是单独生成 source map 文件)和文件大小等方面各不相同。在开发环境和生产环境下,我们对于 source map 功能的期望也有所不同:
在 Webpack 中,通过设置 devtool 来选择 source map 的预设类型,文档中共有 20 余种 source map 的预设(注意:其中部分预设实际效果与其他预设相同,即页面表格中空白行条目)可供选择,这些预设通常包含了 "eval" "cheap" "module" "inline" "hidden" "nosource" "source-map" 等关键字的组合,这些关键字的具体逻辑如下:
webpack/lib/WebpackOptionsApply.js:232
if (options.devtool.includes("source-map")) {
const hidden = options.devtool.includes("hidden");
const inline = options.devtool.includes("inline");
const evalWrapped = options.devtool.includes("eval");
const cheap = options.devtool.includes("cheap");
const moduleMaps = options.devtool.includes("module");
const noSources = options.devtool.includes("nosources");
const Plugin = evalWrapped
? require("./EvalSourceMapDevToolPlugin")
: require("./SourceMapDevToolPlugin");
new Plugin({
filename: inline ? null : options.output.sourceMapFilename,
moduleFilenameTemplate: options.output.devtoolModuleFilenameTemplate,
fallbackModuleFilenameTemplate:
options.output.devtoolFallbackModuleFilenameTemplate,
append: hidden ? false : undefined,
module: moduleMaps ? true : cheap ? false : true,
columns: cheap ? false : true,
noSources: noSources,
namespace: options.output.devtoolNamespace
}).apply(compiler);
} else if (options.devtool.includes("eval")) {
const EvalDevToolModulePlugin = require("./EvalDevToolModulePlugin");
new EvalDevToolModulePlugin({
moduleFilenameTemplate: options.output.devtoolModuleFilenameTemplate,
namespace: options.output.devtoolNamespace
}).apply(compiler);
}
如上面的代码所示, devtool 的值匹配并非精确匹配,某个关键字只要包含在赋值中即可获得匹配,例如:'foo-eval-bar' 等同于 'eval','cheapfoo-source-map' 等同于 'cheap-source-map'。
各字段的作用各不相同,为了便于记忆,我们在这里简单整理下这些关键字的作用:
从上面的规则中我们还可以看到,根据不同规则,实际上 Webpack 是从三种插件中选择其一作为 source map 的处理插件。
通过上面的代码分析,我们了解了不同参数在 Webpack 运行时起到的作用。那么这些不同参数组合下的各种预设对我们的 source map 生成又各自会产生什么样的效果呢?下面我们通过示例来看一下。
下面,以课程示例代码 03_develop_environment 为例,我们来对比下几种常用预设的差异(为了使时间差异更明显,示例中引入了几个大的类库文件):
*注1:“/”前后分别表示产物 js 大小和对应 .map 大小。 *注2:“/”前后分别表示初次构建时间和开启 watch 模式下 rebuild 时间。对应统计的都是 development 模式下的笔者机器环境下几次构建时间的平均值,只作为相对快慢与量级的比较。
从上图的数据中我们不难发现,选择不同的 devtool 类型在以下几个方面会产生不同的效果。
在这里我们对开发环境下使用的推荐预设做一个总结(生产环境的预设我们将在之后的构建效率篇中再具体分析):
了解了开发环境下如何选择 source map 预设后,我们再来补充几种工具和脚手架中的默认预设:
除了上面讨论的这些简单的预设外,Webpack 还允许开发者直接使用对应插件来进行更精细化的 source map 控制,在开发环境下我们首选的还是 EvalSourceMapDevToolPlugin。下面我们再来看看如何直接使用这个插件进行优化。
在 EvalSourceMapDevToolPlugin 的 传入参数中,除了上面和预设相关的 filename、append、module、columns 外,还有影响注释内容的 moduleFilenameTemplate 和 protocol,以及影响处理范围的 test、include、exclude。这里重点看处理范围的参数,因为通常我们需要调试的是开发的业务代码部分,而非依赖的第三方模块部分。因此在生成 source map 的时候如果可以排除第三方模块的部分而只生成业务代码的 source map,无疑能进一步提升构建的速度,例如示例:
webpack.config.js
...
//devtool: 'eval-source-map',
devtool: false,
plugins: [
new webpack.EvalSourceMapDevToolPlugin({
exclude: /node_modules/,
module: true,
columns: false
})
],
...
在上面的示例中,我们将 devtool 设为 false,而直接使用 EvalSourceMapDevToolPlugin,通过传入 module: true 和 column:false,达到和预设 eval-cheap-module-source-map 一样的质量,同时传入 exclude 参数,排除第三方依赖包的 source map 生成。保存设定后通过运行可以看到,在文件体积减小(尽管开发环境并不关注文件大小)的同时,再次构建的速度相比上面表格中的速度提升了将近一倍,达到了最快一级。
类似这样的优化可以帮助我们在一些大型项目中,通过自定义设置来获取比预设更好的开发体验。
在今天这一课时中,我们主要了解了提升开发效率的另一个重要工具——source map 的用途和使用方法。我们分析了 Webpack 中 devtool 的各种参数预设的组合规则、使用效果及其背后的原理。对于开发环境,我们根据一组示例对比分析来了解通常情况下的最佳选择,也知道了如何直接使用插件来达到更细致的优化。
限于篇幅原因,关于 source map 这一课时还有两个与提效无关的小细节没有提到,一个是生成的 source map 的内容,即浏览器工具是如何将 source map 内容映射回源文件的,如果你感兴趣可以通过这个链接进一步了解;另一个是我们在控制台的网络面板中通常看不到 source map 文件的请求,其原因是出于安全考虑 Chrome 隐藏了 source map 的请求,需要通过 net-log 来查询。
最后还是留一个小作业:不知道你有没有留意过自己项目里的 source map 使用的是哪一种生成方式吗?可以根据这一课时的内容对它进行调整和观察效果,也欢迎你在课后留言区讨论项目里对 source map 的优化方案。