本文首發於政採雲前端團隊博客:webpack-dev-middleware 源碼解讀php
https://www.zoo.team/article/webpack-dev-middleware
Webpack 的使用目前已是前端開發工程師必備技能之一。如果想在本地環境啓動一個開發服務,你們只需在 Webpack 的配置中,增長 devServer (https://www.webpackjs.com/configuration/dev-server/) 的配置來啓動。devServer 配置的本質是 webpack-dev-server 這個包提供的功能,而 webpack-dev-middleware 則是這個包的底層依賴。前端
截至本文發表前,webpack-dev-middleware 的最新版本爲 webpack-dev-middleware@3.7.2
,本文的源碼來自於此版本。本文會講解 webpack-dev-middleware 的核心模塊實現,相信你們把這篇文章看完,再去閱讀源碼,會容易理解不少。webpack
const wdm = require('webpack-dev-middleware'); const express = require('express'); const webpack = require('webpack'); const webpackConf = require('./webapck.conf.js'); const compiler = webpack(webpackConf); const app = express(); app.use(wdm(compiler)); app.listen(8080);經過啓動一個 Express (http://www.expressjs.com.cn/) 服務,將
wdm(compiler)
的結果經過
app.use
方法註冊爲 Express 服務的中間函數。從這裏,咱們不難看出
wdm(compiler)
的執行結果返回的是一個
express
的中間件。它做爲一個容器,將
webpack
編譯後的文件存儲到內存中,而後在用戶訪問
express
服務時,將內存中對應的資源輸出返回。
熟悉 webpack
的同窗都知道,webpack
能夠經過 watch mode (https://www.webpackjs.com/configuration/watch/) 方式啓動,那爲什麼咱們不直接使用此方式來監聽資源變化呢?答案就是,webpack
的 watch mode
雖然能監聽文件的變動,而且自動打包,可是每次打包後的結果將會存儲到本地硬盤中,而 IO 操做是很是耗資源時間的,沒法知足本地開發調試需求。web
而 webpack-dev-middleware 擁有如下幾點特性:express
watch mode
啓動 webpack
,監聽的資源一旦發生變動,便會自動編譯,生產最新的 bundle
bundle
而且將請求延遲到最新的編譯結果完成以後webpack
編譯後的資源會存儲在內存中,當用戶請求資源時,直接於內存中查找對應資源,減小去硬盤中查找的 IO 操做耗時本文將主要圍繞這三個特性和主流程邏輯進行分析。json
讓咱們先來看下 webpack-dev-middleware 的源碼目錄:app
... ├── 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
下面咱們就來分析核心文件 index.js
、middleware.js
的源碼實現webpack-dev-server
wdm(compiler)
返回的是一個
express
中間件,因此入口文件
index.js
則爲一箇中間件的容器包裝函數。它接收兩個參數,一個爲
webpack
的
compiler
、另外一個爲配置對象,通過一系列的處理,最後返回一箇中間件函數。下面我將對
index.js
中的核心代碼進行講解:
... setupHooks(context); ... // start watching context.watching = compiler.watch(options.watchOptions, (err) => { if (err) { context.log.error(err.stack || err); if (err.details) { context.log.error(err.details); } } }); ... setupOutputFileSystem(compiler, context);
index.js
最爲核心的是以上 3 個部分的執行,分別完成了咱們上文提到的兩點特性:
webpack
webpack
的編譯內容,輸出至內存中compiler
的
invalid
、
run
、
done
、
watchRun
這 4 個編譯生命週期上,註冊對應的處理方法
context.compiler.hooks.invalid.tap('WebpackDevMiddleware', invalid); context.compiler.hooks.run.tap('WebpackDevMiddleware', invalid); context.compiler.hooks.done.tap('WebpackDevMiddleware', done); context.compiler.hooks.watchRun.tap( 'WebpackDevMiddleware', (comp, callback) => { invalid(callback); } );
在 done
生命週期上註冊 done
方法,該方法主要是 report
編譯的信息以及執行 context.callbacks
回調函數ide
invalid
、run
、watchRun
等生命週期上註冊 invalid
方法,該方法主要是 report
編譯的狀態信息此部分的做用是,調用 compiler
的 watch 方法,以後 webpack
便會監聽文件變動,一旦檢測到文件變動,就會從新執行編譯。函數
compiler
的文件系統對象,讓
webpack
編譯後的文件輸出到內存中。
fileSystem = new MemoryFileSystem(); // eslint-disable-next-line no-param-reassign compiler.outputFileSystem = fileSystem;經過以上 3 個部分的執行,咱們以
watch mode
的方式啓動了
webpack
,一旦監測的文件變動,便會從新進行編譯打包,同時咱們又將文件的存儲方法改成了內存存儲,提升了文件的存儲讀取效率。最後,咱們只須要返回
express
的中間件就能夠了,而中間件則是調用
middleware(context)
函數獲得的。下面,咱們來看看
middleware
是如何實現的。
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 acceptedMethods = context.options.methods || ['GET', 'HEAD']; if (acceptedMethods.indexOf(req.method) === -1) { return goNext(); }
接着,判斷 HTTP
協議的請求的類型,若請求不包含於配置中(默認 GET
、HEAD
請求),則直接調用 goNext()
方法處理請求:
let filename = getFilenameFromUrl( context.options.publicPath, context.compiler, req.url ); if (filename === false) { return goNext(); }
而後,根據請求的 req.url
地址,在 compiler
的內存文件系統中查找對應的文件,若查找不到,則直接調用 goNext()
方法處理請求:
return new Promise((resolve) => { // eslint-disable-next-line consistent-return function proce***equest() { ... } ... ready(context, proce***equest, req); });
最後,中間件返回一個 Promise
實例,而在實例中,先是定義一個 proce***equest
方法,此方法的做用是根據上文中找到的 filename
路徑獲取到對應的文件內容,並構造 response
對象返回,隨後調用 ready(context, proce***equest, req)
函數,去執行 proce***equest
方法。這裏咱們着重看下 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
,因此當用戶發起請求時,並不會直接返回對應的文件內容,而是會將回調函數
proce***equest
添加至
context.callbacks
中,而上文中咱們說到在
compile.hooks.done
上註冊了回調函數
done
,等編譯完成以後,將會執行這個函數,並循環調用
context.callbacks
。
源碼的閱讀是一個很是枯燥的過程,可是它的收益也是巨大的。上文的源碼解讀主要分析的是 webpack-dev-middleware
它是如何實現它所擁有的特性、如何處理用戶的請求等主要功能點,未包括其餘分支邏輯處理、容錯。還需讀者在這篇文章基礎之上,再去閱讀詳細的源碼,望這篇文章能對你的閱讀過程起到必定的幫助做用。