下面是來自知乎對前端工程化的小小解釋
我認為,「什么是前端工程化」——這是一個很好的問題,但同時也是一個非常「務虛」的問題。
因為前端工程化是一個極度寬泛且宏大的概念,我們很難去下一個定義,也無法給出一個樣例來解釋。我試圖從工程(構建)工具對比和一個線上 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 個維度的評測:
- Code Splitting
- Hashing
- Importing Modules
- Non-JavaScript Resources
- Output Module Formats
- Transformations

和工程化主題相關的是:這 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
規避了問題。但是解決之道卻沒搞清楚,得過且過,今后依然被類似的困境襲擾。
當我們對配置、工具、構建流程、架構設計、生產發布等環節的各種挑戰和問題能有系統化的思考時,「前端工程化」自然也不會再是一個困惑。
其實很抱歉我無法回答題主這個宏大的問題,我自己也受此困擾,僅以兩個小的細節方面拋磚引玉(閑時我也會持續輸出更多關於「前端工程化」的內容)。
總之,前端既收獲着快速發展,也迎接着批量劣汰;前端技術有着與生俱來的混亂,也有着與之抗衡的規范 —— 這都對前端工程化提出了更高的挑戰。
前端工程化的發展
webpack工具的原理
babel工具原理
webpack/babel配置詳解