Categories
程式開發

前端技術:Webpack 工程化最佳實踐


一、引言

1. 前端構建工具的演變

回想在 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 對於模塊構建的新思路和理念,為了能夠更好的適應其未來的變化,另一方面是在一個好的方案中不斷嘗試,結合項目的基礎設施優化,從而提高效能,保障產品穩定。

2. 本次回顧

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)

二、文件結構

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

1. 對於 dev 日常環境:

1)方便的 debug 和 troubleshooting,有比較強的 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 工程化最佳實踐 1

(圖:webpack 配置文件結構)

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

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

三、基礎/自定義配置

1. 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 的設置,需要重新認真去考慮。

2. OccurrenceOrderPlugin

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

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

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

4. 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)/)

5. alias 和絕對路徑

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

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

6. 大圖片上傳 CDN

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

7. devServer Proxy 的代理能力

去調研這個能力,得益於一次請求層的改造。訴求是希望 Token 不再顯示傳遞,而是通過塞到 Header 去實現。在本地開發的環境,我們通常使用 jsonp 去解決跨域問題,但其本質其實是在網頁中嵌入一段