webpack-dev-middleware@1.12.2 源碼解讀

​ webpack-dev-middleware 是express的一箇中間件,它的主要做用是以監聽模式啓動webpack,將webpack編譯後的文件輸出到內存裏,而後將內存的文件輸出到epxress服務器上;下面經過一張圖片來看一下它的工做原理:css

img

瞭解了它的工做原理之後咱們經過一個例子進行實操一下。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

img

源碼連接: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

img

img

​ 問:爲何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/

項目目錄:

img

運行結果:

img

經過上述代碼咱們能夠看出不論是輸出html文件仍是圖片文件都是須要先將這些文件讀取到內存裏,而後才能輸出到response上。


middleware.js

​ 下面咱們就來看看webpack-dev-middleware這個函數內部是如何實現的,它的運行原理是什麼?我的感受讀源碼最主要的就是基礎 + 耐心 + 流程

​ 首先打開node_modules/webpack-dev-middleware/middleware.js文件,注意版本號,我這份代碼的版本號是webpack-dev-middleware@1.12.2。

img

​ middleware.js文件就是webpack-dev-middleware的入口文件,它主要作如下幾件事情:

​ 一、記錄compiler對象和中間件配置

​ 二、建立webpack操做對象shared

​ 三、建立中間件函數webpackDevMiddleware

​ 四、將webpack的一些經常使用操做函數暴露到中間件函數上,供外部直接調用

​ 五、返回中間件函數

Shared.js

img

這個文件對webpack的compiler這個對象進行封裝操做,咱們大概先來看看這個文件主要作了哪些事情:

  1. 首先設置中間件的一些默認選項配置
  2. 使用memory-fs對象替換掉compiler的文件系統對象,讓webpack編譯後的文件輸出到內存中
  3. 監聽webpack的鉤子函數
    1. invalid:監聽模式下,文件發生變化時調用,同時會傳入2個參數,分別是文件名和時間戳
    2. watch-run:監聽模式下,一個新的編譯觸發以後,完成編譯以前調用
    3. done:編譯完成時調用,並傳入webpack編譯日誌對象stats
    4. run:在開始讀取記錄以前調用,只有調用compiler.run()函數時纔會觸發該鉤子函數
  4. 以觀察者模式啓動webpack編譯
  5. 返回share對象,該對象封裝了不少關於compiler的操做函數

經過上面的截圖咱們大概知道了Shared.js文件的運行流程,下面咱們再來看看它一些比較重要的細節。

share.setOptions 設置中間件的默認配置

img

share.setFs(context.compiler) 設置compiler的文件操做對象

img

share.startWatch() 以觀察模式啓動webpack

img

compiler.watch(watchOptions, callback) 這個函數表示以監聽模式啓動webpack並返回一個watching對象,這裏特別須要注意的是當調用compiler.watch函數時會當即執行watch-run這個鉤子回調函數,直到這個鉤子回調函數執行完畢後,纔會返回watching對象。

share.compilerDone(stats) webpack編譯完成回調處理函數

img

img

當webpack的一個編譯完成時會進入done鉤子回調函數,而後調用compilerDone函數,這個函數內部首先將context.state設置爲true表示webpack編譯完成,並記錄webpack的統計信息對象stats,而後將webpack日誌輸出操做和回調函數執行都放到process.nextTick()任務隊列執行,就是等主邏輯全部的代碼執行完畢後才進行webpack的日誌輸出和中間件回調函數的執行。

context.options.reporter (share.defaultReporter) webpack默認日誌輸出函數

context.options.reporter 和 share.defaultReporter 指向的都是同一個函數

img

​ 經過代碼咱們能夠看出這個函數內部首先是要判斷一下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...

handleCompilerCallback() - watch回調函數

img

這個是watch回調函數,它是在compiler.plugin('done')鉤子函數執行完畢以後執行,它有兩個參數,一個是錯誤信息,一個是webpack編譯成功的統計信息對象stats,能夠看到這個回調函數內部只作錯誤信息的輸出。

webpack watch模式鉤子函數執行流程圖

img

使用webpack-dev-middleware中間件

​ 以前我介紹的都是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的內部運行流程

img

二、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中間件的源代碼,我加了一些註釋和我的看法說明這個中間件內部的具體操做,這裏我簡單總結一下這個中間件函數主要作了哪些事情:

  1. 首先判斷若是不是GET請求,則調用下一個中間件函數,並退出當前中間件函數。
  2. 根據請求的URL,拼接出該資源在webpack輸出目錄的絕對路徑。例如:請求的URL爲「/bundle.js」,那麼在我電腦拼接出的絕對路徑就爲"/Users/jameswain/WORK/blog/demo2/dist/bundle.js",若是請求的URL爲/,設置文件爲index.html
  3. 讀取請求文件的內容,是一個Buffer類型,能夠當即爲流
  4. 判斷客戶端是否設置了range請求頭,若是設置了,則須要對內容進行截取限制在指定範圍以內。
  5. 獲取請求文件的mime類型
  6. 設置請求頭Content-Type和Content-Length,循環設置中間件配置的自定義請求頭
  7. 設置狀態碼爲200
  8. 將文件內容輸出到客戶端

​ 下面經過一個流程圖看一下這個中間件函數的執行流程:

img

總結

​ webpack-dev-middleware這個中間件內部其實主就是作了兩件事,第一就是在中間件函數初始化時,修改webpack的文件操做對象,讓webpack編譯後的文件輸出到內存裏,以監聽模式啓動webpack。第二就是當有http get請求過來時,中間件函數內部讀取webpack輸出到內存裏的文件,而後輸出到response上,這時候瀏覽器拿到的就是webpack編譯後的資源文件了。

​ 最後給出本文全部相關源代碼的地址:https://github.com/Jameswain/...

​ 聲明:本文純屬我的閱讀webpack-dev-middleware@1.12.2源碼的一些我的理解和感悟,因爲本人技術水平有限,若有錯誤還望各位大神批評指正。

相關文章
相關標籤/搜索