檢視精彩視訊
自我介紹
我是來自 RichLab 花呗借呗前端團隊的同學。在公司大家喊我玄寂,生活中大家稱呼我 pigcan 或者豬罐頭。除了是一個程式員,我現在也在嘗試做一名 YouTuber 和 up 主,也在微信公衆号中分享我的生活,我自己的方式踐行快樂工作,認真生活。
體感案例

首先為了讓大家有更好的體感,我們先來看一個案例。這個案例是使用 code mirror 加 Antd tab 元件加 Gravity 做的一個實時預覽。大家可以通過這個 gif 能看到,我變更 js 檔案或者樣式檔案的時候,在右側這個預覽區域可以進行實時的更新,這部分的能力是完全由浏覽器作為支撐提供出來的,并不依賴任何本地 server 或者遠端 server 的能力。
在有了這個體感之後,大家可能會更容易了解我之後講的内容。
文章提綱
接下來我會從 5 個方面切入,來談一談基于浏覽器的實時建構探索之路。
- 首先是背景,從曆史來看建構工具每次發生大變更時,都和前端的技術風潮息息相關。而 2019 年前端界發生的變化,也可以說是促使我做這個技術探索的原因。
- 有了這些變化,通常情況下現有的技術架構就可能會出現不滿足現狀的情形,這就是機遇了,這也就是我想要說的第二部分,這些變化會給我們的建構帶來哪些機遇,而面對這些機遇,我們在技術上又會有哪些挑戰。
- 第三點我會來談一下在面對這些機遇和挑戰時,我們在技術上所做的選擇,也就是我們如何來架構整個技術方案。
- 第四點我會基于前面所說的技術架構,談一談需要克服的技術難點,主要是要抛一些我的解決思路。
- 第五點也是最後一點,我會暢想一下這個技術方案可能的未來,其實更多的是我對它的期待。
背景
時間回到 2011 年,那會兒我們前端一直在強調複用性,基于複用性的考慮,我們會把所有的檔案盡可能的按照功能次元進行拆分,拆的越小越好,這種追求我稱它為粒子化。粒子化的結果是工程的檔案會非常非常碎,是以那個時候的建構工具,更多的思路是化零為整,典型工具有 Grunt 和 Gulp。
随着粒子化時代的到來,到 13 年左右很快新的問題出現了,這時的問題在我看來主要集中在了兩個部分:第一個是,傳統的拼接腳本的方式開始不能滿足子產品化的需求,因為子產品之間存在依賴關系,再者還有動态化載入的需求;第二個是那麼多功能子產品被劃分出來了,把劃分後的子產品放哪裡是一個問題,最初 NPM 是并沒有向前端子產品開放的。是以接下來便出現了子產品加載器,和包管理之戰。這場戰役讓我們的前端子產品規範變得五花八門,最後好在所有的包落在了 NPM 了。是以這個時候的建構工具更多的是抹平子產品規範,典型工具 Webpack 的出現意義很大一部分就在于此(當然在這個過程中,其實還出現了各種基于加載器的定向建構工具和包管理,這裡就先不談了)。
那時間再次回到 2019 年,我們聽到了不一樣的聲音,這些聲音都在對抗 bundler 的理念。
比較典型的有兩篇文章:
為什麼會有這些聲音,這些聲音背後的原因是什麼?一方面是因為新的技術标準的出現,另外一方面也來源于日益陡峭的學習曲線。
現在,要運作一個前端項目,我們通常需要知道:
- 前端建構的概念
- 要知道在琳琅滿目的打包工具中做合理選擇
- 要知道如何安裝開發環境,如何執行建構,如何執行調試
- 要知道如何配置 - Webpack、Webpack Loaders and plugins etc.
- 要知道如何寫插件 - Babel APIs、Webpack APIs etc.
- 如何調試插件
- 如何解決依賴更新 - Babel 5 -> 6 -> 7, Webpack 1 -> 2 -> 3 -> 4 -> 5
反正就是一個字——“南”!
再來看看我們的包管理。以 CRA 為例,隻是為了運作一個 React 應用,我們居然還需要附加如此複雜的依賴。
在網上也有一些調侃,前端的依賴比黑洞造成的時間扭曲還要大。
回過頭再來看,2019 的趨勢是什麼,相信大家都感覺到了「雲」這個詞,我們很多的流程都在上雲。
那面向上雲的這種場景,我們如此複雜的 bundler 和包管理是否符合這種趨勢呢?
歸根結底,其實是要探讨一個問題:
前端資源的加載和分發是不是還會有更好的形式?
而對這個問題的回答,我覺得是有空間的——正是這種笃定,才有了接下來的内容。
機遇和挑戰
現狀
在上一小節中我們已經談到了 2019 年不管是 pro / low code 都在朝着上雲的趨勢在變化,那應對這些變化,我們先來看看現有的一些平台,他們對于建構的态度是什麼。
從這些平台中我們可以總結出三種态度:
- 隻做編輯器或者畫闆
- 做編輯器或者畫闆并且提供了一個限制性的研發環境
- 做編輯器或者畫闆并且提供了一個完全開放的研發環境
總結下這三種态度,本質上是使用了兩種技術方案:
- 容器技術
- 基于浏覽器的加載政策
最終其實可以總結為:
- 把服務端的能力進行輸出。這種方案的優勢是服務端擁有和本地研發環境一緻化的環境;缺點是即時性較差、效率較差、無法離線、成本高昂。
- 把用戶端的能力釋放出來。這種方案的優勢是無服務端依賴、即時性、高效率、可離線運作;但缺點也比較明顯,所有能力建設都必須圍繞着浏覽器技術
雲時代的來臨,我認為配套的建構也來到了十字路口,到底是繼續維持現有的技術架構走下去,還是說另辟蹊徑,尋找一條更加輕薄的方式來配合上雲。
Bundless
我們再回過來看看,2019 年為什麼在社群能釋放出這些聲音來(Luke Jackson - Don’t Build That App!、Fred K. Schott - A Future Without Webpack),為什麼會有人敢說,我們可以有一個沒有 webpack 的未來,為什麼 Bundless 的想法能夠成立,支撐他們這些說法的技術依據到底是什麼。
歸納總結下:
- 使用子產品加載器,在運作時進行檔案分析,進而擷取依賴,完成樹結構的梳理,然後對樹結構開始編譯
比較典型的産品有:systemjs 0.21.x & JSPM 1.x 、stackblitz 、codesandbox
- 使用 Native-Module,即在浏覽器中直接加載 ES-Module 的代碼
比較典型的産品有:systemjs >= 3.x & JSPM 2.x 、@pika/web
再看了這些産品和技術實作後,我内心其實非常笃定,我覺得機會來了,未來肯定會是輕薄的方式來配合上雲,隻是這一塊目前還沒有人來專心突破這些點。
是以我覺得未來肯定是 雲 + Browser Based Bundless + Web NPM,這就是 Gravity 這套技術方案出現的背景了。
Gravity 的挑戰
所有的挑戰其實來源于我們從 nodejs 抽出來之後,在浏覽器内的适配問題。
可以羅列下我們會碰到的問題:
- nodejs 檔案系統
- nodejs 檔案 resolve 算法
- nodejs 内置子產品
- 任意子產品格式的加載
- 多媒體檔案
- 單一檔案多種編譯方式
- 緩存政策
- 包管理
- ……
總結下其實是四個方面的問題:
- 如何設計資源檔案的加載器
- 如何設計資源檔案的編譯體系
- 如何設計浏覽器端的檔案系統
- 如何設計浏覽器端的包管理
Gravity 架構大圖
架構圖
從這個圖中其實可以歸納出,我們就是在解決上面提到四個問題,即:
名詞解釋
這裡會提幾個名詞,友善之後大家了解。
Transpiler: 代碼 A 轉換為代碼 B 轉換器
Preset: 是一份建構描述集合,該集合包含了子產品加載器檔案加載的描述,轉換器的描述,插件的描述等。
Ruleset: 具體一個檔案應該被怎麼樣的 transpilers 來轉換。
這裡可以衍生出來說一說為什麼要設計 Preset 的概念。在文章的最前面我提到了現在要建構一個前端的項目學習曲線非常陡峭。在社群我們能看到兩種解法:
- create-react-app: 它把 react 應用開發所需要的所有細節都封裝在了這個庫裡面,對使用者隻是暴露了一些基本的入口,比如啟動應用,那它的好處是為着這一類 react 應用開發者提供了極緻的體驗,降低了整個學習曲線。但缺點也比較明顯就是 CRA 并不支援自定義配置,如果你需要個性化,那不好意思,你隻能 eject,一旦 eject 之後後續所有的配置就交給應用開發者,後續便不能再融入回 CRA 的閉環了。
- @vue/cli: 它和 CRA 一樣做了配置封裝,但是和 CRA 不一樣的地方是,它自身提供了一些個性化的能力,允許使用者修改一些參數。
通過以上兩者不難發現,他們都在做一件事情:解耦應用開發者和工具開發者。
再回到 Preset,我的角色是工具專家,提供一系列的底層能力,而 Preset 則是垂直業務專家,他們基于我的底層能力去做的業務抽象,然後把業務輸出為一個 preset。而真正的應用使用者其實無需感覺這部分的内容,對他們而言或許隻需要知道一些擴充配置。
Gravity 的消費鍊
在 Gravity的設計中,Core 層其實沒有耦合任何的具體業務邏輯(這個邏輯指的是,比如 react 應用要怎麼執行,vue 應用要怎麼執行等),Core 層簡單來講,它是實作浏覽器實時建構的事件流注冊、分發、執行的集合。而具體的業務場景,比如 React,Vue,小程式等則是通過具體的 Preset 來實作整合。而我們的 Preset 會再交給對應的垂直場景的載體,比如 WebIDE 等。
專題深入
專題一:插件機制
事先我們來看一看 Gravity 是如何運作的,上圖隻是一個流程示意,但也能說明一下流程上的設計。注意看我們在 Plugin 類上定義了一些事件,而這些事件是允許被使用者訂閱的,那 Gravity 在執行時,會對這些事件先嘗試綁定。在進入到相關的流程時,會分發這些事件,訂閱了該事件的訂閱者,就會在第一時間收到資訊。舉例來說,Plugin 中的 Code 描述了如何來擷取代碼的方式,而在 Gravity Core 的整個生命周期中,會調用 fetch-data 去分發 Code 事件,如果說使用者訂閱了該事件,那麼就會馬上響應去執行使用者定義的擷取代碼的方式,并得到代碼進而告訴核心。
是以不難看出,Gravity 本質上是事件流機制,它的核心流程就是将插件連接配接起來。
既然如此,其實我們要解決的重點就是:
- 如何進行事件編排
- 如何保證事件執行的有序性
- 如何進行事件的訂閱和消息的分發
說到這裡不知道大家是不是有一種似曾聽聞的感覺,沒錯,其實這些思路都是來自于 webpack 的設計理念,webpack 是由一堆插件來驅動的,而背後的驅動這些插件的底層能力,來源于一個名叫 Tapable 的庫。
Tapable 這個庫我個人非常非常非常喜歡。原因在于它解決了很多我們在處理事件時會碰到的問題,比如有序性。另外要做一個插件系統的設計其實很簡單,但後果是對使用者會有額外的負擔來學習如何書寫,是以我選擇 Tapable 來做還有另外很重要的一個原因,使用者可以繼續延續 webpack 插件寫法到 Gravity 中來。
這裡我羅列一下 Tapable 所擁有的能力。并用僞代碼的方式為例來講一講我們在核心層如何定義一個插件(定義可被訂閱的事件),業務專家如何來使用這個自定義插件(訂閱該事件),以及我們在核心層如何來執行這個插件(綁定,分發)。
定義插件:
自定義插件:
核心層綁定和分發:
是以Gravity-Core 重在事件的編排和分發,Plugin 則重在事件的申明,而 Custom plugins 則是訂閱這些事件來達到個性化的目的。
專題二:如何實作編譯鍊
在講如何實作前,我們再回過來看下 Ruleset,在架構大圖小節中我說明了下,Ruleset 是用來描述一個檔案應該被怎麼樣的 transpilers 來轉換。而 Ruleset 的生成其實是依賴于 preset 中 rule 的配置,這一點,其實 Gravity 和 webpack 是一緻的,這種設計原因有兩點:1. 使用者可以沿用 webpack 的 rule 配置習慣到 Gravity 中來;2. 我們甚至可以複用一些現有的 webpack loader,或者說讓改造量變得更小。
在這裡我們以小程式中的 axml 檔案為例,假設現在有一個 index.axml 需要被被編譯,此時會通過 Preset 中 rule 描述,最終被拆解為一個 ruleset,在這個 set 資訊中我們可以擷取到 index.axml 檔案需要經過怎麼樣的轉換流程(也可以了解為該 index.axml 檔案需要什麼 transpiler 來進行編譯)。該示例中我們可以看到,index.axml 需要經過一層 appx 小程式編譯後再把對應的結果交給 babel 進行編譯,而 babel 編譯的結果再交給下級的消費鍊路。
暫時抛開複雜的業務層實作,我們想一想要實作這條串行的編譯鍊路的本質是什麼。相信大家都能找到這個答案,答案就是如何保證事件的有序性。既然又是事件,是不是我們又可以回過來看一看 Tapable,沒錯,在 Tapable 中就有這樣一個 hook - AsyncSeriesWaterfallHook,異步串行,上一個回調函數的傳回的内容可以作為下一回調函數的參數。說到這是不是很多問題就迎刃而解了。沒錯,那麼在 Tapable 中實作編譯鍊是不是就被簡化為如何基于 ruleset 動态建立 AsyncSeriesWaterfallHook 事件,以及如何分發的問題。
檔案系統和包管理
BrowserFS
如果我們在浏覽器中沒有檔案系統的支撐,其實可以想象本地的檔案的依賴将無法被解析出來(即無法完成 resolve 過程),是以實作浏覽器内的檔案系統是實作浏覽器編譯的前提條件。這裡幸運的是 John Vilk 前輩有一個項目叫做 BrowserFS,這個庫在浏覽器内實作了一個檔案系統,同時這個檔案系統模拟了 Nodejs 檔案系統的 API,這樣的好處就是,我們所有的 resolve 算法就可以在浏覽器内實作了。同時這個庫最棒的一點是提出了 backends 的概念。這個概念的背後是,我們可以自定義檔案的存儲和讀取過程,這樣檔案系統的概念和思路一下子就被打開了,因為這個檔案系統其實本質上并不局限于本地。
在這裡我們可以大概看下如何使用 BFS。
有了檔案系統我們再來想一想前端不可分割的一個部分,包管理。
思路一:浏覽器内實作 NPM
這個思路是最容易想到的,通常做法是我們會拉取包資訊,然後對包進行依賴分析,然後安裝對應的包,最後把安裝的包内容存儲到對應的檔案系統,編譯器會對這些檔案進行具體的編譯,最後把編譯結果存在檔案系統裡面。浏覽器加執行檔案時,子產品加載器會加載這些編譯後的檔案。思路很通暢。但是這種方式的問題是原模原樣照搬了 npm 到浏覽器中,複雜度還是很高。
缺點:
- 首次很慢
- 存儲量大
- 依賴 NPM Scripts 的包得不到解決
思路二:服務化 NPM
這一塊的思路其實來自于對我影響最大的兩篇文章
非常精彩,我也寫過一些文章來分析他們。但是 stackblitz 和 codesandbox 在 npm 思路上各自都有一些缺陷,比如 stackblitz 的資源分發形式,codesandbox 的服務端緩存政策。
服務化的 NPM 本質是基于網絡的本地檔案系統。怎麼來了解這句話呢?我們來舉個例子,一起來構想一下如何基于 unpkg / jsdelivr 做一個的檔案系統。
假設我們現在依賴 lodash 這個庫,那麼在我們對接的檔案系統裡面會發一個
請求給遠端的 unpkg,該請求可以擷取到完整的目錄結構(資料結構),那麼在得到這份資料後,我們便可以初始化一個檔案系統了,因為我們可以通過接口傳回的資料完整的知道目錄内會有什麼,以及這個檔案的尺寸,雖然沒有内容。是以此時檔案系統内包含了一整個完整的樹結構。假設此時我們通過 resolve 發現,我們的檔案中确切依賴了一個檔案是 lodash/upperCase.js,這個檔案系統事先需要做的事情是先在本地檔案數裡面找下是否存在 upperCase.js,這裡毫無疑問是存在的,因為我們在這個
接口中能找到對應的 upperCase.js 這個檔案,能确定肯定是在檔案系統裡面是有标記的但是如之前所說 meta 資訊隻是一種标記,他是沒有内容的,那麼接下來我們就會去往 unpkg 伺服器上那固定的檔案,發送請求擷取該檔案
内容,至此我們的基于 unpkg / jsdelivr 的檔案系統就設計好了。
是以服務化 NPM 的關鍵是:
需要我們抽象
- 如何設計包管理依賴的下發邏輯
需要我們包裝
- 如何把這個下發邏輯橋接到對應的檔案系統
注明:下發邏輯指的是我們按什麼規則去下發使用者的 dependencies。
服務化 NPM 的要點是:
- 建立一個下發政策,比如基于項目次元的 deps,依賴的下發是基于依賴包的入口檔案分析所産生的依賴檔案鍊
- 補充在預設下發政策不滿足需求時,如何建立動态下發的過程
- 依賴下發的資料結構,如何展現依賴關系,父子關系等
- 如何快速分析依賴關系
- 如何緩存依賴關系
- 如何更新緩存的依賴關系
- 如何把以上這些資訊橋接到我們的檔案系統
未來
提到 Gravity 的未來,其實更多的是我對他的憧憬,總結一下可以是三個要點。
PVC
- Pipelined 流水線化
垂直業務場景所對應的 Preset 的産出,可以按着某個流程,用極少的成本自由組合一下就可以使用。
- Visualized 可視化
所有搭建 Preset 、以及 Preset 内配置都可以通過可視化方式露出。
- Clouds 雲化
Gravity 服務化。
以上,就是我在本屆 D2 分享的「基于浏覽器的實時建構探索之路」話題的全部内容,希望能為你帶來一些幫助。
D2 分享 PPT 位址關注「Alibaba F2E」
把握阿裡巴巴前端新動向