webpack-dev-middleware 源码深度解析:内存存储与请求处理机制

作者:知秋时间:2026-10-23 16:52:58浏览:543

本文深入解析 webpack-dev-middleware 核心源码,阐述其作为 webpack-dev-server 底层依赖的工作原理。重点分析如何通过 memory-fs 实现编译结果内存存储,利用 compiler.watch 监听文件变更,以及中间件如何处理 HTTP 请求并延迟响应直至编译完成,从而提升本地开发效率。

前言

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 是什么?

要理解这一概念,首先需了解其基本用法:


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 服务,将 wdm(compiler) 的返回值通过 app.use 注册为 Express 的中间件。由此可见,wdm(compiler) 返回的是一个 Express 中间件函数。该函数充当容器角色,将 Webpack 编译后的文件存储于内存中,并在用户访问 Express 服务时,从内存中读取并返回对应的资源。

使用 webpack-dev-middleware 的优势

为什么要使用 webpack-dev-middleware

熟悉 Webpack 的开发者知道,Webpack 支持 watch mode(监听模式)启动。那么为何不直接使用此模式监听资源变化呢?原因在于,虽然 watch mode 能监听文件变更并自动打包,但每次打包结果都会写入本地硬盘。频繁的磁盘 IO 操作耗时较长,无法满足本地开发调试对实时性的需求。

而 webpack-dev-middleware 具备以下核心特性:

  • 以 watch mode 启动 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);
...
// 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 的核心逻辑主要包含上述三个部分,分别实现了前文提到的两个关键特性:

  • 以监控模式启动 Webpack。
  • 将 Webpack 的编译内容输出至内存中。

生命周期钩子注册

setupHooks

该函数的作用是在 compiler 的 invalid、run、done、watchRun 这四个编译生命周期钩子上,注册相应的处理方法。


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 方法,主要用于报告编译信息并执行 context.callbacks 中的回调函数。
  • 在 invalid、run、watchRun 等生命周期上注册 invalid 方法,主要用于报告编译的状态信息。

文件监听与内存存储

compiler.watch

此部分代码调用 compiler 的 watch 方法。此后,Webpack 将开始监听文件变更,一旦检测到文件发生变化,便会重新执行编译流程。

setupOutputFileSystem

该函数的作用是使用 memory-fs 对象替换 compiler 原有的文件系统对象,使得 Webpack 编译后的文件直接输出到内存中,而非磁盘。


fileSystem = new MemoryFileSystem();
// eslint-disable-next-line no-param-reassign
compiler.outputFileSystem = fileSystem;

通过执行上述三个部分,我们以 watch mode 方式启动了 Webpack。一旦监测到文件变更,便会重新编译打包;同时,我们将文件存储方式改为内存存储,显著提高了文件的读写效率。最后,只需返回由 middleware(context) 函数生成的 Express 中间件即可。接下来,我们将分析 middleware 的具体实现。

中间件请求处理逻辑

middleware.js

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


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 processRequest() {
  ...
 }
 ...
 ready(context, processRequest, req);
});

最后,中间件返回一个 Promise 实例。在实例中,首先定义一个 processRequest 方法,该方法根据前文找到的 filename 路径获取对应的文件内容,并构造 response 对象返回。随后调用 ready(context, processRequest, req) 函数来执行 processRequest 方法。这里重点分析 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;若为假,则将回调函数 fn 添加至 context.callbacks 队列中。这也解释了前文提到的另一个特性:“在编译期间,停止提供旧版的 bundle 并且将请求延迟到最新的编译结果完成之后”。当 Webpack 仍处于编译状态时,context.state 会被设置为 false。因此,当用户发起请求时,不会直接返回文件内容,而是将 processRequest 回调函数添加至 context.callbacks 队列。前文提到在 compile.hooks.done 上注册了 done 回调,待编译完成后,将执行该函数并循环调用 context.callbacks 中的回调。

总结

源码阅读是一个枯燥但收益巨大的过程。本文主要分析了 webpack-dev-middleware 如何实现其核心特性以及处理用户请求的主要功能点,未涵盖其他分支逻辑处理和容错机制。建议读者在本文基础上,进一步阅读详细源码,希望本文能对大家的源码阅读过程有所帮助。

到此这篇关于 webpack-dev-middleware 源码解读的文章就介绍到这了,更多相关 webpack-dev-middleware 源码解读内容请搜索本站以前的文章或继续浏览下面的相关文章,希望大家以后多多支持本站!

作者最新文章