详解webpack-dev-middleware 源码解读
前言
Webpack的使用目前已经是前端开发工程师必备技能之一。若是想在本地环境启动一个开发服务,大家只需在Webpack的配置中,增加devServer的配置来启动。devServer配置的本质是webpack-dev-server这个包提供的功能,而webpack-dev-middleware则是这个包的底层依赖。
截至本文发表前,webpack-dev-middleware的最新版本为webpack-dev-middleware@3.7.2,本文的源码来自于此版本。本文会讲解webpack-dev-middleware的核心模块实现,相信大家把这篇文章看完,再去阅读源码,会容易理解很多。
webpack-dev-middleware是什么?
要回答这个问题,我们先来看看如何使用这个包:
constwdm=require('webpack-dev-middleware'); constexpress=require('express'); constwebpack=require('webpack'); constwebpackConf=require('./webapck.conf.js'); constcompiler=webpack(webpackConf); constapp=express(); app.use(wdm(compiler)); app.listen(8080);
通过启动一个Express服务,将wdm(compiler)的结果通过app.use方法注册为Express服务的中间函数。从这里,我们不难看出wdm(compiler)的执行结果返回的是一个express的中间件。它作为一个容器,将webpack编译后的文件存储到内存中,然后在用户访问express服务时,将内存中对应的资源输出返回。
为什么要使用webpack-dev-middleware
熟悉webpack的同学都知道,webpack可以通过watchmode方式启动,那为何我们不直接使用此方式来监听资源变化呢?答案就是,webpack的watchmode虽然能监听文件的变更,并且自动打包,但是每次打包后的结果将会存储到本地硬盘中,而IO操作是非常耗资源时间的,无法满足本地开发调试需求。
而webpack-dev-middleware拥有以下几点特性:
- 以watchmode启动webpack,监听的资源一旦发生变更,便会自动编译,生产最新的bundle
- 在编译期间,停止提供旧版的bundle并且将请求延迟到最新的编译结果完成之后
- webpack编译后的资源会存储在内存中,当用户请求资源时,直接于内存中查找对应资源,减少去硬盘中查找的IO操作耗时
本文将主要围绕这三个特性和主流程逻辑进行分析。
源码解读
让我们先来看下webpack-dev-middleware的源码目录:
... ├──lib │├──DevMiddlewareError.js │├──index.js │├──middleware.js │└──utils │├──getFilenameFromUrl.js │├──handleRangeHeaders.js │├──index.js │├──ready.js │├──reporter.js │├──setupHooks.js │├──setupLogger.js │├──setupOutputFileSystem.js │├──setupRebuild.js │└──setupWriteToDisk.js ├──package.json ...
其中lib目录下为源代码,一眼望去有近10多个文件要解读。但刨除utils工具集合目录,其核心源码文件其实只有两个index.js、middleware.js
下面我们就来分析核心文件index.js、middleware.js的源码实现
入口文件index.js
从上文我们已经得知wdm(compiler)返回的是一个express中间件,所以入口文件index.js则为一个中间件的容器包装函数。它接收两个参数,一个为webpack的compiler、另一个为配置对象,经过一系列的处理,最后返回一个中间件函数。下面我将对index.js中的核心代码进行讲解:
... setupHooks(context); ... //startwatching context.watching=compiler.watch(options.watchOptions,(err)=>{ if(err){ context.log.error(err.stack||err); if(err.details){ context.log.error(err.details); } } }); ... setupOutputFileSystem(compiler,context);
index.js最为核心的是以上3个部分的执行,分别完成了我们上文提到的两点特性:
- 以监控的方式启动webpack
- 将webpack的编译内容,输出至内存中
setupHooks
此函数的作用是在compiler的invalid、run、done、watchRun这4个编译生命周期上,注册对应的处理方法
context.compiler.hooks.invalid.tap('WebpackDevMiddleware',invalid); context.compiler.hooks.run.tap('WebpackDevMiddleware',invalid); context.compiler.hooks.done.tap('WebpackDevMiddleware',done); context.compiler.hooks.watchRun.tap( 'WebpackDevMiddleware', (comp,callback)=>{ invalid(callback); } );
- 在done生命周期上注册done方法,该方法主要是report编译的信息以及执行context.callbacks回调函数
- 在invalid、run、watchRun等生命周期上注册invalid方法,该方法主要是report编译的状态信息
compiler.watch
此部分的作用是,调用compiler的watch方法,之后webpack便会监听文件变更,一旦检测到文件变更,就会重新执行编译。
setupOutputFileSystem
其作用是使用memory-fs对象替换掉compiler的文件系统对象,让webpack编译后的文件输出到内存中。
fileSystem=newMemoryFileSystem(); //eslint-disable-next-lineno-param-reassign compiler.outputFileSystem=fileSystem;
通过以上3个部分的执行,我们以watchmode的方式启动了webpack,一旦监测的文件变更,便会重新进行编译打包,同时我们又将文件的存储方法改为了内存存储,提高了文件的存储读取效率。最后,我们只需要返回express的中间件就可以了,而中间件则是调用middleware(context)函数得到的。下面,我们来看看middleware是如何实现的。
middleware.js
此文件返回的是一个express中间件函数的包装函数,其核心处理逻辑主要针对request请求,根据各种条件判断,最终返回对应的文件内容:
functiongoNext(){ if(!context.options.serverSideRender){ returnnext(); } returnnewPromise((resolve)=>{ ready( context, ()=>{ //eslint-disable-next-lineno-param-reassign res.locals.webpackStats=context.webpackStats; //eslint-disable-next-lineno-param-reassign res.locals.fs=context.fs; resolve(next()); }, req ); }); }
首先,middleware中定义了一个goNext()方法,该方法判断是否是服务端渲染。如果是,则调用ready()方法(此方法即为ready.js文件,作用为根据context.state状态判断直接执行回调还是将回调存储callbacks队列中)。如果不是,则直接调用next()方法,流转至下一个express中间件。
constacceptedMethods=context.options.methods||['GET','HEAD']; if(acceptedMethods.indexOf(req.method)===-1){ returngoNext(); }
接着,判断HTTP协议的请求的类型,若请求不包含于配置中(默认GET、HEAD请求),则直接调用goNext()方法处理请求:
letfilename=getFilenameFromUrl( context.options.publicPath, context.compiler, req.url ); if(filename===false){ returngoNext(); }
然后,根据请求的req.url地址,在compiler的内存文件系统中查找对应的文件,若查找不到,则直接调用goNext()方法处理请求:
returnnewPromise((resolve)=>{ //eslint-disable-next-lineconsistent-return functionprocessRequest(){ ... } ... ready(context,processRequest,req); });
最后,中间件返回一个Promise实例,而在实例中,先是定义一个processRequest方法,此方法的作用是根据上文中找到的filename路径获取到对应的文件内容,并构造response对象返回,随后调用ready(context,processRequest,req)函数,去执行processRequest方法。这里我们着重看下ready方法的内容:
if(context.state){ returnfn(context.webpackStats); } context.log.info(`waituntilbundlefinished:${req.url||fn.name}`); context.callbacks.push(fn);
非常简单的方法,判断context.state的状态,将直接执行回调函数fn,或在context.callbacks中添加回调函数fn。这也解释了上文提到的另一个特性“在编译期间,停止提供旧版的bundle并且将请求延迟到最新的编译结果完成之后”。若webpack还处于编译状态,context.state会被设置为false,所以当用户发起请求时,并不会直接返回对应的文件内容,而是会将回调函数processRequest添加至context.callbacks中,而上文中我们说到在compile.hooks.done上注册了回调函数done,等编译完成之后,将会执行这个函数,并循环调用context.callbacks。
总结
源码的阅读是一个非常枯燥的过程,但是它的收益也是巨大的。上文的源码解读主要分析的是webpack-dev-middleware它是如何实现它所拥有的特性、如何处理用户的请求等主要功能点,未包括其他分支逻辑处理、容错。还需读者在这篇文章基础之上,再去阅读详细的源码,望这篇文章能对你的阅读过程起到一定的帮助作用。
到此这篇关于webpack-dev-middleware源码解读的文章就介绍到这了,更多相关webpack-dev-middleware源码解读内容请搜索毛票票以前的文章或继续浏览下面的相关文章希望大家以后多多支持毛票票!