摘要:如果此時我們不想把文件輸出到內(nèi)存里,可以通過修改的源代碼來實(shí)現(xiàn)。服務(wù)啟動成功。。。根據(jù)請求的,拼接出
? webpack-dev-middleware 是express的一個中間件,它的主要作用是以監(jiān)聽模式啟動webpack,將webpack編譯后的文件輸出到內(nèi)存里,然后將內(nèi)存的文件輸出到epxress服務(wù)器上;下面通過一張圖片來看一下它的工作原理:
了解了它的工作原理以后我們通過一個例子進(jìn)行實(shí)操一下。
demo1:初始化webpack-dev-middleware中間件,啟動webpack監(jiān)聽模式編譯,返回express中間件函數(shù)
// 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開發(fā)中間件 const HtmlWebpackPlugin = require("html-webpack-plugin"); // webpack插件:根據(jù)模版生成html,并且自動注入引用webpack編譯出來的css和js文件 /** * 創(chuàng)建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({ // 根據(jù)模版自動生成html文件插件,并將webpack打包輸出的js文件注入到html文件中 title: "webpack-dev-middleware" }) ] }); /** * 執(zhí)行webpack-dev-middleware初始化函數(shù),返回express中間件函數(shù) * 這個函數(shù)內(nèi)部以監(jiān)聽模式啟動了webpack編譯,相當(dāng)于執(zhí)行cli的: webpack --watch命令 * 也就是說執(zhí)行到這一步,就已經(jīng)啟動了webpack的監(jiān)聽模式編譯了,代碼執(zhí)行到這里可以看到控制臺已經(jīng)輸出了webpack編譯成功的相關(guān)日志了 * 由于webpack-dev-middleware中間件內(nèi)部使用memory-fs替換了compiler的outputFileSystem對象,將webpack打包編譯的文件都輸出到內(nèi)存中 * 所以磁盤上看不到任何webpack編譯輸出的文件 */ const webpackDevMiddlewareInstance = webpackDevMiddleware(comoiler,{ reportTime: true, // webpack狀態(tài)日志輸出帶上時間前綴 stats: { colors: true, // webpack編譯輸出日志帶上顏色,相當(dāng)于命令行 webpack --colors process: true } });
運(yùn)行結(jié)果:
源碼鏈接:https://github.com/Jameswain/...
? 通過上述例子的運(yùn)行結(jié)果,我們可以發(fā)現(xiàn)webpack-dev-middleware實(shí)際上是一個函數(shù),通過執(zhí)行它會返回一個express風(fēng)格的中間件函數(shù),并且會以監(jiān)聽模式啟動webpack編譯。由于webpack-dev-middleware中間件內(nèi)部使用memory-fs替換了compiler的outputFileSystem對象,將webpack打包編譯的文件都輸出到內(nèi)存中,所以雖然我們看到控制臺上有webpack編譯成功的日志,但是并沒有看到任何的輸出文件,就是這個原因,因?yàn)檫@些文件在內(nèi)存里。
? 如果此時我們不想把文件輸出到內(nèi)存里,可以通過修改webpack-dev-middleware的源代碼來實(shí)現(xiàn)。打開node_modules/webpack-dev-middleware/lib/Shared.js文件,將該文件的231行注視掉后,重新運(yùn)行 node demo1/index.js 即可看到文件被輸出到demo1/dist文件夾中。
? 問:為什么webpack-dev-middleware要將webpack打包后的文件輸出到內(nèi)存中,而不是直接到磁盤上呢?
? 答:速度,因?yàn)镮O操作是非常耗資源時間的,直接在內(nèi)存里操作會比磁盤操作會更加快速和高效。因?yàn)榧词故莣ebpack把文件輸出到磁盤,要將磁盤上到文件通過一個服務(wù)輸出到瀏覽器,也是需要將磁盤的文件讀取到內(nèi)存里,然后在通過流進(jìn)行輸出,然后瀏覽器上才能看到,所以中間件這么做其實(shí)還是省了一步讀取磁盤文件的操作。
? 下面通過一個例子演示一下如何將本地磁盤上的文件通過Express服務(wù)輸出到response,在瀏覽器上進(jìn)行訪問:
//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響應(yīng) // res.end(); // 此方法向服務(wù)器發(fā)出信號,表明已發(fā)送所有響應(yīng)頭和主體,該服務(wù)器應(yīng)該視為此消息已完成。 必須在每個響應(yīng)上調(diào)用此 response.end() 方法。 // res.sendFile(path.resolve(__dirname, "index.html")); //傳送指定路徑的文件 -會自動根據(jù)文件extension設(shè)定Content-Type } else if (req.url === "/node.jpg") { res.end(img); // 此方法向服務(wù)器發(fā)出信號,表明已發(fā)送所有響應(yīng)頭和主體,該服務(wù)器應(yīng)該視為此消息已完成。 必須在每個響應(yīng)上調(diào)用此 response.end() 方法。 } }); app.listen(3000, () => console.log("express 服務(wù)啟動成功。。。")); //瀏覽器訪問:http://localhost:3000/node.jpg //瀏覽器訪問:http://localhost:3000/
項(xiàng)目目錄:
運(yùn)行結(jié)果:
通過上述代碼我們可以看出不管是輸出html文件還是圖片文件都是需要先將這些文件讀取到內(nèi)存里,然后才能輸出到response上。
?
middleware.js? 下面我們就來看看webpack-dev-middleware這個函數(shù)內(nèi)部是如何實(shí)現(xiàn)的,它的運(yùn)行原理是什么?個人感覺讀源碼最主要的就是基礎(chǔ) + 耐心 + 流程
? 首先打開node_modules/webpack-dev-middleware/middleware.js文件,注意版本號,我這份代碼的版本號是webpack-dev-middleware@1.12.2。
? middleware.js文件就是webpack-dev-middleware的入口文件,它主要做以下幾件事情:
? 1、記錄compiler對象和中間件配置
? 2、創(chuàng)建webpack操作對象shared
? 3、創(chuàng)建中間件函數(shù)webpackDevMiddleware
? 4、將webpack的一些常用操作函數(shù)暴露到中間件函數(shù)上,供外部直接調(diào)用
? 5、返回中間件函數(shù)
Shared.js這個文件對webpack的compiler這個對象進(jìn)行封裝操作,我們大概先來看看這個文件主要做了哪些事情:
首先設(shè)置中間件的一些默認(rèn)選項(xiàng)配置
使用memory-fs對象替換掉compiler的文件系統(tǒng)對象,讓webpack編譯后的文件輸出到內(nèi)存中
監(jiān)聽webpack的鉤子函數(shù)
invalid:監(jiān)聽模式下,文件發(fā)生變化時調(diào)用,同時會傳入2個參數(shù),分別是文件名和時間戳
watch-run:監(jiān)聽模式下,一個新的編譯觸發(fā)之后,完成編譯之前調(diào)用
done:編譯完成時調(diào)用,并傳入webpack編譯日志對象stats
run:在開始讀取記錄之前調(diào)用,只有調(diào)用compiler.run()函數(shù)時才會觸發(fā)該鉤子函數(shù)
以觀察者模式啟動webpack編譯
返回share對象,該對象封裝了很多關(guān)于compiler的操作函數(shù)
通過上面的截圖我們大概知道了Shared.js文件的運(yùn)行流程,下面我們再來看看它一些比較重要的細(xì)節(jié)。
share.setOptions 設(shè)置中間件的默認(rèn)配置 share.setFs(context.compiler) 設(shè)置compiler的文件操作對象 share.startWatch() 以觀察模式啟動webpackcompiler.watch(watchOptions, callback) 這個函數(shù)表示以監(jiān)聽模式啟動webpack并返回一個watching對象,這里特別需要注意的是當(dāng)調(diào)用compiler.watch函數(shù)時會立即執(zhí)行watch-run這個鉤子回調(diào)函數(shù),直到這個鉤子回調(diào)函數(shù)執(zhí)行完畢后,才會返回watching對象。
share.compilerDone(stats) webpack編譯完成回調(diào)處理函數(shù)當(dāng)webpack的一個編譯完成時會進(jìn)入done鉤子回調(diào)函數(shù),然后調(diào)用compilerDone函數(shù),這個函數(shù)內(nèi)部首先將context.state設(shè)置為true表示webpack編譯完成,并記錄webpack的統(tǒng)計信息對象stats,然后將webpack日志輸出操作和回調(diào)函數(shù)執(zhí)行都放到process.nextTick()任務(wù)隊(duì)列執(zhí)行,就是等主邏輯所有的代碼執(zhí)行完畢后才進(jìn)行webpack的日志輸出和中間件回調(diào)函數(shù)的執(zhí)行。
context.options.reporter (share.defaultReporter) webpack默認(rèn)日志輸出函數(shù)context.options.reporter 和 share.defaultReporter 指向的都是同一個函數(shù)
? 通過代碼我們可以看出這個函數(shù)內(nèi)部首先是要判斷一下state這個狀態(tài),false表示webpack處于編譯中,則直接輸出 webpack: Compiling...。true:則表示webpack編譯完成,則需要判斷webpack-dev-middleware這個中間件都兩個配置,noInfo和quiet,noInfo如果是為true則只輸出錯誤和警告,quiet為true則不輸出任何內(nèi)容,默認(rèn)這倆選項(xiàng)都是false,這時候會判斷webpack編譯成功后返回的stats對象里有沒有錯誤和警告,有錯誤或警告就輸出錯誤和警告,沒有則輸出webpack的編譯日志,并且使用webpack-dev-middleware的options.stats配置項(xiàng)作為webpack日志輸出配置,更多webpack日志輸出配置選項(xiàng)見:https://www.webpackjs.com/con...
handleCompilerCallback() - watch回調(diào)函數(shù)這個是watch回調(diào)函數(shù),它是在compiler.plugin("done")鉤子函數(shù)執(zhí)行完畢之后執(zhí)行,它有兩個參數(shù),一個是錯誤信息,一個是webpack編譯成功的統(tǒng)計信息對象stats,可以看到這個回調(diào)函數(shù)內(nèi)部只做錯誤信息的輸出。
webpack watch模式鉤子函數(shù)執(zhí)行流程圖 使用webpack-dev-middleware中間件? 之前我介紹的都是webpack-dev-middleware中間件初始化階段主要做了什么事情,而且我的第一個代碼例子里也只是調(diào)用了webpack-dev-middleware中間件的初始化函數(shù)而已,并沒有和express結(jié)合使用,當(dāng)時這么做的主要是為了說明這個中間件的初始化階段的運(yùn)行機(jī)制,下面我們通過一個完整一點(diǎn)的例子說明webpack-dev-middleware中間件如何和express進(jìn)行結(jié)合使用以及它的運(yùn)行流程和原理。
// 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"); // 創(chuàng)建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開發(fā)中間件:其實(shí)這個中間件函數(shù)執(zhí)行完成時,中間件內(nèi)部就會執(zhí)行webpack的watch函數(shù),啟動webpack的監(jiān)聽模式,相當(dāng)于執(zhí)行cli的: webpack --watch命令 const webpackDevMiddlewareInstance = webpackDevMiddleware(compiler, { reportTime: true, // webpack狀態(tài)日志輸出帶上時間前綴 stats: { colors: true, // webpack編譯輸出日志帶上顏色,相當(dāng)于命令行 webpack --colors process: true }, // noInfo: true, // 不輸出任何webpack編譯日志(只有警告和錯誤) // quiet: true, // 不向控制臺顯示任何內(nèi)容 // reporter: function (context) { // 提供自定義報告器以更改webpack日志的輸出方式。 // console.log(context.stats.toString(context.options.stats)) // }, }); /** * webpack第一次編譯完成并且輸出編譯日志后調(diào)用 * 之后監(jiān)聽到文件變化重新編譯不會再執(zhí)行此函數(shù) */ webpackDevMiddlewareInstance.waitUntilValid(stats => { console.log("webpack第一次編譯成功回調(diào)函數(shù)"); }); // 創(chuàng)建express對象 const app = express(); app.use(webpackDevMiddlewareInstance); // 使用webpack-dev-middleware中間件,每一個web請求都會進(jìn)入該中間件函數(shù) app.listen(3000, () => console.log("啟動express服務(wù)...")); // 啟動express服務(wù)器在3000端口上 // for (let i = 0; i < 10000022220; i++) {} // 會阻塞webpack的編譯操作
? 源碼地址:https://github.com/Jameswain/...
? 通過app.use進(jìn)行使用中間件,然后我們通過在瀏覽器訪問localhost:3000,然后就可以看到效果了,此時任何一個web請求都會執(zhí)行webpack-dev-middleware的中間件函數(shù),下面我們來看看這個中間件函數(shù)內(nèi)部是如何實(shí)現(xiàn)的,到底做了哪些事情。
? 1、我們先通過一個流程圖看一下上面這段代碼首次執(zhí)行webpack-dev-middleware的內(nèi)部運(yùn)行流程
?
? 2、middleware.js文件中的webpackDevMiddleware函數(shù)代碼解析
// webpack-dev-middleware 中間件函數(shù),每一個http請求都會進(jìn)入次函數(shù) function webpackDevMiddleware(req, res, next) { /** * 執(zhí)行下一個中間件 */ function goNext() { // 如果不是服務(wù)器端渲染,則直接執(zhí)行下一個中間件函數(shù) if(!context.options.serverSideRender) return next(); return new Promise(function(resolve) { shared.ready(function() { res.locals.webpackStats = context.webpackStats; resolve(next()); }, req); }); } // 如果不是GET請求,則直接調(diào)用下一個中間件并返回退出函數(shù) if(req.method !== "GET") { return goNext(); } // 根據(jù)請求的URL獲取webpack編譯輸出文件的絕對路徑;例如:req.url="/bundle.492db0756b0d8df3e6dd.js" 獲取到的filename就是"/Users/jameswain/WORK/blog/demo2/dist/bundle.492db0756b0d8df3e6dd.js" // 可以看到其實(shí)就是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); // 處理當(dāng)前請求是 / 的情況 if(!stat.isFile()) { if(stat.isDirectory()) { // 如果請求的URL是/,則將它的文件設(shè)置為中間件配置的index選項(xiàng) var index = context.options.index; // 如果中間件沒有設(shè)置index選項(xiàng),則默認(rèn)設(shè)置為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 服務(wù)器內(nèi)容 // 讀取文件內(nèi)容 var content = context.fs.readFileSync(filename); // console.log(content.toString()) //輸出文件內(nèi)容 // 處理可接受數(shù)據(jù)范圍的請求頭 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); // 中間件自定義請求頭配置,如果中間件有配置,則循環(huán)設(shè)置這些請求頭 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設(shè)置為200,但不是所有服務(wù)器都這樣做(Koa)。 res.statusCode = res.statusCode || 200; // 將請求的文件或數(shù)據(jù)內(nèi)容輸出到客戶端(瀏覽器) if(res.send) res.send(content); else res.end(content); resolve(); } }); }
? 這是webpack-dev-middleware中間件的源代碼,我加了一些注釋和個人見解說明這個中間件內(nèi)部的具體操作,這里我簡單總結(jié)一下這個中間件函數(shù)主要做了哪些事情:
首先判斷如果不是GET請求,則調(diào)用下一個中間件函數(shù),并退出當(dāng)前中間件函數(shù)。
根據(jù)請求的URL,拼接出該資源在webpack輸出目錄的絕對路徑。例如:請求的URL為“/bundle.js”,那么在我電腦拼接出的絕對路徑就為"/Users/jameswain/WORK/blog/demo2/dist/bundle.js",如果請求的URL為/,設(shè)置文件為index.html
讀取請求文件的內(nèi)容,是一個Buffer類型,可以立即為流
判斷客戶端是否設(shè)置了range請求頭,如果設(shè)置了,則需要對內(nèi)容進(jìn)行截取限制在指定范圍之內(nèi)。
獲取請求文件的mime類型
設(shè)置請求頭Content-Type和Content-Length,循環(huán)設(shè)置中間件配置的自定義請求頭
設(shè)置狀態(tài)碼為200
將文件內(nèi)容輸出到客戶端
? 下面通過一個流程圖看一下這個中間件函數(shù)的執(zhí)行流程:
總結(jié)? webpack-dev-middleware這個中間件內(nèi)部其實(shí)主就是做了兩件事,第一就是在中間件函數(shù)初始化時,修改webpack的文件操作對象,讓webpack編譯后的文件輸出到內(nèi)存里,以監(jiān)聽模式啟動webpack。第二就是當(dāng)有http get請求過來時,中間件函數(shù)內(nèi)部讀取webpack輸出到內(nèi)存里的文件,然后輸出到response上,這時候?yàn)g覽器拿到的就是webpack編譯后的資源文件了。
? 最后給出本文所有相關(guān)源代碼的地址:https://github.com/Jameswain/...
? 聲明:本文純屬個人閱讀webpack-dev-middleware@1.12.2源碼的一些個人理解和感悟,由于本人技術(shù)水平有限,如有錯誤還望各位大神批評指正。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/102835.html
摘要:在學(xué)習(xí)源碼的過程中,給我?guī)椭畲蟮木褪沁@個系列文章,于是決定基于這個系列文章談一下自己的理解。到此為止,首次渲染就完成啦總結(jié)從啟動到元素渲染到頁面,并不像看起來這么簡單,中間經(jīng)歷了復(fù)雜的層級調(diào)用。 前言 React 是一個十分龐大的庫,由于要同時考慮 ReactDom 和 ReactNative ,還有服務(wù)器渲染等,導(dǎo)致其代碼抽象化程度很高,嵌套層級非常深,閱讀其源碼是一個非常艱辛的過...
摘要:依賴注入和控制反轉(zhuǎn),這兩個詞經(jīng)常一起出現(xiàn)。一句話表述他們之間的關(guān)系依賴注入是控制反轉(zhuǎn)的一種實(shí)現(xiàn)方式。而兩者有大量的代碼都是可以共享的,這就是依賴注入的使用場景了。下一步就是創(chuàng)建具體的依賴內(nèi)容,然后注入到需要的地方這里的等于這個對象。 前言 React 是一個十分龐大的庫,由于要同時考慮 ReactDom 和 ReactNative ,還有服務(wù)器渲染等,導(dǎo)致其代碼抽象化程度很高,嵌套層級...
前言 本文所有內(nèi)容全部發(fā)布再個人博客主頁 https://github.com/muwoo/blogs歡迎訂閱。不過最近因?yàn)槭虑楸容^多,有一段時間沒有更新了,后面打算繼續(xù)不斷學(xué)習(xí)更新,歡迎小伙伴一起溝通交流~ 最近更新 前端單測的那些事 基于virtual dom 的canvas渲染 js Event loop 機(jī)制簡介 axios 核心源碼實(shí)現(xiàn)原理 JS 數(shù)據(jù)類型、賦值、深拷貝和淺拷貝 j...
前言 本文所有內(nèi)容全部發(fā)布再個人博客主頁 https://github.com/muwoo/blogs歡迎訂閱。不過最近因?yàn)槭虑楸容^多,有一段時間沒有更新了,后面打算繼續(xù)不斷學(xué)習(xí)更新,歡迎小伙伴一起溝通交流~ 最近更新 前端單測的那些事 基于virtual dom 的canvas渲染 js Event loop 機(jī)制簡介 axios 核心源碼實(shí)現(xiàn)原理 JS 數(shù)據(jù)類型、賦值、深拷貝和淺拷貝 j...
前言 本文所有內(nèi)容全部發(fā)布再個人博客主頁 https://github.com/muwoo/blogs歡迎訂閱。不過最近因?yàn)槭虑楸容^多,有一段時間沒有更新了,后面打算繼續(xù)不斷學(xué)習(xí)更新,歡迎小伙伴一起溝通交流~ 最近更新 前端單測的那些事 基于virtual dom 的canvas渲染 js Event loop 機(jī)制簡介 axios 核心源碼實(shí)現(xiàn)原理 JS 數(shù)據(jù)類型、賦值、深拷貝和淺拷貝 j...
閱讀 2137·2023-04-26 00:23
閱讀 807·2021-09-08 09:45
閱讀 2435·2019-08-28 18:20
閱讀 2542·2019-08-26 13:51
閱讀 1595·2019-08-26 10:32
閱讀 1392·2019-08-26 10:24
閱讀 2027·2019-08-26 10:23
閱讀 2196·2019-08-23 18:10