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源碼的一些我的理解和感悟,因爲本人技術水平有限,若有錯誤還望各位大神批評指正。