写点什么

webpack-dev-middleware 源码解读

  • 2021-03-18
  • 本文字数:3897 字

    阅读完需:约 13 分钟

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-03-18 23:461656

评论

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

Go常用设计模式(下)

海风极客

三周年连更

Django笔记二十二之多数据库操作

Hunter熊

Python django database

深入理解MapReduce:使用Java编写MapReduce程序【上进小菜猪】

上进小菜猪

mapreduce 上进小菜猪

一套前后台全部开源的H5商城送给大家

越长大越悲伤

开源 java‘

架构训练营模块一作业

Geek_3d7c4d

架构训练营

Mac 触控增强神器:BetterTouchTool如何使用?

Rose

苹果软件下载 BetterTouchTool破解 BetterTouchTool教程 Mac 触控增强神器

waves插件更新,Waves V14系统及支持的主机一览

魔仙苹果mac堡

waves下载 waves14

容量成本性能全都要有, Redis 容量版 PegaDB 设计与实践

Baidu AICLOUD

macOS硬盘如何格式转换?用Tuxera NTFS就够了!

Rose

ntfs FAT32

Focus Matrix for Mac(智能任务管理器)

Rose

mac软件下载 Focus Matrix 任务管理器

Office Mac升级提醒如何去掉?关闭Microsoft AutoUpdate弹框提示

Rose

许可证 Office 2019中文版 Office Mac office更新 office2021下载

深入理解 TypeScript 的 type 以及 type 与 interface 和 class 的区别

Lee Chen

typescript

IPPswap流动性LP挖矿系统开发

l8l259l3365

什么是人工智能领域模型的 Presence Penalty 参数?

Jerry Wang

人工智能 机器学习 深度学习 强化学习 三周年连更

Java IO流详解

timerring

Java

macbook触摸板怎么按右键

魔仙苹果mac堡

MacBook 触控板

必知必会的JavaScript前端面试题篇(一),不看后悔!

控心つcrazy

Golang中如何使用Singleflight库进行并发请求合并

Jack

在啥样的公司工作没意义

Jadedev

职场 职场经验 职场发展

云原生文件存储 CFS 线性扩展到千亿级文件数,百度沧海·存储论文被 EuroSys 2023 录用

Baidu AICLOUD

文件存储 元数据 posix

Go常用设计模式(中)

海风极客

三周年连更

2023-05-06:X轴上有一些机器人和工厂。给你一个整数数组robot,其中robot[i]是第i个机器人的位置 再给你一个二维整数数组factory,其中 factory[j] = [posit

福大大架构师每日一题

golang rust 福大大

Macos媒体播放器 Movist Pro 针对 macOS 13 Ventura 进行了优化

Rose

Movist Pro 中文版 Movist Pro下载 Macos媒体播放器 视频播放器下载

苹果Mac最佳卸载程序和清理助手:App Cleaner & Uninstaller

Rose

mac系统清理优化软件 苹果系统清理 App Cleaner

软件测试 | MTV开发模式

测吧(北京)科技有限公司

测试

玛雅Maya 2024 发布 maya2024破解

魔仙苹果mac堡

maya2024下载 maya2024新功能 maya2024安装教程

云原生应用使用的云服务组件介绍

穿过生命散发芬芳

三周年连更 云服务组件

SVN管理工具Cornerstone意外退出怎么办?

魔仙苹果mac堡

SVN管理工具 cornerstone 4破解 Cornerstone mac版 Cornerstone意外退出

Smart Disk Image Utilities for Mac(智能磁盘镜像工具)

魔仙苹果mac堡

Smart Disk Mac磁盘管理

Java实现坦克大战2.0

timerring

Java

【分布式技术专题】「分布式技术架构」手把手教你如何开发一个属于自己的Redis延时队列的功能组件

洛神灬殇

redis 分布式 延时队列 redisson 三周年连更

webpack-dev-middleware 源码解读_语言 & 开发_政采云前端团队_InfoQ精选文章