淺談瀏覽器實時構建

格子熊發表於2021-02-12

前言

在遙遠的幾個月前,還在上家公司的時候,老闆突發奇想,想要搞個程式碼片段平臺,類似於 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 巨多,北上廣深杭皆有坑位。

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

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

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

有意者可傳送郵件到 hubin.gzx@bytedance.com

相關文章