webpack-dev-middleware解讀

weixin_34253539發表於2017-07-31
  1. 簡單介紹
    webpack-dev-middleware,作用就是,生成一個與webpack的compiler繫結的中介軟體,然後在express啟動的服務app中呼叫這個中介軟體。
    這個中介軟體的作用呢,簡單總結為以下三點:通過watch mode,監聽資源的變更,然後自動打包(如何實現,見下文詳解);快速編譯,走記憶體;返回中介軟體,支援express的use格式。特別註明:webpack明明可以用watch mode,可以實現一樣的效果,但是為什麼還需要這個中介軟體呢?
    答案就是,第二點所提到的,採用了記憶體方式。如果,只依賴webpack的watch mode來監聽檔案變更,自動打包,每次變更,都將新檔案打包到本地,就會很慢。

  2. 實踐出真知
    webpack-dev-middleware 使用配置很簡單,只需幾步,就可以。專案程式碼,參考原始碼;

    step1: 配置publicPath.

    publicPath,熟悉webpack的同學都知道,這是生成的新檔案所指向的路徑,可以模擬CDN資源引用。那麼跟此處的主角webpack-dev-middleware什麼關係呢,關係就是,此處採用記憶體的方式,記憶體中採用的檔案儲存write path就是此處的publicPath,因此,這裡的配置publicPath需要使用相對路徑。

    let path = require('path');
    
    module.exports = {
        entry: './app.js',
        output: {
            publicPath: "/assets/",
            filename: 'bundle.js',
            //path: '/'   //只使用 dev-middleware 可以忽略本屬性
        },
    };
    
    

    step2: express server中引入中介軟體。

    const path = require('path');
    const express = require("express");
    var ejs = require('ejs');
    const app = express();
    const webpack = require('webpack');
    const webpackMiddleware = require("webpack-dev-middleware");
    let webpackConf = require('./webpack.config.js');
    
    app.engine('html', ejs.renderFile);
    app.set('views', path.join(__dirname, 'src/html'));
    app.set("view engine", "html");
    
    var compiler = webpack(webpackConf);
    
    app.use(webpackMiddleware(compiler, {
        publicPath: webpackConf.output.publicPath,
    }));
    
    app.get("/", function(req, res) {
        res.render("index");
    });
    
    app.listen(3333);
    

    通過step1以及step2,就能看到webpack的熱載入效果了,效果展示。

    4759918-fc91f6d0ff830fab.gif
    效果展示
  3. 原始碼分析。

    step1:首先看webpack-dev-middleware包,專案目錄結構為:

    4759918-0a9e3aadb7be8619.PNG
    專案結構

    step2:逐一破解:

    middleware.js分析:
    line 6, var require("./lib/GetFilenameFromUrl");引入通過url得到fileName的方法;
    line 11,方法入口,引入compiler以及option配置,可以看到這是常規的結構方法,引入option,然後定義預設值(line 13),處理預設邏輯(line 22).
    line 22, 初始化的處理,我們進入shared.js檔案,深入分析一下。

    shared.js分析:
    share結構:

    4759918-964e273677ed8cce.PNG
    物件結構

    line 223,share.setOptions(context.options);,此時的options是我們配置中的

    {
        publicPath: webpackConf.output.publicPath,
    }
    

    line 9~36,定義了setOptions方法,簡單一撇,重新定義了options的reporter方法,watchOptions.aggregateTimeout,options的stats(統計資訊物件),mimeTypes定義。(配置資訊不熟悉的可以參考官方github倉庫中的example

    line 224, share.setFs(context.compiler);

    可以看到,setFs方法做了兩件事,檢查compiler.outputPath是否為絕對路徑(預設為process.cwd()),如果為相對路徑,丟擲錯誤;定義compiler.outputFileSystem = new MemoryFileSystem();這就是webpack-dev-middleware的精髓所在了,使用記憶體檔案系統,而不是硬碟中的檔案,這樣能夠提升編譯的速度(稍後詳細分析這個玩意兒)。

    line 226, context.compiler.plugin('done', share.compilerDone);

    定義了一個done事件鉤子函式,該函式內主要是reporter編譯的資訊以及執行context.callbacks回撥函式。

    line 227,228,229,原始碼:

    context.compiler.plugin("invalid", share.compilerInvalid);
    context.compiler.plugin("watch-run", share.compilerInvalid);
    context.compiler.plugin("run", share.compilerInvalid);
    

    定義了一個invalid事件(監控的編譯變無效後),watch-run(watch後開始編譯之前),run(讀取記錄之前)的回撥,都是share.compilerInvalid方法,該方法主要還是根據state狀態,report編譯的狀態資訊。

    line 231,share.startWatch(),開始監控.可以看到主要邏輯在compiler.watch();納尼?繞了一圈還是呼叫了compiler的原型方法watch。瞅一瞅,webpack/lib/compiler.js檔案的line 216,

    Compiler.prototype.watch = function(watchOptions, handler) {
       this.fileTimestamps = {};
       this.contextTimestamps = {};
       var watching = new Watching(this, watchOptions, handler);
       return watching;
    };
    

    同理,看到 webpack/lib/webpack.js的42行,可以看到,當webpack命令時,若有--watch,實際同樣是呼叫的compiler.watch方法。

    至此,回到middleware.js的line22. 也就是重點了,webpackDevMiddleware中介軟體函式。

    4759918-c29767e8bace7c6a.PNG
    dev-middleware中介軟體函式

    line 26~35定義了goNext()方法,該方法首先判斷是否伺服器端渲染,如果不是,直接next()處理,否則,呼叫了shared的ready()方法(根據state狀態,處理邏輯)。

    line 36~38,非get請求,直接goNext()。

    line 40~41,找不到請求的檔案,直接goNext()。

    line 43~78,處理邏輯,可以看到精簡後結構。

    4759918-95d95bd13f6cd392.PNG

    也就是呼叫shared.handleRequest方法處理,深入該方法,也即是shared.js的line 189~201,主要邏輯為:判斷是否lazy模式而且沒有定義filename,如果是的話,rebuild(),也就是重新編譯,這就是lazy模式只有在瀏覽器重新重新整理請求的時候才會編譯的原因;如果不是lazy模式,如果所尋找的filename存在(注意此處是通過記憶體fs查詢),那麼呼叫processRequest()處理。

    line 45~76,是processRequest()的邏輯,主要是express()的res處理邏輯了,簡單明瞭。

    line 85,可以看到return webpackDevMiddleware,最終返回了express的中介軟體。

    至此,game over!

  4. 延伸擴充套件;

    lazy模式下什麼表現呢???深入shared.js會發現,當lazy為true(shared.js檔案line 169~175)時,npm run test並不會執行編譯,而是當瀏覽器發出請求req時,在shared.js的handleRequest方法(line 191)的194行執行了rebuild()方法,在rebuild方法的180行執行了context.compiler.run()進行了編譯。在修改後,webpack不會立即執行編譯,而是等到req再次請求時編譯。也就是在lazy模式下,每次只有在瀏覽器請求時,才執行一次compile,watch並沒有什麼卵用啊。

    正常模式呢?表現是怎麼樣?正常模式,npm run test時,程式碼執行到startWatch(),也就是執行到compiler的watch()方法,深入compiler原始碼可以看到,Compiler.js檔案的114行,執行到invalidate()方法,判斷是否已經running,如果為false,進入_go()方法,執行了compile()邏輯。也就是說,在沒有瀏覽器請求時,就已經執行了編譯。然後在修改了entry相關的檔案後,watch會執行編譯,同時會觸發compiler的invalid事件(在Compiler.js的watch方法的116行可以看到)也就是會執行到Shared.js的229行,執行compilerInvalid方法,列印compiling資訊。

    總結就是,lazy模式只有在瀏覽器請求時,才會執行compile編譯,而正常模式下,則是改變後,立即執行compile過程。

相關文章