摘要:不直接使用的原因很簡單首先構(gòu)建一次實(shí)在太慢了,特別是有幾十個(gè)頁面存在的情況下,另一個(gè)原因是我只是想拿到資源依賴,我根本不想對整個(gè)前端進(jìn)行一次構(gòu)建,也不想生成任何。這就達(dá)到了本文題目中目的,用十分之一的構(gòu)建時(shí)間做一場頁面靜態(tài)資源依賴分析。
原文鏈接
作者:梯田
前言:
所謂【靜態(tài)資源依賴分析】,指的是可以通過分析頁面資源后,可以以 json 數(shù)據(jù)或者圖表的方式拿到頁面資源間的依賴關(guān)系。
比如 college-index(酷家樂大學(xué)首頁)的入口文件 entry.js 引用了 banner.js、 同時(shí) banner.js 又引用了 utils.js, 那么我們希望經(jīng)過分析后能拿到一份這樣的數(shù)據(jù):
[ { "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 分表表示它是一個(gè) entry 還是一個(gè) module
拿到資源依賴文件之后可以做什么呢?筆者這里有幾個(gè)利用場景可供參考:
對一個(gè)多頁面 repo 而言,每次要發(fā)布的時(shí)候,我希望通過 git diff 拿到本次改動的文件,再通過依賴分析拿到此次需要構(gòu)建的資源,這樣就可以做到單頁面發(fā)布了。
我可以拿到當(dāng)前的資源依賴,為我剔除 repo 中沒有用到的資源
我希望在 vscode 擴(kuò)展中實(shí)時(shí)預(yù)覽前端 repo 中的資源依賴情況
用處還可能有很多,關(guān)鍵是如何快速拿到這份依賴分析數(shù)據(jù)?
一個(gè)思路
這里給出一個(gè)筆者曾經(jīng)考慮過的思路:通過遍歷頁面入口,然后進(jìn)行關(guān)鍵字匹配,比如對( 【import xx from xxx】、【require】) 等關(guān)鍵字做處理,拿到被依賴的模塊路徑,然后繼續(xù)對模塊路徑做遞歸解析,最終匯總拿到依賴樹。
這個(gè)思路乍看是可行的,而且使用一些措施會使得分析流程更加高效,比如再對關(guān)鍵詞作匹配的時(shí)候可以借助 acorn 這類的 JavaScript 解析器,再通過對文件被解析后的 ast 作處理。
但是簡單嘗試后就放棄了這個(gè)思路,原因是對于現(xiàn)在的前端工程化項(xiàng)目而言,一個(gè)頁面中的依賴不僅有 js 而且還會有各種各樣的資源,比如 sass、less 之類的 css 預(yù)處理器、或者是別的資源等等,所以單單對 js 路徑做處理是不夠的。
借助 webpack 來實(shí)現(xiàn)
在上述思路不可行的情況下,我們將解決辦法瞄向了借助 webpack 來實(shí)現(xiàn),對 webpack 熟悉的開發(fā)者會知道 webpack 對于依賴對處理的過程,這里再簡單的提一下:webpack 拿到入口文件 entry 后,會通過先獲取資源的正確路徑,再經(jīng)過 loader 解析文件,最后通過遍歷 ast 來拿到模塊中引用的依賴 【dependences 】,再對 【dependences】 做遞歸處理,最終拿到依賴樹。
這跟我們最初設(shè)想的思路基本一致,同時(shí)借助 loader 可以將不同的資源無法解析的問題也一并解決了。看到這里的人或許會有疑問:官方不是已經(jīng)給出了 webpack-bundle-analyzer 這類的工具了嗎? 而且每次構(gòu)建后 stats 中都能拿到文件依賴,為啥不能直接使用呢。
不直接使用的原因很簡單:首先構(gòu)建一次實(shí)在太慢了,特別是有幾十個(gè)頁面存在的情況下,另一個(gè)原因是我只是想拿到資源依賴,我根本不想對整個(gè)前端 repo 進(jìn)行一次構(gòu)建,也不想生成任何 bundle。
有沒有一種工具可以如同 webpack 一樣,既可以使用 loader 找到文件依賴,又不需要生成和壓縮 bundle 呢?
在我們改造 webpack 之前它本來是沒有的,如何改造? 一個(gè) webpack plugin 即可。
webpack 的構(gòu)建流程
在介紹如何改造之前,有必要了解一下 webpack 的模塊處理過程以及整體流程。
模塊的處理過程:
webpack 拿到一個(gè)路徑后,他會依次執(zhí)行 reslove 模塊路徑 -> create 模塊 -> build 模塊 -> parse 模塊等主要流程,每一步的流程的作用如下:
-【 reslove 模塊 】:獲取模塊真實(shí)路徑 -【 create 模塊 】 :創(chuàng)建一個(gè)模塊的 context -【 build 模塊 】 :讀取模塊內(nèi)容 -【 parse 模塊 】 :分析模塊內(nèi)容(主要是找到模塊中的 require 關(guān)鍵詞,將依賴添加到該模塊的依賴數(shù)組中。
最后重復(fù)上述流程
每一個(gè)流程都非常復(fù)雜,以【 reslove 模塊】 這個(gè)流程為例: 處理邏輯主要在 webpack/lib/normalModuleFactory.js 這個(gè)文件中, 整個(gè) reslove 流程會依次執(zhí)行 beforeResolve、 factory、 resolver 以及 afterResolve 這些步驟,每個(gè)步驟對應(yīng)一個(gè) hook ,每個(gè) hook 執(zhí)行的時(shí)候會拿到關(guān)于模塊的描述信息,描述信息會隨著 hook 的執(zhí)行愈發(fā)飽滿,最終獲取到了完整的模塊信息,為接下來的【 create 模塊】以及 【 build 模塊】做好準(zhǔn)備,在下文【具體實(shí)現(xiàn)】中我們會插手這部分流程。
本文不再細(xì)談模塊的處理流程,網(wǎng)上有許多優(yōu)秀的文章可以參考,請大家自由查閱。
webpack 的整體流程:
在筆者看來,webpack 的整體流程分為 4 個(gè)步驟,圖示如下:
實(shí)現(xiàn)依賴分析中我們只需要 webpack 的前 3 個(gè)流程就夠了,下文會給出原因。
解決辦法
前面我們提到 webpack 處理依賴的過程是遞歸分析入口的所有依賴, 由于頁面中的依賴包含相對或者絕對路徑引用的依賴,也包含借助 alias 引用的依賴,也包含 node_modules 中的依賴,既然我們只想拿到 repo 前的資源依賴,對于 node_modules 中的依賴可以直接屏蔽掉,這將使得模塊的遞歸時(shí)間大大縮短,如下圖所示:
由:
變成:
在上面提到的 webpack 4 個(gè)主流程中,在 【step3】 結(jié)束后,webpack 能拿到所有 modules (一次構(gòu)建行為產(chǎn)生的所有的模塊),此時(shí)已經(jīng)足夠我們進(jìn)行依賴分析了,我們直接終止 webpack 的后續(xù)流程,不再進(jìn)行生成 chunk 以及對 chunk 做的合并優(yōu)化等過程。這就達(dá)到了本文題目中目的,【用十分之一的構(gòu)建時(shí)間做一場頁面靜態(tài)資源依賴分析】。
具體實(shí)現(xiàn):
寫一個(gè) 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 這個(gè) hook 的回調(diào)中繼續(xù)監(jiān)聽 normalModuleFactory 的 beforeResolve hook 和 beforeResolve hook。
在 complier.hooks.compilation 這個(gè) hooks 的回調(diào)中繼續(xù)監(jiān)聽 compilation 的 finishModules hook。
插手 beforeResolve 流程
beforeResolve(resolveData, callback) { const { context, contextInfo, request } = resolveData; const { issuer } = contextInfo; }
context 表示為 解析目錄的絕對路徑,一個(gè)頁面的 context 都是一樣的, issuer 翻譯為發(fā)行人,在 webpack 中表示本模塊被依賴的對象路徑,也就指向這個(gè)模塊的來源,request 表示當(dāng)前模塊的的請求路徑。 比如:banner.js 的資源路徑為:/xx/xxx/banner.js , 文件內(nèi)容是這樣的:
// 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" 。
此時(shí),我們只能知道當(dāng)前模塊的來源路徑 issuer 以及它被請求的路徑 request,拿不到當(dāng)前模塊的真實(shí)路徑,還無法將它放入我們的依賴樹中,所以我們不會在 beforeReslove 中處理我們的依賴樹,我們在這一步中只是想屏蔽掉一些我們不想被處理的模塊就可以,比如假設(shè) utils 這個(gè) npm 包里有非常多的小模塊,這些模塊不會被放到依賴樹中,所以對于這些模塊我們選擇直接跳過, 事實(shí)上在 webpack 源碼中有這樣一句:
對于在 beforeResolve 中沒有返回值的模塊會直接 callback ,在 webpack 源碼中 callback 里如果沒有參數(shù),往往意味著流程的提前結(jié)束,在 beforeResolve 中 return callback 也就沒有這個(gè)模塊后續(xù)對 reslove 和 build 流程了,這就實(shí)現(xiàn)了模塊跳過。
為此,我們可以實(shí)現(xiàn)一個(gè) skip 函數(shù),這里提供一個(gè)簡單版本,傳入?yún)?shù)為 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,就可以表示當(dāng)前模塊是可以跳過的。當(dāng)然這個(gè)方法只是一個(gè)簡單版本,實(shí)際上要考慮許多特殊情況,這里不會詳細(xì)給出,感興趣的讀者可以自行實(shí)現(xiàn)。
借助 afterResolve
afterResolve(result, callback) { const { resourceResolveData } = result; const { context:{ issuer }, path } = resourceResolveData; } // 這里添加依賴到依賴樹
webpack 使用 enhanced-resolve 對一個(gè)請求路徑作解析,只需要傳入模塊的 contextInfo, context, request, 即可拿到當(dāng)前模塊的真實(shí)路徑,當(dāng)然前提是需要告知 enhanced-resolve ,這個(gè)模塊所在 repo 的 package.json, webpack 配置中的 alias 信息等等,本文不會敘述 enhanced-resolve 的工作方法,感興趣的讀者可以自行查閱。
總之,在 afterReslove 中我們能夠拿到 webpack 借用 enhanced-resolve 解析過后的模塊路徑了,還有依賴這個(gè)模塊的父模塊路徑,將這兩個(gè)路徑添加到依賴樹中,經(jīng)過簡單的遞歸操作就可以拿到完整的依賴樹了,當(dāng)然在依賴樹中我們可以放置各種信息,比如是否是一個(gè)模塊?是否是一個(gè) js 文件、是否是一個(gè) css 文件,這些都可以實(shí)現(xiàn)。
在 finishModules 里結(jié)束
webpack 官方在 4.30 提交了一次 commit,在這次提交中將 finishModules 這個(gè) SyncHook 轉(zhuǎn)化成了 AsyncSeriesHook , 同時(shí)在 finishModules hook 中加入了一行代碼,如下圖:
這使得我們可以監(jiān)聽 finishModules 這個(gè) hook ,然后在 err 方法里傳入一個(gè)值,這就直接屏蔽掉了后續(xù)的文件合并以及優(yōu)化流程了,當(dāng)然這個(gè)操作比較 hack,也不是官方推薦用法,希望在webpack 5.X 的更新中,官方可以提供更合適的 hook ,讓我們方便跳過某些流程。
一些踩坑
對于在 js 中引用的 css 或者 scss 文件,可以通過尋常的 reslove 流程拿到依賴,但是如果在 css 中使用了 @import 語法,由于 css-loader 會自行處理這些語法,所以它不會走 webpack 本身的 reslove 流程,詳見這個(gè) issue,這里得我們自己在 beforeReslove 中對這部分做額外對處理,比如通過字符串截取的方式去掉 request 中關(guān)于 loder 描述的部分,再通過主動調(diào)用 enhance-resolve 這個(gè)方法實(shí)現(xiàn)對 @import 傳進(jìn)來對模塊做處理,最終拿到正確的路徑。
不同版本的 webpack 一些 hook 的用法和名稱會不一樣,開發(fā)者在處理內(nèi)部流程的時(shí)候要注意。
總結(jié)
本文所闡述的原理并不深奧,主要是挖掘了一個(gè) webpack 的一個(gè)用法,希望能啟發(fā)想要利用 webpack 做更多工具的開發(fā)者多借助 webpack 內(nèi)部原理,最后感謝大家的閱讀,歡迎有興趣的朋友在文章底下評論,給出建議和幫助。
文章版權(quán)歸作者所有,未經(jīng)允許請勿轉(zhuǎn)載,若此文章存在違規(guī)行為,您可以聯(lián)系管理員刪除。
轉(zhuǎn)載請注明本文地址:http://specialneedsforspecialkids.com/yun/6717.html
摘要:無論你使用的是解釋型語言還是編譯型語言,都有一個(gè)共同的部分將源代碼作為純文本解析為抽象語法樹的數(shù)據(jù)結(jié)構(gòu)。和抽象語法樹相對的是具體語法樹,通常稱作分析樹。這是引入字節(jié)碼緩存的原因。 這是專門探索 JavaScript 及其所構(gòu)建的組件的系列文章的第 14 篇。 想閱讀更多優(yōu)質(zhì)文章請猛戳GitHub博客,一年百來篇優(yōu)質(zhì)文章等著你! 如果你錯(cuò)過了前面的章節(jié),可以在這里找到它們: JavaS...
摘要:前端靜態(tài)資源緩存最優(yōu)解以及的陷阱合理的使用緩存可以極大地提高網(wǎng)站的性能優(yōu)勢,還可以節(jié)約帶寬從而降低服務(wù)器成本。此處注意和與第一天請求的版本號不同。既支持版本號類型的靜態(tài)資源緩存方式也支持服務(wù)器重新認(rèn)證的方式。 前端靜態(tài)資源緩存最優(yōu)解以及max-age的陷阱 合理的使用緩存可以極大地提高網(wǎng)站的性能優(yōu)勢,還可以節(jié)約帶寬從而降低服務(wù)器成本。但是很多站點(diǎn)有只弄對了一半或者一半都沒有,如果是這樣...
摘要:目前該功能并未完善,敬請期待。反正每次都會有新的東西補(bǔ)充上去一開始我本來想做的是可以使用微信登陸,也可以使用賬戶郵箱登陸,也可以使用短信登陸的。后來發(fā)現(xiàn)微信登陸要企業(yè)認(rèn)證,做不了。 從零開發(fā)項(xiàng)目概述 最近這一直在復(fù)習(xí)數(shù)據(jù)結(jié)構(gòu)和算法,也就是前面發(fā)出去的排序算法八大基礎(chǔ)排序總結(jié),Java實(shí)現(xiàn)單向鏈表,棧和隊(duì)列就是這么簡單,十道簡單算法題等等... 被虐得不要不要的,即使是非常簡單有時(shí)候繞半...
摘要:發(fā)送請求,處理數(shù)據(jù)。在上面這個(gè)場景中,這類數(shù)據(jù)的結(jié)構(gòu)可能是最常碰到的。整個(gè)過程可以簡化成數(shù)據(jù)的變化引起視圖的變化,和現(xiàn)在很多前端框架數(shù)據(jù)驅(qū)動思想有幾分相似。至此一個(gè)對于頁面的抽象出來的數(shù)據(jù)結(jié)構(gòu)雛形基本完成了。 作者:周周(滬江資深Web前端開發(fā)工程師)本文為原創(chuàng)文章,轉(zhuǎn)載請注明作者及出處 前言 近期在小D十周年活動之際,又看到了一個(gè)自家H5專題夢工廠生成的頁面。 showImg(htt...
閱讀 2101·2023-04-25 20:52
閱讀 2487·2021-09-22 15:22
閱讀 2125·2021-08-09 13:44
閱讀 1770·2019-08-30 13:55
閱讀 2809·2019-08-23 15:42
閱讀 2284·2019-08-23 14:14
閱讀 2877·2019-08-23 13:58
閱讀 3008·2019-08-23 11:49