详解webpack-dev-middleware 源码解读

前言

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 源码解读内容请搜索我们以前的文章或继续浏览下面的相关文章希望大家以后多多支持我们!

(0)

相关推荐

  • 关于Webpack dev server热加载失败的解决方法

    利用Webpack dev server作为热加载服务器时,出现以下错误: XMLHttpRequest cannot load http://localhost:8080/dist/06854fc8988da94501a9.hot-update.json. No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost' is therefore not

  • 浅谈webpack-dev-server的配置和使用

    本文介绍了浅谈webpack-dev-server的配置和使用,分享给大家,具体如下: 1安装的WebPack-dev-server 在终端输入 npm i webpack-dev-server 安装webpack-dev-server包 2.配置dev-server 在package.json文件中的脚本中添加代码 "dev":"WebPack-dev-server --config webpack.config.js" 在webpack.config.js文件中

  • 详解Webpack-dev-server的proxy用法

    前言 如果你有单独的后端开发服务器 API,并且希望在同域名下发送 API 请求 ,那么代理某些 URL 会很有用. 解决开发环境的跨域问题(不用在去配置nginx和host, 爽歪歪~~) 在webpack.config.js中配置 下面简单介绍一下五个经常使用的场景 使用一: mmodule.exports = { //... devServer: { proxy: { '/api': 'http://localhost:3000' } } }; 请求到 /api/xxx 现在会被代理到请求

  • 详解webpack-dev-server使用方法

    记录下webpack-dev-server的用法. 首先,我们来看看基本的webpack.config.js的写法 module.exports = { entry: './src/js/index.js', output: { path: './dist/js', filename: 'bundle.js' } } 配置文件提供一个入口和一个出口,webpack根据这个来进行js的打包和编译工作.虽然webpack提供了webpack --watch的命令来动态监听文件的改变并实时打包,输出新

  • 详解webpack-dev-server使用http-proxy解决跨域问题

    文档资料 webpack关于webpack-dev-server开启proxy的官方介绍 Vue-cli proxyTable 解决开发环境的跨域问题--虽然这篇是写vue的,不过用在webpack-dev-server上也是一样的 http-proxy-middleware--webpack-dev-server的实现方法其实是对这个的封装 配置http-proxy 在webpack的配置文件(webpack.config.js)中进行配置 module.exports = { ...此处省略

  • Webpack devServer中的 proxy 实现跨域的解决

    Webpack dev server使用http-proxy解决跨域问题 文档资料 webpack关于webpack-dev-server开启proxy的官方介绍 Vue-cli proxyTable 解决开发环境的跨域问题--虽然这篇是写vue的,不过用在webpack-dev-server上也是一样的 http-proxy-middleware--webpack-dev-server的实现方法其实是对这个的封装 配置http-proxy 在webpack的配置文件(webpack.confi

  • 使用webpack-dev-server处理跨域请求的方法

    在前端调试的时候,跨域一直都是一个比较麻烦的问题,这个在之前的文章 关于跨域问题的一个解决方法中其实已经讨论了一些可以使用的方法. 如果要使用 JSONP,第一是需要修改的地方比较多,而且也不太符合前端发展的大趋势,如果使用 CORS 的话并没有 application/json 类型.而且更重要的是这只是在前端调试时候的需求,并不是在上线以后的需求,所以对后端有太多的入侵也不好. 所以就有一个念想突然在大脑中闪过--加入有一个代理不就可以解决这个问题了?但是又想了一下写起来还挺麻烦,于是就被搁

  • 详解webpack-dev-server 设置反向代理解决跨域问题

    一.设置代理的原因 现在对前端开发的要求越来越高,并且随着自动化以及模块化的诞生,前后端开发模式越来越流行.后端只负责接口,前端负责数据展示.逻辑处理.但是前后端开发模式,有一个重要的问题,就是跨域问题.  二.如何配置webpack的代理 webpack代理需要另外一个插件:webpack-dev-server webpack-dev-server配置代理非常的方便,只需要条件一个proxy属性,然后配置相关的参数就可以了: var webpack = require('webpack');

  • 详解go中panic源码解读

    panic源码解读 前言 本文是在go version go1.13.15 darwin/amd64上进行的 panic的作用 panic能够改变程序的控制流,调用panic后会立刻停止执行当前函数的剩余代码,并在当前Goroutine中递归执行调用方的defer: recover可以中止panic造成的程序崩溃.它是一个只能在defer中发挥作用的函数,在其他作用域中调用不会发挥作用: 举个栗子 package main import "fmt" func main() { fmt.

  • Mybatis-plus使用TableNameHandler分表详解(附完整示例源码)

    为什么要分表 Mysql是当前互联网系统中使用非常广泛的关系数据库,具有ACID的特性. 但是mysql的单表性能会受到表中数据量的限制,主要原因是B+树索引过大导致查询时索引无法全部加载到内存.读取磁盘的次数变多,而磁盘的每次读取对性能都有很大的影响. 这时一个简单可行的方案就是分表(当然土豪也可以堆硬件),将一张数据量庞大的表的数据,拆分到多个表中,这同时也减少了B+树索引的大小,减少磁盘读取次数,提高性能. 两种基础分表逻辑 说完了为什么要分表,下面聊聊业务开发中常见的两种基础的分表逻辑.

  • 详解SpringBoot自动配置源码

    一.引导加载自动配置类 @SpringBootApplication注解相当于@SpringBootConfiguration.@EnableAutoConfiguration.@ComponentScan这三个注解的整合 @SpringBootConfiguration 这个注解也使用了@Configuration标注,代表当前是一个配置类 @ComponentScan 包扫描,指定扫描哪些注解 @EnableAutoConfiguration 这个注解也是一个合成注解 @AutoConfig

  • 详解CentOS 7.0源码包搭建LNMP 实际环境搭建

    Centos7+Nginx1.11.7+MySQL5.7.16+PHP7.1.0+openssl-1.1.0c 一.linux 系统限制配置 1.关闭系统防火墙 systemctl stop firewalld.service 关闭防火墙 systemctl disable firewalld.service 禁用防火墙 2.关闭SElinux sed -i 's/SELINUX=.*/SELINUX=disabled/g' /etc/selinux/config setenforce 0 se

  • 详解从Vue.js源码看异步更新DOM策略及nextTick

    写在前面 因为对Vue.js很感兴趣,而且平时工作的技术栈也是Vue.js,这几个月花了些时间研究学习了一下Vue.js源码,并做了总结与输出. 文章的原地址:https://github.com/answershuto/learnVue. 在学习过程中,为Vue加上了中文的注释https://github.com/answershuto/learnVue/tree/master/vue-src,希望可以对其他想学习Vue源码的小伙伴有所帮助. 可能会有理解存在偏差的地方,欢迎提issue指出,

  • 详解IDEA创建Tomcat8源码工程流程

    上一篇文章的产出,其实离不开网上各位大神们的辅助,正是通过他们的讲解,我才对Tomcat的结构有了更进一步的认识. 但在描述前后端交互的过程中,还有很多细节并没有描述到位,所以就有了研究Tomcat源码的想法. 而在配置Tomcat源码工程的过程中,摸摸爬爬两个多小时,总算是成功启动了. 故撰写此篇博文,授之以渔. 准备工作 1.apache-tomcat-8.5.32-src源码包,官网下载并解压即可: 2.apache-ant-1.10.5(用的最新版)下载并安装:Tomcat源码默认采用的

  • 详解Element 指令clickoutside源码分析

    clickoutside是Element-ui实现的一个自定义指令,顾名思义,该指令用来处理目标节点之外的点击事件,常用来处理下拉菜单等展开内容的关闭,在Element-ui的Select选择器.Dropdown下拉菜单.Popover 弹出框等组件中都用到了该指令,所以这个指令在实现一些自定义组件的时候非常有用. 要分析该源码,首先要了解一下Vue的自定义指令.自定义指令的定义方式如下: // 注册一个全局自定义指令 Vue.directive('directiveName', { bind:

  • Android编程动态加载布局实例详解【附demo源码】

    本文实例讲述了Android编程动态加载布局的方法.分享给大家供大家参考,具体如下: 由于前段时间项目需要,需要在一个页面上加载根据不同的按钮加载不同的布局页面,当时想到用 tabhot .不过美工提供的界面图完全用不上tabhot ,所以想到了动态加载的方法来解决这一需求.在这里我整理了一下,写了一个 DEMO 希望大家以后少走点弯路. 首先,我们先把界面的框架图画出来,示意图如下: 中间白色部门是一个线性布局文件,我喜欢在画图的时候用不同的颜色将一块布局标示出来,方便查看.布局文件代码如下:

  • 微信小程序 授权登录详解(附完整源码)

    一.前言 由于微信官方修改了 getUserInfo 接口,所以现在无法实现一进入微信小程序就弹出授权窗口,只能通过 button 去触发. 官方连接:https://developers.weixin.qq.com/community/develop/doc/0000a26e1aca6012e896a517556c01 二.实现思路 自己写一个微信授权登录页面让用户实现点击的功能,也就是实现了通过 button 组件去触发 getUserInof 接口.在用户进入微信小程序的时候,判断用户是否

  • Android编程实现的微信支付功能详解【附Demo源码下载】

    本文实例讲述了Android编程实现的微信支付功能.分享给大家供大家参考,具体如下: 最近公司弄Ionic框架,项目中需要微信支付,无奈,把我调过去弄,期间也是几近崩溃,好在皇天不负有心人,在看别人的文档,终于是在项目中集成了微信支付,下面作为一个小白的我,想要把我的经验分享给大家,希望对大家有所帮助. 先给一个可用的demo吧(运行前先看txt文件) demo代码点击此处本站下载. 这个demo是基于eclipse开发的,博主也在Android Studio开发过微信支付,原理都是一样的,大家

随机推荐