原文連接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
用處還可能有不少,關鍵是如何快速拿到這份依賴分析數據?github
這裏給出一個筆者曾經考慮過的思路:經過遍歷頁面入口,而後進行關鍵字匹配,好比對( 【import xx from xxx】、【require】) 等關鍵字作處理,拿到被依賴的模塊路徑,而後繼續對模塊路徑作遞歸解析,最終彙總拿到依賴樹。web
這個思路乍看是可行的,並且使用一些措施會使得分析流程更加高效,好比再對關鍵詞做匹配的時候能夠藉助 acorn 這類的 JavaScript 解析器,再經過對文件被解析後的 ast 做處理。npm
可是簡單嘗試後就放棄了這個思路,緣由是對於如今的前端工程化項目而言,一個頁面中的依賴不只有 js 並且還會有各類各樣的資源,好比 sass、less 之類的 css 預處理器、或者是別的資源等等,因此單單對 js 路徑作處理是不夠的。json
在上述思路不可行的狀況下,咱們將解決辦法瞄向了藉助 webpack 來實現,對 webpack 熟悉的開發者會知道 webpack 對於依賴對處理的過程,這裏再簡單的提一下:webpack 拿到入口文件 entry 後,會經過先獲取資源的正確路徑,再通過 loader 解析文件,最後經過遍歷 ast 來拿到模塊中引用的依賴 【dependences 】,再對 【dependences】 作遞歸處理,最終拿到依賴樹。前端工程化
這跟咱們最初設想的思路基本一致,同時藉助 loader 能夠將不一樣的資源沒法解析的問題也一併解決了。看到這裏的人或許會有疑問:官方不是已經給出了 webpack-bundle-analyzer 這類的工具了嗎? 並且每次構建後 stats 中都能拿到文件依賴,爲啥不能直接使用呢。
不直接使用的緣由很簡單:首先構建一次實在太慢了,特別是有幾十個頁面存在的狀況下,另外一個緣由是我只是想拿到資源依賴,我根本不想對整個前端 repo 進行一次構建,也不想生成任何 bundle。
有沒有一種工具能夠如同 webpack 同樣,既可使用 loader 找到文件依賴,又不須要生成和壓縮 bundle 呢?
在咱們改造 webpack 以前它原本是沒有的,如何改造? 一個 webpack plugin 便可。
在介紹如何改造以前,有必要了解一下 webpack 的模塊處理過程以及總體流程。
webpack 拿到一個路徑後,他會依次執行 reslove 模塊路徑 -> create 模塊 -> build 模塊 -> parse 模塊等主要流程,每一步的流程的做用以下:
-【 reslove 模塊 】:獲取模塊真實路徑 -【 create 模塊 】 :建立一個模塊的 context -【 build 模塊 】 :讀取模塊內容 -【 parse 模塊 】 :分析模塊內容(主要是找到模塊中的 require 關鍵詞,將依賴添加到該模塊的依賴數組中。
最後重複上述流程
每個流程都很是複雜,以【 reslove 模塊】 這個流程爲例: 處理邏輯主要在 webpack/lib/normalModuleFactory.js
這個文件中, 整個 reslove 流程會依次執行 beforeResolve
、 factory
、 resolver
以及 afterResolve
這些步驟,每一個步驟對應一個 hook ,每一個 hook 執行的時候會拿到關於模塊的描述信息,描述信息會隨着 hook 的執行愈發飽滿,最終獲取到了完整的模塊信息,爲接下來的【 create 模塊】以及 【 build 模塊】作好準備,在下文【具體實現】中咱們會插手這部分流程。
本文再也不細談模塊的處理流程,網上有許多優秀的文章能夠參考,請你們自由查閱。
在筆者看來,webpack 的總體流程分爲 4 個步驟,圖示以下:
實現依賴分析中咱們只須要 webpack 的前 3 個流程就夠了,下文會給出緣由。
前面咱們提到 webpack 處理依賴的過程是遞歸分析入口的全部依賴, 因爲頁面中的依賴包含相對或者絕對路徑引用的依賴,也包含藉助 alias 引用的依賴,也包含 node_modules 中的依賴,既然咱們只想拿到 repo 前的資源依賴,對於 node_modules 中的依賴能夠直接屏蔽掉,這將使得模塊的遞歸時間大大縮短,以下圖所示:
由:
變成:
在上面提到的 webpack 4 個主流程中,在 【step3】 結束後,webpack 能拿到全部 modules (一次構建行爲產生的全部的模塊),此時已經足夠咱們進行依賴分析了,咱們直接終止 webpack 的後續流程,再也不進行生成 chunk 以及對 chunk 作的合併優化等過程。這就達到了本文題目中目的,【用十分之一的構建時間作一場頁面靜態資源依賴分析】。
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 的回調中繼續監聽 normalModuleFactory
的 beforeResolve
hook 和 beforeResolve
hook。
在 complier.hooks.compilation
這個 hooks 的回調中繼續監聽 compilation
的 finishModules
hook。
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 源碼中有這樣一句:
對於在 beforeResolve
中沒有返回值的模塊會直接 callback ,在 webpack 源碼中 callback
裏若是沒有參數,每每意味着流程的提早結束,在 beforeResolve
中 return callback
也就沒有這個模塊後續對 reslove
和 build
流程了,這就實現了模塊跳過。
爲此,咱們能夠實現一個 skip 函數,這裏提供一個簡單版本,傳入參數爲 issuer
和 request
// 事先獲取到 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(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 文件,這些均可以實現。
webpack 官方在 4.30 提交了一次 commit,在此次提交中將 finishModules
這個 SyncHook
轉化成了 AsyncSeriesHook
, 同時在 finishModules
hook 中加入了一行代碼,以下圖:
這使得咱們能夠監聽 finishModules
這個 hook ,而後在 err 方法裏傳入一個值,這就直接屏蔽掉了後續的文件合併以及優化流程了,固然這個操做比較 hack,也不是官方推薦用法,但願在webpack 5.X 的更新中,官方能夠提供更合適的 hook ,讓咱們方便跳過某些流程。
對於在 js 中引用的 css 或者 scss 文件,能夠經過尋常的 reslove 流程拿到依賴,可是若是在 css 中使用了 @import 語法,因爲 css-loader 會自行處理這些語法,因此它不會走 webpack 自己的 reslove 流程,詳見這個 issue,這裏得咱們本身在 beforeReslove
中對這部分作額外對處理,好比經過字符串截取的方式去掉 request
中關於 loder
描述的部分,再經過主動調用 enhance-resolve
這個方法實現對 @import
傳進來對模塊作處理,最終拿到正確的路徑。
不一樣版本的 webpack 一些 hook 的用法和名稱會不同,開發者在處理內部流程的時候要注意。
本文所闡述的原理並不深奧,主要是挖掘了一個 webpack 的一個用法,但願能啓發想要利用 webpack 作更多工具的開發者多借助 webpack 內部原理,最後感謝你們的閱讀,歡迎有興趣的朋友在文章底下評論,給出建議和幫助。