字節(jié)是如何落地微前端的
?? 加個關注,后續(xù)上新不錯過~
本文內(nèi)提及的 Garfish 微前端解決方案已開源:https://github.com/modern-js-dev/garfish(目前的 Garfish 作為字節(jié)跳動各部門應用最廣泛的微前端解決方案已經(jīng)服務超過 100+ 前端團隊,400+ 項目),另外字節(jié)跳動的現(xiàn)代 Web 工程體系即將開源(Modern.js),深度集成 Garfish 提供了對微前端的原生支持,提供更開箱即用的能力,敬請期待!
微前端的出現(xiàn)的背景和意義
微前端是什么:微前端是一種類似于微服務的架構,是一種由獨立交付的多個前端應用組成整體的架構風格,將前端應用分解成一些更小、更簡單的能夠獨立開發(fā)、測試、部署的應用,而在用戶看來仍然是內(nèi)聚的單個產(chǎn)品。
微前端誕生在兩個大的背景下,在提倡擁抱變化的前端社區(qū)可以看到新的框架、技術、概念層出不窮,并且隨著 Web 標準的演進,前端應用已經(jīng)具備更好的性能、更快的開發(fā)效率。但隨著而來的是應用的復雜程度更高、涉及的團隊規(guī)模更廣、更高的性能要求,應用復雜度已經(jīng)成為阻塞業(yè)務發(fā)展的重要瓶頸。
微前端就是誕生于 Web 應用日益復雜化的場景中,因為隨著網(wǎng)絡速度、計算機硬件水平的提升和 Web 標準的演進,過去 Web 應用用戶體驗遠不如傳統(tǒng)的應用軟件時代已逐漸遠去,兩者之間在用戶體驗上的差距不斷縮減,并且由于 Web 應用開發(fā)速度快、用完即走等特性,導致的一個最終結果就是「能用 Web 技術實現(xiàn)的應用,最終都會通過 Web 來實現(xiàn)」。在近幾年涌現(xiàn)了一大批之前只能在傳統(tǒng) PC 軟件中才能看到的優(yōu)秀產(chǎn)品,例如:Photoshop、Web Office、Web IDE。盡管隨著 Web 標準的演進,前端工程化也在不斷演變,從模塊化到組件化在到現(xiàn)在的工程化,但在面對跨團隊大規(guī)模開發(fā)、跨團隊企業(yè)級應用協(xié)作,現(xiàn)有的分治設計模式仍然顯得有心無力。

大規(guī)模 Web 應用的困局
盡管 Web 應用的復雜度和參與人數(shù)以爆炸式的增長速度,但卻沒有一種新的架構模式來解決現(xiàn)有的困境,并同時兼顧 DX(developer experience)和 UX(user experience)。
以字節(jié)跳動內(nèi)「研發(fā)中臺」舉例,在研發(fā)日常工作中需要使用非常多的研發(fā)系統(tǒng),例如:代碼管理、代碼構建、域名管理、應用發(fā)布、CDN 資源管理、對象存儲等。站在整個公司研發(fā)的角度考慮,最好的產(chǎn)品形態(tài)就是將所有的研發(fā)系統(tǒng)都放置同一個產(chǎn)品內(nèi),用戶是無法感知他在使用不同的產(chǎn)品,對于用戶而言就是單個產(chǎn)品不存割裂感,也不需要去學習多個平臺,僅僅需要學習和了解字節(jié)跳動內(nèi)的「研發(fā)中臺」即可。
在字節(jié)跳動內(nèi)這一類應用隨處可見,由于字節(jié)跳動內(nèi)存在大量業(yè)務線,每一條業(yè)務線都會誕生大量的中臺系統(tǒng),并且還在指數(shù)增長,以字節(jié)跳動內(nèi)電商業(yè)務舉例,對于電商運營的日常工作來說,其實與研發(fā)日常工作一樣,圍繞在:商品、商家、品牌、風控、營銷等工作上,那么對于電商運營來說怎么樣才最高效的電商運營系統(tǒng)呢,由于整個系統(tǒng)涉及范圍較廣,在實際的研發(fā)過程中必然會以功能或業(yè)務需求垂直的切分成更小的子系統(tǒng),切分成各種小系統(tǒng)后盡管由于分治的設計理念提升了開發(fā)者體驗,但是一定程度上降低了用戶體驗。那能否以一種新的架構模式,既保開發(fā)者體驗,又能提升用戶體驗呢。
傳統(tǒng) Web 應用的利與弊
這里簡單分析一下傳統(tǒng) Web 應用在開發(fā)大規(guī)模應用和涉及多研發(fā)團隊協(xié)作時遇到的一些困境,以上面案例中的「電商運營平臺」舉例,對于電商運營而言商品、商家、品牌等都是電商運營平臺能力的一部分,而不是獨立之間的孤島。若以傳統(tǒng)的前端研發(fā)模式進行開發(fā),那么此時有兩種項目設計策略:
將平臺內(nèi)多個系統(tǒng)放置同一個代碼倉庫維護 ,采用 SPA(Single-page Application) 單頁應用模式 將系統(tǒng)分為多個倉庫維護,在首頁聚合所有平臺的入口,采用 MPA(Multi-page Application)多頁應用模式
若采用多個系統(tǒng)放置同一個項目內(nèi)維護:
優(yōu)勢: 更好的性能 具備局部更新,無縫的用戶體驗 提前預加載用戶下一頁的內(nèi)容 統(tǒng)一的權限管控、統(tǒng)一的 Open API 開發(fā)能力 更好的代碼復用,基礎庫復用 統(tǒng)一的運營管理能力 不同系統(tǒng)可以很好的通信 SPA 應用特有優(yōu)勢: 劣勢: 代碼權限管控問題 項目構建時間長 需求發(fā)布相互阻塞 代碼 commit 混亂、分支混亂 技術體系要求統(tǒng)一 無法同時灰度多條產(chǎn)品功能 代碼回滾相互影響 錯誤監(jiān)控無法細粒度拆分
采用方案一的劣勢非常明顯,在日常開發(fā)中研發(fā):代碼構建半小時以上、發(fā)布需求時被需求阻塞、無法局部灰度局部升級、項目遇到問題時回滾影響其他業(yè)務、無法快速引進新的技術體系提高生產(chǎn)力,項目的迭代和維護對于研發(fā)同學而言無疑是噩夢。
盡管降低了開發(fā)體驗,如果對項目整體的代碼拆分,懶加載控制得當,其實對于使用平臺的用戶而言體驗卻是提升的,這一切都歸因于 SPA 應用帶來的優(yōu)勢,SPA 應用跳轉頁面時無需刷新整個頁面,路由變化時僅更新局部,不用讓用戶產(chǎn)生在 MPA 應用切換時整個頁面刷新帶來的抖動感而降低體驗,并且由于頁面不刷新的特性可以極大程度的復用頁面間的資源,降低切換頁面時帶來的性能損耗,用戶也不會感知他在使用不同平臺。
若采用拆分成多個倉庫維護
優(yōu)勢 可以以項目維度拆分代碼,解決權限管控問題 僅構建本項目代碼,構建速度快 可以使用不同的技術體系 不存在同一個倉庫維護時的 commit 混亂和分支混亂等問題 功能灰度互不影響 劣勢 用戶在使用時體驗割裂,會在不同平臺間跳轉,無法達到 SPA 應用帶來的用戶體驗 只能以頁面維度拆分,無法拆分至區(qū)塊部分,只能以業(yè)務為維度劃分 多系統(tǒng)同灰度策略困難 公共包基礎庫重復加載 不同系統(tǒng)間不可以直接通信 公共部分只能每個系統(tǒng)獨立實現(xiàn),同一運維通知困難 產(chǎn)品權限無法進行統(tǒng)一收斂
采用方案二在一定程度上提升了開發(fā)體驗,但卻降低了用戶體驗,研發(fā)在日常開發(fā)工作中需要使用大量的平臺,但是卻需要跳轉到不同的平臺上進行日常的研發(fā)工作,整體使用體驗較差。體驗較差的原因在于將由于通過項目維度拆分了整體「研發(fā)中臺」這樣的一個產(chǎn)品,使各個產(chǎn)品之間是獨立的孤島,系統(tǒng)間相互跳轉都是傳統(tǒng)意義上的 MPA,跳轉需要重新加載整個頁面的資源,除了性能是遠不如 SPA 應用的并且應用間是沒法直接通信,這就進一步增強了用戶在使用產(chǎn)品時的割裂感。
背景和意義總結
通過以上兩個場景案例,其實可以發(fā)現(xiàn)由于 Web 應用在逐步取代傳統(tǒng)的 PC 軟件時,大規(guī)模 Web 應用在面對高復雜度和涉及團隊成員廣下無法同時保證 DX 和 UX 的困境。傳統(tǒng)的分而治之的策略已經(jīng)無法應對現(xiàn)代 Web 應用的復雜性,因此衍生出了微前端這樣一種新的架構模式,與后端微服務相同,它同樣是延續(xù)了分而治之的設計模式,不過卻以全新的方法來實現(xiàn)。
微前端解決方案
上一節(jié)總結了微前端出現(xiàn)的背景和意義,并且了解了兩種傳統(tǒng) Web 應用的研發(fā)模式:SPA(Single-page Application)、MPA(Multi-page Application)在涉及人員廣和項目復雜度高的場景下帶來的劣勢,那么期望能有一種新的架構能同時具備 SPA 和 MPA 兩種架構優(yōu)勢,并同時提升 ?DX(developer experience)和 UX(user experience)呢?
那在理想的情況下,期望能達到,將一個復雜的單體應用以功能或業(yè)務需求垂直的切分成更小的子系統(tǒng),并且能夠達到以下能力:
子系統(tǒng)間的開發(fā)、發(fā)布從空間上完成隔離 子系統(tǒng)可以使用不同的技術體系 子系統(tǒng)間可以完成基礎庫的代碼復用 子系統(tǒng)間可以快速完成通信 子系統(tǒng)間需求迭代互不阻塞 子應用可以增量升級 子系統(tǒng)可以走向同一個灰度版本控制 提供集中子系統(tǒng)權限管控 用戶使用體驗整個系統(tǒng)是一個單一的產(chǎn)品,而不是彼此的孤島 項目的監(jiān)控可以細化到到子系統(tǒng)
那么基于上面理想情況,如何從零設計一套全新的架構用于解決現(xiàn)代 Web 應用在面對企業(yè)級系統(tǒng)遇到的困境呢。
微前端的整體架構
那么如何提供一套既具備 SPA 的用戶體驗,又具備 MPA 應用帶來的靈活性,并且可以實現(xiàn)應用間同灰度,監(jiān)控也可以細化到子系統(tǒng)的解決方案呢?目前在字節(jié)跳動內(nèi)應用的微前端解決方案「Garfish」就是這樣的一套方案 ,該解決方案主要分為三層:部署側、框架運行時、調(diào)試工具,采用的是 SPA 的架構。
解決方案整體架構


微前端部署平臺
部署平臺作為微前端研發(fā)流程中重要的一環(huán),主要提供了:微前端的服務發(fā)現(xiàn)、服務注冊、子應用版本控制、多個子應用間同灰度、增量升級子應用、下發(fā)子應用信息列表,分析子應用依賴信息提取公共基礎庫降低不同應用的依賴重復加載。
用于解決微前端中子應用的獨立部署、版本控制和子應用信息管理,通過 Serverless 平臺提供的接口或在渲染服務中下發(fā)主應用的 HTML 內(nèi)容中包含子應用列表信息,列表中包括了子應用的詳細信息例如:應用 id、激活路徑、依賴信息、入口資源等信息,并通過對于子應用的公共依賴進行分析,下發(fā)子應用的公共依賴,在運行時獲取到子應用的信息后注冊給框架,然后在主應用上控制子應用進行渲染和銷毀。

微前端運行時
Why not iframe
談到微前端繞不開的話題就是為什么不適用 iframe 作為承載微前端子應用的容器,其實從瀏覽器原生的方案來說,iframe 不從體驗角度上來看幾乎是最可靠的微前端方案了,主應用通過iframe 來加載子應用,iframe 自帶的樣式、環(huán)境隔離機制使得它具備天然的沙盒機制,但也是由于它的隔離性導致其并不適合作為加載子應用的加載器,iframe 的特性不僅會導致用戶體驗的下降,也會在研發(fā)在日常工作中造成較多困擾,以下總結了 iframe 作為子應用的一些劣勢:
使用iframe 會大幅增加內(nèi)存和計算資源,因為 iframe 內(nèi)所承載的頁面需要一個全新并且完整的文檔環(huán)境 iframe 與上層應用并非同一個文檔上下文導致 主應用劫持快捷鍵操作 事件無法冒泡頂層,針對整個應用統(tǒng)一處理時效 事件冒泡不穿透到主文檔樹上,焦點在子應用時,事件無法傳遞上一個文檔流 跳轉路徑無法與上層文檔同步,刷新丟失路由狀態(tài) iframe 內(nèi)元素會被限制在文檔樹中,視窗寬高限制問題 iframe 登錄態(tài)無法共享,子應用需要重新登錄 iframe 在禁用三方 cookie 時,iframe 平臺服務不可用 iframe 應用加載失敗,內(nèi)容發(fā)生錯誤主應用無法感知 難以計算出 iframe 作為頁面一部分時的性能情況 無法預加載緩存 iframe 內(nèi)容 無法共享基礎庫進一步減少包體積 事件通信繁瑣且限制多
基于 SPA 的微前端架構
盡管難以將 iframe 作為微前端應用的加載器,但是卻可以參考其設計思想,一個傳統(tǒng)的 iframe 加載文檔的能力可以分為四層:文檔的加載能力、HTML 的渲染、執(zhí)行 JavaScript、隔離樣式和 JavaScript 運行環(huán)境。那么微前端庫的基礎能力也可以參考其設計思想。
從設計層面采取的是基座+子應用分治的概念,部署平臺負責進行服務發(fā)現(xiàn)和服務注冊,將注冊的應用列表信息下發(fā)至基座,通過基座來動態(tài)控制子系統(tǒng)的渲染和銷毀,并提供集中式的模式來完成應用間的通信和應用的公共依賴管理,因此 Garfish 在 Runtime 層面主要提供了以下四個核心能力:
加載器(Loader) HTML 入口類型,拆解 HTML Dom、Script、Style JS 入口類型,提供基礎 Dom 容器 負責注冊平臺側提供的應用列表 負責加載和解析子應用入口資源 預加載能力 解析子應用導出內(nèi)容 沙箱隔離(Sandbox) 提供代碼執(zhí)行能力,收集執(zhí)行代碼時存在的副作用 提供銷毀收集副作用的能力 支持沙箱多實例,收集不同實例的副作用 路由托管(Router) 解決不同應用間的路由不同步問題 提供路由劫持能力,在主應用上管控子應用路由 提供路由驅動能力來拼裝完整的平臺的能力 子應用通信(Store) 建立通信橋梁 提供共享機制

應用生命周期
整個微前端子應用的生命周期基本可以總結為:
渲染階段 若入口類型為 HTML 類型,將開始解析和拆解子應用資源 若入口類型為 JS,創(chuàng)建子應用的掛點 DOM 主應用通過路由驅動或手動掛載的方式觸發(fā)子應用渲染 開始加載應用的資源內(nèi)容,并初始化子應用的沙箱運行時環(huán)境 判斷入口類型 將子應用存在”副作用“(對當前頁面可能產(chǎn)生影響的內(nèi)容)交由沙箱處理 開始渲染子應用的 DOM 樹 觸發(fā)子應用的渲染 Hook 銷毀階段 若路由變化離開子應用的激活范圍或主動觸發(fā)銷毀函數(shù),觸發(fā)應用的銷毀 清除應用在渲染時和運行時產(chǎn)生的副作用 移除子應用的 DOM 元素

加載器的設計
加載器的整體設計理念其實與 React-loadable 非常類似,具備以下能力:
異步加載組件資源 可以預加載資源 可以緩存組件資源 緩存組件實例
與組件不同的是微前端作為一種能夠將單體應用拆解成多個子應用的架構模式,不同于組件,這些被拆分出去的子應用最好的研發(fā)模式是在開發(fā)、測試、部署都與宿主環(huán)境分離,子應用本身應具備自治能力,那么此時就與 iframe 提供的能力非常類似,iframe 通過加載 HTML 文檔的形式加載整個子應用的資源,那么子應用本身就可作為一個獨立站點,天然具備獨立開發(fā)、測試的能力。因此 Garfish 的加載器提供了兩種應用入口類型:HTML 類型和 JS 入口類型,但需要注意的是 Garfish 并非像 iframe 一樣將其分為了另一個文檔流,而是將其與主應用作為同一個文檔流處理,用以規(guī)避其不再同一個文檔流帶來的體驗感割裂問題。
由于 HTML 入口類型天然具備獨立、開發(fā)、測試的特性,在微前端整體架構設計中,對于跨團隊協(xié)作而言,最好的研發(fā)模式是能降低其溝通成本,而降低溝通成本的最好方式是不溝通,所以一般項目類型都盡可能的推薦用戶使用 HTML 的入口類型。
那么針對 HTML 入口類型的加載器需要做一些什么呢,下面是一張瀏覽器的渲染過程圖:

針對瀏覽器的渲染過程也可將其分為:HTML 文本下載、 HTML 拆解為語法樹、拆解語法樹中具備”副作用的內(nèi)容“(對當前頁面可能產(chǎn)生影響的內(nèi)容)如 Script、Style、Link 并交由沙箱處理進行后渲染,與一般的子應用不同的是需要子應用提供 provider,provider 中包含了子應用渲染和銷毀的生命周期,這兩個 Hook 可以應用緩存模式中進一步增強應用的渲染速度和性能。

沙箱的設計
為什么需要沙箱
其實在過去的 Web 應用中是很少提及到沙箱這一概念的,因為組件的開發(fā)一般都會由研發(fā)通過研發(fā)規(guī)范來盡可能的去避免組件對當前應用環(huán)境造成副作用,諸如:組件渲染后添加了定時器、全局變量、滾動事件、全局樣式并且在組件銷毀后會及時的清除子應用對當前環(huán)境產(chǎn)生的副作用。
與組件完全不同的是微前端是由多個獨立運行的應用組成的架構風格,這些系統(tǒng)可能分別來自不同的技術體系。項目的開發(fā)、測試從空間和時間上都是分離的,由于沒有 iframe 一樣原生能力的隔離很難應用間不發(fā)生沖突,這些沖突可能會導致應用發(fā)生異常、報錯、甚至不可用等狀態(tài)。
以 Webpack4 JsonpFunction 為例
在 Webpack5 中提供了一個重要的功能就是 Module Federation,隨著 Webpack 5 推出 Module Federation ,與 Webpack 4 發(fā)生變化的一個重要配置就是 JsonpFunction 屬性變?yōu)榱?chunkLoadingGlobal,并且由原來的默認值 webpackJsonp 變成了默認使用 output.library 名稱或者上下文中的 package.json 的 包名稱(package name)作為唯一值(webpack.js.org/issues/3940)。
為什么會發(fā)生這個轉變呢,如果了解過 Webpack 構建產(chǎn)物的一定會知道 Webpack 通過全局變量存儲了分 chunk 后的產(chǎn)物,用于解決分包 chunk 的加載問題。由于 Webpack 5 引入 Module Federation 頁面中可能會同時存在兩個以上的 Webpack 構建產(chǎn)物,如果還是通過是通過同一個變量存儲要加載的 chunk ,必然會造成產(chǎn)物之間的互相影響。
通過 Webpack 4 到 Webpack 5 支持 Module Federation 之后可以發(fā)現(xiàn),在一個基礎庫尚未考慮默認兼容多實例的場景下,貿(mào)然將其作為多實例使用很可能會造成應用無法按照預期運行,更為嚴重的是你以為其正常運行了其實應用已經(jīng)發(fā)生了嚴重的內(nèi)存泄漏或不可預知的情況,倘若將 Webpack 構建產(chǎn)物的應用多次動態(tài)的在頁面中運行,將會發(fā)現(xiàn)已經(jīng)造成嚴重的內(nèi)存泄漏,因為 Webpack 會增量的向全局存儲 chunk 的變量上掛載模塊以及依賴信息,簡單來說就是每次執(zhí)行 Webpack 構建的子應用代碼都會向 webpackJsonp 數(shù)組 push 大量的數(shù)據(jù),最終造成內(nèi)存泄漏,直至頁面崩潰。
沙箱的核心能力
為了保證應用能夠穩(wěn)定的運行且互不影響,需要提供安全的運行環(huán)境,能夠有效地隔離、收集、清除應用在運行期間所產(chǎn)生的副作用,那應用運行期間主要會產(chǎn)生哪些副作用呢,可以將其分為以下幾類:全局變量、全局事件、定時器、網(wǎng)絡請求、localStorage、Style 樣式、DOM 元素。
在 Garfish Runtime 中的沙箱主要能力也是圍繞在這一塊的能力建設上,針對子應用可能產(chǎn)生的副作用類型主要分為兩類,一類是:靜態(tài)副作用、另一類則是:動態(tài)副作用。這里靜態(tài)副作用和動態(tài)副作用分別指的是什么呢,靜態(tài)副作用指的是 HTML 中靜態(tài)標簽內(nèi)容例如:Script 標簽、Style 標簽、Link 標簽,這些內(nèi)容屬于在 HTML 文檔流中就包含的,另外一部分副作用屬于動態(tài)副作用,動態(tài)副作用指的是由 JavaScript 動態(tài)創(chuàng)建出來的,例如 JavaScript 可以動態(tài)創(chuàng)建 Style、動態(tài)創(chuàng)建 Script、動態(tài)創(chuàng)建 Link、動態(tài)執(zhí)行代碼、動態(tài)添加 DOM 元素、添加全局變量、添加定時器、網(wǎng)絡請求、localStorage 等對當前頁面產(chǎn)生副作用的內(nèi)容。
針對子應用的靜態(tài)副作用的收集比較簡單,Loader 核心模塊上已經(jīng)提供了子應用入口資源類型的分析和拆解,可以從子應用 DOM 樹中輕松拆解獲取副作用內(nèi)容,那么對于靜態(tài)副作用已經(jīng)可以完成有效的收集、清除,但是尚未具備隔離的能力。動態(tài)創(chuàng)建的副作用都是通過 JavaScript 來動態(tài)創(chuàng)建的,需要收集到 JavaScript 運行時產(chǎn)生的副作用,并提供副作用的隔離和銷毀能力。
沙箱設計的兩種思路
在 Garfish 微前端中,如何有效收集、隔離、清除應用的副作用是保障應用能夠平穩(wěn)運行的核心能力之一。沙箱的主要能力也在于能夠捕獲動態(tài)創(chuàng)建的副作用,對應用的副作用進行隔離和清除。
那么如何能夠有效的捕獲到動態(tài)創(chuàng)建的副作用、收集、并隔離呢?目前 Garfish 提供了兩種設計思路,一種是快照模式,另外一種是 VM 模式。
快照沙箱
顧名思義,在應用運行前通過快照的模式來保存當前執(zhí)行環(huán)境,在應用銷毀后恢復回應用之前的執(zhí)行環(huán)境,用于實現(xiàn)應用間副作用的隔離和清除。類似于 “SL 大法”,通過 save 存儲環(huán)境,通過 load 加載環(huán)境的模式。
代碼實現(xiàn)思路

核心設計思想簡述:
針對每一種副作用提供一個 Patch 類,這個類需要提供 save 和 load 兩個方法 Save 對應著該副作用的環(huán)境快照存儲,Load 對應著銷毀該副作用的銷毀恢復環(huán)境 并且針對每一種 Patch 還可以存儲其在運行期間發(fā)生的變化,在優(yōu)化場景時并不用所有代碼,僅恢復執(zhí)行環(huán)境即可
VM 沙箱
通過快照沙箱的最簡化的核心實現(xiàn)后可以發(fā)現(xiàn),它的設計理念依賴于整個代碼的執(zhí)行屬于線性的過程,即:存儲執(zhí)行環(huán)境=>執(zhí)行具備副作用的代碼=>恢復執(zhí)行環(huán)境,但在實際的場景中對于應用的劃分并以頁面為維度劃分,同一個頁面可能存在多個應用,所以它的執(zhí)行順序并非線性,可能同時存在多個快照沙箱的實例環(huán)境,也就是快照沙箱多實例,以下面代碼舉例:

通過上面的代碼可以發(fā)現(xiàn),在同時運行多個快照沙箱實例時,在代碼執(zhí)行順序非線性的場景下,并不能有效的收集和處理應用的副作用,也基于此快照沙箱無法使用在非線性呢多實例的場景中,因此也進一步推出了 VM(virtual machine) 沙箱。
維基百科關于 VM ?的解釋:在計算機科學中的體系結構里,是指一種特殊的軟件,可以在計算機平臺和終端用戶之間創(chuàng)建一種環(huán)境,而終端用戶則是基于虛擬機這個軟件所創(chuàng)建的環(huán)境來操作其它軟件。虛擬機(VM)是計算機系統(tǒng)的仿真器,通過軟件模擬具有完整硬件系統(tǒng)功能的、運行在一個完全隔離環(huán)境中的完整計算機系統(tǒng),能提供物理計算機的功能。
在 Node 中也提供了 VM 模塊,不過不過不同于傳統(tǒng)的 VM,它并不具備虛擬機那么強的隔離性,并沒有從模擬完整的硬件系統(tǒng),僅僅將指定代碼放置了特定的上下文中編譯并執(zhí)行代碼,所以它無法用于不可信來源的代碼。
參考 Node 中 VM 模塊的設計,以及 JavaScript 詞法作用域 的特性,可以設計出 VM 沙箱,不過與傳統(tǒng)的 VM 差異也同樣存在,它并能執(zhí)行不可信的代碼,因為它的隔離能力僅限于將其運行在一個指定的上下文環(huán)境中。
從而得出以下設計


隔離環(huán)境需要哪些上下文
針對副作用的類型:全局變量、全局事件、定時器、網(wǎng)絡請求、localStorage、Style 樣式、DOM 元素,分別提供了全新的執(zhí)行上下文:
Window 用于隔離全局環(huán)境 document 收集 DOM 副作用 收集 Style 副作用,進行處理 收集 Script,繼續(xù)放置沙箱處理 用于捕獲動態(tài)創(chuàng)建的 DOM 節(jié)點、Style、Script timeout、interval 處理定時器 localstorage 隔離 localStorage listener 收集全局事件
新的執(zhí)行上下文哪里來
新的執(zhí)行上下文有兩個來源,
來源于當前環(huán)境 來源于 iframe 的執(zhí)行環(huán)境
由于 iframe 創(chuàng)建后需要需要較多的內(nèi)存資源和計算資源,而微前端中的 VM 沙箱并不需要一個完全的執(zhí)行上下文,所以可以基于當前環(huán)境。

快照沙箱和 VM 沙箱能力對比

路由系統(tǒng)的設計
在于現(xiàn)代 MVC 的設計思想,前端框架的設計思想也一直在發(fā)生變更,現(xiàn)代 Web 前端框架提供的最經(jīng)典的能力莫過于將 MVC 中的 Constroller 變?yōu)榱?Router,目前幾乎主流的前端框架都支持路由驅動視圖,僅提供一個 Router Map 路由表,無需關注控制任何路由狀態(tài)即可完成跳轉后的路由更新。
通過微前端出現(xiàn)的背景和意義,可以了解到微前端主要是用于解決:應用增量升級、多技術體系并存、構建大規(guī)模企業(yè)級 Web 應用而誕生的。那么在基于 SPA 的微前端架構中也可以了解到,目前微前端主要是采用應用分而治之 + 動態(tài)加載 + SPA 應用的模式來解決大規(guī)模應用帶來的一系列問題。在以組件為顆粒度的 SPA 應用中組件內(nèi)部是不需要關心路由的,但是在微前端中主要通過應用維度來拆分,那么拆分的應用也可能是一個獨立的 SPA 應用,那么此時主應用與子應用的關系如何編排呢?
微前端應用中理想的路由調(diào)度
假設存在一個 Garfish 站點,這個站點它是由主應用+三個子應用構成,主應用的 basename 為 /demo,并存在三個 Tab 分別指向跳轉至不同的應用,理想的路由效果:
在點擊 vue-app Tab,跳轉至 /demo/vue-app路由后,分別激活vue-app下,為 Vue 類型的 A 應用和 B 應用,并激活 A 應用和 B 應用中的 Home 組件點擊 React-app Tab 進入到 /demo/react-app路由后,分別激活react-app下,為 React 類型的 C 應用,并激活 C 應用的 Home 組件在激活 C 應用的基礎上,點擊 Detail 按鈕,跳轉至 /demo/react-app/detail,并激活 C 應用的 detail 組件。點擊瀏覽器返回按鈕展示,跳轉 /demo/react-app/detail,并激活 C 應用的 Home 組件,至此完成瀏覽器的基本路由跳轉能力。

不考慮任何路由處理的場景
假設存在一個 Garfish 站點,這個站點它是由主應用+一個子應用構成。由于 Garfish 采用的是 ?SPA 架構,子應用與主應用所處于同一個執(zhí)行上下文,子應用的路由原樣反應在主應用上。

那么此時分別跳轉到:/home、/detail路由會發(fā)現(xiàn)哪些問題呢?
假定跳轉的方法可以同時觸發(fā)主子應用路由更新,主應用路由和子應用路由會同時發(fā)生搶占情況,后渲染的組件會覆蓋先渲染的路由組件 在觸發(fā)路由跳轉方后,只有主應用視圖觸發(fā)刷新、只有子應用視圖刷新、或都不刷新 「視圖的路由狀態(tài)維護在框架內(nèi)部」,通過原生跳轉無法觸發(fā)視圖更新
此時當分別跳轉到:/home、/detail、/test 路由時分別觸發(fā)對應的組件視圖,但是倘若子應用路由中也存在 /detail視圖呢,由于應用的開發(fā)采用分治的模式,應用的開發(fā)從空間和時間上都是分離的,無法保證應用間的路由不發(fā)生路由搶占的情況。
「通過 history 路由跳轉無法保證應用能夠觸發(fā)視圖更新」,在通過 history api 進行路由跳轉時,是無法觸發(fā)應用視圖更新,假設存在一個 React 應用 A,存在一個組件視圖 Test,分別通過 React 提供的路由方法跳轉和原生的路由跳轉進行觀察:

Hash 和 History 路由模式
目前主流的 SPA 前端應用基本上都支持兩種路由模式,一種是:hash 模式、另一種則是 History 路由模式,兩者的優(yōu)劣和使用并不在本文的討論范圍之內(nèi),這里僅做在微前端這種分離式開發(fā)模式下的介紹,在微前端這種分離式 SPA 應用開發(fā)的模式下該選擇哪種路由模式,以及多 SPA 應用下他們的路由應該如何編排:
假設站點地址為:http://garfish.bytedance.net
正常路由情況
主應用 history 模式、子應用 history 模式
主應用(
basename: /example):主應用所有路由基于:
http://garfish.bytedance.net/example例例如跳轉到:/appA,
http://garfish.bytedance.net/example/appA/子子應用(
basename: /example/appA):子應用所有路由基于:
http://garfish.bytedance.net/example/appA跳轉到子應用的 /detail 頁,
http://garfish.bytedance.net/example/appA/detail特點:
當主子應用分別為 history 模式時,子應用的路由基于主應用基礎路由并帶上自己的業(yè)務路由
路由同步到主應用路由上,通過 子應用 scope 命名空間隔離(子應用 A,提供 appA 的 scope)主應用和其他應用的路由沖突,并將子應用
路徑符合用戶和開發(fā)者認知和理解
支持嵌套層級使用,并繼續(xù)通過 scope 的命名空間保證路由可讀
主應用 history 模式、子應用 hash 模式
主應用(
basename: /example):主應用所有路由基于:
http://garfish.bytedance.net/example例如跳轉到:/appA,
http://garfish.bytedance.net/example/appA/子應用(
basename: /example/appA):子應用所有路由基于:
http://garfish.bytedance.net/example/appA從主應用:
http://garfish.bytedance.net/example/appA,跳轉到子應用的?/detail 頁,http://garfish.bytedance.net/example/appA#/detail特特點:
在一定程度上具備主子應用都為 history 模式的優(yōu)勢,不支持嵌套層級使用
目前多數(shù)框架都不支持以 hash 值作為 basename
可讀性尚可
異常路由情況
主應用 hash 模式、子應用 history 模式
主應用(
basename: /example):主應用所有路由基于:
http://garfish.bytedance.net/example例如跳轉到:/detail,
http://garfish.bytedance.net/example#/appA子應用(
basename: /example#/appA):子應用所有路由基于:
http://garfish.bytedance.net/example#/appA跳轉到子應用的 /detail 頁,
http://garfish.bytedance.net/example/detail#/appA特點:
「路由混亂」,不符合用戶和開發(fā)者直覺
目前多數(shù)框架都不支持以 hash 值作為 basename
主應用 hash 模式、子應用 hash 模式
主應用(
basename: /example):主應用所有路由基于:
http://garfish.bytedance.net/example例如跳轉到:/detail,
http://garfish.bytedance.net/example#/appA子應用(
basename: /example#/appA):子應用所有路由基于:
http://garfish.bytedance.net/example#/appA跳轉到子應用的 /detail 頁,
http://garfish.bytedance.net/example#/detail特點:
「路由混亂」,不符合用戶和開發(fā)者直覺
目前多數(shù)框架都不支持以 hash 值作為 basename
可能與主應用或其他子應用發(fā)生路由沖突
Garfish ?Router 如何處理路由
通過上面理想的路由模式案例發(fā)現(xiàn),微前端應用拆分成子應用后,子應用路由應具備自治能力,可以充分的利用應用解耦后的開發(fā)優(yōu)勢,但與之對應的是應用間的路由可能會發(fā)生沖突、兩種路由模式下可能產(chǎn)生用戶難以理解的路由狀態(tài)、無法激活不同前端框架的下帶來的視圖無法更新等問題。
目前 Garfish 主要提供了以下四條策略
提供 Router Map,減少典型中臺應用下的開發(fā)者理解成本 為不同子應用提供不同的 basename 用于隔離應用間的路由搶占問題 路由發(fā)生變化時能準確激活并觸發(fā)應用視圖更新
Router Map 降低開發(fā)者理解成本
在典型的中臺應用中,通常可以將應用的結構分為兩塊,一塊是菜單另一塊則是內(nèi)容區(qū)域,依托于現(xiàn)代前端 Web 應用的設計理念的啟發(fā),通過提供路由表來自動化完成子應用的調(diào)度,將公共部分作為拆離后的子應用渲染區(qū)域。


自動計算出子應用所需的 basename
當應用處于激活狀態(tài)時,根據(jù)應用的激活條件自動計算出應用所需的基礎路徑,并在渲染時告訴框架,以便于應用間路由不發(fā)生沖突。

如何有效的觸發(fā)不同應用間的視圖更新
目前主流框架實現(xiàn)路由的方式并不是監(jiān)聽路由變化觸發(fā)組件更新,讓開發(fā)者通過框架包裝后的 API 進行跳轉,并內(nèi)部維護路由狀態(tài),在使用框架提供 API 方法發(fā)生路由更新時,內(nèi)部狀態(tài)發(fā)生變更觸發(fā)組件更新。
由于框架的路由狀態(tài)分別維護在各自的內(nèi)部,那么如何保證在路由發(fā)生變化時能及時有效的觸發(fā)應用的視圖更新呢,答案是可以的,目前主要有兩種實現(xiàn)策略:
收集框架監(jiān)聽的 popstate 事件 主動觸發(fā) popstate 事件
因為目前支持 SPA 應用的前端框架都會監(jiān)聽瀏覽器后退事件,在瀏覽器后退時根據(jù)路由狀態(tài)觸發(fā)應用視圖的更新,那么其實也可以利用這種能力主動觸發(fā)應用視圖的更新,可以通過收集框架的監(jiān)聽事件,也可以觸發(fā) popstate 來響應應用的 popstate 事件
基于「現(xiàn)代 Web 框架」的微前端最佳實踐
微前端作為一種全新的 Web 應用類型,不同于以往傳統(tǒng)的 Web 應用開發(fā),微前端需要采用主子應用分治的開發(fā)模式后帶來了一系列新的挑戰(zhàn),這些挑戰(zhàn)包括但不限于:主子應用開發(fā)調(diào)試、普通 Web 應用如何快速變?yōu)槲⑶岸藨谩⑷绾沃С治⑶岸藨?SSR、主子應用數(shù)據(jù)通信觸發(fā)視圖更新。Modern.js 作為 Garfish 上層的現(xiàn)代 Web 框架,能夠很好的解決這些問題,并提供開箱即用的開發(fā)體驗。
微前端應用的調(diào)試開發(fā)
由于微前端應用采用分治的開發(fā)策略,應用間的維護和開發(fā)可能在時間和空間上都是分離的,那么在開發(fā)環(huán)境時啟動整個微前端項目的所有主子應用,是一個并不明智的策略,不僅需要 clone 其他倉庫并完成應用的運行,還要保證其代碼的時效性。Modern.js 提供了更優(yōu)的的策略:
某些子應用需要更新時 主應用線上環(huán)境 需要開發(fā)的子應用線下環(huán)境 不需要開發(fā)的子應用上線 主應用需要更新時 主應用線下環(huán)境 所有子應用線上環(huán)境
通過以上更優(yōu)的調(diào)試策略,可以保證開發(fā)者僅運行自己的關注的應用即可。那么如何達到這種更優(yōu)的,可以采用應用列表的下發(fā)模式,框架運行時加載下發(fā)的應用列表,在開發(fā)主應用時拉取線上的應用列表,在開發(fā)某個子應用時代理代理列表中的資源為子應用的列表。
傳統(tǒng) Web 應用支持微前端模式
通過微前端運行時章節(jié)可以發(fā)現(xiàn)傳統(tǒng) Web 應用與微前端應用間進行切換成本并不高,但需要研發(fā)關注應用的路由的調(diào)度、應用的生命周期導出、額外的構建配置、應用通信數(shù)據(jù)觸發(fā)視圖更新,微前端模式應用和傳統(tǒng) Web 應用間如何進行切換都存在一定的學習和理解成本。
在 Modern.js 中作為上層框架集成了 Garfish,原生支持微前端應用,可以通過簡單配置即可完成微前端應用類型的轉換,幫助用戶快速搭建應用基礎結構,以降低其學習成本,快速生成微前端應用。
微前端應用如何支持 SSR
微前端作為一種全新的架構模式,其分治設計模式除了帶來的諸多優(yōu)點外,但與之對應的是引入了新的問題,如何支持傳統(tǒng) Web 應用提供的 SSR 能力,由于微前端采用了分治的開發(fā)模式,應用拆分成了多個子應用,那么需要實現(xiàn)整體應用的 SSR 能力,則需要與具體的 Web 框架相結合,通過制定微前端應用的加載規(guī)則,達到微前端應用也能有效的實現(xiàn) SSR 能力。
Modern.js 作為 Garfish 的上層框架,提供更開箱即用的上層能力 ,并解決了以上微前端不同于傳統(tǒng) Web 應用開發(fā)后帶來的弊端,文末有關于 Modern.js 的發(fā)布預告,可以了解并關注。
微前端的優(yōu)點
適用于大規(guī)模 Web 應用的開發(fā) 更快的開發(fā)速度 支持迭代可開發(fā)和增強升級 拆解后的部分降低了開發(fā)者的理解成本 同時具備 UX 和 DX 的開發(fā)模式
微前端的缺點
復雜度從代碼轉向基礎設施 整個應用的穩(wěn)定性和安全性變得更加不可控 具備一定的學習和了解成本 需要建立全面的微前端周邊設施,才能充分發(fā)揮其架構的優(yōu)勢 調(diào)試工具 監(jiān)控系統(tǒng) 上層 Web 框架 部署平臺
何時使用微前端
大規(guī)模企業(yè)級 Web 應用開發(fā) 跨團隊及企業(yè)級應用協(xié)作開發(fā) 長期收益高于短期收益 不同技術選型的項目 內(nèi)聚的單個產(chǎn)品中部分需要獨立發(fā)布、灰度等能力 微前端的目標并非用于取代 iframe 應用的來源必須可信 用戶體驗要求更高
總結
微前端概念的出現(xiàn)是前端發(fā)展的必然階段,PC 互聯(lián)網(wǎng)轉向移動互聯(lián)網(wǎng)時代時,PC 的場景并未完全被消滅,反而轉向了衍生出了更多沉浸感更高、體驗感更強的應用,與之對應的應該是出現(xiàn)新的架構模式來應對這些應用規(guī)模的增長。
微前端也并非銀彈,采用微前端后復雜度并未憑空消失,而是由代碼轉向了基礎設施,對架構設計帶來了更大的挑戰(zhàn),并且在新的架構下需要設計并提供更多的周邊工具和生態(tài)來助力這一新的研發(fā)模式。
本文更多的是從背景和設計層面講清楚微前端解決方案應具備哪些能力,以及核心模塊的設計。每一部分并未包含過于詳細的細節(jié),如果想要了解「微前端運行時」詳細設計,可以通過 https://github.com/modern-js-dev/garfish 倉庫了解細節(jié)。
參考
如何設計微前端中的主子路由調(diào)度:https://mp.weixin.qq.com/s/TAXP7ipDdtb2Jb-L3QHszA 如何取巧實現(xiàn)一個沙箱:https://mp.weixin.qq.com/s/Mg3fU0WvZUQnlWHdxc-b5A 微服務架構及其最重要的 10 個設計模式:https://www.infoq.cn/article/kdw69bdimlx6fsgz1bg3 single-spa:https://github.com/single-spa/single-spa
Modern.js 開源預告
Modern.js 和 Garfish 都是字節(jié)跳動 Web Infra 發(fā)起的「現(xiàn)代 Web 工程體系」開源項目,Modern.js 原生支持微前端,在 Garfish 基礎上提供了完整的微前端最佳實踐。
Modern.js 計劃在 10 月 14 號發(fā)布 1.0.0 版和上線文檔站,歡迎關注和參與。
- END -
?? 點擊 「閱讀原文」,查看 Garfish 倉庫
