关于前端:我是如何调试-Webpack-运行问题的

48次阅读

共计 6321 个字符,预计需要花费 16 分钟才能阅读完成。

全文 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…

代码量也就 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 字,最初写了 3000+。。。

然而,过程中的确用到了《如何浏览源码 —— 以 Vetur 为例》提及的流程和技巧:

  • 先明确定义指标
  • 再回顾背景,理解要害知识点
  • 再再定义切入点
  • 再再再剖析代码构造,猜想问题可能出在那
  • 再再再再部分深入分析,逐层解密直到问题的本源

算是对《如何浏览源码 —— 以 Vetur 为例》的补充样例吧,心愿读者有所思,有所得,人人都能做源码剖析。

正文完
 0