专栏首页秋风的笔记我是如何调试 Webpack 问题的

我是如何调试 Webpack 问题的

全文 3000 字,欢迎点赞转发

事情是这样的,前两天有个小伙伴问我:「为啥我的 webpack 运行完看不到我写的页面,而是:」

嗯?文件列表页?好吧,这种情况我似乎没遇到过,一下子没法给出答案,只能要来关键代码:

重点看看 webpack.config.js 配置,用到 devServer + HMR 功能,其中:

  • Webpack 版本为 5.37.0
  • webpack-dev-server 版本为 3.11.2

看了半天,没问题呀,给了几个纸糊的建议还是解决不了问题,刚好在开会这事就暂且放下了。过了一会,小伙伴兴冲冲跑过来跟我说经过一番盲猜,问题被解决了:

  • output.publicPath = '/' 时一切正常
  • output.publicPath = './' 时出错,返回文件列表页

啊?这玩意还会影响 devServer 的效果,直觉告诉我不应该啊。

emmm,成功勾起我的好奇心了,虽然写过一些 Webpack 源码分析的文章,但 webpack-dev-server 确实不在我的知识范围,好在我有秘籍《如何阅读源码 —— 以 Vetur 为例》,是时候展示真正的技术了!

第一步:定义问题

先复盘一下问题发生的过程:

  • webpack.config.js 同时配置了 ouput.publicPathdevServer
  • 运行 npx webpack serve 启动开发服务器
  • 浏览器访问 http://localhost:9000 没有按预期返回用户代码,而是返回了文件列表页面;但如果恢复 output.publicPath 的默认配置,一切如常

讲道理, ouput.publicPath 应该只是影响了最终产物引用的路径,试试命令行工具运行 curl 检测首页返回的内容:

Tips:有时候可以试试绕过浏览器的复杂逻辑,用最简单的工具验证 http 请求返回的内容。

可以看到,请求 http://localhost:9000 地址返回一大串 html 代码,且页面的 title 为 listing directory —— 也就是我们看到的文件列表页面:

虽然不知道这是在那一层生成的,但可以肯定绝对不是我写的,而且这是在 HTTP 层面发生的。

所以问题的核心就是:「为何 Webpack 的 output.publicPath 会影响 webpack-dev-server 的运行效果」

第二步:回顾背景

带着问题我又 review 了一遍 Webpack 官方文档。

publicPath 配置

首先 output.publicPath 是这么描述的:

❝This is an important option when using on-demand-loading or loading external resources like images, files, etc. If an incorrect value is specified you'll receive 404 errors while loading these resources. ❞

大意就是,这是一个控制按需加载或资源文件加载的选项,如果对应的路径资源加载失败时会返回 404。

嗐,其实这段描述就非常不明所以了,简单理解 output.publicPath 会改变产物资源在 html 文件的路径,比如说 Webpack 编译完生成了 bundle.js 文件,默认情况下写到 html 的路径是:

<script src="bundle.js" />

如果设置了 output.publicPath 值,就会在路径前增加前缀:

<script src="${output.publicPath}/bundle.js" />

看起来很简单。

devServer 配置项

再来看看 devServer 配置:

❝This set of options is picked up by webpack-dev-server and can be used to change its behavior in various ways. ❞

大意就是,devServer 配置最终会被 webpack-dev-server 消费,而 webpack-dev-server 提供了包括 HMR —— 模块热更新在内的 web 服务。

感受一下,包括 vue-cli、create-react-app 之类的脚手架工具底层都依赖于 webpack-dev-server ,它的作用和重要性就可想而知了吧。

第三步:分析问题

按照现有的情报,加上我对 HTTP 协议的理解,可以基本推断问题必然是出在 webpack-dev-server 框架处理首页请求的逻辑上,大概率是 output.publicPath 属性影响到首页资源的判定逻辑,导致 webpack-dev-server 找不到对应的资源文件,返回兜底的文件列表页面。

嗯,我觉得靠谱,那就沿着这个思路挖一挖源码,找到具体原因吧。

第四步:分析代码

结构分析

书上得来终须浅,debug 还需看源码啊,啥都别说了先打开 webpack-dev-server 包的代码看看内容吧:

Tips: 读者也可以试试 clone webpack-dev-server 仓库的代码,有惊喜~~

项目结构并不复杂,按 Webpack 的习惯可以推断主要代码都在 lib 目录:

cloc 是一个非常好用的代码统计工具,官网:https://www.npmjs.com/package/cloc

代码量也就 2000 出头,还好还好。

接下来再打开 package.json 文件,看看有哪些 dependency,一个个捋过去之后,与我们的问题强相关的依赖有:

  • express:应用不用多介绍了吧
  • webpack-dev-middleware:这个应该大多数人没有注意过,从官网文档判断这是一个桥接 Webpack 编译过程与 express 的中间件
  • serve-index「提供特定目录下文件列表页面的 express 中间件」!!!

按照这个描述,这锅肯定出在 serve-index 的调用上啊,感觉离答案很近了。

局部分析

切入点:验证 serve-index 包的作用

经过上面的分析,虽然我还不知道问题具体出在哪里,但大致可以判定跟 serve-index 包强相关,先搜一下 webpack-dev-server 在哪些地方引用这个包:

很幸运,只在 lib/Server.js 文件中用到,那就简单多了,「静态分析」调用语句前后的语句,大致上可以推导出:

  • serveIndex 调用被包裹在 this.app.use 内,推测 this.app 指向 express 示例,use 函数用于注册中间件,所以整个 serveIndex 就是一个中间件
  • setupStaticServeIndexFeature 外,Server 类型中还包含了其它命名为 setupXXXFeature 的函数,基本上都用于添加 express 中间件,这些中间件组合拼装出 webpack-dev-server 提供的 HMR、proxy、ssl 等功能

也看不出别的啥了,先做个对照实验,运行起来「动态分析」代码的实际执行过程,验证到底是不是这个地方出错吧。先在 serveIndex 函数之前插入 debugger 语句,之后:

  • 先按照正常情况,也就是 output.publicPath = '/' 执行 ndb npx webpack serve,结果是如常打开了页面,没有命中断点,没有中断
  • 再按照 ouput.publicPath = './' 执行 ndb npx webpack serve,进入断点:

Tips: ndb 是一个开箱即用的 node debugger 工具,不需要做任何配置就能调试 node 应用,非常方便

OK,答案揭晓了,在 ouput.publicPath = './' 场景下会命中这个中间件,执行 serveIndex 函数返回文件目录列表,这很 make sense。

不过,作为一个有追求的程序员怎么会止步于此呢,我们继续往下挖呀:到底是那一段代码决定了流程会不会进入 serveIndex 中间件?

切入点:确定 serveIndex 的上游中间件

思考一下,express 架构的特点就是 —— 基于中间件的洋葱模型(非严格来说),而中间件之间通过 next 函数调起下一个中间件。

嗯,有思路了,我们沿着 webpack-dev-server 的 middleware 队列,找到 serveIndex 之前都有哪些中间件,分析这些中间件的代码应该就能解答:

到底是那一段代码决定了流程会不会进入 serveIndex 中间件?

但是,express 中间件架构下,从 next 调用到实际中间件函数隔着很远的调用链路,很难通过断点的调用堆栈判断出上一级中间件,以及更更上一级中间件在哪里啊:

这时候不能硬刚,得换一个技巧了 —— 找到创建 express 示例的代码,用魔法包裹住 use 函数:

Tips: 这种技巧在某些复杂场景下特别有用,比如我在学习 Webpack 源码的时候,就经常配合 Proxy 类对 hook 植入 debugger 语句,追踪钩子被谁监听,在哪里被触发

通过这种重写函数,植入断点的方式,我们就能轻松追溯到 webpack-dev-server 用到了哪些中间件,以及中间件注册的顺序:

setupCompressFeature => 注册资源压缩中间件
setupMiddleware => 注册 webpack-dev-middleware 中间件
setupStaticFeature => 注册静态资源服务中间件
setupServeIndexFeature => 注册 serveIndex 中间件

可以看到,在当前 Webpack 配置下总共注册了这四个中间件函数,按照 express 的执行逻辑这四个中间件会按注册顺序从上往下执行,所以 serveIndex 函数的直接上游就是 setupStaticFeature 注册的静态资源服务中间件了。

继续看看 setupStaticFeature 函数的代码:

这里只是调用标准化的 [express.static](https://expressjs.com/en/starter/static-files.html) 函数,注入静态资源服务功能,如果这个中间件运行的时候按路径找不到对应的文件资源,会调用下一个中间件继续处理请求,看起来跟我们的问题没啥关系。

继续往上,看看 setupMiddleware 函数:

注册了 webpack-dev-middleware,从名字就可以看出这个中间件跟 webpack-dev-server 应该关系匪浅,那就继续打开 webpack-dev-middleware 看看里面的代码:

我去。。。也不少啊,这看起来太费劲了,我只是想找到这个 bug 的原因,没必要全看吧!那就直接搜关键词 publicPath 试试吧:

比较幸运,publicPath 关键字出现的频率还是比较少的:

  • webpack-dev-middleware/lib/middleware.js 文件中被使用了 1 次
  • webpack-dev-middleware/lib/util.js 文件中被使用了 23 次

那,就先挑软柿子捏,看看 middleware.js 文件中是怎么用的:

const { getFilenameFromUrl } = require('./util');

module.exports = function wrapper(context) {
  return function middleware(req, res, next) {
    function goNext() {
      // ...
      resolve(next());
    }
    // ...
    let filename = getFilenameFromUrl(
      context.options.publicPath,
      context.compiler,
      req.url
    );

    if (filename === false) {
      return goNext();
    }

    return new Promise((resolve) => {
      handleRequest(context, filename, processRequest, req);
      // ...
    });
  };
};

注意代码中有一个逻辑,就是调用 util 文件的 getFilenameFromUrl 函数,并判断返回的 filename 值是否为 false,是的话调用 next 函数,这看起来很像那么回事了!

那就继续进去看看 getFilenameFromUrl 的代码:

逐行分析下来,注意看红框框出来这一句:

if(xxx && url.indexOf(publicPath) !== 0){
  return false;
}

讲道理,从字面意义上这个 url 应该是客户端发过来的请求连接,publicPath 应该就是我们在 webpack.config.js 中配置的 output.publicPath 项的值了吧?运行起来看看:

果然,断点进去之后可以看到这两个值确确实实符合前面的猜想,问题就出在这里,此时:

  • url = '/`'
  • publicPath = output.publicPath = '/helloworld'
  • 所以 url.indexOf(publicPath) === false 实锤

getFilenameFromUrl 函数执行结果为 false,所以 webpack-dev-middleware 会直接调用 next 方法进入下一个中间件。

如果手动在默认打开的路径后加上 output.publicPath 的内容:

果然,它又行了。

第五步:总结

嗐,你看,这就是源码分析的过程,繁琐但不复杂,简直人人都能成为技术大牛啊。回顾一下代码的流程:

  • webpack-dev-server 启动后会调用自动打开浏览器访问默认路径 http://localhost:9000
  • 此时 webpack-dev-server 接收到默认路径请求,沿着 express 逻辑逐步走到 webpack-dev-middleware 中间件中
  • webpack-dev-middleware 中间件内部呢,又继续调用 webpack-dev-middleware/lib/util.js 文件的 getFilenameFromUrl 方法
  • getFilenameFromUrl 内部判断 url.indexOf(publicPath)
  • getFilenameFromUrl 返回 falsewebpack-dev-middleware 直接调用 next ,流程进入下一个中间件 express.static
  • express.static 尝试读取 http://localhost:9000 对应的资源文件,发现文件不存在,流程继续进入最后一个中间件 serveIndex
  • serveIndex 返回产物目录结构界面,不符合开发者预期

归根结底,这里面的问题:

  1. Webpack 官网关于 output.publicPath 的介绍只说了会影响 bundle 产物路径,没说会影响主页面的索引路径,开发者表示很 confuse 咯
  2. webpack-dev-server 启动后,自动打开页面时没有在链接后面自动追加 output.publicPath 值导致默认打开的路径与真正的 index 首页不一致,而且还没返回 「404」 一类通用的错误提示,取而代之以一个不明所以的「文件列表页」,开发者很难迅速 get 到问题到底出在哪

到这里就把问题从表象,到原理,到最最根本的问题所在都挖出来了,以后可以跟其他同学说:

开发阶段,尽量避免配置 output.publicPath 项,否则会有惊喜哦~~

真·总结

整个 debbug 过程大概花了半小时,文档写到半夜。。。原本以为撑死 1000 字,最后写了 4000+ 。。。

但是,过程中确实用到了《如何阅读源码 —— 以 Vetur 为例》 提及的流程和技巧:

  • 先明确定义目标
  • 再回顾背景,了解关键知识点
  • 再再定义切入点
  • 再再再分析代码结构,猜测问题可能出在那
  • 再再再再局部深入分析,逐层解密直到问题的根源

算是对《如何阅读源码 —— 以 Vetur 为例》的补充样例吧,希望读者有所思,有所得,人人都能做源码分析,关注我,了解更多源码分析技巧。

本文分享自微信公众号 - 秋风的笔记(qiufengnote)

原文出处及转载信息见文内详细说明,如有侵权,请联系 yunjia_community@tencent.com 删除。

原始发表时间:2021-08-13

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

我来说两句

0 条评论
登录 后参与评论

相关文章

  • 我是如何调试 Webpack 问题的

    嗯?文件列表页?好吧,这种情况我似乎没遇到过,一下子没法给出答案,只能要来关键代码:

    ConardLi
  • 我是如何用IDEA调试BUG的?

    右击断点旁边的小红点(断点),弹出面板,在Condition这里填入暂停条件,这样调试时,当程序满足条件时就会稳稳地停在student= "小明"的位置。

    程序员小明
  • 如何应对面试官的JVM调优问题

    我猜你肯定是为了面试,现在很多公司都会问这个,虽然你工作了N年JVM调优可能都不会接触到,但我觉得还是有考察的必要的。因为很多时候我们考察一个人不光要考察他的硬...

    诺浅
  • 我是如何面试QA的。

    最近遇见一个好玩的bug, 现象是页面刷新白屏,RootCause是Header里放的cookie太多了, 大小超出了4kb的限制.

    iTesting
  • 如何调试Python 程序的内存泄露问题

    如果大家在 Linux 或者 macOS 下面运行一段可能导致内存泄露的程序,那么你可能会看到下面这样的情况:

    青南
  • gdb调试多线程 如何解死锁问题

    基础_多线程 Q1 gdb调试多线程 如何解死锁问题? A1 说明:排版不是很好可以直接查看原文链接 gdb基本用法 info threads(show al...

    程序员小王
  • SAP Fiori Elements遇到问题时,该如何调试?

    Jerry Wang
  • 服务化带来的问题,我们是如何解决的

    服务化确实带来不少好处,那么服务化有没有什么问题呢?答案是肯定的!下面分享一下我们曾经在服务化过程中经历的问题:

    用户7927337
  • 我是如何做测试组长的

    我们性能测试组算我总共5个人,人员少,任务重。将测试人员分为2组,每组一名小组长,一个人兼顾几个职能(脚本开发,监控,数据准备,分析),即要完成任务,又要保证质...

    软件测试君
  • 原创 | 如果我是推荐算法面试官,我会问哪些问题?

    不得不说这是一个很好的问题,一千个人眼里有一千个哈姆雷特,同样,一千个面试官里面也有一千个面试标准和面试风格。首先说明,本文的内容只代表我个人的观点,大家辩证吸...

    TechFlow-承志
  • 面试官问我:什么是JavaScript闭包,我该如何回答?

    闭包,有人说它是一种设计理念,有人说所有的函数都是闭包。到底什么是闭包?这个问题在面试的时候经常都会被问,很多小白一听就懵逼了,不知道如何回答好。

    用户8671053
  • 深度学习如何解决生物调试问题?

    【编者按】深度学习在近几年实现了巨大的突破,而这一方法也被应用到越来越多的领域,用于解决过去在这些领域难以解决的问题。可以预见,在未来的科研中“深度学习+学科应...

    AI科技评论
  • 我是如何通过阿里面试的?

    用户1269200
  • 我是如何准备技术面试的

    我是如何准备技术面试的 前拿到的知名公司的offer是腾讯和新美大(大众点评、美团)。一直想写一篇面经分享给大家,但因为一些琐碎的事情,就一直耽误着。 今天就...

    用户1289394
  • 我是如何准备 Android 技术面试的(含面试题及答案)

    这两个月听的最多的两个词就是 寒冬 和 裁员,作为被裁大军中的一员,我的关键词就是 遗憾 和 还好。

    Android技术干货分享
  • 我是如何用 Webpack 虐待代码尺寸的 (第三回合)

    解释一下, 原因是 im 这个项目希望可以做到平台化, 具体来说就是, 这个项目拆成两个部分, 一部分是基础功能, 比如正常的聊天, 头像, 表情等, 另一部分...

    普通程序员
  • 我是如何用 Webpack 虐待代码尺寸的 (第二回合)

    base64 从 css 中去掉, 直接使用外部文件, 因为本身这些文件只是一些表情, 显示的时候现加载影响也不大。

    普通程序员
  • 我是如何用 Webpack 虐待代码尺寸的 (第一回合)

    在说压缩之前,先说一下这个项目。这是一个手机 WAP版的 IM 在线客服页面,如图

    普通程序员
  • 面试官问你“有什么问题问我吗?”,你该如何回答?

    我还记得当时我去参加面试的时候,几乎每一场面试,特别是HR面和高管面的时候,面试官总是会在结尾问我:“问了你这么多问题了,你有什么问题问我吗?”。这个时候很多人...

    乔戈里

扫码关注云+社区

领取腾讯云代金券