详解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 Ajax学习实例4 向WebService发出请求,返回实体对象的异步调用
Mar 16 Javascript
jquery ajax jsonp跨域调用实例代码
Dec 11 Javascript
JavaScript每天定时更换皮肤样式的方法
Jul 01 Javascript
学习Javascript面向对象编程之封装
Feb 23 Javascript
jQuery获取cookie值及删除cookie用法实例
Apr 15 Javascript
原生JS实现几个常用DOM操作API实例
Jan 19 Javascript
JavaScript+Canvas实现彩色图片转换成黑白图片的方法分析
Jul 31 Javascript
antd Upload 文件上传的示例代码
Dec 14 Javascript
jQuery实现动态添加和删除input框实例代码
Mar 26 jQuery
微信小程序云开发使用方法新手初体验
May 16 Javascript
javascript 原型与原型链的理解及应用实例分析
Feb 10 Javascript
jquery实现直播视频弹幕效果
Feb 25 jQuery
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控制用户的浏览器--ob*函数的使用说明
2007/03/16 PHP
PHP5 面向对象(学习记录)
2009/12/02 PHP
PHP版 汉字转码的实现详解
2013/06/09 PHP
9段PHP实用功能的代码推荐
2014/10/14 PHP
Thinkphp中的curd应用实用要点
2015/01/04 PHP
PHP生成器简单实例
2015/05/13 PHP
深入解析WordPress中加载模板的get_template_part函数
2016/01/11 PHP
浅谈关于PHP解决图片无损压缩的问题
2017/09/01 PHP
JS无法捕获滚动条上的mouse up事件的原因猜想
2012/03/21 Javascript
两种方法实现文本框输入内容提示消失
2013/03/17 Javascript
jquery如何根据值设置默认的选中项
2014/03/17 Javascript
AngularJS基础 ng-readonly 指令简单示例
2016/08/02 Javascript
微信小程序 toast 详解及实例代码
2016/11/09 Javascript
JS常见简单正则表达式验证功能小结【手机,地址,企业税号,金额,身份证等】
2017/01/22 Javascript
javascript 中的try catch应用总结
2017/04/01 Javascript
vue-router+vuex addRoutes实现路由动态加载及菜单动态加载
2017/09/28 Javascript
Angular使用动态加载组件方法实现Dialog的示例
2018/05/11 Javascript
JS实现方形抽奖效果
2018/08/27 Javascript
解决angularjs WdatePicker ng-model的问题
2018/09/13 Javascript
浅谈layui使用模板引擎动态渲染元素要注意的问题
2019/09/14 Javascript
jQuery 查找元素操作实例小结
2019/10/02 jQuery
微信小程序实现单个卡片左滑显示按钮并防止上下滑动干扰功能
2019/12/06 Javascript
vue组件中传值EventBus的使用及注意事项说明
2020/11/16 Javascript
Vant+postcss-pxtorem 实现浏览器适配功能
2021/02/05 Javascript
Python 列表list使用介绍
2014/11/30 Python
关于python多重赋值的小问题
2019/04/17 Python
python 执行终端/控制台命令的例子
2019/07/12 Python
python3中rank函数的用法
2019/11/27 Python
python 识别登录验证码图片功能的实现代码(完整代码)
2020/07/03 Python
socket.io 和canvas 实现的共享画板功能
2019/05/22 HTML / CSS
机电一体化大学生求职信
2013/11/08 职场文书
高级销售求职信
2014/02/21 职场文书
给校长的一封建议书
2014/03/12 职场文书
十八大标语口号
2014/10/09 职场文书
新兵入伍决心书
2015/09/22 职场文书
MySQL系列之十一 日志记录
2021/07/02 MySQL