Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

webpack-dev-middleware 源码解读[整理] #9

Open
fengma1992 opened this issue Mar 24, 2020 · 0 comments
Open

webpack-dev-middleware 源码解读[整理] #9

fengma1992 opened this issue Mar 24, 2020 · 0 comments

Comments

@fengma1992
Copy link
Owner

前言

他人原文:https://juejin.im/post/5e7782dbf265da57584dc95e

Webpack 的使用目前已经是前端开发工程师必备技能之一。若是想在本地环境启动一个开发服务,大家只需在 webpack 的配置中,增加 devServer 的配置来启动。而 devServer 配置的本质是 webpack-dev-server 这个包提供的功能,而 webpack-dev-middleware 则是这个包的底层依赖。
截止2020.03.23,本文解读的

  • webpack-dev-server版本:webpack-dev-server@3.10.3
  • webpack-dev-middleware版本:webpack-dev-middleware@3.7.2

webpack-dev-middleware 是什么?

先看看webpack-dev-server主要做了啥:

const wdm = require('webpack-dev-middleware');
const express = require('express');
const webpack = require('webpack');
const webpackConf = require('./webapck.conf.js'); // 引入webpack配置
const compiler = webpack(webpackConf); // 首先创建一个webpack compiler
const app = express(); // 建一个express服务
app.use(wdm(compiler)); // 用wdm(compiler)生成中间件并注册为express的中间函数
app.listen(8080); // 开启监听端口

一句话概括,webpack-dev-middlewarewebpack编译后的文件存在内存,并在用户访问express时将对应资源输出。

为什么要使用 webpack-dev-middleware

webpack可以通过watch mode启动,实现实时监听项目文件变化并编译输出,但输出结果需要存储到本地硬盘,而I/O操作非常消耗资源时间,无法满足本地开发调试需求(此处匿名吐槽gitbook的本地调试)。

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

watch mode 启动 webpack,监听的资源一旦发生变更,便会自动编译,生产最新的 bundle
在编译期间,停止提供旧版的 bundle 并且将请求延迟到最新的编译结果完成之后
webpack 编译后的资源会存储在内存中,当用户请求资源时,直接于内存中查找对应资源,减少去硬盘中查找的 I/O 操作耗时(这也是webpack-dev-middleware的核心部分)

到这一步了,其实不看源码,也能写出 webpack-dev-middleware 的大概流程:

  • watch mode启动webpack
  • webpack上挂编译中、编译完成、编译出错这些钩子
  • webpackbundle输出地址从原本的文件系统换成内存系统(关键)
  • 生成中间件给express

源码解读

webpack-dev-middleware源码目录:

...
├── lib
│   ├── DevMiddlewareError.js
│   ├── index.js // 主文件
│   ├── middleware.js
│   └── utils
│       ├── getFilenameFromUrl.js // 解析url得到文件名
│       ├── handleRangeHeaders.js
│       ├── index.js
│       ├── ready.js
│       ├── reporter.js
│       ├── setupHooks.js // 挂钩子
│       ├── setupLogger.js // 日志设置
│       ├── setupOutputFileSystem.js // 输出文件系统设置
│       ├── setupRebuild.js // 重构建逻辑设置
│       └── setupWriteToDisk.js // 写入磁盘设置
├── package.json
...

其实看文件名就知道干了些啥了。
其中 lib 目录下为源代码,一眼望去有近 10 多个文件要解读。但刨除 utils 工具集合目录,其核心源码文件其实只有两个 index.jsmiddleware.js
下面我们就来分析核心文件 index.jsmiddleware.js 的源码实现

入口文件index.js

从上文我们已经得知 wdm(compiler) 返回的是一个 express 中间件,所以入口文件 index.js 则为一个中间件的容器包装函数。它接收两个参数,一个为 webpackcompiler、另一个为配置对象,经过一系列的处理,最后返回一个中间件函数。下面对 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 个部分的执行,分别完成了我们上文提到的两点特性:

  • 挂钩子
  • watch mode 启动 webpack
  • webpack 的编译内容,输出至内存中

setupHooks

此函数的作用是在 compilerinvalidrundonewatchRun 这 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 回调函数
  • invalidrunwatchRun 等生命周期上注册 invalid 方法,该方法主要是 report 编译的状态信息

compiler.watch

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

setupOutputFileSystem

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

fileSystem = new MemoryFileSystem();
compiler.outputFileSystem = fileSystem;

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

middleware.js

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

function goNext() {
  if (!context.options.serverSideRender) {
    return next();
  }
  return new Promise((resolve) => {
    ready(
      context,
      () => {
        res.locals.webpackStats = context.webpackStats;
        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 协议的请求的类型,若请求不包含于配置中(默认 GETHEAD 请求),则直接调用 goNext() 方法处理请求:

let filename = getFilenameFromUrl(
  context.options.publicPath,
  context.compiler,
  req.url
);
if (filename === false) {
  return goNext();
}

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

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

总结

整体来看其实还挺简单的,前提是对 webpack 的完整打包流程及Hook 有了解。

强烈建议项目内使用 webpack 的同学完整读一遍 webpack 源码,那么像这些 webpack 的插件看用途就知道怎么实现的。

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

No branches or pull requests

1 participant