一 引言
Webpack 最初是爲了解決前端模塊化以及使用 Node.Js 生態的問題而出現,在過去的 8 年時間裏,Webpack 的能力愈來愈強大。前端
但由於多了打包構建這一層,隨着項目的增加,打包構建速度愈來愈慢,每次啓動都要等待幾十秒甚至幾分鐘,而後啓動一輪構建優化,隨着項目的進一步增大,構建速度又會下降,陷入不斷優化的循環。node
在項目達到必定的規模時,基於 Bundle 的構建優化的收益變得愈來愈有限,沒法實現質的提高。咱們從另外一個角度思考,webpack 之因此慢,主要的緣由仍是在於他將各個資源打包整合在一塊兒造成 bundle,若是咱們不須要 bundle 打包的過程,直接讓瀏覽器去加載對應的資源,咱們將有可能能夠跳出這個循環,實現質的提高。react
在 Bundleless 的架構下,咱們再也不須要構建一個完整的 bundle,同時在修改文件時,瀏覽器也只須要從新加載單個文件便可。因爲沒有了構建這一層咱們將可以實現如下的目標:webpack
- 極快的本地啓動速度,只須要啓動本地服務。
- 極快的代碼編譯速度,每次只須要處理單個文件。
- 項目開發構建的時間複雜度始終爲 O(1),使得項目可以持續保持高效的構建。
- 更加簡單的調試體驗,再也不強依賴 sourcemaps 便可實現穩定的單文件的 debug。
基於以上的可能性 Bundleless 將從新定義前端的本地開發,讓咱們從新找回前端在 10 年前修改單個文件以後,只須要刷新便可即時生效的體驗,同時疊加上前端的 HotModuleReplace 相關技術,咱們能夠把刷新也省去,最終實現保存即生效。es6
實現 Bundleless 一個很重要的基礎能力是模塊的動態加載能力,這一主要的思路會有兩個:web
- System.js 之類的 ES 模塊加載器,好處是具備較高的兼容性。
- 直接利用 Web 標準的 ESModule,面向將來,同時總體架構也更加簡單。
在本地開發過程當中兼容性的影響不是特別大,同時 ESModule 已經覆蓋了超過 90% 的瀏覽器,咱們徹底能夠利用 ESModule 的能力讓瀏覽器自主加載須要的模塊,從而更加低成本同時面向將來實現 Bundleless。chrome
社區中在近一兩年也出現了不少基於 ESModule 的開發工具,如 Vite、Snowpack、es-dev-server 等。本文將主要分享基於瀏覽器的 ESModule 能力實現 Bundless 本地開發的相關思路、核心技術點以及 Vite 的相關實現和在供應鏈 POS 場景下的落地實踐。json
二 從資源加載看 Bundle 和 Bundleless 的不一樣
下面以你們最熟悉的 create-react-app 默認項目爲例,從實際的頁面渲染資源的加載過程對比 Bundle 和 Bundleless 的區別。前端工程化
基於 Webpack 的 bundle 開發模式瀏覽器
上面的圖具體的模塊加載機制能夠簡化爲下圖:
在項目啓動和有文件變化時從新進行打包,這使得項目的啓動和二次構建都須要作較多的事情,相應的耗時也會增加。
基於 ESModule Bundleless 模式
從上圖能夠看到,已經再也不有一個構建好的 bundle、chunk 之類的文件,而是直接加載本地對應的文件。
從上圖能夠看到,在 Bundleless 的機制下,項目的啓動只須要啓動一個服務器承接瀏覽器的請求便可,同時在文件變動時,也只須要額外處理變動的文件便可,其餘文件可直接在緩存中讀取。
對比總結
Bundleless 模式能夠充分利用瀏覽器自主加載的特性,跳過打包的過程,使得咱們能在項目啓動時獲取到極快的啓動速度,在本地更新時只須要從新編譯單個文件。下面將分享如何基於瀏覽器 ESModule 的能力實現 Bundleless 的開發。
三 如何實現 Bundleless
如何使用 ESModule 模塊加載
實現 Bundleless 的第一步是要讓瀏覽器自主加載對應的模塊。
使用 type="module" 開啓 ESModule
利用 import-maps 支持 bare import
分享一個在 chrome 中已經實現了的 import-maps 的標準 ,可讓咱們直接用 import React from 'react' 這樣的寫法,將來咱們能夠利用此能力實現線上的 Bundleless 部署。
以上咱們介紹了瀏覽器中原生的 ESModule 是如何使用的。面向本地開發的場景,咱們只須要啓動一個本地的 devServer 承載瀏覽器的請求映射到對應的本地文件,同時動態地將項目中 import 的資源路徑指向咱們的本地地址,便可讓瀏覽器直接加載本地的文件,好比可使用下面的寫法,將入口 JS 文件直接指向本地的路徑,而後 devServer 再攔截相應的請求返回對應的文件。
如何加載非 JS 的文件資源
經過 ESModule 咱們藉助瀏覽器的能力實現了 JS 的自主加載,但實際的項目代碼中咱們不只僅會 import JS 文件,也會有下面的寫法:
而瀏覽器在處理文件時是依據 Content-Type 的,不關心具體的文件類型,因此咱們須要在瀏覽器發起請求時,將對應的資源轉化爲 ESModule 格式,同時設置對應的 Content-Type 爲 JS,返回給瀏覽器執行,瀏覽器就會按照 JS 的語法進行解析處理,總體的流程可見下圖:
如下是 Vite 的相關實現,在請求返回的過程當中,對不一樣的文件進行動態處理:
如何實現 HotModuleReplace
HotModuleReplace 可以在咱們修改代碼後,不須要刷新頁面,直接在當前場景下生效,結合 Bundleless 極快的生效速度,咱們可以實現幾乎沒有延遲的保存即生效的體驗。對於 React,在 Webpack 場景下目前只能經過使用 react-hot-loader 來實現,但這一塊受限於具體的實現,有一些場景會存在 bug,做者也建議遷移到 React 團隊實現的 react-refresh,而這一塊在 Webpack 中尚未相應的實現。在 Bundleless 場景下,由於咱們的每一個組件都是獨立加載的,因此要集成 react-refresh,咱們只須要在瀏覽器請求返回時在文件的頂部和底部加上相應的腳本便可完成集成。
要完整的實現 HotModuleReplace 會比上面畫得更加複雜,還須要有一套依賴分析機制來判斷當一個文件發生變動以後要替換哪些文件以及是否須要 reload。在 Bundleless 的場景下,由於再也不須要打包爲一個完整的 bundle,同時咱們也能更加靈活地對單個文件進行修改,這一塊相關的實現會更加容易。
如下是在 Vite 中的相關實現:
如何優化大量請求致使頁面加載慢 Bundleless 的模式再也不打包,提高了啓動的速度,但對於一些有較多外部依賴或者自身文件數量較多的模塊,須要發起大量請求才能獲取到所有的資源,這個會下降開發過程當中頁面加載的時間。好比下面是直接在瀏覽器中 import lodash-es 會併發出大量的請求:
在這一塊上咱們能夠作相應的優化,將外部的依賴提早打包成單個文件來減小在開發過程當中因爲外部依賴過多而發起過多的網絡請求。
在 Vite 的啓動流程中有一個 vite optimize 的過程會自動將 package.json 中的 depenencies 藉助 Rollup 打包成 ES6 Module。
提早打包帶來的好處除了可以提高頁面的加載速度,藉助 @rollup/plugin-commonjs 咱們可以將 commonjs 的外部依賴打包爲 ESModule 的形式引入,進一步擴大 Bundleless 的適用範圍。
四 在供應鏈 POS 場景下落地實踐
咱們團隊負責的供應鏈 POS 業務主要可分爲面向建材家居的家裝行業和線下小店的零售行業,在技術架構上採用了各個域 bundle 獨立開發,而後最終藉助底層的 sdk 合併爲一個大的 SPA 的形式。因爲項目的複雜性,在平常開發過程當中,有如下的一些痛點:
- 項目的啓動和耗時相對較長。
- 改動後二次編譯時間長。
- 缺乏穩定的 HMR 能力,開發過程當中須要重複造場景。
- debug 依賴 sourcemaps 能力,有時會出現不穩定的狀況。
基於以上的問題,藉助 Vite 的相關實現,咱們對本地開發環境進行了 Bundleless 的嘗試和落地,在實驗的一些項目中對於本地的開發體驗有了很大的提高。
在啓動以及修改生效的速度上帶來極大的提高
目前已實現單 bundle 維度的開發,打包構建速度:
Vite Bundleless
在啓動單個 bundle 時,Webpack 須要 10s 左右的時間,而基於 Bundleless 的 Vite 只須要 1s 左右,提高 10 倍。
總體的頁面加載時間在 4s 左右,仍然比 Webpack 的打包構建時間要短,同時從上面的視頻中也能夠看到 HMR 的速度達到了毫秒級的響應,實現了基本無感的保存即生效。
不依賴 sourcemap 調試單個文件
落地過程當中遇到的問題和解決
在實際落地過程當中,遇到的問題主要是相關模塊不符合 ESModule 規範以及一些寫法上的標準化:
- 部分模塊沒有 ESModule 的打包。
- less 依賴 node_modules 的寫法的規範。
- jsx 文件後綴規範。
- babel-runtime 的處理。
部分模塊沒有 ESModule 的打包
對於沒有 ESModule 打包輸出或者輸出的錯誤的包,根據不一樣的類型使用不一樣的策略:
- 內部的包:經過升級腳手架,發佈帶有 ESModule 的包的新版本。
- 外部依賴:經過 issue、pull request 等形式,推進了 number-precision 等模塊的升級。
- 同時有一些因爲歷史緣由沒法打出 ESModule 的包能夠藉助 @rollup/plugin-commonjs 打包爲 ESModule。
less 依賴 node_modules 的寫法的規範
JSX 文件後綴規範
Vite 在運行的過程當中會依據文件不一樣的後綴名進行對應的編譯處理,而在 Webpack 模式下咱們一般會將 JSX、JS 等文件都丟給 babel-loader 進行處理,這使得有一些本來是 JSX 的文件沒有寫 JSX 後綴。Vite 只會對 /.(tsx?|jsx)$/ 的文件進行 esbuild 編譯,對於純 JS 會直接跳過 esbuild 的過程。對於這種狀況咱們是逐步將錯誤的原先沒有寫 JSX 的文件遷移爲 JSX 文件。
babel-runtime 的處理
在使用了 babel-plugin-transform-runtime 以後,打包的輸出結果會是下面這樣:
上面所引用的 @babel/runtime/helpers/extends 是 commonjs 的格式沒法直接使用,針對這個狀況,有兩種解法:
1)針對內部本身打包的模塊,能夠在進行 es6 打包時添加 useModules 配置,這樣打包出來的代碼就會是直接引用@babel/runtime/helpers/esm/extends :
2)針對從新打包成本較高的模塊,能夠經過 Vite 的插件機制進行轉換,將 @babel/runtime/helpers 在運行時替換爲 @babel/runtime/helpers/esm 能夠經過 alias 配置實現:
以上是在 Vite 開發環境的遷移過程當中遇到的一些問題和處理的分享,這一塊的更大範圍的落地還在進行中。Bundleless 的落地不只僅是爲了適配 Vite 的開發模式,同時也是面向將來規範各個模塊代碼的過程,將咱們的模塊進行標準的 ESModule 化,在有新的工具和思想出現時能夠用更低成本進行落地。
五 直接使用 Bundleless 進行部署的可行性
受限於網絡請求和瀏覽器的解析速度,對於較大型的應用,bundle 在加載速度上仍是可以帶來較大的收益。V8 在 2018 年也給出了相關性能上的建議:在本地開發和小型的 Web 應用中使用。在今天的場景下,隨着瀏覽器和網絡性能的不斷提高,結合 ServiceWorker 之類的緩存能力,網絡加載的影響和愈來愈小,對於一些不須要考慮兼容性問題的場景能夠進行內部的嘗試,直接部署經過 ESModule 加載的代碼。
六 總結
本文主要分享了 Bundleless 架構下,如何提高前端的研發效率、實現思路以及在具體業務場景下落地實踐。Bundleless 本質上是將原先 Webpack 中模塊依賴解析的工做交給瀏覽器去執行,使得在開發過程當中代碼的轉換變少,極大地提高了開發過程當中的構建速度,同時也能夠更好地利用瀏覽器的相關開發工具。
站在當前的背景下,Web 各個領域 JavaScript/CSS/HTML 相關的標準都已成熟,同時瀏覽器內核也趨於統一,前端工程化的核心重點已逐步遷移到研發提效上,而 Bundleless 的模式可以帶來長效的啓動和 HMR 的速度,是將來的一大發展趨勢。隨着瀏覽器內核和 Web 標準的不斷統一,前端的代碼能夠再也不打包直接運行將成爲可能,這將進一步提升總體的研發效率。
最後很是感謝 ESModule、Vite、Snowpack 等標準和工具的出現,讓前端的開發體驗往前跨了一大步。