详解webpack-dev-middleware 源码解读


Posted in Javascript onMarch 23, 2020

前言

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 的同学都知道,webpack 可以通过watch mode 方式启动,那为何我们不直接使用此方式来监听资源变化呢?答案就是,webpack 的 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 最为核心的是以上 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 = new MemoryFileSystem();
// eslint-disable-next-line no-param-reassign
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,
   () => {
    // 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,或在 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 源码解读内容请搜索三水点靠木以前的文章或继续浏览下面的相关文章希望大家以后多多支持三水点靠木!

Javascript 相关文章推荐
读jQuery之四(优雅的迭代)
Jun 20 Javascript
映彩衣的js随笔(js图片切换效果)
Jul 31 Javascript
使用 Node.js 做 Function Test实现方法
Oct 25 Javascript
图片上传插件jquery.uploadify详解
Nov 15 Javascript
Javascript获取background属性中url的值
Oct 17 Javascript
微信小程序中form 表单提交和取值实例详解
Apr 20 Javascript
十分钟带你快速了解React16新特性
Nov 10 Javascript
Vue项目组件化工程开发实践方案
Jan 09 Javascript
Angular4 反向代理Details实践
May 30 Javascript
vue生命周期实例小结
Aug 15 Javascript
详解如何在vue项目中使用lodop打印插件
Sep 27 Javascript
vue 解决data中定义图片相对路径页面不显示的问题
Aug 13 Javascript
vscode调试node.js的实现方法
Mar 22 #Javascript
如何优雅地取消 JavaScript 异步任务
Mar 22 #Javascript
Vue-cli3多页面配置详解
Mar 22 #Javascript
redux处理异步action解决方案
Mar 22 #Javascript
JS+CSS实现3D切割轮播图
Mar 21 #Javascript
vue-autoui自匹配webapi的UI控件的实现
Mar 20 #Javascript
jQuery实现中奖播报功能(让文本滚动起来) 简单设置数值即可
Mar 20 #jQuery
You might like
PHP-MySQL教程归纳总结
2008/06/07 PHP
Discuz 模板语句分析及知识技巧
2009/08/21 PHP
php中判断文件空目录是否有读写权限的函数代码
2012/08/07 PHP
关于Zend Studio 配色方案插件的介绍
2013/06/24 PHP
PHP开发框架kohana3 自定义路由设置示例
2014/07/14 PHP
PHP 实现类似js中alert() 提示框
2015/03/18 PHP
PHP使用Mysqli类库实现完美分页效果的方法
2016/04/07 PHP
php根据命令行参数生成配置文件详解
2019/03/15 PHP
解决Laravel5.5下的toArray问题
2019/10/15 PHP
发一个自己用JS写的实用看图工具实现代码
2008/07/26 Javascript
jQuery AJAX timeout 超时问题详解
2016/06/21 Javascript
JavaScript 拖拽实例代码
2016/09/21 Javascript
javascript ES6中箭头函数注意细节小结
2017/02/17 Javascript
微信小程序三级联动地址选择器的实例代码
2017/07/12 Javascript
解决vue点击控制单个样式的问题
2018/09/05 Javascript
又拍云 Node.js 实现文件上传、删除功能
2018/10/28 Javascript
详解Vue中组件的缓存
2019/04/20 Javascript
swiper Scrollbar滚动条组件详解
2019/09/08 Javascript
关于angular浏览器兼容性问题的解决方案
2020/07/26 Javascript
vue keep-alive实现多组件嵌套中个别组件存活不销毁的操作
2020/10/30 Javascript
python中利用队列asyncio.Queue进行通讯详解
2017/09/10 Python
python3中pip3安装出错,找不到SSL的解决方式
2019/12/12 Python
利用CSS3的border-radius绘制太极及爱心图案示例
2016/05/17 HTML / CSS
杰夫·班克斯男士服装网上商店:Jeff Banks
2019/10/24 全球购物
经贸韩语专业大学生职业规划
2014/02/14 职场文书
物控部经理职务说明书
2014/02/25 职场文书
捐赠仪式主持词
2014/03/19 职场文书
工作总结与自我评价
2014/09/18 职场文书
常务副总经理岗位职责
2015/02/02 职场文书
北京英文导游词
2015/02/12 职场文书
2019公司借款合同范本2篇!
2019/07/24 职场文书
员工保密协议范本,您一定得收藏!很有用!
2019/08/08 职场文书
MySQL5.7并行复制原理及实现
2021/06/03 MySQL
梳理总结Python开发中需要摒弃的18个坏习惯
2022/01/22 Python
mybatis源码解读之executor包语句处理功能
2022/02/15 Java/Android
Redis实现订单过期删除的方法步骤
2022/06/05 Redis