【译文】更有效的调试webpack在构建时出现的错误
在这篇文章中,我们将写一个非常基础的webpack插件,然后向你们展示在webpack构建的时候,如何调试此插件触发的错误。
原文地址:This will make you more efficient at debugging Webpack unspecified build errors
最近,我在webpack构建中引入了许多的webpack插件。从目前的状况来看,大部分插件都缺乏完善的文档,这不可避免的导致我们错误的配置,最终导致在构建的时候出错。当然了这是我们学习过程的一部分,本身并不是什么大问题。但是目前最大的障碍是:webpack并不能准确的告诉我们错误出现在哪里(注:就是说哪个插件出错了)。我经常遇到以下的未指定的错误:
webpack在构建的时候会报这个错,但是它不会提供任何错误来自于哪里的线索。如果你也不知道在哪里查看错误,你在调试的时候可能会花费好几天的时间。你必须将插件一个接一个的删掉来确定导致出现错误的插件。这是非常耗时且低效的。
本文将向你展示一种简单的方法:从而快速的确定错误出现的原因。这是一项非常有用的技术,它可以潜在的节约你的时间。当我尝试在angular-cli
之外使用ngtools/webpack AOT plugin
插件的时候非常好用。我相信它也能帮助到你。
HelloWorldCheckerPlugin
在向你展示如何调试错误之前,首先让我看看这些可能出现的错误是如何发生的。为此,我们将写一个简单的webpack插件:用一个文件的路径作为选项,并检查这个文件是否包含Hello World!
字符串,然后将检查结果打印出来。
plugins: [
new HelloWorldCheckerPlugin({path: 'toinspect.txt'})
]
这个插件配置看上去很简单。但是,假设插件的作者忘了说明他期望的文件路径是绝对路径而不是相对路径。这是一种很常见的:我们开始自己写了个插件使用,之后开源到github上。结果就是,插件文档(如果有的话)可能不包含作者自己知道并且认为是很明显(注:插件作者认为简单的不用来提醒)的说明。
接着我们来看下这个插件的实现:
const fs = require('fs');
const path = require('path');
class HelloWorldCheckerPlugin {
constructor(options) {
this.options = options;
}
apply(compiler) {
compiler.plugin('make', (compilation, cb) => this._make(compilation, cb));
}
_make(compilation, cb) {
try {
const file = fs.readFileSync(path.resolve('/', this.options.path), 'utf8');
if (file.includes('Hello World!')) {
console.log(`The file ${this.options.path} contains 'Hello World!' string`);
} else {
console.log(`The file ${this.options.path} doesn't contain 'Hello World!' string`)
}
cb();
} catch (e) {
compilation.errors.push(e);
cb();
}
}
}
exports.HelloWorldCheckerPlugin = HelloWorldCheckerPlugin;
我们简单的进入编译过程的make
阶段时,会在_make
方法中尝试去执行检查。这行代码表明当前的实现是希望一个文件的绝对路径.
fs.readFileSync(path.resolve('/', this.options.path), 'utf8');
现在假设你下载了此插件并且以如下的方式用相对路径进行配置:
const HelloWorldCheckerPlugin = require('./plugin').HelloWorldCheckerPlugin;
const path = require('path');
module.exports = {
entry: "./main",
output: {
path: __dirname + "/dist",
filename: "bundle.js"
},
plugins: [
new HelloWorldCheckerPlugin({path: 'toinspect.txt'})
]
};
在执行构建的时候会出现以下未指定的错误:
在以上示例中我们可以很简单的确定错误就出现在HelloWorldCheckerPlugin
中。但是,我们想象以下场景:
- 像我们在开篇中提到的那种描述性很少的错误
- 你配置了10+的插件,但是只有一部分插件使用了
toinspect.txt
文件 toinspect.txt
文件不仅在插件中使用,在好几个业务代码文件中也使用了
我们当场就懵逼了。我们甚至不清楚从哪里开始调试。如果webpack能提供出现错误的文件名称那就好办了。但是我们从webpack当前的实现来看这似乎不现实,因为这也依赖于插件作者提供所有必须的错误详情去解决问题。
Compilation errors(编译错误)
理解Compilation.errors
数组很重要:webpack将编译过程中相关的错误全部存储于此:
class Compilation extends Tapable {
constructor(compiler) {
super();
...
this.errors = [];
}
}
当错误发生时,每一个插件都被要求将出现的错误填充在此数组。我们写的非常牛逼的HelloWorldCheckerPlugin
插件当然也遵守此规范,将错误添加到数组中。
_make(compilation, cb) {
try {
...
} catch (e) {
compilation.errors.push(e);
cb();
}
}
去理解错误发生的位置我们需要简单的在数组执行push
时候打断点然后去检查调用堆栈。因此我们在错误被添加到数组的时候就能看到错误发生的位置。完美,我们就按照这个去搞。
Intercepting push
to errors array(在执行push
方法时打断点)
我目前使用chrome进行调试node脚本。当然调试node脚本可以用任何的node debugger。我使用chrome是因为它比webstorm内置的调试快很多。这篇文章介绍了如何使用chrome去调试node脚本。
在上边文章中提到的用chrome去调试node脚本,我们只需要使用--inspect
选项。我使用它的变种--inspect-brk
在第一条语句的时候暂停,因为我需要将相关的文件中打断点。所以我们运行一下的命令来以调试模式运行webpack:
node --inspect-brk node_modules/webpack/bin/webpack.js
我经常对这个进行别名处理,比如dlwpc
(表示本地调试webpack),能从任何webpack安装文件夹中进行调试。(注:alias [别名]=[指令名称] , alias为linux命令)
alias dlwpc="node --inspect-brk node_modules/webpack/bin/webpack.js"
现在我们在push
断点处进行替换:
this.errors = []
替换为
this.errors.push = ()=> { Array.prototype.push.call(this, arguments); debugger }
无论在哪里执行push
方法,程序都会暂停,我们能知道它是被哪块代码调用的。以这种方式注入我们就不用去修改webpack的源码。我们可以利用console控制台进行替换。首先,我们来以调试模式运行webpack:
$ dlwpc
根据教程我们在浏览器地址栏输入:
chrome://inspect
在Remote Target
中点击inspect
有时候,出现如下画面会花费一些时间,所以不要着急。一旦你点击后,会出现一个单独的chrome调试窗口并且在执行首行代码的时候停下:
现在,我们需要访问Compilation
类在node_modules/webpack/lib/Compilation.js
文件中,但是它是不可用的因为chrome还没有加载它。你有三种方法:
- 在源码中
Compilation
类的构造函数处添加debugger
语句。这是最不方便的因为你还有将源码还原,但是有时我还是会用这种方式(源码文件已打开,并且不希望有别的麻烦发生) - 通过文件系统手动上传webpack文件。这应该是最方便的选项,但有时候这个方法会失效(当你知道你只调试一个package时是很容易找到并上传的,但是当有很多package时你不知道上传那个package时,你上传整个
node_modules
文件夹是不可取的。还有,有可能文件并不在node_modules
里边)。 - 运行webpack一次直至结束,然后inspector 将会加载和保存上次运行中用的文件。然后你找到文件,然后打上断点。我也经常使用这种方法。
让我们展示第二种和第三种方式:
Running the webpack till the end(完成运行一次webpack)
首次运行webpack的时候,我在webpack.js
主文件的最后一行放置了一个断点:
接着用Resume script execution (F8)
恢复脚本执行,并等待webpack到达断点。此时,在所有执行期间的文件已经被加载进来,在windows系统使用Ctrl+P
可以很轻松打开Compilation
类:
在this.errors=[]
被定义后打一个断点:
因为webpack已经执行此文件,我们重新运行wepack时执行到此处时应该暂停。有时候chrome会忘记上次执行的断点,我们需要执行webpack两次或者使用debugger
语句。
Uploading webpack files(上传webpack文件)
为了上传webpack文件,我们需要到Sources
面板Filesystem
选项下。点击Add folder to workspace
并在文件系统中选择webpack
模块。
按照上边操作后,Compilation
类就可以被访问了,你可以打开它
放置一个断点:
Overriding errors
array(重写errors
数组)
现在,一旦在运行时到达断点暂时,你需要用我们自定义对象覆盖this.errors
。正如之前提到过的,使用控制面板来实现:
在windows系统下按Esc
打开控制面板。如果你打不开,请查阅相关文档。在任何代码写入数组之前用自定义对象替换this.errors
非常重要。
现在我们回复代码执行,看到如下发生。这个断点处暂停运行了,在函数调用堆栈中我们可以很清晰的看到错误来自哪里:
如果我在调用堆栈中点击plugin.js:22
,在我们的插件能看到正在添加错误:
芜湖,起飞。我们刚刚定位到了错误。现在我们知道错误来自哪里了,我们可以尝试配置或者阅读插件源码来了解我们写错了什么。
为了向你们展示我们的插件牛逼和正确的实现,我们提供了一个绝对路径:
plugins: [
new HelloWorldCheckerPlugin({path: path.resolve(__dirname, 'toinspect.txt')})
]
我们能得到如下:
Github
如果你想尝试上述配置,我在github上创建了仓库。你下载仓库后,在HelloWorldCheckerPlugin
中修改提供给HelloWorldCheckerPlugin
插件的参数(路径),就会报错:
plugins: [
new HelloWorldCheckerPlugin({path: 'toinspect.txt'})
]