One 什麼是小程式
Ⅰ 小程式概念
微信小程式算是小程式的鼻祖了,2017年1月9日微信正式上線了小程式。在探究小程式技術架構之前,我們先看看小程式究竟是什麼,微信官網對微信小程式的產品定位及功能介紹是: “微信小程式是一種全新的連線使用者與服務的方式,它可以在微信內被便捷地獲取和傳播,同時具有出色的使用體驗。”
這個介紹有種看了跟沒看一樣的感覺。網上對於微信小程式是什麼還有一個介紹的版本: “小程式是一種不需要下載安裝即可使用的應用,它實現了應用「觸手可及」的夢想,使用者掃一掃或搜一下即可開啟應用。也體現了「用完即走」的理念,使用者不用關心是否安裝太多應用的問題。應用將無處不在,隨時可用,但又無需安裝解除安裝。”
這個概念就更清晰一些,可以看出小程式是眾多例項執行在一個宿主應用中,小程式本身也是一種可插拔的外接應用。
Ⅱ 使用者角度的小程式
下面從使用者使用互動角度來看一下小程式:
(圖1)iOS:從小程式獨立性角度(小程式與小程式之間,小程式與宿主應用之間切換)來說,BATT 的小程式與招商銀行的小程式基本互動相似。 Android:從互動上來看BATT 的小程式都可以看做是獨立的應用程式,獨立存在於後臺(多程式),可以在小程式與小程式之間,小程式與宿主應用之間切換。可以直觀的理解為這類小程式為小程式應用。招商銀行的的小程式是與宿主應用共存的,也就是在一個程式中,不能在小程式與小程式之間,小程式與宿主應用之間切換。這類小程式可以直觀的理解為小程式頁面。
BATT: 微信,支付寶,頭條,百度小程式。由於互動相似,所以並稱。
所以,從使用者使用角度來看,BATT的互動體驗更有優勢。從對小程式概念的理解來看,各app理解有所差異,但這並不影響功能層面的使用。
Ⅲ 平臺角度的小程式
最早應用小程式的微信為什麼會創造出小程式這個東西呢?它到底有什麼作用?在我看來,主要目的還是在於管控為目的,使用了多個手段來實現,主要管控在於兩個方面:
UI管控:以微信為例,微信自己定義了一套DSL,而不是用HTML來開發頁面。這樣就不能讓開發者隨意開發,而是在微信的DSL框架中開發。開發者寫的DSL具體轉換成什麼,是通過什麼渲染,都是微信平臺來決定。基於自定義的這套DSL,可以更好的做程式碼管控方面的工作,比如:請求白名單,程式碼掃描等。
服務管控:還是以微信為例,微信中的宿主平臺提供的服務(比如:支付,微信運動,卡券,發票,使用者賬號資訊等)對於無論是二方使用者還是三方使用者,都有許可權管控的需求。目的也是不能讓接入的小程式,在沒有授權機制的前提下,隨意呼叫微信基礎服務。
Two 小程式技術架構
基於從使用者角度的體驗需求,以及平臺角度的管控需求,我們來看看BATT系小程式在技術上做了哪些達到了這些目的。
Ⅰ 渲染流程
下圖是小程式的渲染流程,裡面包含了部分技術選型,後面的部分會提到:
(圖2)Ⅱ 主要技術點
- DSL(Domain-specific language):
在我們聊DSL之前,我們先看看編譯程式碼需要做哪些工作。無論是解釋性語言(JS,Ruby,Python)還是編譯型語言(Java,C++,C#),都會有一個共同的部分,將原始碼解析為AST(抽象語法樹)。AST不僅能夠以結構化的方式呈現原始碼,而且在語義分析中起到關鍵作用。AST不僅僅應用在直譯器和編譯器,而且在靜態程式碼分析中也比較常用,比如:我們在重構程式碼的時候,希望提取出公共模組,以便減少重複程式碼方便複用。這時我們單純的用字串比對的方式會比較片面不能達到效果,這時生成AST就比較有用。另一個應用例子對於我們的DSL設計會比較有借鑑意義:程式碼轉換器。下圖是一種語言程式碼轉換為另一種語言程式碼的主要步驟:
(圖3:圖片來源於網路)原始碼先解析成AST,解析之前它是遵循語言規則的文字,解析之後成為與輸入文字完全相同的樹形結構,這個過程是可逆的。然後再對AST遍歷以及替換,這個過程對於前端來說類似於DOM樹的生成,最後根據修改後的AST生成編譯後的程式碼。我們以JS為例,用acorn生成的AST,同樣我們也可以使用其他的解析器,例如:babylon,esprima等,下面是一個簡單的例子(限於篇幅,右側的AST樹沒有完全展開,讀者可以到astexplorer上生成結果):
(圖4)由於小程式的渲染容器有可能是Webview容器,原生Native容器,Flutter容器(雖然Flutter也是Native渲染,為了與原生Native區別,這裡把它單獨出來,下同),所以我們可以借鑑前面的程式碼轉換器的思路,用AST抹平具體渲染容器的區別,下圖是DSL轉換的整體思路:
(圖5)有了以上的的設計並不是大功告成,還需要有很多需要做的工作要做。我們可以簡單的把DSL的處理分為編譯時和執行時,編譯時負責把DSL程式碼預編譯為目的碼,目的碼可以在相應的執行時環境執行。生成的目的碼的作用是,可以在具體的執行時通過當前環境的引數來執行出實際的程式碼,簡單的理解就是為了在多渲染環境執行的一個介面卡。
對於編譯時來說,從零寫起肯定是不現實的。首先我們繼續上面AST的話題,上面已經提到了幾個AST的解析器:acorn,babylon,esprima。當然還有很多其他的例如:cherow,espree,shift等。所以我們不用再造一個輪子,下面用babylon舉例,因為babylon在babel中使用,會與最近的JS功能同步,並且API設計良好易於使用。babel是js編譯器(並不僅僅是ES6支援的工具包,否則就變成了類似於Android裡面的support包了),可以用於程式碼壓縮,語法轉換等。對於生成目的碼的過程:解析(babylon),轉換(babel-traverse)都有很好的支援。由於不同的渲染容器有不同的元件庫和API,同樣功能的元件或API的使用也不盡相同,所以需要封裝出適配層程式碼。
對於執行時來說,只需要把編譯生成的適配程式碼生成具體渲染環境的程式碼執行就可以了。這裡比較類似babel把ECMAScript新版程式碼轉換成舊版程式碼的邏輯比較像。
- Native渲染
從效能角度出發,把小程式最終通過Native方式渲染會比用Webview作為渲染容器得到更好的效果。DSL的設計可以很好的遮蔽底層實際渲染的實現,可以用Native,也可以用H5,也可以使用兩者結合的方式,底層渲染引擎的切換也不會影響到小程式開發者的外部接入。目前移動端跨平臺Native渲染的技術非常流行,比較常用的有Weex/RN/Flutter。市場上有基於Weex和RN進行小程式的案例,Flutter畢竟是後起之秀,目前還沒有見到用Flutter作為渲染引擎的案例。
- Android多程式:
前面我們在從“使用者角度的小程式”部分看到,BATT的方案讓小程式真正可以做到像應用一樣的體驗。由於iOS應用無法開啟新程式讓小程式本身在獨立程式中執行,所以iOS中的小程式只能與宿主應用共享同一程式。對於Android來說就不一樣了,小程式佔用獨立程式,從安全形度來說,二方後者三方的小程式應用與宿主應用程式隔離,小程式出現的問題不會影響宿主應用。而且,從效能角度來說,小程式不會共享宿主應用的記憶體。從BATT的小程式實際應用操作來看,基本都會控制五個後臺開啟的程式保活,可以用五個容器Activity各自在自己的程式中渲染小程式,有的還會有後臺的保活時間限制。再開啟新的小程式,會關閉最早開啟的小程式程式,這樣達到了高效熱啟動的目標。
- 多執行緒:
邏輯渲染隔離: 首先,在聊具體的多執行緒之前,先說一下小程式的邏輯和渲染的問題。小程式的邏輯和渲染是分離的,當然從功能層面,不分離一樣可以實現。這裡說的邏輯和渲染分離是指小程式的邏輯執行在單獨JS環境的執行緒中,只需要JS引擎就可以,渲染執行在Webview執行緒中。邏輯和渲染分離到兩個執行緒有幾點好處:第一個是可以邏輯和渲染程式碼分離沒有耦合,第二個是可以讓邏輯執行緒和渲染執行緒並行執行,JS執行不會阻塞UI。第三是補充了前面所說的UI管控的目的,邏輯執行緒裡面JS在JS引擎中執行,而不是在Webview裡面,這樣就限制了通過注入JS程式碼來操作dom的可能,任何與UI相關的API都沒有辦法通過JS來改變,這樣就與DSL一起達到了UI管控的目的。第四個好處是多個小程式頁面共享同一個JS邏輯執行環境,可以方便高效的共享資料。
(圖6)上圖展示了邏輯層與檢視層的通訊過程,通訊通過Bridge中轉,利用釋出/訂閱模式。檢視層通過觸發UI事件,會把事件通過bridge傳遞到Native,Native再通過bridge把事件中轉給邏輯層,邏輯層處理事件完成後,把資料再通過bridge傳遞到Native,之後再由bridge返回給檢視層做渲染。
優化: 邏輯和渲染分離之後,邏輯執行緒需要把資料傳送給渲染執行緒,渲染執行緒需要把事件傳送給邏輯執行緒,這都需要序列化為字串進行傳輸。這樣會帶來一個問題,頻繁的資料傳輸,和單次大資料量傳輸都會帶來效能問題。針對這個問題,支付寶小程式的設計思路比較值得借鑑,支付寶小程式重新設計了V8虛擬機器,讓邏輯和渲染都有自己的Local Runtime,存放私有的模組和資料。又提供了共享的Global Runtime的Shared Heap來共享資料,這樣依然保證了邏輯和渲染的隔離,又減少了序列化和傳輸成本。
- 預載入:
小程式的開發者在小程式應用方面,做了很多優化,比如:資料的預載入。從使用者點選頁面,到新頁面onload(),會延遲100ms-300ms,這個延遲時間,可以做資料的預載入。這裡所說的小程式啟動預載入,是小程式渲染框架層面的。iOS的優化會預載入比實際渲染小程式數多一個wkwebview放在後臺,開啟新的小程式會直接把預先載入的wkwebview直接渲染,節省了初始化時間;Android上實現稍微複雜一點,不過依然是空間換時間的思路,從Android宿主應用啟動開始,就會啟動一個預留程式,當開啟新的小程式,會佔用這個程式,並再預載入新的程式,直到開啟第五個程式的上限。
- 離線包(分包):
離線包機制的根本目的在於讓小程式開啟的時候,可以讓頁面資源從網路IO替換為本地IO。其實就是在app開啟之前從網路拉取或者推拉結合,預置等方式讓離線包可以在開啟小程式之前就已經在本地了。離線包模組的職責包括:更新,解壓,儲存,讀取和校驗等,當然也可以做二進位制的差量包以。有了離線包機制,也要考慮把整個小程式整體作為一個離線包會影響效率的問題,所以這裡需要增加分包的方案,可以把離線包分為一個主包和多個子包的形式,主包裡面主要包括:首屏資源,公共程式碼,相關子包的資訊等;子包可以包含二級頁面的頁面資源。這樣就可以提高首屏開啟速度,可以做到按需載入的目的,如下圖:
(圖7)Ⅲ 技術選型
- IDE
小程式平臺都有自己的IDE,對於多系統平臺的現狀,選取跨平臺桌面技術開發小程式IDE,肯定是最好的選擇,這裡選擇了Electron和NW.JS做了一下對比:
(圖8)對比結果簡單的說,兩者開源協議都比較友好,如果重視程式碼安全性或者相容XP需求,就選擇NW.JS,也是國內廠商的選擇;如果從開發支援角度來比較,就選擇Electron。
- JS引擎
前面已經說了,邏輯層具有單獨的JS環境,也從管控角度說明了這樣做也可以防止js修改UI的風險。就技術選型角度來說,iOS可以使用自帶的JScore,雖然iOS上wkwebview的JS引擎比JScore多了JIT優化,執行速度快很多,但是比起額外引入js引擎來說,使用自帶js引擎具有穩定且不增加包大小的先天優勢。這塊可能有人會提到Flutter在iOS裡面引入了skia渲染引擎的問題,Flutter在iOS引入skia的好處是與Android自帶的skia引擎使用相同渲染引擎,這樣會在UI相容性上有更好的提升。而js引擎相容性問題就小的多。 Android方面,可選擇性多一下,以下是一個主流JS引擎對比:
(圖9)微信小程式舊版本用的JScore,新版本用的V8;支付寶小程式用的重新設計的V8;頭條小程式也是使用的V8;可以看到V8的中標率還是很高的,而且開源協議也比較友好。
Three 結語
本文算是介紹了一種小程式渲染架構的一種實現方式,就小程式平臺本身來說,還有一些其他的功能和優化點,比如:小程式路由,Debug包載入,埋點統計,虛擬Dom的優化等。文章只是介紹了一些主要流程和技術點,真正做一個完善的小程式平臺還是需要很多細節需要考慮的,就小程式開發者的角度來說,也是有優化空間的,比如:骨架屏。做一個小程式平臺需要多平臺多種技術能力的綜合應用才能不斷完善,隨著新技術的湧現,未來會有更多的技術應用到小程式中。