当前位置: 首页 > 科技观察

Webpack出现问题怎么调试?

时间:2023-03-12 11:26:15 科技观察

事情是这样的。前两天有朋友问我:“为什么我运行webpack后看不到我写的页面,但是:”咦?文件列表页?嗯,我好像没遇到过这种情况。我不能一下子给出答案。只能求关键代码:关注webpack.config.js配置,其中使用了devServer+HMR功能,其中:webpackversion5.37.0webpack-dev-serverversion3.11看了半天.2,没问题,给了一些纸质的建议还是解决不了问题,开会暂时搁置。过了一会儿,小伙伴兴奋地跑过来告诉我,经过一番瞎猜,问题解决了:output.publicPath='/'时一切正常,output.publicPath='./'时出错,返回文件列表页面啊?这个东西也会影响devServer的效果。我的直觉告诉我不应该。emmm,成功勾起了我的好奇心。虽然写过一些Webpack源码分析的文章,但是webpack-dev-server确实是我所不知的。幸好我有一本秘籍《如何阅读源码 —— 以 Vetur 为例》,是时候展现真正的技术了!第一步:定义问题先回顾一下问题的过程:webpack.config.js同时配置ouput.publicPath和devServer运行npxwebpackserve启动开发服务器浏览器访问http://localhost:9000没有按预期返回用户代码,而是返回文件列表页面;但是如果你恢复output.publicPath的默认配置,一切如常。按道理,ouput.publicPath应该只影响最终产品引用的路径。尝试运行命令行工具运行curl来检测首页返回的内容内容:Tips:有时候可以尝试绕过浏览器的复杂逻辑,使用最简单的工具来验证http请求返回的内容。可以看到请求http://localhost:9000返回一大串html代码,页面标题是listingdirectory——也就是我们看到的文件列表页面:虽然不知道是第几层生成于,但它绝对不是我的,而且它发生在HTTP级别。所以问题的核心就是:“为什么webpack的output.publicPath会影响webpack-dev-server的运行效果”?Step2:回顾背景我又带着疑问回顾了一遍Webpack官方文档。publicPath配置首先output.publicPath说明如下:当使用按需加载或加载外部资源如图像、文件等时,这是一个重要的选项。如果指定的值不正确,加载这些资源时会收到404错误大意是这是一个控制按需加载或者资源文件加载的选项。如果对应路径资源加载失败,则返回404。嗯,其实这个描述很不明确。简单理解output.publicPath,会改变html文件中产品资源的路径。比如webpack编译生成bundle.js文件后,默认写入html的路径为:如果设置了output.publicPath的值,则会在path:看起来很简单。devServer配置项让我们再看一下devServer配置:这组选项由webpack-dev-server选取,可用于以各种方式更改其行为。大意是devServer的配置最终会被webpack-dev-server消费,webpack-dev-server提供的web服务包括HMR——HotModuleUpdates。感受一下,包括vue-cli、create-react-app等脚手架工具的底层都依赖于webpack-dev-server,其作用和重要性可想而知。第三步:分析问题根据现有资料和本人对HTTP协议的理解,基本可以推断问题一定出在webpack-dev-server框架处理首页请求的逻辑上,大概率是output.publicPath属性。首页资源的判断逻辑导致webpack-dev-server找不到对应的资源文件,返回到最下面的文件列表页面。嗯,我觉得靠谱,所以顺着这个思路去挖源码,找找具体原因。第四步:分析代码结构。分析书不好读。您需要查看源代码进行调试。先打开webpack-dev-server包的代码看看内容:Tips:读者也可以试试clonewebpack-dev-server仓库的代码有惊喜~~项目结构并不复杂。根据Webpack的习惯,可以推断主要代码在lib目录:cloc是一个非常好用的代码统计工具,官网:https://www.npmjs.com/package/cloc代码量只是在2000年代初,这没关系。接下来,打开package.json文件以查看其中有哪些依赖项。一一过一遍,和我们问题强相关的依赖是:express:applicationneedstointroductionwebpack-dev-middleware:这应该是大多数人之前没有注意到的。从官网文档来看,这是一个桥接Webpack编译过程和express的中间件serve-index:“expressmiddlewarethatprovidesafilelistpageinaspecificdirectory”!!!按照这个描述,故障肯定是调用了serve-index导致的,感觉和答案很接近。部分分析切入点:验证serve-index包的功能经过上面的分析,虽然不知道问题出在哪里,但是可以大致判断是和serve-index包强相关。先搜索webpack-dev-server所在位置本地引用这个包:还好只在lib/Server.js文件中用到,就简单多了。对call语句前后的语句进行“静态分析”,大致可以推导出:serveIndex调用被包裹在this.app中.use中,推测this.app指向express实例,使用use函数注册中间件,所以整个serveIndex就是一个中间件。Server类型除了setupStaticServeIndexFeature之外,还包含了其他的函数setupXXXFeature,这些函数基本上都是用来添加Express中间件的,这些中间件组合起来组装webpack-dev-server提供的HMR、proxy、ssl等功能,除此之外别无其他看得到。首先做一个对照实验并运行它来“动态分析”代码的实际执行过程,验证这是哪里出了问题。先在serveIndex函数前插入debugger语句,然后:先按照正常情况执行ndbnpxwebpackserve,即output.publicPath='/',结果页面正常打开,没有打a断点或者中断然后按照ouput.publicPath='./'执行ndbnpxwebpackserve,进入断点:Tips:ndb是开箱即用的node调试工具,无需任何配置即可调试node应用,非常方便OK,答案揭晓,在ouput.publicPath='./'的场景下,会命中这个中间件,执行serveIndex函数,返回文件目录列表,这是有道理的。然而,作为一个有抱负的程序员,我们怎么能止步于此呢?我们继续往下挖掘:是哪段代码决定了进程是否会进入serveIndex中间件?切入点:确定serveIndex的上游中间件想一想,express架构的特点是基于中间件的洋葱模型,中间件之间由next函数调用下一个中间件。好吧,我们有个主意。下面我们跟着webpack-dev-server的中间件队列,看看serveIndex之前有哪些中间件。分析这些中间件的代码应该可以回答:哪一段代码决定了进程是否会进入serveIndex中间件?但是在express中间件架构下,从下次调用到真正的中间件功能之间有一个很长的调用环节。通过断点的调用栈很难判断是上层中间件还是上层中间件。where:这个时候不能死板,得换个技巧——找到创建expressexample的代码,把use函数用魔法包裹起来:Tips:这个技巧在一些复杂的场景下特别有用,比如当我在学习Webpack源码的时候使用Proxy类的时候,我们经常会使用Proxy类将debugger语句植入到hook中,跟踪hook在监听谁,在什么地方被触发。通过这个重写函数和植入断点,我们可以很容易的追溯到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在webpack-dev-middleware/lib中使用了一次如果/util.js文件已经被使用了23次,先摘下软柿子,看看middleware.js文件中是如何使用的:const{getFilenameFromUrl}=require('./util');module.exports=functionwrapper(context){returnfunctionmiddleware(req,res,next){functiongoNext(){//...resolve(next());}//...letfilename=getFilenameFromUrl(context.options.publicPath,context.compiler,req.url);if(filename===false){returngoNext();}returnnewPromise((resolve)=>{handleRequest(context,filename,processRequest,req);//...});};};注意代码中有一个逻辑,就是调用util文件的getFilenameFromUrl函数,判断返回的filename值是否为false,如果是则调用next函数,看起来很像!然后进去看getFilenameFromUrl的代码:逐行分析,注意红框里的那句:if(xxx&&url.indexOf(publicPath)!=??=0){returnfalse;}有道理,这个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收到默认路径请求,走到webpack一步步沿着express逻辑在-dev-middleware中间件的webpack-dev-middleware中间件内部,继续调用webpack-dev-middleware/lib/util.js文件的getFilenameFromUrl方法getFilenameFromUrl,对url进行内部判断。indexOf(publicPath)-dev-middleware直接调用next,进程进入下一个中间件express.staticexpress.static尝试读取http://localhost:9000对应的资源文件,发现文件不存在,流程继续进入最后一个中间件serveIndexserveIndex返回产品目录结构接口不符合开发者的预期。归根结底,问题出在这里:Webpack官网对output.publicPath的介绍,只说会影响bundle产品路径,并没有说会影响主页面的index路径。开发商表示很迷茫。webpack-dev-server启动后,自动打开页面时并没有自动在链接后面追加output.publicPath值,导致默认打开路径与真实索引首页不一致,并没有返回通用诸如“404”之类的错误信息,换成了未知的“文件列表页面”,开发者很难快速定位到问题出在哪里。到这里,把问题从表象,到原理,到最根本的问题都挖出来了,以后可以分享给其他同学。说:在开发阶段,尽量避免配置output.publicPath项,不然会有惊喜哦~~