一、背景介紹
【資料圖】
小程序在其誕生后的幾年內(nèi),憑借其簡單、輕量、流暢、無需安裝等特點,引來了爆發(fā)式的增長。伴隨小紅書電商業(yè)務(wù)的發(fā)展,我們洞察到越來越多的商家和品牌大客戶有自己定制化需求場景,傳統(tǒng)的電商和薯店存在下面三大問題:
為了解決上述問題,并快速打通基于小紅書體系的支付與賬號體系。過去的一年內(nèi),我們踏上了自研小程序之路。目前,在小紅書店鋪主頁、 筆記詳情、品牌專區(qū)、開屏均可喚起小程序。
本文將主要介紹小紅書進(jìn)行小程序自研時的一些業(yè)務(wù)背景及工程化、容器能力的落地方案,以及運行時針對雙線程架構(gòu) bridge,framework 能力的設(shè)計。
二、運行時工程能力建設(shè)
2.1 小程序 "運行時" 定義
運行時在不同語言中含義有所不同,但基本可以概括為「運行在代碼執(zhí)行階段的代碼」,類似 vue-runtime, 提供了對于頁面狀態(tài)的劫持,生命周期的解析,api 的調(diào)用能;nodejs 提供了 JS 運行時執(zhí)行能力等。小程序 “運行時” 則提供了在不同線程內(nèi),借助 Bridge 消息通道,進(jìn)行邏輯調(diào)度的能力。
那么可以基本概括為:運行在小程序代碼執(zhí)行階段、用于提供在獨立線程中操作其他線程的頁面(或視圖),正確響應(yīng)用戶交互行為、并調(diào)度用戶業(yè)務(wù)邏輯能力的代碼。
2.2 小程序基礎(chǔ)架構(gòu)
小紅書小程序也是對齊業(yè)界經(jīng)典架構(gòu)進(jìn)行建設(shè):
經(jīng)典雙線程架構(gòu)
經(jīng)典架構(gòu)下,運行時主要分為 渲染層 - Render、邏輯層 - Service。Service 用于與系統(tǒng)能力進(jìn)行交互,在安全的 JS 線程內(nèi)調(diào)度用戶業(yè)務(wù)邏輯。而 Render 則負(fù)責(zé)接受渲染指令、進(jìn)行視圖的繪制與用戶交互的響應(yīng)。邏輯層與渲染層則通過 js-bridge 進(jìn)行消息的通訊,容器則負(fù)責(zé)接受 api 指令進(jìn)行端能力的調(diào)用。
之所以需要一個獨立的線程來執(zhí)行 JS,其主要目的是為了限制 JS 靈活性。為了提供一個可用的 JS 環(huán)境,其實也有比較多的方案。比如,我們可以使用瀏覽器內(nèi)核提供的 Service Worker ,來單獨運行 service 層 JS 代碼。或者我們可以使用多個 webview 實例來分別承載雙端 js 的執(zhí)行環(huán)境。
2.3 容器架構(gòu)實現(xiàn)
按照經(jīng)典架構(gòu)的設(shè)計,我們需要在三端 (iOS、android、小程序開發(fā)者工具) 提供面向雙線程的容器方案。在不同的容器環(huán)境下,渲染層和邏輯層選用的方案會存在一定差異,小紅書三端選用容器的分布如下:
雖然運行環(huán)境存在一定差異,但容器對于基礎(chǔ)庫和業(yè)務(wù)腳本的加載順序是基本一致的。我們可以將整個啟動階段拆解為下面幾個階段:
首先,當(dāng)用戶點擊時,會經(jīng)歷一個基本的啟動過程。
在這個啟動流程的背后,會對應(yīng)著上面提到渲染層 webview 容器被加載出來。于此同時,在用戶看不到的地方,容器會進(jìn)行邏輯層 v8/JsCore 的初始化, 同時會加載小程序的基礎(chǔ)庫代碼。
腳本注入結(jié)束后,容器會立即通知「運行時邏輯層框架」進(jìn)行依賴分析、并準(zhǔn)備初渲染數(shù)據(jù)。
渲染層接受到 initialData 消息后,會進(jìn)行后續(xù)渲染操作,用戶即刻看到了頁面的內(nèi)容 至此,初渲染的流程基本結(jié)束。
當(dāng)然,實際的容器的啟動過程中的流程會更加復(fù)雜,整個啟動流程可以用下面的這張圖來表示:
黑色、藍(lán)色、橙色分別代表了 端側(cè)、邏輯層、渲染層三個線程
實際場景中,容器還面臨更多的挑戰(zhàn),比如如何確保雙線程的是否 ready,再進(jìn)行消息的推送等。核心在于,我們通過不同線程的容器,完成了頁面渲染行為的控制。
可以看到,上述啟動流程中容器側(cè)分別在 Render 和 Service 分別注入了 page.render.js 和 service.js 的業(yè)務(wù)代碼。那么如何進(jìn)行業(yè)務(wù)代碼構(gòu)建,來分別在雙線程下執(zhí)行呢?這就需要依靠前端工程化的能力來實現(xiàn)了。
2.4 實現(xiàn)基礎(chǔ)架構(gòu)的工程化能力
通過前端工程化能力,我們可以對資源進(jìn)行分類構(gòu)建,小紅書小程序使用 webpack 作為工程化構(gòu)建工具。通常,小程序的構(gòu)建分兩塊,一塊是針對基礎(chǔ)庫的打包,一塊是針對業(yè)務(wù)組件的構(gòu)建。
基礎(chǔ)庫的打包需要構(gòu)建基礎(chǔ)庫代碼,產(chǎn)出分別用于提供運行時框架能力的render.base.js及service.base.js
而業(yè)務(wù)組件的構(gòu)建,則相對復(fù)雜。一個原生小程序組件或頁面通常包含下面四個文件:
通過拆分多個文件,我們可以在構(gòu)建時指定入口依賴,將對應(yīng)的依賴打入所需要的模塊內(nèi),在工程構(gòu)建時,需要對文件進(jìn)行分類打包:
我們使用 loader 作為 webpack 的 entry 入口進(jìn)行構(gòu)建,每個頁面都會作為一個 entry 獨立打包。這使得從行為上來說小程序更像一個 MPA(多頁應(yīng)用)。入口側(cè)會進(jìn)行 app.json 的校驗,對配置以頁面維度來進(jìn)行解析,針對小程序業(yè)務(wù)代碼,會分別構(gòu)建出 page.render.js 和 service.js 分別交給不同的線程進(jìn)行加載(如上圖)。
構(gòu)建會將代碼打包成 UMD 格式文件,當(dāng)在不同線程內(nèi)執(zhí)行基礎(chǔ)庫腳本時,部分腳本會自動執(zhí)行,端側(cè)只需要關(guān)注容器加載 Js腳本的時機(jī)及消息發(fā)送的順序即可。
運行時基礎(chǔ)能力與框架
容器和工程化能力是小程序運行的基石,但小程序之所以可以做到高效開發(fā)、并擁有極強(qiáng)的跨平臺能力和優(yōu)秀的體驗,這也得益于在框架底層提供了完善的組件及模塊化能力,更有豐富的 api 來滿足原生場景下各種系統(tǒng)能力調(diào)用的述求。
3.1 運行時總架構(gòu)
·這張圖主要將運行時架構(gòu)分為了渲染層、邏輯層和jsBridge:
·渲染層面向業(yè)務(wù)提供了組件、沙箱、性能收集等框架能力,這一層業(yè)務(wù)是無法接觸到的
·邏輯層則在 JsContext 內(nèi)提供了 invoke 層來與端側(cè)進(jìn)行數(shù)據(jù)交互
·邏輯層通過適配層,完成導(dǎo)航、Render和頁面實例的管理
·邏輯層內(nèi)核主要用于向業(yè)務(wù)代碼的執(zhí)行環(huán)境,提供 Page、Component、behavior ·這類能力,并預(yù)置 JS Polyfill 來確保業(yè)務(wù)的 js 正常運行。
·JSON schema 則用于定義 api 標(biāo)準(zhǔn)結(jié)構(gòu)和定義,并通過 js-bridge 層完成端能力調(diào)用與通訊
3.2 基礎(chǔ)能力分布
為了豐富小程序的基礎(chǔ)能力,初期我們盤點了業(yè)界的功能矩陣,盡可能豐富小紅書小程序的基礎(chǔ)能力,目前運行時的基礎(chǔ)能力分布如下:
灰色部分為暫未支持的能力
其中包含了:
·App, Compnent, Page 等基礎(chǔ)能力
·網(wǎng)絡(luò)、文件系統(tǒng),設(shè)備等 API 能力
·xhs-view, xhs-button 等面向業(yè)務(wù)的組件能力
·目前,矩陣列出的功能,在小紅書小程序基礎(chǔ)庫 ≥ v3.32.x 版本上已經(jīng)得到支持。
3.3 雙線程框架能力建設(shè)
熟悉小程序語法的同學(xué)都知道,小程序可以通過 Page、Component 來進(jìn)行非常靈活的組件化開發(fā)。通過 selectComponent 、triggerEvent這類功能可以非常方便的進(jìn)行 子 → 父 或 父 → 子 實例的追溯,這就要求框架側(cè)需要維護(hù)組件之間的依賴關(guān)系。
實現(xiàn)這種架構(gòu)有多種思路,不同廠商的做法也不同。譬如微信在 Page 體系和 Component 自定義組件的實現(xiàn)上就采用了不同的設(shè)計。微信在渲染側(cè)通過Exparser模塊完成小程序內(nèi)的所有組件,包括內(nèi)置組件和自定義組件組織管理。
小紅書側(cè)在渲染層則是 Fork Vue 框架,通過定制 Vue 的一些能力來完成頁面渲染工作。借助 Vue優(yōu)秀的組件化能力的來完成 Page, Component 的渲染工作。在邏輯層,則通過消息維護(hù)一棵類 vdom 樹 , 來完成視圖 ←→ 邏輯的映射與綁定關(guān)系,整個關(guān)系大概如下圖所示:
3.4 事件系統(tǒng)
有了上述基礎(chǔ)能力和雙線程架構(gòu),運行時還需要實現(xiàn)一套事件系統(tǒng),讓 UI 界面與用戶產(chǎn)生互動。事件通常分為兩塊,一塊是服務(wù)于用戶的手勢交互,比如用戶的點擊 tap, 長按 longtap 等事件,另一塊則是渲染層交互組件的回調(diào)時間,譬如 swiper 組件的 onChange 等回調(diào)。
在小程序的事件系統(tǒng)下,我們把這些用戶的手勢操作和組件回調(diào),進(jìn)行攔截與收集,全部轉(zhuǎn)入消息隊列轉(zhuǎn)發(fā)到邏輯線程。每條消息攜帶自己的實例 ID,找到邏輯層實例進(jìn)行對應(yīng)函數(shù)的觸發(fā)。
3.5 bridge 能力設(shè)計
框架側(cè)借助 bridge 通道可以非常方便進(jìn)行消息的轉(zhuǎn)發(fā)。但實際上,一條消息需要經(jīng)過多次序列化和反序列化,才可以到達(dá)“目的地”。小紅書小程序的 bridge 側(cè)是如何實現(xiàn)的呢?
我們以渲染層事件消息舉例,當(dāng)渲染層收到一條點擊 消息,會經(jīng)過如下幾個階段:
不同容器下,對 webview 內(nèi)核消息的攔截機(jī)制不同,ios 使用 messageHandler, android 則使用 console 通道攔截消息,但內(nèi)核底層對消息的處理流程基本一致。
這個過程可以簡單描述為以下幾個環(huán)節(jié):
·Render 側(cè)發(fā)送 postMessage 消息,此時消息需要經(jīng)過一次序列化轉(zhuǎn)成字符串
·瀏覽器攔截到消息,反序列化成 JSONObject 并發(fā)送到 Naive 容器側(cè)
·容器開始進(jìn)行跨線程事件分發(fā),并轉(zhuǎn)發(fā)消息到 service
·Service 運行環(huán)境將消息反序列化成 string,并轉(zhuǎn)成 JS 數(shù)據(jù)類型,傳到 Service 所在的 JsContext 中
·JsContext 中 invokeCallback 函數(shù)被調(diào)用
·至此,render 消息已成功轉(zhuǎn)發(fā)至 service 層
可以看到,這個過程非常復(fù)雜,不僅要完成消息的轉(zhuǎn)發(fā),還要完成 jsonObject 和 js 數(shù)據(jù)類型的互轉(zhuǎn)。為了在兩個線程內(nèi)方便的完成這種互調(diào),并保證 bridge 的安全線,我們分別在雙端分別實現(xiàn)了 handleMessage 和 postMessage 的封裝,通過 schema 來定義 bridge 和 api 標(biāo)準(zhǔn)協(xié)議,來完成線程消息的轉(zhuǎn)發(fā)和消息類型的校驗工作。
一個標(biāo)準(zhǔn)的 api schema 定義大概是這樣:
消息會在 JsContext 內(nèi)完成校驗,并在校驗通過后以序列化的方式完成上述流程的傳遞。
3.6 數(shù)據(jù)編譯能力與 JS 沙箱
為什么這里要提下數(shù)據(jù)編譯能力和 js 沙箱呢。因為小程序雙線程的框架下,邏輯層通過setData 發(fā)起頁面更新請求,攜帶的數(shù)組字段在被渲染層對應(yīng)的組件解析時,需要配合小程序的一些語法特性進(jìn)行特殊轉(zhuǎn)換。
在運行時側(cè),我們將字段的解析能力與數(shù)據(jù)字段的處理,都收攏到沙箱 環(huán)境中進(jìn)行字段編譯。通過沙箱,我們可以攔截業(yè)務(wù)代碼對于變量的訪問,從而實現(xiàn)變量的劫持,并配合完成 sjs 這類能力的實現(xiàn)。同時,沙箱可以有效防止業(yè)務(wù)動態(tài)注入一些變量或函數(shù),帶來的變量訪問逃逸的安全問題。因此,沙箱在小程序語法和變量計算的過程中起到了至關(guān)重要的作用。
例如,下面這段代碼片段:
在編譯側(cè),我們會將 loader上面代碼通過 ast 進(jìn)行轉(zhuǎn)換:
通過沙箱,我們可以攔截到業(yè)務(wù)對 sjs 模塊訪問,將訪問屬性替換為 sjs 的模塊導(dǎo)出,從而實現(xiàn)類似 sjs 這樣的腳本拓展能力。
性能優(yōu)化與監(jiān)控
雙線程在線程隔離方案上,將原本在同一線程內(nèi)執(zhí)行的腳本、渲染等工作分散到多個線程內(nèi)執(zhí)行,帶來了更好的性能。但如果單個 webview 線程的渲染負(fù)擔(dān)過重或?qū)υO(shè)備內(nèi)存占用過大一樣會影響到整體的體驗。
于此同時,線程隔離也帶來了通訊的損耗,對于一次消息需要經(jīng)過多次序列化和反序列化,消息序列化的損耗與轉(zhuǎn)發(fā)也對性能有著直接的影響。
因此,小程序的性能優(yōu)化不同于傳統(tǒng)的 web,需要從框架、通道、容器三方面來考慮。
4.1 bridge 消息調(diào)度機(jī)制
bridge 消息通道的繁忙程度,會在很大程度上影響小程序的性能表現(xiàn)。通過上面對于 bridge 消息轉(zhuǎn)發(fā)機(jī)制的介紹也可以看出,頻繁的借助 bridge 進(jìn)行消息轉(zhuǎn)發(fā),意味著消息要不斷進(jìn)行序列化和反序列化的操作。
實際場景中,數(shù)據(jù)量小于64KB時,時長基本在10 - 40ms內(nèi)。傳輸時間與數(shù)據(jù)量上呈現(xiàn)正相關(guān)關(guān)系,傳輸過大的數(shù)據(jù)將使這一時間顯著增加,因此減少傳輸數(shù)據(jù)量是降低數(shù)據(jù)傳輸時間的有效方式。
但是如果數(shù)據(jù)量較小,確在短時間內(nèi)多次使用 bridge ,也會導(dǎo)致通道過于繁忙。小紅書在 bridge 側(cè),通過一定消息調(diào)度能力,將特定場景下的消息進(jìn)行聚合,確保一次序列化盡可能在不影響序列化性能的情況下,多攜帶一些消息到對應(yīng)的線程內(nèi)。
4.2 渲染層任務(wù)調(diào)度與優(yōu)先級隊列
前面我們曾經(jīng)提到,雙線程背景下,小程序的更新機(jī)制與事件系統(tǒng)全部都是通過消息進(jìn)行處理的,但消息本身的收發(fā)都存在一定的延時性,這就注定了小程序是一個異步通訊的世界。那么在一個異步多線程的場景下,線程之間“生產(chǎn)“和“消費“的消息的速度會因性能、穩(wěn)定性等因素而不一致,這時,我們便要借助消息隊列的思想來管理我們的消息:
有了消息隊列,我們可以更好的管理框架層拋出的消息體,但小程序框架內(nèi),除了更新消息和事件消息外,還有不同的消息體會與這些框架消息搶占消息通道。比如,框架收集不同的 render 線程 webview 內(nèi)的性能指標(biāo),這些性能消息會與事件消息共享同一隊列。但有些場景下,事件消息的優(yōu)先級要遠(yuǎn)高于性能指標(biāo)消息。
此外,不同的渲染層 render 實例的消息所擁有的優(yōu)先級也不同,比如 A、B 頁面在同一時間段內(nèi),因其“棧頂?shù)牡匚弧睍蛴脩舨僮鞫粩嘧兓?,此時棧頂頁面的框架消息優(yōu)先級高于 B 頁面的框架消息優(yōu)先級,在底層。我們使用 二叉堆 結(jié)構(gòu)來維護(hù)優(yōu)先級隊列。
4.3 容器預(yù)加載
小程序的啟動分為冷啟動和熱啟動, 從用戶的角度看:
冷啟動:如果用戶首次打開,或小程序銷毀后被用戶再次打開,此時小程序需要重新加載啟動,即冷啟動。
熱啟動:如果用戶已經(jīng)打開過某小程序,然后在一定時間內(nèi)再次打開該小程序,此時小程序并未被銷毀,只是從后臺狀態(tài)進(jìn)入前臺狀態(tài),這個過程就是熱啟動。
通常在容器側(cè)的優(yōu)化,就是針對冷啟動來進(jìn)行。那么容器的預(yù)載,顧名思義,就是在合適的時間提前預(yù)載小程序容器,預(yù)載的同時,會提前進(jìn)行基礎(chǔ)庫的下載和渲染容器(webview)的加載。
通過前置容器的初始化時機(jī),來達(dá)到快速換起小程序,提高首屏的優(yōu)化效果。這是小程序這類容器技術(shù)方案常用的優(yōu)化策略。
4.4 性能監(jiān)控與告警
性能優(yōu)化的同時,框架側(cè)需要對業(yè)務(wù)代碼的性能和行為有一定感知能力。在底層,我們通過 aop 的方式,建設(shè)了一套監(jiān)控和插件機(jī)制。在開發(fā)階段,可以感知到業(yè)務(wù)各項指標(biāo)的健康狀況,業(yè)務(wù)可以接收到底層框架給出的性能告警信息,并通過告警信息中的修復(fù)建議,針對性的進(jìn)行優(yōu)化。
業(yè)務(wù)側(cè),則可以通過 performance api 拿到這些性能指標(biāo),來進(jìn)行基礎(chǔ)性能數(shù)據(jù)的收集與上報。
性能告警會結(jié)合性能標(biāo)準(zhǔn)閾值來給出提示和修復(fù)建議,未來在審核階段也會結(jié)合這些指標(biāo)進(jìn)行小程序健康度的洞察。
4.5 ServiceTiming 與 RenderTiming
除了卡頓、渲染指標(biāo)外,為了滿足高級開發(fā)者洞察平臺的性能信息的需要,我們對容器和框架在啟動階段的關(guān)鍵節(jié)點,都預(yù)留了性能點位。開發(fā)者可以通過 performance.serviceTiming和performance.renderTiming 來分別獲取到各個關(guān)鍵階段的時間戳信息。
各個線程內(nèi)所預(yù)留的性能點位和其在啟動階段中的位置如下圖所示:
五、總結(jié)
以上就是小紅書小程序運行時方案的原理解析。
小程序本身是一個依托宿主流量體系衍生出的技術(shù)體系,它的價值往往緊貼應(yīng)用主體的流量,而應(yīng)用主體本身,又依賴小程序的靈活性及低成本的特點快速完成流量的轉(zhuǎn)換。社交、支付與搜索,這些都是互聯(lián)網(wǎng)產(chǎn)品提供的服務(wù)形態(tài),各大廠商都是結(jié)合用戶的需求和行為差異進(jìn)行更開放、安全的技術(shù)方案探索,小紅書亦是如此。小紅書依靠用戶產(chǎn)生內(nèi)容,而內(nèi)容產(chǎn)生商品,那么結(jié)合各類消費場景,如店鋪、筆記等都可以通過小程序容器快速進(jìn)行交易鏈路閉環(huán)。
未來,我們也將在不同的品牌和賽道上,尋找更多的服務(wù)商與品牌大客戶商家與我們一起,共同豐富小紅書的商品服務(wù)供給,增加小紅書商業(yè)收入。技術(shù)上,我們則會不斷對齊業(yè)界,優(yōu)化技術(shù)架構(gòu),在提高框架性能的同時,建立完善的服務(wù)市場、巡檢機(jī)制等來幫助小紅書服務(wù)商與自開發(fā)商家細(xì)致、高效的開發(fā)與管理自己的小程序。
小程序是一個比較龐大的技術(shù)體系,如果你覺得本文對你有幫助,歡迎點贊轉(zhuǎn)發(fā)。我們后續(xù)會根據(jù)反饋繼續(xù)展開介紹更多的技術(shù)建設(shè)細(xì)節(jié)。也歡迎訪問我們的小程序官方網(wǎng)站與我們交流:
·小紅書小程序介紹:https://miniapp.xiaohongshu.com/docs/guide/miniIntroduce
·小紅書社區(qū)專業(yè)號:https://pro.xiaohongshu.com/
·你也可以通過 https://github.com/redengineer/redmini 與我們進(jìn)行交流
六、作者信息
哈笛
商業(yè)技術(shù)組 - 小程序團(tuán)隊成員(tailiang@xiaohongshu.com),目前負(fù)責(zé)小紅書運行時相關(guān)技術(shù)開發(fā)工作。