本文首发于政采云前端团队博客:webpack-dev-middleware 源码解读php
https://www.zoo.team/article/webpack-dev-middleware
Webpack 的使用目前已是前端开发工程师必备技能之一。如果想在本地环境启动一个开发服务,你们只需在 Webpack 的配置中,增长 devServer (https://www.webpackjs.com/configuration/dev-server/) 的配置来启动。devServer 配置的本质是 webpack-dev-server 这个包提供的功能,而 webpack-dev-middleware 则是这个包的底层依赖。前端
截至本文发表前,webpack-dev-middleware 的最新版本为 webpack-dev-middleware@3.7.2
,本文的源码来自于此版本。本文会讲解 webpack-dev-middleware 的核心模块实现,相信你们把这篇文章看完,再去阅读源码,会容易理解不少。webpack
const wdm = require('webpack-dev-middleware'); const express = require('express'); const webpack = require('webpack'); const webpackConf = require('./webapck.conf.js'); const compiler = webpack(webpackConf); const app = express(); app.use(wdm(compiler)); app.listen(8080);经过启动一个 Express (http://www.expressjs.com.cn/) 服务,将
wdm(compiler)
的结果经过
app.use
方法注册为 Express 服务的中间函数。从这里,咱们不难看出
wdm(compiler)
的执行结果返回的是一个
express
的中间件。它做为一个容器,将
webpack
编译后的文件存储到内存中,而后在用户访问
express
服务时,将内存中对应的资源输出返回。
熟悉 webpack
的同窗都知道,webpack
能够经过 watch mode (https://www.webpackjs.com/configuration/watch/) 方式启动,那为什么咱们不直接使用此方式来监听资源变化呢?答案就是,webpack
的 watch mode
虽然能监听文件的变动,而且自动打包,可是每次打包后的结果将会存储到本地硬盘中,而 IO 操做是很是耗资源时间的,没法知足本地开发调试需求。web
而 webpack-dev-middleware 拥有如下几点特性:express
watch mode
启动 webpack
,监听的资源一旦发生变动,便会自动编译,生产最新的 bundle
bundle
而且将请求延迟到最新的编译结果完成以后webpack
编译后的资源会存储在内存中,当用户请求资源时,直接于内存中查找对应资源,减小去硬盘中查找的 IO 操做耗时本文将主要围绕这三个特性和主流程逻辑进行分析。json
让咱们先来看下 webpack-dev-middleware 的源码目录:app
... ├── 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
的源码实现webpack-dev-server
wdm(compiler)
返回的是一个
express
中间件,因此入口文件
index.js
则为一个中间件的容器包装函数。它接收两个参数,一个为
webpack
的
compiler
、另外一个为配置对象,通过一系列的处理,最后返回一个中间件函数。下面我将对
index.js
中的核心代码进行讲解:
... setupHooks(context); ... // start watching 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
的编译内容,输出至内存中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
回调函数ide
invalid
、run
、watchRun
等生命周期上注册 invalid
方法,该方法主要是 report
编译的状态信息此部分的做用是,调用 compiler
的 watch 方法,以后 webpack
便会监听文件变动,一旦检测到文件变动,就会从新执行编译。函数
compiler
的文件系统对象,让
webpack
编译后的文件输出到内存中。
fileSystem = new MemoryFileSystem(); // eslint-disable-next-line no-param-reassign compiler.outputFileSystem = fileSystem;经过以上 3 个部分的执行,咱们以
watch mode
的方式启动了
webpack
,一旦监测的文件变动,便会从新进行编译打包,同时咱们又将文件的存储方法改成了内存存储,提升了文件的存储读取效率。最后,咱们只须要返回
express
的中间件就能够了,而中间件则是调用
middleware(context)
函数获得的。下面,咱们来看看
middleware
是如何实现的。
express
中间件函数的包装函数,其核心处理逻辑主要针对
request
请求,根据各类条件判断,最终返回对应的文件内容:
function goNext() { if (!context.options.serverSideRender) { return next(); } return new Promise((resolve) => { ready( context, () => { // eslint-disable-next-line no-param-reassign res.locals.webpackStats = context.webpackStats; // eslint-disable-next-line no-param-reassign res.locals.fs = context.fs; resolve(next()); }, req ); }); }
首先,middleware
中定义了一个 goNext()
方法,该方法判断是不是服务端渲染。若是是,则调用 ready()
方法(此方法即为 ready.js
文件,做用为根据 context.state
状态判断直接执行回调仍是将回调存储 callbacks
队列中)。若是不是,则直接调用 next()
方法,流转至下一个 express
中间件。
const acceptedMethods = context.options.methods || ['GET', 'HEAD']; if (acceptedMethods.indexOf(req.method) === -1) { return goNext(); }
接着,判断 HTTP
协议的请求的类型,若请求不包含于配置中(默认 GET
、HEAD
请求),则直接调用 goNext()
方法处理请求:
let filename = getFilenameFromUrl( context.options.publicPath, context.compiler, req.url ); if (filename === false) { return goNext(); }
而后,根据请求的 req.url
地址,在 compiler
的内存文件系统中查找对应的文件,若查找不到,则直接调用 goNext()
方法处理请求:
return new Promise((resolve) => { // eslint-disable-next-line consistent-return function proce***equest() { ... } ... ready(context, proce***equest, req); });
最后,中间件返回一个 Promise
实例,而在实例中,先是定义一个 proce***equest
方法,此方法的做用是根据上文中找到的 filename
路径获取到对应的文件内容,并构造 response
对象返回,随后调用 ready(context, proce***equest, req)
函数,去执行 proce***equest
方法。这里咱们着重看下 ready
方法的内容:
if (context.state) { return fn(context.webpackStats); } context.log.info(`wait until bundle finished: ${req.url || fn.name}`); context.callbacks.push(fn);很是简单的方法,判断
context.state
的状态,将直接执行回调函数
fn
,或在
context.callbacks
中添加回调函数
fn
。这也解释了上文提到的另外一个特性 “在编译期间,中止提供旧版的
bundle
而且将请求延迟到最新的编译结果完成以后”。若
webpack
还处于编译状态,
context.state
会被设置为
false
,因此当用户发起请求时,并不会直接返回对应的文件内容,而是会将回调函数
proce***equest
添加至
context.callbacks
中,而上文中咱们说到在
compile.hooks.done
上注册了回调函数
done
,等编译完成以后,将会执行这个函数,并循环调用
context.callbacks
。
源码的阅读是一个很是枯燥的过程,可是它的收益也是巨大的。上文的源码解读主要分析的是 webpack-dev-middleware
它是如何实现它所拥有的特性、如何处理用户的请求等主要功能点,未包括其余分支逻辑处理、容错。还需读者在这篇文章基础之上,再去阅读详细的源码,望这篇文章能对你的阅读过程起到必定的帮助做用。