GMTC全球大前端技术大会(北京站)门票9折特惠截至本周五,点击立减¥480 了解详情
写点什么

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:461176

评论

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

iOS面试基础知识 (二)

iOSer

ios 面试题 iOS面试

实战排查|为什么遮挡推流摄像头,会导致播放绿屏?

阿里云视频云

音视频 WebRTC RTC bug RTMP

最简单的 K8S 部署文件编写姿势,没有之一!

Kevin Wan

golang Kubernetes

智慧社区服务平台开发,平安小区建设

t13823115967

智慧城市 平安小区

线程池的7种创建方式,强烈推荐你用它...

王磊

Java

警惕”被讲故事“ | 读《叙事改变人生》

邓瑞恒Ryan

读书笔记 哲学 创业心态 社会学 世界观

盘点 2020 | 一枚程序员的跑步之路

Simon

程序员 跑步 锻炼 盘点2020

微信昵称可以加雪花了,个性又好看

程序员生活志

工具 微信名 雪花

年终盘点 | 七年零故障支撑 双11 的消息中间件 RocketMQ,怎么做到的?

阿里巴巴云原生

阿里云 开源 云原生 中间件 消息队列

tron波场智能合约系统软件开发|tron波场智能合约APP开发

开發I852946OIIO

系统开发

JVM的艺术—JAVA内存模型

云流

Java jdk JVM

六个步骤,从零开始教你搭建基于WordPress的个人博客

华为云开发者社区

网站 WordPress 搭建

区块链农产品溯源应用开发,农产品可追溯系统

135深圳3055源中瑞8032

深入浅出 Go - sync/atomic 源码分析

哈希说

golang

IT民工闲话·点一盏灯

IT民工大叔

成长 IT 传承

英特尔正式发布全新一代内存和存储产品

商业资讯

智能警务平台搭建,公安一体化警务实战解决方案

t13823115967

智慧公安 智慧警务系统开发

刚刚,阿里云知行动手实验室正式开放公测了

阿里巴巴云原生

阿里云 开发者 云原生 k8s dubbo

3. 搞定收工,PropertyEditor就到这

YourBatman

Spring Framework 类型转换 PropertyEditor

未雨绸缪,数据保护之NBU介质备份

华为云开发者社区

安全 数据 保护

记一次网络请求连接超时的事故

AI乔治

Java 架构 HTTP

亿级流量背后战场,京东11.11大促全方位技术揭秘

京东科技开发者

云计算

智能合约DAPP软件系统开发

开發I852946OIIO

系统开发

十一周作业

走走,停停……

原来只想简单看一下String源码,没想到整理了这么多知识点

小Q

Java 学习 编程 面试 string

mybatis分页插件如何实现?

田维常

mybatis

《Tensorflow:实战Google深度学习框架》.pdf

田维常

Android uni-app 封装原生插件

anyRTC开发者

uni-app android 音视频 跨平台 聊天室

我是如何拿到蚂蚁金服offer?看完2020年Java研发岗复盘经验总结,是时候让面试官懵逼了

比伯

Java 编程 架构 面试 程序人生

实践大于一切!Alibaba最新MySQL性能优化+高可用架构全彩版PDF

Java架构追梦

Java MySQL 学习 架构 面试

为了SpringBoot提交Tomcat执行,我总结了这么多

996小迁

Java tomcat 架构 springboot

webpack-dev-middleware 源码解读-InfoQ