详解webpack-dev-middleware 源码解读
前言
Webpack 的使用目前已经是前端开发工程师必备技能之一。若是想在本地环境启动一个开发服务,大家只需在 Webpack 的配置中,增加 的配置来启动。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 piler = webpack(webpackConf); const app = express(); app.use(wdm(piler)); app.listen(8080);
通过启动一个 服务,将 wdm(piler) 的结果通过 app.use 方法注册为 Express 服务的中间函数。从这里,我们不难看出 wdm(piler) 的执行结果返回的是一个 express 的中间件。它作为一个容器,将 webpack 编译后的文件存储到内存中,然后在用户访问 express 服务时,将内存中对应的资源输出返回。
为什么要使用 webpack-dev-middleware
熟悉 webpack 的同学都知道,webpack 可以通过方式启动,那为何我们不直接使用此方式来监听资源变化呢?答案就是,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
狼蚁网站SEO优化我们就来分析核心文件 index.js 、middleware.js 的源码实现
入口文件 index.js
从上文我们已经得知 wdm(piler) 返回的是一个 express 中间件,所以入口文件 index.js 则为一个中间件的容器包装函数。它接收两个参数,一个为 webpack 的 piler、另一个为配置对象,经过一系列的处理,返回一个中间件函数。狼蚁网站SEO优化我将对 index.js 中的核心代码进行讲解
... setupHooks(context); ... // start watching context.watching = piler.watch(options.watchOptions, (err) => { if (err) { context.log.error(err.stack || err); if (err.details) { context.log.error(err.details); } } }); ... setupOutputFileSystem(piler, context);
index.js 最为核心的是以上 3 个部分的执行,分别完成了我们上文提到的两点特性
- 以监控的方式启动 webpack
- 将 webpack 的编译内容,输出至内存中
setupHooks
此函数的作用是在 piler 的 invalid、run、done、watchRun 这 4 个编译生命周期上,注册对应的处理方法
context.piler.hooks.invalid.tap('WebpackDevMiddleware', invalid); context.piler.hooks.run.tap('WebpackDevMiddleware', invalid); context.piler.hooks.done.tap('WebpackDevMiddleware', done); context.piler.hooks.watchRun.tap( 'WebpackDevMiddleware', (p, callback) => { invalid(callback); } );
- 在 done 生命周期上注册 done 方法,该方法主要是 report 编译的信息以及执行 context.callbacks 回调函数
- 在 invalid、run、watchRun 等生命周期上注册 invalid 方法,该方法主要是 report 编译的状态信息
piler.watch
此部分的作用是,调用 piler 的 watch 方法,之后 webpack 便会监听文件变更,一旦检测到文件变更,就会重新执行编译。
setupOutputFileSystem
其作用是使用 对象替换掉 piler 的文件系统对象,让 webpack 编译后的文件输出到内存中。
fileSystem = new MemoryFileSystem(); // eslint-disable-next-line no-param-reassign piler.outputFileSystem = fileSystem;
通过以上 3 个部分的执行,我们以 watch mode 的方式启动了 webpack,一旦监测的文件变更,便会重新进行编译打包,我们又将文件的存储方法改为了内存存储,提高了文件的存储读取效率。,我们只需要返回 express 的中间件就可以了,而中间件则是调用 middleware(context) 函数得到的。狼蚁网站SEO优化,我们来看看 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 aeptedMethods = context.options.methods || ['GET', 'HEAD']; if (aeptedMethods.indexOf(req.method) === -1) { return goNext(); }
接着,判断 HTTP 协议的请求的类型,若请求不包含于配置中(默认 GET、HEAD 请求),则直接调用 goNext() 方法处理请求
let filename = getFilenameFromUrl( context.options.publicPath, context.piler, req.url ); if (filename === false) { return goNext(); }
然后,根据请求的 req.url 地址,在 piler 的内存文件系统中查找对应的文件,若查找不到,则直接调用 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 中,而上文中我们说到在 pile.hooks.done 上注册了回调函数 done,等编译完成之后,将会执行这个函数,并循环调用 context.callbacks。
源码的阅读是一个非常枯燥的过程,它的收益也是巨大的。上文的源码解读主要分析的是 webpack-dev-middleware 它是如何实现它所拥有的特性、如何处理用户的请求等主要功能点,未包括其他分支逻辑处理、容错。还需读者在这篇文章基础之上,再去阅读详细的源码,望这篇文章能对你的阅读过程起到一定的帮助作用。
到此这篇关于webpack-dev-middleware 源码解读的文章就介绍到这了,更多相关webpack-dev-middleware 源码解读内容请搜索狼蚁SEO以前的文章或继续浏览狼蚁网站SEO优化的相关文章希望大家以后多多支持狼蚁SEO!
编程语言
- 如何快速学会编程 如何快速学会ug编程
- 免费学编程的app 推荐12个免费学编程的好网站
- 电脑怎么编程:电脑怎么编程网咯游戏菜单图标
- 如何写代码新手教学 如何写代码新手教学手机
- 基础编程入门教程视频 基础编程入门教程视频华
- 编程演示:编程演示浦丰投针过程
- 乐高编程加盟 乐高积木编程加盟
- 跟我学plc编程 plc编程自学入门视频教程
- ug编程成航林总 ug编程实战视频
- 孩子学编程的好处和坏处
- 初学者学编程该从哪里开始 新手学编程从哪里入
- 慢走丝编程 慢走丝编程难学吗
- 国内十强少儿编程机构 中国少儿编程机构十强有
- 成人计算机速成培训班 成人计算机速成培训班办
- 孩子学编程网上课程哪家好 儿童学编程比较好的
- 代码编程教学入门软件 代码编程教程