在日常开发中,我们多多少少会遇到些问题,有时候是自己的写法有错误,这时候可能就要先检查一遍,看看文档,看看是哪里的问题。
但有时候也有可能是框架/工具的源码错误,虽然一般这种情况很少发生,因为一般框架/工具都会做了比较多的单元测试,经过开源社区的验证,出错的概率比较少,但也不一定所有情况都能测试到。
那么,如果真的认为是源码的 Bug,我们该怎么去定位呢?
本篇文章讲解介绍我最近遇到的一个真实例子,并通过自己的一些经验、调试技巧,去定位问题
发现问题
在我的某个项目中,当我使用 pnpm i --fix-lockfile
时,一定会报如下错误:
运行 pnpm i
的时候,不会报错,只有运行 pnpm i --fix-lockfile
会报错。在一些业务场景下,我们偏向于使用 pnpm i --fix-lockfile
,当然我也可以改为用 pnpm i
,那故事就结束了,全剧终hhh。
当然我还是稍微努力了一下下,准备提个 issue 看看。
既然要提 issue,那就得首先觉得它是 pnpm 自身的问题,不是我写的代码有问题。我个人主要是有以下原因:
- 我就是安装个依赖,这能有什么错哦。。。而且它
pnpm i
是能安装的 --fix-lockfile
这个选项,肯定比仅仅使用pnpm i
的场景少,那在极端场景下,可能pnpm
的单元测试没覆盖到,有问题也是正常的- 我是学过英文的,错误信息很明显就说,
vite@4.0.4_@types+node@17.0.45
这个版本解析不出来,个人感觉应该是要解析成 vite 4.0.4,我 package.json 也是这么写的,pnpm 自己加的其他东西,那肯定不关我的事情呀。而且 pnpm 的 lock 文件也是用vite@4.0.4_@types+node@17.0.45
,那是你的问题没错了 - 错误信息中出现
@vitejs/plugin-basic-ssl
,有可能是这个包不行,但pnpm i
既然能正常安装,就证明人家本身没问题,是 pnpm 的问题。
因此,我提了个 issue,就贴了个截图,然后写 pnpm i --fix-lockfile
安装失败,是解析版本失败了,还贴了 pnpm 的锁文件。
我觉得我已经写得很明白了,这么一个 package 的版本解析错误问题,作者应该一看就懂。。。了吧
结果不出所料,作者也看不懂,让我提供一个最小的复现 Demo。
这里补充一个知识点,一般提 issue 的时候,都要带上最小的复现 Demo,不然人家作者也没办法复现你的问题。
但是鸭,很多时候,开发者可能遇到问题了,却提供不出来,主要有以下原因:
- 项目非常大,不知道哪里有问题,因此不知道怎么做一个最小复现的 Demo
- 是公司的项目,不能将代码提供出去
我是两个原因都有,因此不是我不想提供 Demo,而是我也搞不出来。。。因此想碰碰运气,说不定作者一看就知道呢hhh,结果不出所料,还是得提供 Demo。
很多人提供不了最小复现 Demo,开源库作者也没办法知道问题,然后问题就不了了之。
因此,很多人也只能走到这一步,然后故事就结束了。
但其实不是完全不可能提供一个 Demo,看要不要再努力一下下。这时候人和人的差别就会显现出来了。
- 有的人可能觉得换一种方式就行了
- 有的人可能觉得没多大影响,不折腾了
- 有的人可能觉得,我就是要搞出来。
当我第一次遇到这个问题的时候,我也是抱着,算了不管了
后来再遇上,真烦,不如提个 issue 碰碰运气吧
再后来多遇上几次,实在不想忍了,晚上调试一下看看,就花一个晚上,不行拉倒
因此才有了接下来的一些努力。
有时候,你离开源贡献,就只有一念之差。只是,有些人选择放弃,有的人选择再努力一下。
调试代码
光有决心还是没有的,得实际行动。
但一个巨大的问题摆在面前,pnpm 的代码我也没看过鸭,调个啥玩意???
因此,第一个问题,是怎么把 pnpm 源码跑起来调试呢?
pnpm 源码调试
之前看了神光大佬的调试小册,学到了很多调试相关的知识,感兴趣的可以学习一下
一般情况下,如何知道一个开源仓库要怎么进行调试呢?
- 看仓库的 CONTRIBUTING.md 文档,按道理比较常见的开源仓库都会有
- 找别人总结过调试文章
我随便在掘金,找了一遍文章,毕竟能调试,能打断点就行。因此如何调试的问题就解决了。
这里总结一下:
- pnpm i 先安装 pnpm 源码的依赖
- pnpm run compile,执行源码所有包的构建(pnpm 是 monorepo 仓库)
- 用 node 执行 pnpm 的入口脚本
下图是我在 webstorm 的调试配置,qf-tds-vue-plugins
是我的项目文件夹,下面配置的意思是,我要在这个文件夹运行以下命令(因为是在项目目录安装依赖):
# 实际上 pnpm i,也是运行全局安装的 pnpm 目录下的 bin/pnpm.cjs node /candy/app/pnpm/pnpm/bin/pnpm.cjs i --fix-lockfile
找个地方打个断点,代码能停住(停不住可能是根本没运行这行代码,换个别的),就代表这一步已经成功了
定位问题
这一步才是最核心、但又最麻烦的步骤
**如何在茫茫源码中定位问题?**下面是我的一些个人经验:
从错误信息出发,找到报错的代码
我们全局搜索关键字:isn't supported by any available resolver
,找到是哪一行报错的,找到之后,打个断点。
这就找到了报错源头了。因为 resolution
不为真值,所以报错了,那我们的问题就变成了,为什么 resolution
不为真值。这就将很大很抽象的问题,转化成了一个更小更明确的问题
resolution
是由 resolveFromNpm
返回的,那我们就修改一下断点位置
这里有一个小经验,**断点位置要改到哪里比较好?**有两种方式:
- 找到
resolveFromNpm
的函数源码实现,在函数实现里面打断点 - 直接在
resolveFromNpm
函数调用的位置打断点。
我个人更偏向与在调用的位置打断点,因为更方便。可以看上图的例子,resolveFromNpm
是另一个函数返回的,如果你想要找到它的实现,还得进去 createNpmResolver
函数里面找,说不定里面函数比较复杂,就比较麻烦,需要找到 resolveFromNpm
函数真正的内部实现,才能打断点 。
如果是在调用位置打断点,就会在 resolveFromNpm
函数调用前停住,此时,我们按进入函数,就能直接找到源码了
因此断点会改到这里,但我们运行后会发现,每个 package 都会在这里暂停,一个项目这么多包,不行啊。
这时候就要用到条件断点,如何设置条件断点呢?可以先观察一下一些变量的值
可以看到 wantedDependency.pref
的值为 4.0.4_@types+node@17.0.45
,那就用这个了。断点的条件就设置为
wantedDependency.pref === '4.0.4_@types+node@17.0.45'
这就能在出错前将代码定住了,然后我们进入函数
进入 resolveFromNpm
调试,然后发现 spec
为 null,所以函数 return null 了,因此又可以将问题转化:为什么 spec
会使 null?
那就要排查 parsePref
函数了,还是用上述的思路,打断点,进入函数,
同样的,按照上述思路就是 parsePref
函数的问题了,这里就不重复了。
最后发现,是 wantedDependency.pref
这个属性,应该为 4.0.4
,才能使后面的代码不报错,而不是 4.0.4_@types+node@17.0.45
那接下来的问题就转化成了: wantedDependency.pref
为什么不为 4.0.4
?我们需要找到 wantedDependency.pref
被赋值的地方
下面又是一些经验:
- 全局搜索
.pref =
,是为了所有出wantedDependency.pref = xxx
的这些代码 - 全局搜索
pref:
(注意前面有空格),这个是为了搜索{ pref: xxx }
的代码
不过很可惜,在 pnpm 中都搜不到太多有用的信息,那就只能通过调试找了。接下来该怎么办呢?
我们可以利用函数的调用栈,逐级往上找,调试方法跟之前一样,目标是,找到 wantedDependency.pref
被赋值的地方。
有较多调试经验的开发者,也可以不逐级网上找,如果觉得肯定不会在当前函数层级被赋值,可以直接跳到更深的函数调用层级中
最终,我找到了整个 wantedDependency 初始化的地方:resolveDependency
函数。
这里我直接回顾一下整个错误的相关信息:
@vitejs/plugin-basic-ssl
在安装 vite 的时候,遇到了版本解析错误,4.0.4_@types+node@17.0.45
- 在
resolveDependency
函数中,会解析@vitejs/plugin-basic-ssl
的 package.json。直接注意的是,它的 package.json 没有 dependencies 字段 - pkg 对象根据 package.json 生成,这一句代码中,由于
pkg.dependencies
不存在,因此会导致使用了锁文件的dependencies
字段,这是不应该的,导致取了锁文件的 vite 版本号4.0.4_@types+node@17.0.45
。
既然知道了这个,我们就知道了这个错误出现的场景:
- 装了多个 Vite,有的 Vite 版本号是
4.0.4
,有的是4.0.4_@types+node@17.0.45
,出现多个 Vite 的原因,是因为peerDependencies
,感兴趣可以查看官网的说明文档 @vitejs/plugin-basic-ssl
的dependencies
字段不存在(不是为空,是不存在)
只有同时满足以上条件才会报错,因此很多非 monorepo 仓库,都不会有这个问题,因为它们只装了一个 Vite。
当我知道了以上信息之后,我就可以提供一个最小的可复现 Demo 了
不过,我觉得既然都看到这里了,不如尝试一下自己修复。
直觉告诉我,只要加一点代码就行了,判断 pkg.dependencies
是否为空,为空就设置为 {}
if (!pkg.dependencies) { pkg.dependencies = {} }
然后我把出错原因写到了 issue 中,顺便提了个 pull request 给开源作者,然后被告知需要补一下单元测试(这也的确是正常且稳妥的做法),至于后续单元测试怎么补,就不是本文该关心的问题了,以后有机会再聊。
总结
本文用我个人的例子,从发现问题,到调试代码,一步步地深入,直到最终找到问题。里面用到了很多调试相关的技巧,这些技巧可以帮助我们,即使在不熟悉源码的情况下,也能深入源码进行定位问题
这些技巧主要包括以下这些:
- 全局搜索查找关键词/错误信息,找到相关的源码
- 转化问题,将大的抽象问题,变小变具体
- 在合理的位置打断点
- 巧用条件断点,巧妙的设置断点条件
- 利用函数调用栈
当然,仅仅有技巧也不行,你需要有解决问题的决心。那么,当你遇到问题时,你是选择避开它,还是选择解决它呢?
如果这篇文章对您有所帮助,可以点赞加收藏👍,您的鼓励是我创作路上的最大的动力。也可以关注我的公众号订阅后续的文章:Candy 的修仙秘籍(点击可跳转)