webpack-dev-middleware 是express的一个中间件,它的主要做用是以监听模式启动webpack,将webpack编译后的文件输出到内存里,而后将内存的文件输出到epxress服务器上;下面经过一张图片来看一下它的工做原理:css
了解了它的工做原理之后咱们经过一个例子进行实操一下。html
demo1:初始化webpack-dev-middleware中间件,启动webpack监听模式编译,返回express中间件函数node
// 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 } });
运行结果:webpack
源码连接:https://github.com/Jameswain/... git
经过上述例子的运行结果,咱们能够发现webpack-dev-middleware其实是一个函数,经过执行它会返回一个express风格的中间件函数,而且会以监听模式启动webpack编译。因为webpack-dev-middleware中间件内部使用memory-fs替换了compiler的outputFileSystem对象,将webpack打包编译的文件都输出到内存中,因此虽然咱们看到控制台上有webpack编译成功的日志,可是并无看到任何的输出文件,就是这个缘由,由于这些文件在内存里。github
若是此时咱们不想把文件输出到内存里,能够经过修改webpack-dev-middleware的源代码来实现。打开node_modules/webpack-dev-middleware/lib/Shared.js文件,将该文件的231行注视掉后,从新运行 node demo1/index.js 便可看到文件被输出到demo1/dist文件夹中。web
问:为何webpack-dev-middleware要将webpack打包后的文件输出到内存中,而不是直接到磁盘上呢?express
答:速度,由于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上。
下面咱们就来看看webpack-dev-middleware这个函数内部是如何实现的,它的运行原理是什么?我的感受读源码最主要的就是基础 + 耐心 + 流程
首先打开node_modules/webpack-dev-middleware/middleware.js文件,注意版本号,我这份代码的版本号是webpack-dev-middleware@1.12.2。
middleware.js文件就是webpack-dev-middleware的入口文件,它主要作如下几件事情:
一、记录compiler对象和中间件配置
二、建立webpack操做对象shared
三、建立中间件函数webpackDevMiddleware
四、将webpack的一些经常使用操做函数暴露到中间件函数上,供外部直接调用
五、返回中间件函数
这个文件对webpack的compiler这个对象进行封装操做,咱们大概先来看看这个文件主要作了哪些事情:
经过上面的截图咱们大概知道了Shared.js文件的运行流程,下面咱们再来看看它一些比较重要的细节。
compiler.watch(watchOptions, callback) 这个函数表示以监听模式启动webpack并返回一个watching对象,这里特别须要注意的是当调用compiler.watch函数时会当即执行watch-run这个钩子回调函数,直到这个钩子回调函数执行完毕后,才会返回watching对象。
当webpack的一个编译完成时会进入done钩子回调函数,而后调用compilerDone函数,这个函数内部首先将context.state设置为true表示webpack编译完成,并记录webpack的统计信息对象stats,而后将webpack日志输出操做和回调函数执行都放到process.nextTick()任务队列执行,就是等主逻辑全部的代码执行完毕后才进行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...
这个是watch回调函数,它是在compiler.plugin('done')钩子函数执行完毕以后执行,它有两个参数,一个是错误信息,一个是webpack编译成功的统计信息对象stats,能够看到这个回调函数内部只作错误信息的输出。
以前我介绍的都是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的中间件函数,下面咱们来看看这个中间件函数内部是如何实现的,到底作了哪些事情。
一、咱们先经过一个流程图看一下上面这段代码首次执行webpack-dev-middleware的内部运行流程
二、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中间件的源代码,我加了一些注释和我的看法说明这个中间件内部的具体操做,这里我简单总结一下这个中间件函数主要作了哪些事情:
下面经过一个流程图看一下这个中间件函数的执行流程:
webpack-dev-middleware这个中间件内部其实主就是作了两件事,第一就是在中间件函数初始化时,修改webpack的文件操做对象,让webpack编译后的文件输出到内存里,以监听模式启动webpack。第二就是当有http get请求过来时,中间件函数内部读取webpack输出到内存里的文件,而后输出到response上,这时候浏览器拿到的就是webpack编译后的资源文件了。
最后给出本文全部相关源代码的地址:https://github.com/Jameswain/...
声明:本文纯属我的阅读webpack-dev-middleware@1.12.2源码的一些我的理解和感悟,因为本人技术水平有限,若有错误还望各位大神批评指正。