淺談瀏覽器實時構建

前言

在遙遠的幾個月前,還在上家公司的時候,老闆突發奇想,想要搞個代碼片段平台,類似於 snipit,實現代碼片段的復用。本身這個需求並不難實現——簡單的前端界面 + 簡單的 node CURD,搞定收工,下班回家。

但是,在實際使用中,發現了一個使用痛點——沒有在線調試功能,所有代碼只能 copy 到本地,在本地進行調試。本着發現痛點就要解決痛點的指導思想,我當時思考了一段時間,希望尋找一個合適的解決方案來完美的解決這個痛點。總體來說,分為兩種方案:

  1. 服務端構建方案:最常見也是最成熟的解決方案,每次修改代碼時,將代碼傳給服務端,由服務端構建後,最終進行代碼替換。由於是在服務端進行打包,所以被稱為服務端構建方案。在前幾年,客戶端涉及到構建問題一般採用服務端構建方案。
  2. 瀏覽器實時構建方案:當前前端最熱門的方向之一,在瀏覽器端進行代碼構建,基本不需要與服務端交互(具體分不同細分方案),所以被稱為瀏覽器實時構建。目前各個大廠的 Web IDE 基本採用這種方案。

相比於服務端構建方案,瀏覽器實時構建方案的優勢在於:即時、高效以及最寶貴的——可離線運行(前提是做了合適的緩存方案)。

最終,出於各種因素,最終我選擇了瀏覽器實時構建方案。

瀏覽器實時構建方案

瀏覽器實時構建是最近兩年前端的熱門方向,所以也湧現了一大批成熟的解決方案。

個人總結為:bundle 方案以及 unbundle 方案兩種。

bundle 方案(類 CodeSandBox 方案)

大部分投入生產環境的瀏覽器實時構建方案都採用了該方案,該方案基本採用了 codesandbox-client 的方案,所以我一般稱之為類 CodeSandBox 方案。

bundle 方案的核心在於在瀏覽器上實現一個打包工具,如 webpack,配合 indexDB 進行本地文件存儲。當然,不僅僅是這麼簡單,由於在瀏覽器端做構建工作效率相對較低,所以需要大量的性能優化,比如 CodeSandBox 在瀏覽器端實現了一個線程池,當進行構建時,從線程池中取出線程,從而實現多線程打包。

總的來說,bundle 方案依然沒有跳出構建的思路,當項目較為複雜時,依然會出現構建工具遇到的那個問題——慢。針對 bundle 方案的缺點,業界推出了 unbundle 方案(當然最主要還得感謝瀏覽器的支持)。

unbundle方案

unbundle 方案的出現,需要感謝 ESM。啥是 ESM 呢?ESM 全稱為 ECMAScript modules,即瀏覽器原生支持模塊化規範。效果如下:

<script type="module">
  // 引用別的模塊
  import { util } from './utils.js';
	// 使用別的模塊中的函數
  util()
</script>

當瀏覽器解析到 import 語句時,會像開發態一樣自動引入對應模塊。得益於 ESM,我們可以不經過構建即可直接在瀏覽器端運行模塊化代碼。

相比於 bundle 方案,unbundle 方案在各種意義上都快了許多,特別當項目複雜度上來以後,這個差異將會異常明顯;另一方面,unbundle 不需要在瀏覽器端實現一個打包工具,對於快速實現瀏覽器實時構建也有着很大的意義。

由於 unbundle 的各種優點,最終我選擇了使用 unbundle 方案來實現瀏覽器實時構建。

實現一個單文件瀏覽器實時構建

接下來,到了實戰環節,我們來嘗試實現一個單文件瀏覽器實時構建。

該系統分為兩部分:

  • 客戶端(瀏覽器實時構建)
  • 服務端(依賴服務器)

客戶端

首先是 UI 部分,UI 簡單的分成三部分:代碼編輯器(如:monaco editor),按鈕(用於執行實時構建函數)以及構建結果展示部分,主要負責調試代碼並展示結果。

當點擊按鈕後,觸發構建函數,開始執行構建邏輯,整個構建流程主要分為以下幾步:

  1. 將 Vue 文件拆分成 template、script 以及 style
  2. 對 script 進行處理
    • 初始化 es-module-lexer,解析出所有導入語句(import)
    • 重寫 import,將請求地址指向依賴服務器
    • 開始生成最終在瀏覽器中執行的代碼,將重寫後的 script 寫入
  3. 解析模板,生成 render 函數,將 render 函數掛載到 script 上
  4. 解析 style,更新樣式有兩種方案
  5. 最終,新建一個 script,去除原有的 script,插入最新的 script

總體思路其實和 vite 非常像,也可以認為是在瀏覽器端實現了一個小 vite。詳細的代碼可以參考 vite 源碼,在此不再贅述。

服務端

服務端的功能非常簡單,即接收請求,返回對應的依賴打包文件。由於 ESM 只支持 ESM 規範,所以,需要將各種模塊規範(主要指的是 commonjs)統一轉為 ESM。

當依賴服務器接收到客戶端的請求時,具體工作流程如下:

  1. 服務端安裝依賴
  2. 通過 es-build 將依賴轉為 esm
  3. 將依賴返回給客戶端

未來的路

通過上面的思路,我們就可以實現一個最簡單的單文件瀏覽器實時構建了,但是其實有非常多的問題,比如:

  1. 如何進行依賴版本控制?
  2. sourceMap 的問題怎麼解決?
  3. 服務端可以繼續優化嗎?
  4. 可以純瀏覽器實時構建(即沒有服務端)嗎?
  5. 如何實現多文件瀏覽器實時構建?

本來我很想直接來一句:這些問題留作課後思考[doge],但是怕被打,所以接下來就聊下我對這幾個問題的看法吧。

  1. 用戶可以採用注釋標註版本,解析時,使用正則或 babel 解析即可,思路類似於 magic comments
  2. 多次映射即可解決
  3. 依賴服務器優化的主要思路可以放在緩存管理上,試想,如果不做緩存處理,每次用戶請求都需要重新下載、轉換、打包依賴,當用戶量增大時,服務器的壓力該有多大?
  4. 依賴請求使用 unpkg,不過,這個相當於從個人寫的依賴服務器轉換到了公司提供的 unpkg 服務器,只不過理論上確實不用自己寫依賴服務器了[doge]。
  5. 如果能夠實現多文件瀏覽器實時構建,可以解決非常多單文件瀏覽器構建實現的問題,最直接的就是可以解決依賴版本問題。多文件瀏覽器實時構建複雜度相對於單文件瀏覽器實時構建高了不止一個數量級,所以這個問題我思考了非常久。最常見的方案就是之前說過的,CodeSandBox 方案,但是複雜度實在太高。所以要想降低複雜度,還需要使用 unbundle 方案。經過思考,我採用了 service worker,service worker 可以攔截所有請求,攔截後,判斷是否請求的是本地文件,如果是本地文件,發消息給本地,如果是第三方依賴,請求依賴服務器。但是 service worker 有個致命的問題,第一次請求不能攔截,只有第二次之後的請求才能攔截(畢竟 service worker 本質是個緩存)。

以上就是我的思路,如果大佬們有不同的思路,歡迎一起探討。

最後

按照慣例,發個招聘帖:

位元組跳動招人啦,HC 巨多,北上廣深杭皆有坑位。

團隊詳情見://webinfra.org/about

提供內推及面試輔導服務,目前我內推的幾個同學皆通過了面試,歡迎諮詢~

暫時不看機會,之後有想法來位元組試試的同學,也一樣歡迎你加入 😁。

有意者可發送郵件到 [email protected]