webpack-dev-middleware 源碼解讀

本文首發於政採雲前端團隊博客: webpack-dev-middleware 源碼解讀

前言

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

截至本文發表前,webpack-dev-middleware 的最新版本爲 webpack-dev-middleware@3.7.2,本文的源碼來自於此版本。本文會講解 webpack-dev-middleware 的核心模塊實現,相信你們把這篇文章看完,再去閱讀源碼,會容易理解不少。node

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 服務,將 wdm(compiler) 的結果經過 app.use 方法註冊爲 Express 服務的中間函數。從這裏,咱們不難看出 wdm(compiler) 的執行結果返回的是一個 express 的中間件。它做爲一個容器,將 webpack 編譯後的文件存儲到內存中,而後在用戶訪問 express 服務時,將內存中對應的資源輸出返回。web

爲何要使用 webpack-dev-middleware

熟悉 webpack 的同窗都知道,webpack 能夠經過 watch mode 方式啓動,那爲什麼咱們不直接使用此方式來監聽資源變化呢?答案就是,webpackwatch mode 雖然能監聽文件的變動,而且自動打包,可是每次打包後的結果將會存儲到本地硬盤中,而 IO 操做是很是耗資源時間的,沒法知足本地開發調試需求。express

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

  • watch mode 啓動 webpack,監聽的資源一旦發生變動,便會自動編譯,生產最新的 bundle
  • 在編譯期間,中止提供舊版的 bundle 而且將請求延遲到最新的編譯結果完成以後
  • webpack 編譯後的資源會存儲在內存中,當用戶請求資源時,直接於內存中查找對應資源,減小去硬盤中查找的 IO 操做耗時

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

源碼解讀

讓咱們先來看下 webpack-dev-middleware 的源碼目錄:api

...
├── 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.jsmiddleware.jsapp

下面咱們就來分析核心文件 index.js middleware.js 的源碼實現webpack-dev-server

入口文件 index.js

從上文咱們已經得知 wdm(compiler) 返回的是一個 express 中間件,因此入口文件 index.js 則爲一箇中間件的容器包裝函數。它接收兩個參數,一個爲 webpackcompiler、另外一個爲配置對象,通過一系列的處理,最後返回一箇中間件函數。下面我將對 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

此函數的做用是在 compilerinvalidrundonewatchRun 這 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 回調函數
  • invalidrunwatchRun 等生命週期上註冊 invalid 方法,該方法主要是 report 編譯的狀態信息

compiler.watch

此部分的做用是,調用 compilerwatch 方法,以後 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 協議的請求的類型,若請求不包含於配置中(默認 GETHEAD 請求),則直接調用 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, processRequest, 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 它是如何實現它所擁有的特性、如何處理用戶的請求等主要功能點,未包括其餘分支邏輯處理、容錯。還需讀者在這篇文章基礎之上,再去閱讀詳細的源碼,望這篇文章能對你的閱讀過程起到必定的幫助做用。

招賢納士

政採雲前端團隊(ZooTeam),一個年輕富有激情和創造力的前端團隊,隸屬於政採雲產品研發部,Base 在風景如畫的杭州。團隊現有 50 餘個前端小夥伴,平均年齡 27 歲,近 3 成是全棧工程師,妥妥的青年風暴團。成員構成既有來自於阿里、網易的「老」兵,也有浙大、中科大、杭電等校的應屆新人。團隊在平常的業務對接以外,還在物料體系、工程平臺、搭建平臺、性能體驗、雲端應用、數據分析及可視化等方向進行技術探索和實戰,推進並落地了一系列的內部技術產品,持續探索前端技術體系的新邊界。

若是你想改變一直被事折騰,但願開始能折騰事;若是你想改變一直被告誡須要多些想法,卻無從破局;若是你想改變你有能力去作成那個結果,卻不須要你;若是你想改變你想作成的事須要一個團隊去支撐,但沒你帶人的位置;若是你想改變既定的節奏,將會是「5 年工做時間 3 年工做經驗」;若是你想改變原本悟性不錯,但老是有那一層窗戶紙的模糊… 若是你相信相信的力量,相信平凡人能成就非凡事,相信能遇到更好的本身。若是你但願參與到隨着業務騰飛的過程,親手推進一個有着深刻的業務理解、完善的技術體系、技術創造價值、影響力外溢的前端團隊的成長曆程,我以爲咱們該聊聊。任什麼時候間,等着你寫點什麼,發給 ZooTeam@cai-inc.com

相關文章
相關標籤/搜索