從客戶端角度窺探小程序架構 | 掘金年度徵文

目錄

1、說在前面

2、從微信小程序的發展史提及

3、微信小程序原理分析

  • 快速加載和原生的體驗
  • 渲染層
  • 預加載
  • 基礎庫內部優化
  • 注入小程序WXML結構和WXSS樣式
  • 邏輯層

4、看看JavaScriptCore是怎麼執行JS腳本的

5、再說說支付寶小程序

  • 運行時架構
  • 小程序SDK

6、最後

1、說在前面:

小程序自誕生以來。就以一種百家爭鳴的姿態展示在開發者的面前。繼2017年1月9日微信小程序誕生後,小程序市場又陸續出現了支付寶小程序、頭條小程序、百度智能小程序等等,甚至平安內部,也在發展本身的小程序生態。各家都在微信小程序的基礎上,面向本身的業務,對架構進行逐步優化調整,可是萬變不離其宗,微信小程序終歸爲小程序鼻祖,也是得益於微信小程序的思想,才造就了現在這百花齊放的業態。提及微信小程序,在體驗上的優化,讓我很長一段時間認爲,這是 Native 層渲染。事實並不徹底是,至今不敢相信,webView 的渲染竟能帶來如此體驗。本篇主要以一個客戶端開發者的角度,來對微信小程序、支付寶小程序一探究竟。本篇旨在原理分析,我並未有真實的小程序架構設計經驗。html

說到小程序,不得不須要指出另一個問題,蘋果爸爸對於 HTML5 app 的更新的審覈問題,目前會有開發者存在這樣的疑問,Hybrid 和 H5 是否是要被蘋果拒審了呢?其實從更新描述來看,不難發現蘋果的主要目的是針對「核心功能未在二進制文件內」的 App ,實際上小程序不管是在設計理念上,仍是核心技術上,都不存在這樣的問題,小程序並不是App,小程序是以 App 爲載體,儘量的對 web 頁面進行優化而生成的產物。還有一點是馬甲包日益猖獗,馬甲包最後基本都轉化成爲了條款內描述的「現金Bocai、彩票抽獎和慈善捐款」類型,因此蘋果想要儘量的禁止它。並且從微信小程序開發文檔來看,微信小程序是典型的技術推進產品的結果。關於RN類技術,更不存在這樣的問題了,RN本質爲 JS 經過 JSCore 調用 Native 組件。實際上它的核心仍然在 Native 端,固然對 code push 我還尚存疑問。關於 RN 的動態更新上,從bang's的描述也不難發現蘋果爸爸的態度,只要不是爲了繞過審覈去作動態更新就能夠接受前端

2、從微信小程序的發展史提及

微信小程序是什麼,微信把小程序定義爲是一種全新的鏈接用戶與服務的方式,它能夠在微信內被便捷地獲取和傳播,同時具備出色的使用體驗。便捷和出色有何而來?小程序技術最初來源於 H5 和 Native 間的簡單調用,微信構建了一個 WeixinJSBridge 來爲H5提供一些 Native 的功能,例如地圖、播放器、定位、拍照、預覽等功能。關於 Bridge 的具體實現能夠參考《寫一個易於維護使用方便性能可靠的Hybrid框架》。可是微信逐漸的又遇到了另一個問題,那就是 H5 頁面的體驗問題,微信團隊爲了解決 H5 頁面的白屏問題,他們引入了最近很火的離線包概念,固然微信稱之爲微信 Web 資源離線存儲,其實是一個東西。Web 開發者可藉助微信提供的資源存儲能力,直接從微信本地加載 Web 資源而不須要再從服務端拉取,從而減小網頁的加載時間。關於離線包的概念,不瞭解的話能夠參考《h5離線技術原理》 。可是當頁面加載大量 CSS 和 JS 時,依然會有白屏問題,包括 H5 頁面點擊事件的遲鈍感和頁面跳轉的體驗問題。那麼基於此問題,應運而生的,小程序技術就誕生了。web

從微信小程序的發展史,不難看出,小程序其實是近幾年開發者對 H5 體驗優化而來的,這也切合了前面所說的,小程序其實是典型的技術推進產品的結果json

3、微信小程序原理分析

微信小程序自稱可以解決如下問題:小程序

  • 快速的加載。
  • 更強大的能力。
  • 原生的體驗。
  • 易用且安全的微信數據開發。
  • 高效和簡單的開發。

快速加載和原生的體驗,這其實都是在體驗上的升級,更強大能力實際上源於微信小程序爲開發者提供了大量的組件,這些組件有基於web技術,也有基於Native技術,在我看來這和 RN 技術不謀而合。後面我會舉一個模仿 RN 實現的小例子來闡述一下它的原理。微信小程序

高效和簡單的開發是由於微信小程序開發語言實質上仍是基於 web 開發規範,這使得開發前端的人來開發小程序顯得更容易。數組

還有一點更重要的就是安全,爲何說小程序是安全的?後面會逐步展開,揭開小程序的神祕面紗。瀏覽器

快速加載和原生的體驗

小程序的架構設計與 web 技術仍是有必定的差異,吸收了 web 技術的一些優點,也摒棄了 web 技術中體驗很差的地方。最主要的特色就是小程序採用雙線程機制,即視圖渲染和業務邏輯分別運行在不一樣的線程中。在傳統的 web 開發中,網頁開發渲染線程和腳本線程是互斥的,因此 H5 頁面中長時間的腳本運行可能會致使頁面失去響應或者白屏,體驗糟糕。緩存

爲了更好的體驗,將頁面渲染線程和腳本線程分開執行:安全

  • 渲染層:界面渲染相關的邏輯所有 在webView 線程內執行,一個小程序存在多個頁面,一個頁面對應一個 webView,微信小程序限制開發者最多隻能建立五個頁面。
  • 邏輯層:Android採用 JSCore ,iOS採用的 JavaScriptCore 框架運行 JS 腳本。怎麼在 JavaScriptCore 運行腳本文件後面會講。

雙線程模型是小程序框架與大多數前端 web 框架的不一樣之處,基於這個模型能夠更好的管控以及提供更安全的環境。由於邏輯層運行在 JSCore 中,並無一個完整瀏覽器對象,於是缺乏相關的DOM API和BOM API。客戶端的開發者可能對 DOM 有些陌生,瞭解編譯過程的同窗應該知道在編譯器編譯代碼的時候,會有一個語法分析的過程,生成抽象語法樹 AST,編譯器會根據語法樹去檢查表達式是否合法、括號是否匹配等。實際上DOM也是一種樹結構,通過瀏覽器的解析,最終呈如今用戶面前。經過 JavaScript 操縱 DOM 能夠隨意改變元素的位置,這對於小程序來講是極爲不安全的。因此說邏輯層爲小程序帶來的另外一個特色,易於管控和安全。線程通訊基於前面提到的 WeixinJSBridge :邏輯層把數據變化通知到視圖層,觸發視圖層頁面的更新,視圖層把觸發的事件通知到邏輯層進行業務處理。

當咱們對渲染層進行事件操做後,會經過 WeixinJSBridge 將數據傳遞到 Native 系統層。Native 系統層決定是否要用 Native 處理,而後丟給 邏輯層進行用戶的邏輯代碼處理。邏輯層處理完畢後會將數據經過 WeixinJSBridge 返給 View 層,View 渲染更新視圖。

渲染層

根據《微信小程序開發者文檔》描述,在視圖層內,小程序的每個頁面都獨立運行在一個頁面層級上。小程序啓動時僅有一個頁面層級,每次調用wx.navigateTo,都會建立一個新的頁面層級;相對地,wx.navigateBack會銷燬一個頁面層級。大概能夠理解爲,每一個 web 頁面都是運行在單獨的 webView 裏面,這樣的好處就是讓每一個 webView 單純的處理當前頁面的渲染邏輯,不須要加載其餘頁面的邏輯代碼,減輕負擔可以加速頁面渲染,使其可以儘量的接近原生,這與小程序跳轉頁面的體驗上也是一致的。

實際上在小程序源碼內有一個 index.html 文件的存在,這是小程序啓動後的入口文件。初次加載的時候,主入口會加載相應的 webView ,這其中就會包括前面所提到的,視圖層和邏輯層。邏輯層雖然也提供了 webView ,可是並不提供瀏覽器相關接口,而是單純的爲了獲取當前的 JSCore ,執行相關的 JS 腳本文件,這也是開發小程序是沒辦法直接操做 DOM 的根本緣由。

當咱們每打開一個新頁面的時候,調用 navigateTo 都至關於打開了一個新的 webView ,這樣一直打開,內存也會變得吃緊,這也是爲何小程序對頁面打開數量有限制的緣由了。

預加載

根據小程序開發文檔描述:對於每個新的頁面層級,視圖層都須要進行一些額外的準備工做。在小程序啓動前,微信會提早準備好一個頁面層級用於展現小程序的首頁。除此之外,每當一個頁面層級被用於渲染頁面,微信都會提早開始準備一個新的頁面層級,使得每次調用wx.navigateTo都可以儘快展現一個新的頁面。這在客戶端的角度來看,至關於打開新頁面以後,對下一個頁面的 webView 提早作了預加載,這個思路與當前比較流行的 webView 緩存池的思路不謀而合,緣由是在 iOS 和 Android 系統上,操做系統啓動 webView 都須要一小段時間,預加載會提高頁面打開速度,優化白屏問題。

基礎庫內部優化

再往深層次來看,經過小程序開發工具的源碼,能找到一個 pageframe.html 的模版文件,具體位置在package.nw/html/pageframe.html

看標題就應該很清楚了,這是渲染層的核心模塊,它的做用就是爲小程序準備一個新的頁面,小程序每一個視圖層頁面內容都是經過 pageframe.html 模板來生成的,包括小程序啓動的首頁。經過查看源碼,裏面定義了一個屬性var __webviewId__,我猜測這是每一個 webView 頁面的頁面 ID ,邏輯層處理多個視圖層間的業務邏輯可能就是經過這個ID來作的映射關係。在首次啓動時,後臺會緩存生成的 pageframe.html 模版,在後面的頁面打開時,直接加載緩存的 pageframe.html 模版,頁面引入的資源文件也能夠直接在緩存中加載,包括小程序基礎庫視圖層底層、頁面的模版信息、配置信息以及樣式等內容,這樣避免重複生成,快速打開頁面,提高頁面渲染性能。

注入小程序WXML結構和WXSS樣式

關於 pageframe.html 最後是怎麼生成相應頁面的歸功於一個叫 nw.js 的框架,具體實現這裏就不講了(主要是我不會)。

邏輯層

上面瞭解了渲染層都作了什麼以後,下面在窺探一下,小程序的邏輯層都作了什麼。參考eux.baidu.com/blog/fe/微信小…不難發現,sevice 層的代碼是由 WAService.js 實現的,邏輯層實際上主要提供了 Page, App,GetApp 接口和更爲豐富 wx 接口模塊,包括數據綁定、事件分發、生命週期管理、路由管理等等。關於視圖層和邏輯層間的具體交互細節能夠看下這張圖:

咱們寫的頁面邏輯最後都被引入到了一個叫 appservice.html 的頁面中,而且分別從 app.js 開始一一執行;小程序代碼調用 Page 構造器的時候,小程序基礎庫會記錄頁面的基礎信息,如初始數據(data)、方法等。須要注意的是,若是一個頁面被屢次建立,並不會使得這個頁面所在的JS文件被執行屢次,而僅僅是根據初始數據多生成了一個頁面實例(this),在頁面 JS 文件中直接定義的變量,在全部這個頁面的實例間是共享的。對於邏輯層,從客戶端的角度看,咱們應該更關注於邏輯層的JS是怎麼注入到JSCore中的。

4、看看JavaScriptCore是怎麼執行JS腳本的

說到JavaScriptCore,咱們先來討論下Hybrid App 的構建思路,Hybird App是指混合模式移動應用,即其中既包含原生的結構又有內嵌有 Web 的組件。這種 App 不只性能和用戶體驗能夠達到和原生所差無幾的程度,更大的優點在於 bug 修復快,版本迭代無需發版。Hybird App的實質並無修改原生 Native 的行爲,而是將下發的資源進行加載和界面渲染,相似 WebView。下面經過一個例子來模擬一下 JavaScriptCore 執行 JS 腳原本讓 Native 和 JS 之間的通訊。關於 JavaScriptCore 的具體使用能夠參考下戴銘的《深刻剖析 JavaScriptCore》

咱們打算實現這樣的功能:經過下發JS腳本建立原生的 UILabel 和 UIButton 控件並響應事件,首先編寫 JS 代碼以下:

(function(){
 console.log("ProgectInit");
 //JS腳本加載完成後 自動render界面
 return render();
 })();

//JS標籤類
function Label(rect,text,color){
    this.rect = rect;
    this.text = text;
    this.color = color;
    this.typeName = "Label";
}
//JS按鈕類
function Button(rect,text,callFunc){
    this.rect = rect;
    this.text = text;
    this.callFunc = callFunc;
    this.typeName = "Button";
}
//JS Rect類
function Rect(x,y,width,height){
    this.x = x;
    this.y = y;
    this.width = width;
    this.height = height;
}
//JS顏色類
function Color(r,g,b,a){
    this.r = r;
    this.g = g;
    this.b = b;
    this.a = a;
}
//渲染方法 界面的渲染寫在這裏面
function render(){
    var rect = new Rect(20,100,280,30);
    var color = new Color(1,0,0,1);
    var label = new Label(rect,"這是一個原生的Label",color);

    var rect4 = new Rect(20,150,280,30);
    var button = new Button(rect4,"這是一個原生的Button",function(){
                            var randColor = new Color(Math.random(),Math.random(),Math.random(),1);
                            TestBridge.changeBackgroundColor(randColor);
                            });
    //將控件以數組形式返回
    return [label,button];
}
複製代碼

建立一個 OC 類 TestBridge 綁定到 JavaScriptCore 全局對象上:

@protocol TestBridgeProtocol <JSExport>
- (void)changeBackgroundColor:(JSValue *)value;
@end

@interface TestBridge : NSObject<TestBridgeProtocol>

@property(nonatomic, weak) UIViewController *ownerController;

@end
複製代碼
#import "TestBridge.h"

@implementation TestBridge

- (void)changeBackgroundColor:(JSValue *)value{
    self.ownerController.view.backgroundColor = [UIColor colorWithRed:value[@"r"].toDouble green:value[@"g"].toDouble blue:value[@"b"].toDouble alpha:value[@"a"].toDouble];
}

@end
複製代碼

在 ViewController 中實現一個界面渲染的 render 解釋方法:

#import "ViewController.h"
#import <JavaScriptCore/JavaScriptCore.h>
#import "TestBridge.h"

@interface ViewController ()

@property(nonatomic, strong)JSContext *jsContext;
@property(nonatomic, strong)NSMutableArray *actionArray;
@property(nonatomic, strong)TestBridge *bridge;

@end

@implementation ViewController

- (void)viewDidLoad {
    [super viewDidLoad];
    //建立JS運行環境
    self.jsContext = [JSContext new];
    //綁定橋接器
    self.bridge =  [TestBridge new];
    self.bridge.ownerController = self;
    self.jsContext[@"TestBridge"] = self.bridge;
    self.actionArray = [NSMutableArray array];
    [self render];
}

-(void)render{
    NSString * path = [[NSBundle mainBundle] pathForResource:@"main" ofType:@"js"];
    NSData * jsData = [[NSData alloc]initWithContentsOfFile:path];
    NSString * jsCode = [[NSString alloc]initWithData:jsData encoding:NSUTF8StringEncoding];
    JSValue * jsVlaue = [self.jsContext evaluateScript:jsCode];
    for (int i=0; i<jsVlaue.toArray.count; i++) {
        JSValue * subValue = [jsVlaue objectAtIndexedSubscript:i];
        if ([[subValue objectForKeyedSubscript:@"typeName"].toString isEqualToString:@"Label"]) {
            UILabel * label = [UILabel new];
            label.frame = CGRectMake(subValue[@"rect"][@"x"].toDouble, subValue[@"rect"][@"y"].toDouble, subValue[@"rect"][@"width"].toDouble, subValue[@"rect"][@"height"].toDouble);
            label.text = subValue[@"text"].toString;
            label.textColor = [UIColor colorWithRed:subValue[@"color"][@"r"].toDouble green:subValue[@"color"][@"g"].toDouble blue:subValue[@"color"][@"b"].toDouble alpha:subValue[@"color"][@"a"].toDouble];
            label.textAlignment = NSTextAlignmentCenter;
            [self.view addSubview:label];
        }else if ([[subValue objectForKeyedSubscript:@"typeName"].toString isEqualToString:@"Button"]){
            UIButton * button = [UIButton buttonWithType:UIButtonTypeSystem];
            button.frame = CGRectMake(subValue[@"rect"][@"x"].toDouble, subValue[@"rect"][@"y"].toDouble, subValue[@"rect"][@"width"].toDouble, subValue[@"rect"][@"height"].toDouble);
            [button setTitle:subValue[@"text"].toString forState:UIControlStateNormal];
            button.tag = self.actionArray.count;
            [button addTarget:self action:@selector(buttonAction:) forControlEvents:UIControlEventTouchUpInside];
            [self.actionArray addObject:subValue[@"callFunc"]];
            [self.view addSubview:button];
            
        }
    }
}

-(void)buttonAction:(UIButton *)btn{
    JSValue * action  = self.actionArray[btn.tag];
    [action callWithArguments:nil];
}

@end
複製代碼

這樣就完成了一個簡單的 JS 腳本注入,實際上執行後的樣子是這樣的:

這就是一個簡單的執行 JS 腳本的邏輯,實際上 ReactNative 的原理也是基於此,小程序邏輯層與微信客戶端的交互邏輯也是基於此。

到這裏,關於微信小程序渲染層與邏輯層作了什麼、怎麼作的、優化了什麼以及爲何要採用這樣的架構來設計,基本都梳理完畢了。小程序這樣的分層設計顯然是有意爲之的,它的中間層徹底控制了程序對於界面進行的操做, 同時對於傳遞的數據和響應時間也作到的監控。一方面程序的行爲受到了極大限制, 另外一方面微信能夠確保他們對於小程序內容和體驗有絕對的控制。咱們在小程序的 JS 代碼裏面是不能直接使用瀏覽器提供的 DOM 和 BOM 接口的,這一方面是由於 JS 代碼外層使用了局部變量進行屏蔽,另外一方面即使咱們能夠操做 DOM 和 BOM 接口,它們對應的也是邏輯層模塊,並不會對頁面產生影響。這樣的結構也說明了小程序的動畫和繪圖 API 被設計成生成一個最終對象而不是一步一步執行的樣子, 緣由就是 json 格式的數據傳遞和解析相比與原生 API 都是損耗不菲的,若是頻繁調用極可能損耗 過多性能,進而影響用戶體驗。

總結一句話就是webView渲染,JSCore處理邏輯,JSBridge作線程通訊。後面再簡要的分析下支付寶小程序,支付寶小程序屬於後起之秀,支付寶小程序在微信小程序的基礎上,作了一些優化,單從技術角度來看,有點後來者居上的意思。目前支付寶技術經過官方的媒體帳號對外暴漏的一些實現細節也在逐步增多。

6、再說說支付寶小程序

前端框架下面是小程序 native 引擎,包括了小程序容器、渲染引擎和 JavaScript 引擎,這塊主要是把客戶端 native 的能力和前端框架結合起來,給開發者提供系統底層能力的接口。在渲染引擎上面,支付寶小程序不只提供 JavaScript+Webview 的方式,還提供 JavaScript+Native 的方式,在對性能要求較高的場景,能夠選擇 Native 的渲染模式,給用戶更好的體驗。

這段文字來源於支付寶對外開放的技術博客的描述,從這段描述中,咱們可以發現支付寶小程序在架構設計上一樣採用的渲染引擎加 JavaScript 引擎兩部分,包括頁面間的切換實際上和微信小程序邏輯基本一致。下面這張是支付寶小程序應用框架的架構圖:

運行時架構

單從這個運行時架構來看,它與微信小程序不一樣的地方是,webView 頁面也就是渲染層經過消息服務直接與邏輯層進行通信,而不須要像微信的 JSBridge 那樣做爲中間層,消息服務具體實現細節目前尚不得知。支付寶的JSBridge只會與邏輯層進行通信,來給小程序提供一些 Native 能力。支付寶的這種架構主要目的是解決渲染層與邏輯層交互的對象較複雜、數據量較大時,交互的性能比較差的問題。支付寶小程序的設計思路比較值得借鑑,微信小程序線程間的通信是經過 JSBridge ,序列化 json 進行傳遞的。支付寶小程序從新設計了V8虛擬機,讓邏輯和渲染都有本身的 Local Runtime,存放私有的模塊和數據。在渲染層和邏輯層交互時,setData 的 對象會直接建立在 Shared Heap 裏面,所以渲染層的 Local Runtime 能夠直接讀到該對象,而且用於 render 層的渲染,保證了邏輯和渲染的隔離,又減小了序列化和傳輸成本。固然支付寶還有些其餘的優化,包括首頁離線緩存,緩存時機的處理以及閃屏處理等等問題,這裏就再也不延伸討論了(由於不少細節我也不知道😂)。

小程序SDK

根據支付寶小程序對外開放的技術文章來看,架構設計仍是很是巧妙的,也很值得咱們學習,先看圖:

參考: 《獨家!支付寶小程序技術架構全解析》

小程序SDK在架構設計上把它分爲了兩部分,一部分是核心庫基礎引擎,一部分是基於基礎庫開發的插件功能。從上往下看:

  • 第一層小程序層,這是小程序開發者使用小程序 DSL 及各類組件開發的代碼層。
  • 第二層和第三層架應該是小程序內部封裝的一些組件和對外提供的相關API等。
  • 第四層和第五層是基於 React 框架,構建的小程序運行基礎框架,這是小程序的核心層,主要包含小程序的邏輯處理引擎及渲染層。支付寶基於 ReactNative 增長了 Native 引擎,能夠用原生來渲染 UI 。根據支付寶 mPaaS 的介紹來看,目前支付寶的小程序使用的是 React 版,螞蟻內部的其餘 App 有在使用 React Native 版的小程序。
  • 基礎組件部分和擴展能力部分更像是基於 Bridge 調用的原生能力。擴展能力應該是支付寶內部的一些基礎組件,同樣經過JSBridge給小程序進行賦能。

支付寶小程序架構設計上採用分層的設計,邏輯很是清晰。在管控上,和微信小程序基本一致,使用本身的一套 DSL 來保證它的管控能力,編寫小程序只能使用框架提供的自定義的模板樣式,既保證了安全性,又解決了H5開發質量良莠不齊的問題。

6、最後

差很少半年多沒有寫文章了,這一年幾乎把全部的精力都撲在了公司的業務上,趁着公司年會時間稍顯充裕,對當前的小程序架構進行了下分析和總結,順便參加下掘金的徵文活動。固然,真正的小程序應該比這還要複雜的多,小程序其實是多年來大前端融合的一個結果,是一套很是成體系的技術方案,是技術推進產品而產生的概念。看了這麼多我想你對小程序也有了初步認識,小程序的核心實際上仍是渲染層邏輯層的構建,那麼若是讓你開發一套小程序SDK,你會怎樣設計它們呢?

掘金年度徵文 | 2019 與個人技術之路 徵文活動正在進行中......

相關文章
相關標籤/搜索