天天看点

webpack-dev-middleware 源码解读

 这是第 42 篇不掺水的原创

本文首发于政采云前端团队博客:webpack-dev-middleware 源码解读 https://www.zoo.team/article/webpack-dev-middleware
webpack-dev-middleware 源码解读

Webpack 的使用目前已经是前端开发工程师必备技能之一。若是想在本地环境启动一个开发服务,大家只需在 Webpack 的配置中,增加 devServer (https://www.webpackjs.com/configuration/dev-server/) 的配置来启动。devServer 配置的本质是 webpack-dev-server 这个包提供的功能,而 webpack-dev-middleware 则是这个包的底层依赖。

截至本文发表前,webpack-dev-middleware 的最新版本为 <code>[email protected]</code>,本文的源码来自于此版本。本文会讲解 webpack-dev-middleware 的核心模块实现,相信大家把这篇文章看完,再去阅读源码,会容易理解很多。

要回答这个问题,我们先来看看如何使用这个包:

通过启动一个 Express (http://www.expressjs.com.cn/) 服务,将 <code>wdm(compiler)</code> 的结果通过 <code>app.use</code> 方法注册为 Express 服务的中间函数。从这里,我们不难看出 <code>wdm(compiler)</code> 的执行结果返回的是一个 <code>express</code> 的中间件。它作为一个容器,将 <code>webpack</code> 编译后的文件存储到内存中,然后在用户访问 <code>express</code> 服务时,将内存中对应的资源输出返回。

熟悉 <code>webpack</code> 的同学都知道,<code>webpack</code> 可以通过 watch mode (https://www.webpackjs.com/configuration/watch/) 方式启动,那为何我们不直接使用此方式来监听资源变化呢?答案就是,<code>webpack</code> 的 <code>watch mode</code> 虽然能监听文件的变更,并且自动打包,但是每次打包后的结果将会存储到本地硬盘中,而 IO 操作是非常耗资源时间的,无法满足本地开发调试需求。

而 webpack-dev-middleware 拥有以下几点特性:

以 <code>watch mode</code> 启动 <code>webpack</code>,监听的资源一旦发生变更,便会自动编译,生产最新的 <code>bundle</code>

在编译期间,停止提供旧版的 <code>bundle</code> 并且将请求延迟到最新的编译结果完成之后

<code>webpack</code> 编译后的资源会存储在内存中,当用户请求资源时,直接于内存中查找对应资源,减少去硬盘中查找的 IO 操作耗时

本文将主要围绕这三个特性和主流程逻辑进行分析。

让我们先来看下 webpack-dev-middleware 的源码目录:

其中 <code>lib</code> 目录下为源代码,一眼望去有近 10 多个文件要解读。但刨除 <code>utils</code> 工具集合目录,其核心源码文件其实只有两个 <code>index.js</code>、<code>middleware.js</code>

下面我们就来分析核心文件 <code>index.js</code>、<code>middleware.js</code> 的源码实现

从上文我们已经得知 <code>wdm(compiler)</code> 返回的是一个 <code>express</code> 中间件,所以入口文件 <code>index.js</code> 则为一个中间件的容器包装函数。它接收两个参数,一个为 <code>webpack</code> 的 <code>compiler</code>、另一个为配置对象,经过一系列的处理,最后返回一个中间件函数。下面我将对 <code>index.js</code> 中的核心代码进行讲解:

<code>index.js</code> 最为核心的是以上 3 个部分的执行,分别完成了我们上文提到的两点特性:

以监控的方式启动 <code>webpack</code>

将 <code>webpack</code> 的编译内容,输出至内存中

此函数的作用是在 <code>compiler</code> 的 <code>invalid</code>、<code>run</code>、<code>done</code>、<code>watchRun</code> 这 4 个编译生命周期上,注册对应的处理方法

在 <code>done</code> 生命周期上注册 <code>done</code> 方法,该方法主要是 <code>report</code> 编译的信息以及执行 <code>context.callbacks</code> 回调函数

在 <code>invalid</code>、<code>run</code>、<code>watchRun</code> 等生命周期上注册 <code>invalid</code> 方法,该方法主要是 <code>report</code> 编译的状态信息

此部分的作用是,调用 <code>compiler</code> 的 watch 方法,之后 <code>webpack</code> 便会监听文件变更,一旦检测到文件变更,就会重新执行编译。

其作用是使用 memory-fs 对象替换掉 <code>compiler</code> 的文件系统对象,让 <code>webpack</code> 编译后的文件输出到内存中。

通过以上 3 个部分的执行,我们以 <code>watch mode</code> 的方式启动了 <code>webpack</code>,一旦监测的文件变更,便会重新进行编译打包,同时我们又将文件的存储方法改为了内存存储,提高了文件的存储读取效率。最后,我们只需要返回 <code>express</code> 的中间件就可以了,而中间件则是调用 <code>middleware(context)</code> 函数得到的。下面,我们来看看 <code>middleware</code> 是如何实现的。

此文件返回的是一个 <code>express</code> 中间件函数的包装函数,其核心处理逻辑主要针对 <code>request</code> 请求,根据各种条件判断,最终返回对应的文件内容:

首先,<code>middleware</code> 中定义了一个 <code>goNext()</code> 方法,该方法判断是否是服务端渲染。如果是,则调用 <code>ready()</code> 方法(此方法即为 <code>ready.js</code> 文件,作用为根据 <code>context.state</code> 状态判断直接执行回调还是将回调存储 <code>callbacks</code> 队列中)。如果不是,则直接调用 <code>next()</code> 方法,流转至下一个 <code>express</code> 中间件。

接着,判断 <code>HTTP</code> 协议的请求的类型,若请求不包含于配置中(默认 <code>GET</code>、<code>HEAD</code> 请求),则直接调用 <code>goNext()</code> 方法处理请求:

然后,根据请求的 <code>req.url</code> 地址,在 <code>compiler</code> 的内存文件系统中查找对应的文件,若查找不到,则直接调用 <code>goNext()</code> 方法处理请求:

最后,中间件返回一个 <code>Promise</code> 实例,而在实例中,先是定义一个 <code>processRequest</code> 方法,此方法的作用是根据上文中找到的 <code>filename</code> 路径获取到对应的文件内容,并构造 <code>response</code> 对象返回,随后调用 <code>ready(context, processRequest, req)</code> 函数,去执行 <code>processRequest</code> 方法。这里我们着重看下 <code>ready</code> 方法的内容:

非常简单的方法,判断 <code>context.state</code> 的状态,将直接执行回调函数 <code>fn</code>,或在 <code>context.callbacks</code> 中添加回调函数 <code>fn</code>。这也解释了上文提到的另一个特性 “在编译期间,停止提供旧版的 <code>bundle</code> 并且将请求延迟到最新的编译结果完成之后”。若 <code>webpack</code> 还处于编译状态,<code>context.state</code> 会被设置为 <code>false</code>,所以当用户发起请求时,并不会直接返回对应的文件内容,而是会将回调函数 <code>processRequest</code> 添加至 <code>context.callbacks</code> 中,而上文中我们说到在 <code>compile.hooks.done</code> 上注册了回调函数 <code>done</code>,等编译完成之后,将会执行这个函数,并循环调用 <code>context.callbacks</code>。

源码的阅读是一个非常枯燥的过程,但是它的收益也是巨大的。上文的源码解读主要分析的是 <code>webpack-dev-middleware</code> 它是如何实现它所拥有的特性、如何处理用户的请求等主要功能点,未包括其他分支逻辑处理、容错。还需读者在这篇文章基础之上,再去阅读详细的源码,望这篇文章能对你的阅读过程起到一定的帮助作用。

webpack-dev-middleware 源码解读
webpack-dev-middleware 源码解读

继续阅读