巧用 webpack 作頁面靜態資源依賴分析

原文連接css

做者:梯田前端

前言:

所謂【靜態資源依賴分析】,指的是能夠經過分析頁面資源後,能夠以 json 數據或者圖表的方式拿到頁面資源間的依賴關係。node

好比 college-index(酷家樂大學首頁)的入口文件 entry.js 引用了 banner.js、 同時 banner.js 又引用了 utils.js, 那麼咱們但願通過分析後能拿到一份這樣的數據:webpack

[
    {
        "type": "entry",
        "path": "/xx/xx/college-index/entry.js",
        "deps": [
            {
                "type": "module",
                "path": "/xx/xx/college-index/banner.js",
                "deps": [
                    {
                        "type": "module",
                        "path": "/xx/xx/college-index/utils.js",
                        "deps": []
                    }
                ]
            }
        ]
    }
]
// type 分表表示它是一個 entry 仍是一個 module 
複製代碼

拿到資源依賴文件以後能夠作什麼呢?筆者這裏有幾個利用場景可供參考:git

  • 對一個多頁面 repo 而言,每次要發佈的時候,我但願經過 git diff 拿到本次改動的文件,再經過依賴分析拿到這次須要構建的資源,這樣就能夠作到單頁面發佈了。
  • 我能夠拿到當前的資源依賴,爲我剔除 repo 中沒有用到的資源
  • 我但願在 vscode 擴展中實時預覽前端 repo 中的資源依賴狀況

用處還可能有不少,關鍵是如何快速拿到這份依賴分析數據?github

一個思路

這裏給出一個筆者曾經考慮過的思路:經過遍歷頁面入口,而後進行關鍵字匹配,好比對( 【import xx from xxx】、【require】) 等關鍵字作處理,拿到被依賴的模塊路徑,而後繼續對模塊路徑作遞歸解析,最終彙總拿到依賴樹。web

這個思路乍看是可行的,並且使用一些措施會使得分析流程更加高效,好比再對關鍵詞做匹配的時候能夠藉助 acorn 這類的 JavaScript 解析器,再經過對文件被解析後的 ast 做處理。npm

可是簡單嘗試後就放棄了這個思路,緣由是對於如今的前端工程化項目而言,一個頁面中的依賴不只有 js 並且還會有各類各樣的資源,好比 sass、less 之類的 css 預處理器、或者是別的資源等等,因此單單對 js 路徑作處理是不夠的。json

藉助 webpack 來實現

在上述思路不可行的狀況下,咱們將解決辦法瞄向了藉助 webpack 來實現,對 webpack 熟悉的開發者會知道 webpack 對於依賴對處理的過程,這裏再簡單的提一下:webpack 拿到入口文件 entry 後,會經過先獲取資源的正確路徑,再通過 loader 解析文件,最後經過遍歷 ast 來拿到模塊中引用的依賴 【dependences 】,再對 【dependences】 作遞歸處理,最終拿到依賴樹。前端工程化

這跟咱們最初設想的思路基本一致,同時藉助 loader 能夠將不一樣的資源沒法解析的問題也一併解決了。看到這裏的人或許會有疑問:官方不是已經給出了 webpack-bundle-analyzer 這類的工具了嗎? 並且每次構建後 stats 中都能拿到文件依賴,爲啥不能直接使用呢。

不直接使用的緣由很簡單:首先構建一次實在太慢了,特別是有幾十個頁面存在的狀況下,另外一個緣由是我只是想拿到資源依賴,我根本不想對整個前端 repo 進行一次構建,也不想生成任何 bundle。

有沒有一種工具能夠如同 webpack 同樣,既可使用 loader 找到文件依賴,又不須要生成和壓縮 bundle 呢?

在咱們改造 webpack 以前它原本是沒有的,如何改造? 一個 webpack plugin 便可。

webpack 的構建流程

在介紹如何改造以前,有必要了解一下 webpack 的模塊處理過程以及總體流程。

模塊的處理過程:

webpack 拿到一個路徑後,他會依次執行 reslove 模塊路徑 -> create 模塊 -> build 模塊 -> parse 模塊等主要流程,每一步的流程的做用以下:

-【 reslove 模塊 】:獲取模塊真實路徑 -【 create 模塊 】 :建立一個模塊的 context -【 build 模塊 】 :讀取模塊內容 -【 parse 模塊 】 :分析模塊內容(主要是找到模塊中的 require 關鍵詞,將依賴添加到該模塊的依賴數組中。

最後重複上述流程

每個流程都很是複雜,以【 reslove 模塊】 這個流程爲例: 處理邏輯主要在 webpack/lib/normalModuleFactory.js 這個文件中, 整個 reslove 流程會依次執行 beforeResolvefactoryresolver 以及 afterResolve 這些步驟,每一個步驟對應一個 hook ,每一個 hook 執行的時候會拿到關於模塊的描述信息,描述信息會隨着 hook 的執行愈發飽滿,最終獲取到了完整的模塊信息,爲接下來的【 create 模塊】以及 【 build 模塊】作好準備,在下文【具體實現】中咱們會插手這部分流程。

本文再也不細談模塊的處理流程,網上有許多優秀的文章能夠參考,請你們自由查閱。

webpack 的總體流程:

在筆者看來,webpack 的總體流程分爲 4 個步驟,圖示以下:

---------3-

實現依賴分析中咱們只須要 webpack 的前 3 個流程就夠了,下文會給出緣由。

解決辦法

前面咱們提到 webpack 處理依賴的過程是遞歸分析入口的全部依賴, 因爲頁面中的依賴包含相對或者絕對路徑引用的依賴,也包含藉助 alias 引用的依賴,也包含 node_modules 中的依賴,既然咱們只想拿到 repo 前的資源依賴,對於 node_modules 中的依賴能夠直接屏蔽掉,這將使得模塊的遞歸時間大大縮短,以下圖所示:

由:

-----

變成:

-------1-

在上面提到的 webpack 4 個主流程中,在 【step3】 結束後,webpack 能拿到全部 modules (一次構建行爲產生的全部的模塊),此時已經足夠咱們進行依賴分析了,咱們直接終止 webpack 的後續流程,再也不進行生成 chunk 以及對 chunk 作的合併優化等過程。這就達到了本文題目中目的,【用十分之一的構建時間作一場頁面靜態資源依賴分析】。

具體實現:

寫一個 webpack plugin

plugin 名字就叫 FastDependenciesAnalyzerPlugin ,plugin 寫法參照官方文檔

class FastDependenciesAnalyzerPlugin {
  beforeResolve = (resolveData, callback) => {}

  afterResolve = (result, callback) => {}

  handleFinishModules = (modules, callback) => {}

  apply(compiler) {
    compiler.hooks.normalModuleFactory.tap(
      "FastDependenciesAnalyzerPlugin",
      nmf => {
        
        nmf.hooks.beforeResolve.tapAsync(
          "FastDependenciesAnalyzerPlugin",
          this.beforeResolve
        );
        
        nmf.hooks.afterResolve.tapAsync(
          "FastDependenciesAnalyzerPlugin",
          this.afterResolve
        );
      }
    );
    
    compiler.hooks.compilation.tap(
      "FastDependenciesAnalyzerPlugin",
      
      compilation => {
        compilation.hooks.finishModules.tapAsync(
          "FastDependenciesAnalyzerPlugin",
          this.handleFinishModules
        );
      }
    );
    
  }
}
複製代碼

complier.hooks.normalModuleFactory 這個 hook 的回調中繼續監聽 normalModuleFactorybeforeResolve hook 和 beforeResolve hook。

complier.hooks.compilation 這個 hooks 的回調中繼續監聽 compilationfinishModules hook。

插手 beforeResolve 流程

beforeResolve(resolveData, callback) {
    const { context, contextInfo, request } = resolveData;
    const { issuer } = contextInfo;
  }
複製代碼

context 表示爲 解析目錄的絕對路徑,一個頁面的 context 都是同樣的, issuer 翻譯爲發行人,在 webpack 中表示本模塊被依賴的對象路徑,也就指向這個模塊的來源,request 表示當前模塊的的請求路徑。 好比:banner.js 的資源路徑爲:/xx/xxx/banner.js , 文件內容是這樣的:

// 1
import utils from './utils'

// 2
import utils from '@utils'

// 3
import utils from 'utils'
複製代碼

因此對於 utils 模塊來講, issuer 的值爲 /xx/xxx/banner.js, request 的值分別爲 "./utils.js"、"@utils"、"utils" 。

此時,咱們只能知道當前模塊的來源路徑 issuer 以及它被請求的路徑 request,拿不到當前模塊的真實路徑,還沒法將它放入咱們的依賴樹中,因此咱們不會在 beforeReslove 中處理咱們的依賴樹,咱們在這一步中只是想屏蔽掉一些咱們不想被處理的模塊就能夠,好比假設 utils 這個 npm 包裏有很是多的小模塊,這些模塊不會被放到依賴樹中,因此對於這些模塊咱們選擇直接跳過, 事實上在 webpack 源碼中有這樣一句:

webpack normalModuleFactory 源碼

對於在 beforeResolve 中沒有返回值的模塊會直接 callback ,在 webpack 源碼中 callback 裏若是沒有參數,每每意味着流程的提早結束,在 beforeResolvereturn callback 也就沒有這個模塊後續對 reslovebuild 流程了,這就實現了模塊跳過。

爲此,咱們能夠實現一個 skip 函數,這裏提供一個簡單版本,傳入參數爲 issuerrequest

// 事先獲取到 package.json 裏的 dependencies 

const ignoreDependenciesArr = Object.keys(dependencies);

function skip(request, issuer) {
  return (
    ignoreDependenciesArr.some(item => request.includes(item)) ||
    issuer.includes("node_modules")
  );
}

複製代碼

經過比較 request 是否在 package.json 裏定義的 dependencies 裏,或者若是 issuer 自己包含 node_modules,就能夠表示當前模塊是能夠跳過的。固然這個方法只是一個簡單版本,實際上要考慮許多特殊狀況,這裏不會詳細給出,感興趣的讀者能夠自行實現。

藉助 afterResolve

afterResolve(result, callback) {
    const { resourceResolveData } = result;
    const { 
        context:{
           issuer
         },
         path
    } = resourceResolveData;
  }

  // 這裏添加依賴到依賴樹
複製代碼

webpack 使用 enhanced-resolve 對一個請求路徑做解析,只須要傳入模塊的 contextInfo, context, request, 便可拿到當前模塊的真實路徑,固然前提是須要告知 enhanced-resolve ,這個模塊所在 repo 的 package.json, webpack 配置中的 alias 信息等等,本文不會敘述 enhanced-resolve 的工做方法,感興趣的讀者能夠自行查閱。

總之,在 afterReslove 中咱們可以拿到 webpack 借用 enhanced-resolve 解析事後的模塊路徑了,還有依賴這個模塊的父模塊路徑,將這兩個路徑添加到依賴樹中,通過簡單的遞歸操做就能夠拿到完整的依賴樹了,固然在依賴樹中咱們能夠放置各類信息,好比是不是一個模塊?是不是一個 js 文件、是不是一個 css 文件,這些均可以實現。

在 finishModules 裏結束

webpack 官方在 4.30 提交了一次 commit,在此次提交中將 finishModules 這個 SyncHook 轉化成了 AsyncSeriesHook , 同時在 finishModules hook 中加入了一行代碼,以下圖:

這使得咱們能夠監聽 finishModules 這個 hook ,而後在 err 方法裏傳入一個值,這就直接屏蔽掉了後續的文件合併以及優化流程了,固然這個操做比較 hack,也不是官方推薦用法,但願在webpack 5.X 的更新中,官方能夠提供更合適的 hook ,讓咱們方便跳過某些流程。

一些踩坑

  1. 對於在 js 中引用的 css 或者 scss 文件,能夠經過尋常的 reslove 流程拿到依賴,可是若是在 css 中使用了 @import 語法,因爲 css-loader 會自行處理這些語法,因此它不會走 webpack 自己的 reslove 流程,詳見這個 issue,這裏得咱們本身在 beforeReslove 中對這部分作額外對處理,好比經過字符串截取的方式去掉 request 中關於 loder 描述的部分,再經過主動調用 enhance-resolve 這個方法實現對 @import 傳進來對模塊作處理,最終拿到正確的路徑。

  2. 不一樣版本的 webpack 一些 hook 的用法和名稱會不同,開發者在處理內部流程的時候要注意。

總結

本文所闡述的原理並不深奧,主要是挖掘了一個 webpack 的一個用法,但願能啓發想要利用 webpack 作更多工具的開發者多借助 webpack 內部原理,最後感謝你們的閱讀,歡迎有興趣的朋友在文章底下評論,給出建議和幫助。

相關文章
相關標籤/搜索