前端 Webpack 工程化的最佳實踐

前端 Webpack 工程化的最佳實踐

作者 | 阿里文娛前端開發專家 芃蘇

頭圖 | CSDN 下載自視覺中國

前端 Webpack 工程化的最佳实践

引言

前端構建工具的演變

回想在2015-2016年的時候,開發者們開始漸漸把視線從大量使用Task Runner的Grunt工具,轉移到Gulp這種Pipeline形式的工具。Gulp還可以配合上眾多個性化插件(如gulp-streamify),從而使得整個前端的準備工作鏈路,變得清晰易控,如刷新頁面、代碼的編譯和壓縮等等。自動化“流水線”工具取代了很多繁雜的手動工作,可以說,是具有跨時代意義的。之於Webpack而言,其本質是是基於“模塊化”思想的一個“JS預編譯”解決方案,誕生初期,和其相似的方案還有Browserify,和Webpack屬於同門不同派別的還有sea.js或require.js,這二者需“在線依賴”解釋器編譯。

時至今日,多數日常工作接觸的項目,已經可以完全的捨棄Gulp了。但工作中有時還會接觸一些老項目,其中Gulp的使用和維護屢見不鮮。2019年初之時,通過一個老項目(gulp 3.x + webpack 3.x)的技術升級,藉機瞭解了gulp 4.x的動態,又不禁讓人回想起gulp-browserify,和gulp-webpack(五年前發佈,目前改名為webpack-stream)。所以,Webpack做為某一個垂直方向的解決方案,當然可以manaually built-in Gulp中。在拿Webpack“方案”和Gulp類“工具”去做正面比較的時候,需要明晰兩者解決問題的範圍和思路。如今再次回顧歷史,對技術的發展演變順序,能有一個基本客觀的概念。

在2017年的時候,Gulp和Webpack在用戶的使用率和“將繼續使用”的意向上,還不分伯仲。但從《State of Javascript 2019》 中可以看到,Webpack已經完全碾壓了其它工具和類庫,成為了首屈一指被大家廣泛使用、討論的Build Tool。2018年2月25日Webpack 發佈了4.0.0正式版本;那是對不少項目進行了Webpack 4.10.2版本的升級;過年前後,又將部分項目升級到了4.29.0 最新版本。這一系列的“跟進式升級”中,一方面是在不斷融入Webpack對於模塊構建的新思路和理念,為了能夠更好的適應其未來的變化(Webpack 5.x的beta版過應該不久就會面世了),另一方面是在一個好的方案中不斷嘗試,結合項目的基礎設施優化,從而提高效能,保障產品穩定。

▐ 本次回顧

Webpack工具雖說只是前端項目CI流程的一個小部分(構建 build),就它自身而言,所涉及到的Node知識和包依賴管理經驗,是一整塊技能。細節來看,裡面涉及了Webpack自己的包和第三方plugin生態,還要配合恰當的babel、typescript、flow.js、eslint配置等多個生態,去處理Javascript語言本身的編譯/轉譯。以及,正確管理本地靜態資源文件和遠端CDN資源文件路徑(打包配置決定打包結果),涉及到了跨域知識和Node層服務配置、模板配置知識。更進一步還有,NPM眾多包的版本管理等讓人頭疼的問題。其中瑣碎細節數不勝數,當所有第三方工具正確使用的前提下,也許還有些plugin小工具,需要開發者去自研發。知識譜系之大,可見一斑。

本文不描述Webpack Docs使用指南,也不描述第三方插件的使用“指北”。更多的是結合過往項目經驗,記錄實踐得出的使用技巧,也記錄一些走過的彎路所帶來的問題,希望對其它眾多的前端技術人能夠起到一點借鑑作用。 (Package Checking List:React: 16.3.2,Babel: 7.0.0,Webpack: 4.29.0,Node: 11.8.0)

前端 Webpack 工程化的最佳实践

文件結構

在4.x版本中的早期,CLI工具集裡的命令是Webpack主包自帶的,但在Webpack 4.x後期的版本,將webpack-cli作為獨立包剔除出去,需要手動單獨安裝才可以執行tnpm run start這樣的腳本命令。其次,對於開發/日常環境(dev)和預發/生產環境(prod)來說,打包的策略是截然不同的:

1. 對於dev日常環境:

  1. 方便的debug和troubleshootin,有比較強的source mapping;

  2. 希望能夠得到顆粒度較小、且有根據變動代碼針對性的的加載(live reloading/hot module replacement);

  3. 希望可以做一些代理Proxy相關的調試;

  4. 可以方便的根據開發者的情況,對本地的dev-server進行配置等。

2. 對於Prod生產環境:

  1. 通過壓縮Javscript/CSS代碼,獲取更小的文件加載體積;

  2. 通過包的拆解來得到更優的加載策略,從而降低load time;

  3. 比較輕量的source mapping(當然,當你需要一些trace信息做日誌和報警的時候是另外一番情景);

  4. 線上的產品的一些個性訴求(比如,對同一份Javascript代碼也許要匹配不同的樣式文件)等。

3. 通常評估效率維度主要有以下幾個,穩重提到的數據來源主要屬於前三個:

  • 本地開發compile(w/ DLL or NO DLL)

  • 本地開發re-compile(w/ DLL or NO DLL)

  • 本地測試build(webpack analyse分析的重點部分)

  • 雲構建時長 (NO DLL or 配置化OSS支撐DLL)

在Webpack的新版本中,webpack-merge: 4.2.1 這個獨立包的使用,開發者使用webpack.common.js文件對開發和生產環境中的公共部分進行配置,webpack.dev.js針對開發環境,webpack.prod.js針對生產環境。區分後,兩種環境的配置差異,一目瞭然:

前端 Webpack 工程化的最佳实践

(圖:webpack配置文件結構)

關於cz.config.js和flowGlobalVars.js裡面“話題點”頗多,不在此處重點描述。

如果需要DLL配置(在後面的優化部分會重點講),還需要單獨加入一個webpack.dll.js打包的配置文件。當然,dll其實也是一個普通的文件Output,我們可以在webpack.common.js文件中module.exports時,寫兩個區分開。通過這種不是很常見的靈活寫法(Exporting multiple configurations),可以更多的去理解文件的I/O和module模塊的概念。

前端 Webpack 工程化的最佳实践

基礎/自定義配置

CommonsChunkPlugin被取代

被移入到了webpack.optimization.splitChunks中。有關拆包切分和顆粒度控制,這個其實從Webpack的層面已經為我們做了很多優化,自身也是有一套基礎默認的優化策略的。類比來看,React生態裡面diff算法本身也是有策略機制的,更多的優化,使用者可以在這個對象裡面加入回調方法,自己去細化控制。

這裡需要特別注意的是cacheGroups,當不明確哪些內容需要被cache時,或者是顆粒度不好把控時,這樣的切分會給我們帶來非常多的冗餘文件。下面的代碼中,定義了一個vendors對象,那麼我們的output文件(不包含chunksFiles)的每一個都會生成一個cache文件。加入output的有app.bundle.js和polyfill.bundle.js,一旦加入這個vendors對象,打包的時候會額外的生成兩份文件,分別是vendors-app.js和vendors-polyfill.js。雖然不用擔心這兩個文件內容會重新打包代碼進去,裡面只是放一些cache索引,但這兩個文件如果在不確定要用他們來做什麼的時候,cacheGroups的設置,需要重新認真去考慮。

OccurrenceOrderPlugin

本身不在是一個webpack類下面的構造器,而是被重新命名(之前的名稱因為單詞拼寫錯誤了),然後放入到新的位置,調用起來需要重新去書寫:new webpack.optimize.OccurrenceOrderPlugin。

terser(默認的內置壓縮工具包)

webpack.optimization.minimizer的新版本中,default built-in的工具已經由舊有的uglifyJS變成了terserJS,舊的uglify已經被depreacted處理,相信不久之後的狀態就會變成legacy,新的terser更好的性能,對ES6+的語法支持的更多,也同時兼容了babel 7的生態,同步其它第三方庫代碼壓縮後的訴求。目前我在使用的是terser-webpack-plugin,和普通的terser配置的參數上有一些差異,需要自己手動引入(官方文檔推薦)。

module.rules.exclude[0]

module.rules.exclude[0]的文件地址書寫,要求更加嚴格(4.11.0以後的版本)。

以往我們在對module.rules做配置時,有些文件不希望被遍歷到,那麼我們通過exclude這個參數配置,將其跳過,有時候會使用'src/contianer/xx.jsx'這樣的寫法,如果是多個path索引,那就放到一個Array中就好。但這種寫法,在新版本中是不被允許的,我們只能使用path.resolve或/regExp/的寫法去聲明文件路徑地址。(Bonus Basic Tips,如何用正則書寫並集和特定路徑,如我希望include所有src加上一個指定的npm包: /(src\\/.*)|(node_modules\\/.*@ali\\/lark-components)/)

alias和絕對路徑

webpack在打包的時候,通常需要對文件的路徑去做查找、搜索,它需要明確知道文件的引用位置和引用關係,從而能夠完整的知道整個映射mapping關係。減少這方面的開銷,我們可以考慮去配置alias,從而以絕對路徑的寫法代替大量相對路徑寫法。好處的話,一方面是幫助webpack更快的去定位文件位置,另一方面書寫起來,也不再用被輸入 '../../*' 還是 '../../../*' 而困擾。

  • Webstorm尋找絕對路徑:在配置裡面對webpack配置項加入webpack文件路徑就好,Webstorm IDE會自己找到對應的alias關係。

  • VSCode尋找絕對路徑:插件層面沒有發現太好的辦法,如果項目正在使用typescript,可以在tsconfig.json裡面配置相關的編譯項,可以達到和上面Webstorm同樣的效果。

大圖片上傳CDN

上傳CDN後可以大幅減小包體積。另外,webpack也不需要再去關注那些圖片的文件索引路徑了。項目稍微大一些,本地圖片5Mb ~ 10Mb的情況非常普遍,亟待優化。

devServer Proxy的代理能力

去調研這個能力,得益於一次請求層的改造。訴求是希望Token不再顯示傳遞,而是通過塞到Header去實現。在本地開發的環境,我們通常使用jsonp去解決跨域問題,但其本質其實是在網頁中嵌入一段<script>,自然也就不能寫入Header信息,這個和我們的初衷並不相符,無法滿足訴求。所以對於這樣的跨域問題,我們通過幾個簡單的參數配置,在請求發起和請求返回的兩端,分別做了代理配置,從而“欺騙”了“源Origin”,得以解決本地開發的跨域問題:

<code>devServer: {
// ...
headers: {
'Access-Control-Allow-Origin': '*', // CORS
},
proxy: { // for ajax cors
'/h5/ajaxObj': {
target: 'http://xxx.xxx.xxx.com',
onProxyReq: (proxyReq) => {
proxyReq.setHeader('Origin', 'http://xxx.xxx.com');
},
onProxyRes: (proxyRes) => { // …},
},
},
},/<code>
前端 Webpack 工程化的最佳实践

優化性能 by Node / Happypack

基礎配置和需要的自定義配置已經有了,整個項目的構建時間有可能還是非常不理想的,當前本文提及的測試項目,大概有57s的時間,還是有很多地方沒有補足的,可優化的空間非常大。

第一步可以先關注下Node版本,經過測試,是對整體速度可以至少提升30%的事情,尤其是在Node V8版本到V10的時候,以下是之前在另一個項目做技術改造時記錄到的數據:

<table><tbody>

Node版本

v 8.x

v 10.x

compile

32s - 36s

26s

re-compile

8s - 9s

4s

/<tbody>/<table>

但是這次,在把項目直接升級到了 v 11.x 後發現,有帶node-sass的項目編譯構建都崩潰了。才意識到,node-sass的版本也需要相應的版本更新。也測試了Babel v 6.x 到 v 7.x 版本的升級效果,本來以為babel的大版本升級會帶來顯著的編譯速度提高,實際上卻並不理想(基本可以忽略不計)。

打算開啟多線程能力,去處理模塊化打包裡面那些本是單線程執行的 loaders 們的工作。Happypack的提升效率對整個項目的首次編譯而言,效果是20%左右,比較明顯。加入Happypack能力的時候,有兩點需要注意:

  • 其對file-loader和url-loader的支持不好,可以考慮不加,畢竟我們項目裡面圖片類(最好上傳CDN)的和非常規格式的文件只是小部分;

  • 這次也嘗試了把ts-loader加入到多線程中,但是也出現了不少編譯問題。大概率懷疑是我個人的配置問題,但過程中去看issues見到了不少ts-loader和ts生態依賴兼容性的問題。目前這個項目.ts只是少數文件,作為一種嘗試,大部分文件還都是.jsx和.js,所以針對ts也先不加入Happypack能力了。

前端 Webpack 工程化的最佳实践

優化性能 by DLL/ Optimization

首先需要藉助一些工具來進行分析,如:webpack-bundle-analyzer ,通過這個工具我們可以對整個構建(用於生產,Webpack Analyse針對的build過程,不是compile)過程和結果進行數據、圖形上的分析,從而得知問題具體出現在了哪裡。進而得知DLL所需拆分的內容是什麼。以下內容是在第一次分析時得出的:

前端 Webpack 工程化的最佳实践

這個圖片的 3532 modules和62 chunks可以看到具體的模塊以及chunks劃分後的情況。更加直觀的我們來看下面這張圖,可以看到Parsed的尺寸,入口文件(7.09MB)和主chunk(2.04MB,主要是一些首頁就需要加載的node_module)的大小都很誇張,並且node_modules裡面的包基本上是一一打包、整整齊齊:

前端 Webpack 工程化的最佳实践

有了這些分析結果,對應解法的思路就很清晰了:首先要抽離常用的node_modules(這是DLL的意義),然後要逐個分析,把不被經常用到的node_module們(僅被某些頁面使用,不具有公共特點)也抽出去。

對於React項目中的React、React-Dom、React-router、Redux等,還要一些第三方比較大的庫,比如antv或者G2相關的,也要進行DLL抽離了:

前端 Webpack 工程化的最佳实践

modules數量由3532降低到1500,編譯時間縮短了三倍

在做了上述DLL的抽離後其實效果已經很明顯了,進一步的提升空間,可以對optimization進行了配置(用法詳見官方文檔):

  • terser

  • chunksAll

  • no mimimizer sourceMap

前端 Webpack 工程化的最佳实践

結尾

本文大概主要介紹了一些工具衍變背景、基礎的組織結構和自定義配置,以及如何通過分析工具去來做性能優化,其中很多小的細節沒辦法一一提到,比如我們看到加載的chunk都是hash值的時候,如何能夠辨別是什麼組件呢:解法是可以在路由處通過配置moduleName的方式去做:

<code> => import(/* webpackChunkName: "chunkNameDisplay" */'../containers/UserList/chunkNameDisplay')/<code>

諸如此類,實在繁多。隨著Webpack 5.x版本的陸續發佈和眾多團隊使用之後,也許很多東西又會有大的改變。並且各種框架的集成已經越來越豐富,更多的解放程序員在工程化維護上的雙手,我們關注工程化的演進,看看Webpack生態會給我們帶來什麼樣的驚喜。


分享到:


相關文章: