webpack-dev-middleware 是express的一个中间件,它的主要作用是以监听模式启动webpack,将webpack编译后的文件输出到内存里,然后将内存的文件输出到epxress服务器上;下面通过一张图片来看一下它的工作原理:
了解了它的工作原理以后我们通过一个例子进行实操一下。
demo1:初始化webpack-dev-middleware中间件,启动webpack监听模式编译,返回express中间件函数
// src/app.js
console.log('App.js');
document.write('webpack-dev-middleware');
// demo1/index.js
const path = require('path');
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware'); // webpack开发中间件
const HtmlWebpackPlugin = require('html-webpack-plugin'); // webpack插件:根据模版生成html,并且自动注入引用webpack编译出来的css和js文件
/**
* 创建webpack编译器
*/
const comoiler = webpack({ // webpack配置
entry: path.resolve(__dirname, 'src/app.js'), // 入口文件
output: { // 输出配置
path: path.resolve(__dirname, 'dist'), // 输出路径
filename: 'bundle.[hash].js' // 输出文件
},
plugins: [ // 插件
new HtmlWebpackPlugin({ // 根据模版自动生成html文件插件,并将webpack打包输出的js文件注入到html文件中
title: 'webpack-dev-middleware'
})
]
});
/**
* 执行webpack-dev-middleware初始化函数,返回express中间件函数
* 这个函数内部以监听模式启动了webpack编译,相当于执行cli的: webpack --watch命令
* 也就是说执行到这一步,就已经启动了webpack的监听模式编译了,代码执行到这里可以看到控制台已经输出了webpack编译成功的相关日志了
* 由于webpack-dev-middleware中间件内部使用memory-fs替换了compiler的outputFileSystem对象,将webpack打包编译的文件都输出到内存中
* 所以磁盘上看不到任何webpack编译输出的文件
*/
const webpackDevMiddlewareInstance = webpackDevMiddleware(comoiler,{
reportTime: true, // webpack状态日志输出带上时间前缀
stats: {
colors: true, // webpack编译输出日志带上颜色,相当于命令行 webpack --colors
process: true
}
});
运行结果:
源码链接:https://github.com/Jameswain/...
通过上述例子的运行结果,我们可以发现webpack-dev-middleware实际上是一个函数,通过执行它会返回一个express风格的中间件函数,并且会以监听模式启动webpack编译。由于webpack-dev-middleware中间件内部使用memory-fs替换了compiler的outputFileSystem对象,将webpack打包编译的文件都输出到内存中,所以虽然我们看到控制台上有webpack编译成功的日志,但是并没有看到任何的输出文件,就是这个原因,因为这些文件在内存里。
如果此时我们不想把文件输出到内存里,可以通过修改webpack-dev-middleware的源代码来实现。打开node_modules/webpack-dev-middleware/lib/Shared.js文件,将该文件的231行注视掉后,重新运行 node demo1/index.js 即可看到文件被输出到demo1/dist文件夹中。
问:为什么webpack-dev-middleware要将webpack打包后的文件输出到内存中,而不是直接到磁盘上呢?
答:速度,因为IO操作是非常耗资源时间的,直接在内存里操作会比磁盘操作会更加快速和高效。因为即使是webpack把文件输出到磁盘,要将磁盘上到文件通过一个服务输出到浏览器,也是需要将磁盘的文件读取到内存里,然后在通过流进行输出,然后浏览器上才能看到,所以中间件这么做其实还是省了一步读取磁盘文件的操作。
下面通过一个例子演示一下如何将本地磁盘上的文件通过Express服务输出到response,在浏览器上进行访问:
//demo3/app.js
const express = require('express');
const path = require('path');
const fs = require('fs');
const app = express();
// 读取index.html文件
const htmlIndex = fs.readFileSync(path.resolve(__dirname,'index.html'));
// 读取图片
const img = fs.readFileSync(path.resolve(__dirname, 'node.jpg'));
app.use((req, res, next) => {
console.log(req.url)
if (req.url === '/' || req.url === '/index.html') {
res.setHeader("Content-Type", 'text/html;charset=UTF-8');
res.setHeader("Content-Length", htmlIndex.length);
res.send(htmlIndex); // 传送HTTP响应
// res.end(); // 此方法向服务器发出信号,表明已发送所有响应头和主体,该服务器应该视为此消息已完成。 必须在每个响应上调用此 response.end() 方法。
// res.sendFile(path.resolve(__dirname, 'index.html')); //传送指定路径的文件 -会自动根据文件extension设定Content-Type
} else if (req.url === '/node.jpg') {
res.end(img); // 此方法向服务器发出信号,表明已发送所有响应头和主体,该服务器应该视为此消息已完成。 必须在每个响应上调用此 response.end() 方法。
}
});
app.listen(3000, () => console.log('express 服务启动成功。。。'));
//浏览器访问:http://localhost:3000/node.jpg
//浏览器访问:http://localhost:3000/
项目目录:
运行结果:
通过上述代码我们可以看出不管是输出html文件还是图片文件都是需要先将这些文件读取到内存里,然后才能输出到response上。
middleware.js
下面我们就来看看webpack-dev-middleware这个函数内部是如何实现的,它的运行原理是什么?个人感觉读源码最主要的就是基础 + 耐心 + 流程
首先打开node_modules/webpack-dev-middleware/middleware.js文件,注意版本号,我这份代码的版本号是webpack-dev-middleware@1.12.2。
middleware.js文件就是webpack-dev-middleware的入口文件,它主要做以下几件事情:
1、记录compiler对象和中间件配置
2、创建webpack操作对象shared
3、创建中间件函数webpackDevMiddleware
4、将webpack的一些常用操作函数暴露到中间件函数上,供外部直接调用
5、返回中间件函数
Shared.js
这个文件对webpack的compiler这个对象进行封装操作,我们大概先来看看这个文件主要做了哪些事情:
- 首先设置中间件的一些默认选项配置
- 使用memory-fs对象替换掉compiler的文件系统对象,让webpack编译后的文件输出到内存中
- 监听webpack的钩子函数
- invalid:监听模式下,文件发生变化时调用,同时会传入2个参数,分别是文件名和时间戳
- watch-run:监听模式下,一个新的编译触发之后,完成编译之前调用
- done:编译完成时调用,并传入webpack编译日志对象stats
- run:在开始读取记录之前调用,只有调用compiler.run()函数时才会触发该钩子函数
- 以观察者模式启动webpack编译
- 返回share对象,该对象封装了很多关于compiler的操作函数
通过上面的截图我们大概知道了Shared.js文件的运行流程,下面我们再来看看它一些比较重要的细节。
share.setOptions 设置中间件的默认配置
share.setFs(context.compiler) 设置compiler的文件操作对象
share.startWatch() 以观察模式启动webpack
compiler.watch(watchOptions, callback) 这个函数表示以监听模式启动webpack并返回一个watching对象,这里特别需要注意的是当调用compiler.watch函数时会立即执行watch-run这个钩子回调函数,直到这个钩子回调函数执行完毕后,才会返回watching对象。
share.compilerDone(stats) webpack编译完成回调处理函数
当webpack的一个编译完成时会进入done钩子回调函数,然后调用compilerDone函数,这个函数内部首先将context.state设置为true表示webpack编译完成,并记录webpack的统计信息对象stats,然后将webpack日志输出操作和回调函数执行都放到process.nextTick()任务队列执行,就是等主逻辑所有的代码执行完毕后才进行webpack的日志输出和中间件回调函数的执行。
context.options.reporter (share.defaultReporter) webpack默认日志输出函数
context.options.reporter 和 share.defaultReporter 指向的都是同一个函数
通过代码我们可以看出这个函数内部首先是要判断一下state这个状态,false表示webpack处于编译中,则直接输出 webpack: Compiling...。true:则表示webpack编译完成,则需要判断webpack-dev-middleware这个中间件都两个配置,noInfo和quiet,noInfo如果是为true则只输出错误和警告,quiet为true则不输出任何内容,默认这俩选项都是false,这时候会判断webpack编译成功后返回的stats对象里有没有错误和警告,有错误或警告就输出错误和警告,没有则输出webpack的编译日志,并且使用webpack-dev-middleware的options.stats配置项作为webpack日志输出配置,更多webpack日志输出配置选项见:https://www.webpackjs.com/con...
handleCompilerCallback() - watch回调函数
这个是watch回调函数,它是在compiler.plugin('done')钩子函数执行完毕之后执行,它有两个参数,一个是错误信息,一个是webpack编译成功的统计信息对象stats,可以看到这个回调函数内部只做错误信息的输出。
webpack watch模式钩子函数执行流程图
使用webpack-dev-middleware中间件
之前我介绍的都是webpack-dev-middleware中间件初始化阶段主要做了什么事情,而且我的第一个代码例子里也只是调用了webpack-dev-middleware中间件的初始化函数而已,并没有和express结合使用,当时这么做的主要是为了说明这个中间件的初始化阶段的运行机制,下面我们通过一个完整一点的例子说明webpack-dev-middleware中间件如何和express进行结合使用以及它的运行流程和原理。
// demo2/index.js
const path = require('path');
const express = require('express');
const webpack = require('webpack');
const webpackDevMiddleware = require('webpack-dev-middleware');
const HtmlWebpackPlugin = require('html-webpack-plugin');
// 创建webpack编译器
const compiler = webpack({
entry: path.resolve(__dirname, 'src/app.js'),
output: {
path: path.resolve(__dirname, 'dist'),
filename: 'bundle.[hash].js'
},
plugins: [
new HtmlWebpackPlugin({
title: 'webpack-dev-middleware'
})
]
});
// webpack开发中间件:其实这个中间件函数执行完成时,中间件内部就会执行webpack的watch函数,启动webpack的监听模式,相当于执行cli的: webpack --watch命令
const webpackDevMiddlewareInstance = webpackDevMiddleware(compiler, {
reportTime: true, // webpack状态日志输出带上时间前缀
stats: {
colors: true, // webpack编译输出日志带上颜色,相当于命令行 webpack --colors
process: true
},
// noInfo: true, // 不输出任何webpack编译日志(只有警告和错误)
// quiet: true, // 不向控制台显示任何内容
// reporter: function (context) { // 提供自定义报告器以更改webpack日志的输出方式。
// console.log(context.stats.toString(context.options.stats))
// },
});
/**
* webpack第一次编译完成并且输出编译日志后调用
* 之后监听到文件变化重新编译不会再执行此函数
*/
webpackDevMiddlewareInstance.waitUntilValid(stats => {
console.log('webpack第一次编译成功回调函数');
});
// 创建express对象
const app = express();
app.use(webpackDevMiddlewareInstance); // 使用webpack-dev-middleware中间件,每一个web请求都会进入该中间件函数
app.listen(3000, () => console.log('启动express服务...')); // 启动express服务器在3000端口上
// for (let i = 0; i < 10000022220; i++) {} // 会阻塞webpack的编译操作
源码地址:https://github.com/Jameswain/...
通过app.use进行使用中间件,然后我们通过在浏览器访问localhost:3000,然后就可以看到效果了,此时任何一个web请求都会执行webpack-dev-middleware的中间件函数,下面我们来看看这个中间件函数内部是如何实现的,到底做了哪些事情。
1、我们先通过一个流程图看一下上面这段代码首次执行webpack-dev-middleware的内部运行流程
2、middleware.js文件中的webpackDevMiddleware函数代码解析
// webpack-dev-middleware 中间件函数,每一个http请求都会进入次函数
function webpackDevMiddleware(req, res, next) {
/**
* 执行下一个中间件
*/
function goNext() {
// 如果不是服务器端渲染,则直接执行下一个中间件函数
if(!context.options.serverSideRender) return next();
return new Promise(function(resolve) {
shared.ready(function() {
res.locals.webpackStats = context.webpackStats;
resolve(next());
}, req);
});
}
// 如果不是GET请求,则直接调用下一个中间件并返回退出函数
if(req.method !== "GET") {
return goNext();
}
// 根据请求的URL获取webpack编译输出文件的绝对路径;例如:req.url="/bundle.492db0756b0d8df3e6dd.js" 获取到的filename就是"/Users/jameswain/WORK/blog/demo2/dist/bundle.492db0756b0d8df3e6dd.js"
// 可以看到其实就是webpack编译输出文件的绝对路径和名称
var filename = getFilenameFromUrl(context.options.publicPath, context.compiler, req.url);
if(filename === false) return goNext();
return new Promise(function(resolve) {
shared.handleRequest(filename, processRequest, req);
function processRequest(stats) {
try {
var stat = context.fs.statSync(filename);
// 处理当前请求是 / 的情况
if(!stat.isFile()) {
if(stat.isDirectory()) {
// 如果请求的URL是/,则将它的文件设置为中间件配置的index选项
var index = context.options.index;
// 如果中间件没有设置index选项,则默认设置为index.html
if(index === undefined || index === true) {
index = "index.html";
} else if(!index) {
throw "next";
}
// 将webpack的输出目录outputPath和index.html拼接起来
filename = pathJoin(filename, index);
stat = context.fs.statSync(filename);
if(!stat.isFile()) throw "next";
} else {
throw "next";
}
}
} catch(e) {
return resolve(goNext());
}
// server content 服务器内容
// 读取文件内容
var content = context.fs.readFileSync(filename);
// console.log(content.toString()) //输出文件内容
// 处理可接受数据范围的请求头
content = shared.handleRangeHeaders(content, req, res);
// 获取文件的mime类型
var contentType = mime.lookup(filename);
// do not add charset to WebAssembly files, otherwise compileStreaming will fail in the client
// 不要将charset添加到WebAssembly文件中,否则编译流将在客户端失败
if(!/\.wasm$/.test(filename)) {
contentType += "; charset=UTF-8";
}
res.setHeader("Content-Type", contentType);
res.setHeader("Content-Length", content.length);
// 中间件自定义请求头配置,如果中间件有配置,则循环设置这些请求头
if(context.options.headers) {
for(var name in context.options.headers) {
res.setHeader(name, context.options.headers[name]);
}
}
// Express automatically sets the statusCode to 200, but not all servers do (Koa).
// Express自动将statusCode设置为200,但不是所有服务器都这样做(Koa)。
res.statusCode = res.statusCode || 200;
// 将请求的文件或数据内容输出到客户端(浏览器)
if(res.send) res.send(content);
else res.end(content);
resolve();
}
});
}
这是webpack-dev-middleware中间件的源代码,我加了一些注释和个人见解说明这个中间件内部的具体操作,这里我简单总结一下这个中间件函数主要做了哪些事情:
- 首先判断如果不是GET请求,则调用下一个中间件函数,并退出当前中间件函数。
- 根据请求的URL,拼接出该资源在webpack输出目录的绝对路径。例如:请求的URL为“/bundle.js”,那么在我电脑拼接出的绝对路径就为"/Users/jameswain/WORK/blog/demo2/dist/bundle.js",如果请求的URL为/,设置文件为index.html
- 读取请求文件的内容,是一个Buffer类型,可以立即为流
- 判断客户端是否设置了range请求头,如果设置了,则需要对内容进行截取限制在指定范围之内。
- 获取请求文件的mime类型
- 设置请求头Content-Type和Content-Length,循环设置中间件配置的自定义请求头
- 设置状态码为200
- 将文件内容输出到客户端
下面通过一个流程图看一下这个中间件函数的执行流程:
总结
webpack-dev-middleware这个中间件内部其实主就是做了两件事,第一就是在中间件函数初始化时,修改webpack的文件操作对象,让webpack编译后的文件输出到内存里,以监听模式启动webpack。第二就是当有http get请求过来时,中间件函数内部读取webpack输出到内存里的文件,然后输出到response上,这时候浏览器拿到的就是webpack编译后的资源文件了。
最后给出本文所有相关源代码的地址:https://github.com/Jameswain/...
声明:本文纯属个人阅读webpack-dev-middleware@1.12.2源码的一些个人理解和感悟,由于本人技术水平有限,如有错误还望各位大神批评指正。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。