写点什么

webpack-dev-middleware 源码解读

2021 年 3 月 18 日

webpack-dev-middleware 源码解读

前言


Webpack 的使用目前已经是前端开发工程师必备技能之一。若是想在本地环境启动一个开发服务,大家只需在 Webpack 的配置中,增加 devServer (https://www.webpackjs.com/configuration/dev-server/) 的配置来启动。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 (http://www.expressjs.com.cn/) 服务,将 wdm(compiler) 的结果通过 app.use 方法注册为 Express 服务的中间函数。从这里,我们不难看出 wdm(compiler) 的执行结果返回的是一个 express 的中间件。它作为一个容器,将 webpack 编译后的文件存储到内存中,然后在用户访问 express 服务时,将内存中对应的资源输出返回。


为什么要使用 webpack-dev-middleware


熟悉 webpack 的同学都知道,webpack 可以通过 watch mode (https://www.webpackjs.com/configuration/watch/) 方式启动,那为何我们不直接使用此方式来监听资源变化呢?答案就是,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.jsmiddleware.js


下面我们就来分析核心文件 index.jsmiddleware.js 的源码实现


入口文件 index.js


从上文我们已经得知 wdm(compiler) 返回的是一个 express 中间件,所以入口文件 index.js 则为一个中间件的容器包装函数。它接收两个参数,一个为 webpack 的 compiler、另一个为配置对象,经过一系列的处理,最后返回一个中间件函数。下面我将对 index.js 中的核心代码进行讲解:


...setupHooks(context);...// start watchingcontext.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);
复制代码


 最为核心的是以上 3 个部分的执行,分别完成了我们上文提到的两点特性:


  • 以监控的方式启动 webpack

  • 将 webpack 的编译内容,输出至内存中


setupHooks


此函数的作用是在 compiler 的 invalidrundonewatchRun 这 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


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


setupOutputFileSystem


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


fileSystem = new MemoryFileSystem();// eslint-disable-next-line no-param-reassigncompiler.outputFileSystem = fileSystem;
复制代码


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


middleware.js


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


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


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


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


returnnewPromise((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 它是如何实现它所拥有的特性、如何处理用户的请求等主要功能点,未包括其他分支逻辑处理、容错。还需读者在这篇文章基础之上,再去阅读详细的源码,望这篇文章能对你的阅读过程起到一定的帮助作用。



头图:Unsplash

作者:蜗牛

原文:https://mp.weixin.qq.com/s/mp4iolzgZQkJMjK6oq5wHQ

原文:webpack-dev-middleware 源码解读

来源:政采云前端团队 - 微信公众号 [ID:Zoo-Team]

转载:著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

2021 年 3 月 18 日 23:461093

评论

发布
暂无评论
发现更多内容

轻松使用TensorFlow进行数据增强

计算机与AI

tensorflow 学习 数据增强

浅析一个较完整的SpringBoot项目

田维常

sping

【原创】Spring Boot集成Redis的玩法

田维常

spring Boot Starter

【API进阶之路】API带来的微创新,打动投资人鼓励我创业

华为云开发者社区

学习 视频 API

电子劳动合同来了 足不出户也能签约

CECBC区块链专委会

社会保险 电子合同

华为云专家私房课:视频传输技术选型的三大法宝

华为云开发者社区

音视频 视频 传输

熟悉又陌生的 k8s 字段:SecurityContext

郭旭东

Kubernetes Kubernetes源码

【原创】SpringBoot快速整合Thymeleaf模板引擎

田维常

springboot

登陆!Let's Start Coding

蚂蚁集团移动开发平台 mPaaS

移动开发 mPaaS

架构训练营-week7-学习总结-性能测试,操作系统,锁

于成龙

架构训练营

4年Java经验,去面试居然10分钟就结束了,现在面试为什么这么难?

Java架构之路

Java 程序员 架构 面试 编程语言

饱受毕设摧残计算机系师兄,怒而分享纯净版SSM框架(附源码)

小Q

Java 学习 面试 开发 SSM框架

Spring Boot 集成 Druid 监控数据源

田维常

springboot

Spring Boot 如何快速实现定时任务

田维常

springboot

关于DevOps的七大误解,99%的人都曾中过招!

华为云开发者社区

DevOps 敏捷开发 测试

量化交易系统开发、自动对冲策略搭建

薇電13242772558

区块链

【原创】SpringBoot 这几种配置文件方式,你都用过吗?

田维常

springboot

如何快速构建Spring Boot基础项目?

田维常

spring Boot Starter

【原创】Spring Boot一口气说自动装配与案例

田维常

springboot

美国大选观战感受:用区块链投票吧,少操点心

CECBC区块链专委会

区块链 投票机制

偷师Kubernetes源码,学会怎么用Go实现调度队列

网管

go Kubernetes 并发编程 并发控制

华为云FusionInsight MRS:助力企业构建“一企一湖,一城一湖”

华为云开发者社区

数据库 云原生 数据

快速掌握并发编程---ArrayBlockingQueue 底层原理和实战 java

田维常

并发编程 LinkedBlockingQueue

【原创】Spring Boot集成Mybatis的玩法

田维常

springboot

日常工作参数分析

hasWhere

如何降低young gc时间

AI乔治

Java 架构 GC GC算法

快速掌握并发编程---关于乐观锁、悲观锁、可重入锁

田维常

并发编程

第六周学习技术选型2总结

三板斧

运筹帷幄之后,决胜千里之外!GitHub上标星75k+的《Java面试突击手册》助你拿到满意的offer。

Java架构之路

Java 程序员 架构 面试 编程语言

Github标星35K+超火的Spring Boot实战项目,附超全教程文档

Java架构之路

Java 程序员 架构 面试 编程语言

【原创】Spring Boot 集成Spring Data JPA的玩法

田维常

springboot

4月17日 HarmonyOS 开发者日·上海站

4月17日 HarmonyOS 开发者日·上海站

webpack-dev-middleware 源码解读-InfoQ