什麼是前端工程化
作者:Lucas HC
https://www.zhihu.com/question/433854153/answer/1713597311
因爲前端工程化是一個極度寬泛且宏大的概念,我們很難去下一個定義,也無法給出一個樣例來解釋。我試圖從工程(構建)工具對比和一個線上 bug 的處理來側面說明。
工具篇
提到工程化(構建)工具,作爲經驗豐富的前端開發者,相信你能列舉出不同時代的代表:從 Browserify + Gulp 到 Parcel,從 Webpack 到 Rollup,甚至 @尤雨溪編寫的 Vite,相信你也並不陌生。沒錯,前端發展到現在,工程化工具琳琅滿目。但很多工具的實現和設計非常複雜,甚至出現了「面向 webpack 編程」的調侃。
ToolingReport 是由 Chrome core team 核心成員以及業內著名開發者打造的構建工具比對平臺。這個平臺對比了 Webpack v4、Rollup v2、Parcel v2、Browserify + Gulp 在不同維度下的表現,如下圖所示:
測評通過的 test 得分只是一個方面,實際情況也和不同構建工具的設計目標有關。比如,Webpack 的構建主要依賴了插件和 loader,因此它的能力雖然強大,但配置信息較爲煩瑣。而 Parcel 的設計目標之一就是零配置,開箱即用,但是在功能的集成上相對有限。
但從工程化的角度出發,我們還是從上面的分數分析,來看看這些分數評測的維度。這些分數來自以下 6 個維度的評測:
和工程化主題相關的是:這 6 個維度到底是什麼,爲什麼它們能作爲考量指標被選取爲評測參考標準?下面我們逐一進行分析。
Code Splitting,即代碼分割。這意味着在構建打包時,能夠將靜態資源拆分,因此在頁面加載時,實現最合理的按需加載策略。
實際上,Code Splitting 是一個很深的話題。比如:不同模塊間的代碼分割機制能否支持不同的上下文環境(Web worker 環境等特殊上下文情況),如何實現對 Dynamic Import 語法特性的支持,應用配置多入口 / 單入口時是否支持重複模塊的抽取並打包,代碼模塊間是否支持 Living Bindings(如果被依賴的 module 中的值發生了變化,則會映射到所有依賴該值的模塊中)。
總之,Code Splitting 直接決定了前端的靜態資源產出情況,影響着項目應用的性能表現。是前端工程化這顆大樹的一個分支。
Hashing,即對打包資源進行版本信息映射。這個話題背後的重要技術點是最合理地利用緩存機制。我們知道有效的緩存策略將直接影響頁面加載表現,決定用戶體驗。那麼對於前端工程化來說,爲了實現更合理的 hash 機制,工具就需要分析各種打包資源,導出模塊間依賴關係,依據依賴關係上下文決定產出包的哈希值。因爲一個資源的變動,將會引起其依賴下游的關聯資源變動,因此**工程工具進行打包的前提就是對各個模塊依賴關係進行分析,並根據依賴關係,支持開發者自行定義哈希策略。**比如,Webpack 提供的不同類型 hash 的區別:hash/chunkhash/contenthash,這三種 hashing 策略你都瞭解嗎?爲什麼有這三種策略的設計呢?具體我就不展開了。
Output Module Formats,工程輸出的模塊化方式也需要更加靈活,比如開發者可配置 ESM、CommonJS 等規範的構建內容導出。
Transformations,前端工程化離不開編譯 / 轉義過程。比如對 JavaScript 代碼的壓縮、對無用代碼的刪除(DCE)等。這裏需要站在工程化視覺上注意的是,我們在設計構建工具時,對於類似 JSX 的編譯、.vue 文件的編譯,不會內置到工具當中,而是利用 Babel 等社區能力,「無縫融合」到工程化流程裏。工程化工具只做分內的事情,其他擴展能力通過插件化機制來完成,顯然是一個非常工程化的設計。
其他 Importing Modules 以及 Non-JavaScript Resources 我不多說了,雖然這是評測工程化工具的幾個大方向,但每一個都是前端工程化的重要主題。
線上問題篇
這一部分,讓我們以一篇文章《報告老闆,我們的 H5 頁面在 iOS 11 系統上白屏了!》分析,我先簡單梳理和總結一下文章表達的內容,讀者看我總結即可:
-
筆者發現某些機型上出現頁面白屏情況;
-
出現在報錯頁面上的信息非常明顯,即當前瀏覽器不支持
...
擴展運算符; -
出錯的代碼(使用了擴展運算符的代碼)屬於某個公共庫代碼,它沒有使用 Babel 插件進行降級處理,因此線上源代碼出現了
...
擴展運算符。
現在問題找到了,或許直接將出現問題的公共庫代碼用 Babel 進行編譯降級就可以了。在文中環境下,需要在 vue.config.js
中加入對問題公共庫 module-name/library-name
的 Babel 編譯流程:
transpileDependencies: [
'module-name/library-name' // 出現問題的那個庫
],
vue-cli 對 transpileDependencies 也有如下說明:
默認情況下 babel-loader 會忽略所有
node_modules
中的文件。如果你想要通過 Babel 顯式轉譯一個依賴,可以在這個選項中列出來。
按照上述操作,卻得到了新的報錯:Uncaught TypeError: Cannot assign to read only property 'exports' of object '#<Object>'
。
究其原因,module-name/library-name
這個庫對外輸出的是 CommonJS 類型源碼,我們對該庫進行編譯後,項目基礎設施中會通過 babel-transform-runtime 在編譯時增加 helper 代碼,而這些 helper 使用的是 import 引入。最終編譯結果出現了 ESM 包含 CommonJS 的情況,是不會被 Webpack 處理的。
我再次分析下出現的新的問題:
-
plugin-transform-runtime 會根據 sourceType 選擇注入 import 或者 require,sourceType 的默認值是 module,就會默認注入 import;
-
Webpack 不會處理包含 import/export 的文件中的 module.exports 導出,所以需要讓 Babel 自動判斷 sourceType,根據文件內是否存在 import/export 來決定注入什麼樣的代碼。
爲了適配上述問題,Babel 設置了 sourceType
屬性,sourceType:unambiguous
表示 Babel 會根據文件上下文(比如是否含有 import/export)來決定是否按照 ESM 語法處理文件。
這時候就需要配置 Babel 內容了:
module.exports = {
... // 省略的配置
sourceType: 'unambiguous',
... // 省略的配置
}
但是這種做法在工程上並不推薦,上述更改方式對所有編譯文件都生效,但也增加了編譯成本(因爲設置 sourceType:unambiguous
後,編譯時需要做的事情更多),還有個潛在問題:
Unambiguous can be quite useful in contexts where the type is unknown, but it can lead to false matches because it's perfectly valid to have a module file that does not use import/export statements.
翻譯過來,就是說並不是所有的 ESM 模塊(這裏指使用 ESNext 特性的文件)都含有 import/export,因此即便某個待編譯文件屬於 ESM 模塊,也可能被 Babel 錯誤地判斷爲 CommonJS 模塊而引發誤判。
** 基於這一點,一個更合適的做法是:** 只對目標第三方庫 'module-name/library-name'
使用 sourceType:unambiguous
,這時 Babel overrides 屬性就派上用場了:
Allows users to provide an array of options that will be merged into the current configuration one at a time. This feature is best used alongside the "test"/"include"/"exclude" options to provide conditions for which an override should apply.
具體使用方式:
module.exports = {
... // 省略的配置
overrides: [
{ include: './node_modules/module-name/library-name/name.common.js', // 使用的第三方庫
sourceType: 'unambiguous'
}
],
... // 省略的配置
};
至此,這個 “iOS 11 系統白屏” 問題就算告一段落了(你有沒有被各種配置和設計搞得雲裏霧裏?)。
我整理瞭解決路線,如下圖所示:
我們回過頭再來看這個問題,問題其實出現在一個公共庫上,因而前端生態的混亂和複雜也許是更本質的原因,但這都轉嫁爲前端工程化的難點。
我們進一步思考:
-
作爲公共庫,我應該如何構建編譯代碼,讓業務方更有保障地使用?
-
作爲使用者,我應該如何處理第三方公共庫,是否還需要對其進行額外編譯和處理?
被動地發現問題、解決問題只會讓我們被「牽着鼻子走」——這不是我們的目的。感興趣的讀者可以點贊,關注,我會很快輸出更多關於「前端工程化」的內容。
最後的話
對於很多前端工程師來說,你可能配置過 Babel/Webpack,也可能看過一些關於 Babel/Webpack 插件或原理的文章。但我認爲,通過閱讀幾篇 Babel/Webpack 插件編寫甚至 AST 分析的文章並不能讓我們真正掌握前端工程化。這也完全完全不是前端工程化的要義。
「配置工程師」只是我們的起點。作爲前端開發者,你可能會被繁瑣的配置和工具所困擾,自己的終端脆弱無比,出現各種報錯。此時,你可能花費了一天的時間,通過 Google 找到了最終的配置解法;或者通過:
rm -rf node_modules + npm install + npm run dev
規避了問題。但是解決之道卻沒搞清楚,得過且過,今後依然被類似的困境襲擾。
當我們對配置、工具、構建流程、架構設計、生產發佈等環節的各種挑戰和問題能有系統化的思考時,「前端工程化」自然也不會再是一個困惑。
其實很抱歉我無法回答題主這個宏大的問題,我自己也受此困擾,僅以兩個小的細節方面拋磚引玉(閒時我也會持續輸出更多關於「前端工程化」的內容)。
總之,前端既收穫着快速發展,也迎接着批量劣汰;前端技術有着與生俱來的混亂,也有着與之抗衡的規範 —— 這都對前端工程化提出了更高的挑戰。
本文由 Readfog 進行 AMP 轉碼,版權歸原作者所有。
來源:https://mp.weixin.qq.com/s/wODIBDRvujK8GXRGg1XNmQ