1. 程式人生 > 程式設計 >詳解webpack-dev-middleware 原始碼解讀

詳解webpack-dev-middleware 原始碼解讀

前言

Webpack 的使用目前已經是前端開發工程師必備技能之一。若是想在本地環境啟動一個開發服務,大家只需在 Webpack 的配置中,增加 devServer 的配置來啟動。devServer 配置的本質是 webpack-dev-server 這個包提供的功能,而 webpack-dev-middleware 則是這個包的底層依賴。

截至本文發表前,webpack-dev-middleware 的最新版本為 [email protected],本文的原始碼來自於此版本。本文會講解 webpack-dev-middleware 的核心模組實現,相信大家把這篇文章看完,再去閱讀原始碼,會容易理解很多。

webpack-dev-middleware 是什麼?

要回答這個問題,我們先來看看如何使用這個包:

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 服務,將 wdm(compiler) 的結果通過 app.use 方法註冊為 Express 服務的中間函式。從這裡,我們不難看出 wdm(compiler) 的執行結果返回的是一個 express 的中介軟體。它作為一個容器,將 webpack 編譯後的檔案儲存到記憶體中,然後在使用者訪問 express 服務時,將記憶體中對應的資源輸出返回。

為什麼要使用 webpack-dev-middleware

熟悉 webpack 的同學都知道,webpack 可以通過watch mode 方式啟動,那為何我們不直接使用此方式來監聽資源變化呢?答案就是,webpack 的 watch mode 雖然能監聽檔案的變更,並且自動打包,但是每次打包後的結果將會儲存到本地硬碟中,而 IO 操作是非常耗資源時間的,無法滿足本地開發除錯需求。

而 webpack-dev-middleware 擁有以下幾點特性:

  • 以 watch mode 啟動 webpack,監聽的資源一旦發生變更,便會自動編譯,生產最新的 bundle
  • 在編譯期間,停止提供舊版的 bundle 並且將請求延遲到最新的編譯結果完成之後
  • webpack 編譯後的資源會儲存在記憶體中,當用戶請求資源時,直接於記憶體中查詢對應資源,減少去硬碟中查詢的 IO 操作耗時

本文將主要圍繞這三個特性和主流程邏輯進行分析。

原始碼解讀

讓我們先來看下 webpack-dev-middleware 的原始碼目錄:

...
├── 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 的原始碼實現

入口檔案 index.js

從上文我們已經得知 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 的編譯內容,輸出至記憶體中

setupHooks

此函式的作用是在 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 回撥函式
  • 在 invalid、run、watchRun 等生命週期上註冊 invalid 方法,該方法主要是 report 編譯的狀態資訊

compiler.watch

此部分的作用是,呼叫 compiler 的 watch 方法,之後 webpack 便會監聽檔案變更,一旦檢測到檔案變更,就會重新執行編譯。

setupOutputFileSystem

其作用是使用 memory-fs 物件替換掉 compiler 的檔案系統物件,讓 webpack 編譯後的檔案輸出到記憶體中。

fileSystem = new MemoryFileSystem();
// eslint-disable-next-line no-param-reassign
compiler.outputFileSystem = fileSystem;

通過以上 3 個部分的執行,我們以 watch mode 的方式啟動了 webpack,一旦監測的檔案變更,便會重新進行編譯打包,同時我們又將檔案的儲存方法改為了記憶體儲存,提高了檔案的儲存讀取效率。最後,我們只需要返回 express 的中介軟體就可以了,而中介軟體則是呼叫 middleware(context) 函式得到的。下面,我們來看看 middleware 是如何實現的。

middleware.js

此檔案返回的是一個 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 processRequest() {
  ...
 }
 ...
 ready(context,processRequest,req);
});

最後,中介軟體返回一個 Promise 例項,而在例項中,先是定義一個 processRequest 方法,此方法的作用是根據上文中找到的 filename 路徑獲取到對應的檔案內容,並構造 response 物件返回,隨後呼叫 ready(context,req) 函式,去執行 processRequest 方法。這裡我們著重看下 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,所以當用戶發起請求時,並不會直接返回對應的檔案內容,而是會將回調函式 processRequest 新增至 context.callbacks 中,而上文中我們說到在 compile.hooks.done 上註冊了回撥函式 done,等編譯完成之後,將會執行這個函式,並迴圈呼叫 context.callbacks。

總結

原始碼的閱讀是一個非常枯燥的過程,但是它的收益也是巨大的。上文的原始碼解讀主要分析的是 webpack-dev-middleware 它是如何實現它所擁有的特性、如何處理使用者的請求等主要功能點,未包括其他分支邏輯處理、容錯。還需讀者在這篇文章基礎之上,再去閱讀詳細的原始碼,望這篇文章能對你的閱讀過程起到一定的幫助作用。

到此這篇關於webpack-dev-middleware 原始碼解讀的文章就介紹到這了,更多相關webpack-dev-middleware 原始碼解讀內容請搜尋我們以前的文章或繼續瀏覽下面的相關文章希望大家以後多多支援我們!