如何寫出一手好的小程序之多端架構篇

做爲微信小程序底層 API 維護者之一,經歷了風風雨雨、各類各樣的吐槽。爲了讓你們能更好的寫一手小程序,特意梳理一篇文章介紹。若是有什麼吐槽的地方,歡迎去 https://developers.weixin.qq.... 開發者社區吐槽。

PS: 老闆要找人,對本身有實力的前端er,能夠直接發簡歷到個人郵箱: villainthr@gmail.comhtml

簡述小程序的通訊體系

爲了你們能更好的開發出一些高質量、高性能的小程序,這裏帶你們理解一下小程序在不一樣端上架構體系的區分,更好的讓你們理解小程序一些特有的代碼寫做方式。前端

整個小程序開發生態主要能夠分爲兩部分:java

  • 桌面 nwjs 的微信開發者工具(PC 端)
  • 移動 APP 的正式運行環境

一開始的考慮是使用雙線程模型來解決安全和可控性問題。不過,隨着開發的複雜度提高,原有的雙線程通訊耗時對於一些高性能的小程序來講,變得有些不可接受。也就是每次更新 UI 都是經過 webview 來手動調用 API 實現更新。原始的基礎架構,能夠參考官方圖:node

官方架構

不過上面那張圖其實有點誤導行爲,由於,webview 渲染執行在手機端上實際上是內核來操做的,webview 只是內核暴露的一下 DOM/BOM 接口而已。因此,這裏就有一個性能突破點就是,JSCore 可否經過 Native 層直接拿到內核的相關接口?答案是能夠的,因此上面那種圖其實能夠簡單的再進行一下相關劃分,新的如圖所示:android

new_structure

簡單來講就是,內核改改,而後將規範的 webview 接口,選擇性的抽一份給 JsCore 調用。可是,有個限制是 Android 端比較自由,經過 V8 提供 plugin 機制能夠這麼作,而 IOS 上,蘋果爸爸是不容許的,除非你用的是 IOS 原生組件,這樣的話就會扯到同層渲染這個邏輯。其實他們的底層內容都是一致的。web

後面爲了你們能更好理解在小程序具體開發過程當中,手機端調試和在開發者工具調試的大體區分,下面咱們來分析一下二者各自的執行邏輯。canvas

tl;dr

  • 開發者工具 通訊體系 (只能採用雙向通訊) 即,全部指令都是經過 appservice <=> nwjs 中間層 <=> webview
  • Native 端運行的通訊體系:小程序

    • 小程序基礎通訊:雙向通訊-- ( core <=> webview <=> intermedia <=> appservice )
    • 高階組件通訊:單向通訊體系 ( appservice <= android/Swift => core)
  • JSCore 具體執行 appservice 的邏輯內容

開發者工具的通訊模式

一開始考慮到安全可控的緣由使用的是雙線程模型,簡單來講你的全部 JS 執行都是在 JSCore 中完成的,不管是綁定的事件、屬性、DOM操做等,都是。swift

開發者工具,主要是運行在 PC 端,它內部是使用 nwjs 來作,不過爲了更好的理解,這裏,直接按照 nwjs 的大體技術來說。開發者工具使用的架構是 基於 nwjs 來管理一個 webviewPool,經過 webviewPool 中,實現 appservice_webview 和 content_webview。微信小程序

因此在小程序上的一些性能難點,開發者工具上並不會構成很大的問題。好比說,不會有 canvas 元素上不能放置 div,video 元素不能設置自定義控件等。整個架構如圖:

圖片架構

當你打開開發者工具時,你第一眼看見的實際上是 appservice_webview 中的 Console 內容。

appservice_webview

content_webview 對外其實不必暴露出來,由於裏面執行的小程序底層的基礎庫和 開發者實際寫的代碼關係不大。你們理解的話,能夠就把顯示的 WXML 假想爲 content_webview。

content_webview

當你在實際預覽頁面執行邏輯時,都是經過 content_webview 把對應觸發的信令事件傳遞給 service_webview。由於是雙線程通訊,這裏只要涉及到 DOM 事件處理或者其餘數據通訊的都是異步的,這點在寫代碼的時候,其實很是重要。

若是在開發時,須要什麼困難,歡迎聯繫:開發者專區 | 微信開放社區

IOS/Android 協議分析

前面簡單瞭解了開發者工具上,小程序模擬的架構。而實際運行到手機上,裏面的架構設計可能又會有所不一樣。主要的緣由有:

  • IOS 和 Android 對於 webview 的渲染邏輯不一樣
  • 手機上性能瓶頸,JS 原始不適合高性能計算
  • video 等特殊元素上不能被其餘 div 覆蓋

一開始作小程序的雙線程架構和開發者工具比較相似,content_webview 控制頁面渲染,appservice 在手機上使用 JSCore 來進行執行。它的默認架構圖其實就是這個:

JSCore_content_webview

可是,隨着用戶量的滿滿增多,對小程序的指望也就越高:

  • 小程序的性能是被狗吃了麼?
  • 小程序打開速度能快一點麼?
  • 小程序的包大小爲何這麼小?

這些,咱們都知道,因此都在慢慢一點一點的優化。考慮到原生 webview 的渲染性能不好,組內大神 rex 提出了使用同層渲染來解決性能問題。這個辦法,不只搞定了 video 上不能覆蓋其餘元素,也提升了一下組件渲染的性能。

開發者在手機上具體開發時,對於某些 高階組件,像 video、canvas 之類的,須要注意它們的通訊架構和上面的雙線程通訊來講,有了一些本質上的區別。爲了性能,這裏底層使用的是原生組件來進行渲染。這裏的通訊成本其實就回歸到 native 和 appservice 的通訊。

爲了你們更好的理解 appservice 和 native 的關係,這裏順便簡單介紹一下 JSCore 的相關執行方法。

JSCore 深刻淺出

在 IOS 和 Android 上,都提供了 JSCore 這項工程技術,目的是爲了獨立運行 JS 代碼,並且還提供了 JSCore 和 Native 通訊的接口。這就意味着,經過 Native 調起一個 JSCore,能夠很好的實現 Native 邏輯代碼的平常變動,而不須要過度的依靠發版原本解決對應的問題,其實若是不是特別嚴謹,也能夠直接說是一種 "熱更新" 機制。

在 Android 和 IOS 平臺都提供了各自運行的 JSCore,在國內大環境下運行的工程庫爲:

  • Anroid: 國內平臺較爲分裂,不過因爲其使用的都是 Google 的 Android 平臺,因此,大部分都是基於 chromium 內核基礎上,加上中間層來實現的。在騰訊內部一般使用的是 V8 JSCore。
  • IOS: 在 IOS 平臺上,因爲是一整個生態閉源,在使用時,只能是基於系統內嵌的 webkit 引擎來執行,提供 webkit-JavaScriptCore 來完成。

這裏咱們主要以具備官方文檔的 webkit-JavaScriptCore 來進行講解。

JSCore 核心基礎

廣泛意義上的 JSCore 執行架構能夠分爲三部分 JSVirtualMachine、JSContext、JSValue。由這三者構成了 JSCore 的執行內容。具體解釋參考以下:

  • JSVirtualMachine: 它經過實例化一個 VM 環境來執行 js 代碼,若是你有多個 js 須要執行,就須要實例化多個 VM。而且須要注意這幾個 VM 之間是不能相互交互的,由於容易出現 GC 問題。
  • JSContext: jsContext 是 js代碼執行的上下文對象,至關於一個 webview 中的 window 對象。在同一個 VM 中,你能夠傳遞不一樣的 Context。
  • JSValue: 和 WASM 相似,JsValue 主要就是爲了解決 JS 數據類型和 swift 數據類型之間的相互映射。也就是說任何掛載在 jsContext 的內容都是 JSValue 類型,swift 在內部自動實現了和 JS 之間的類型轉換。

大致內容能夠參考這張架構圖:

JSCore

固然,除了正常的執行邏輯的上述是三個架構體外,還有提供接口協議的類架構。

  • JSExport: 它 是 JSCore 裏面,用來暴露 native 接口的一個 protocol。簡單來講,它會直接將 native 的相關屬性和方法,直接轉換成 prototype object 上的方法和屬性。

簡單執行 JS 腳本

使用 JSCore 能夠在一個上下文環境中執行 JS 代碼。首先你須要導入 JSCore:

import JavaScriptCore    //記得導入JavaScriptCore

而後利用 Context 掛載的 evaluateScript 方法,像 new Function(xxx) 同樣傳遞字符串進行執行。

let contet:JSContext = JSContext() // 實例化 JSContext

context.evaluateScript("function combine(firstName, lastName) { return firstName + lastName; }")

let name = context.evaluateScript("combine('villain', 'hr')")
print(name)  //villainhr

// 在 swift 中獲取 JS 中定義的方法
let combine = context.objectForKeyedSubscript("combine")

// 傳入參數調用:
// 由於 function 傳入參數實際上就是一個 arguemnts[fake Array],在 swift 中就須要寫成 Array 的形式
let name2 = combine.callWithArguments(["jimmy","tian"]).toString() 
print(name2)  // jimmytian

若是你想執行一個本地打進去 JS 文件的話,則須要在 swift 裏面解析出 JS 文件的路徑,並轉換爲 String 對象。這裏能夠直接使用 swift 提供的系統接口,Bundle 和 String 對象來對文件進行轉換。

lazy var context: JSContext? = {
  let context = JSContext()
  
  // 1
  guard let
    commonJSPath = Bundle.main.path(forResource: "common", ofType: "js") else { // 利用 Bundle 加載本地 js 文件內容
      print("Unable to read resource files.")
      return nil
  }
  
  // 2
  do {
    let common = try String(contentsOfFile: commonJSPath, encoding: String.Encoding.utf8) // 讀取文件
    _ = context?.evaluateScript(common) // 使用 evaluate 直接執行 JS 文件
  } catch (let error) {
    print("Error while processing script file: \(error)")
  }
  
  return context
}()

JSExport 接口的暴露

JSExport 是 JSCore 裏面,用來暴露 native 接口的一個 protocol,可以使 JS 代碼直接調用 native 的接口。簡單來講,它會直接將 native 的相關屬性和方法,直接轉換成 prototype object 上的方法和屬性。

那在 JS 代碼中,如何執行 Swift 的代碼呢?最簡單的方式是直接使用 JSExport 的方式來實現 class 的傳遞。經過 JSExport 生成的 class,實際上就是在 JSContext 裏面傳遞一個全局變量(變量名和 swift 定義的一致)。這個全局變量其實就是一個原型 prototype。而 swift 其實就是經過 context?.setObject(xxx) API ,來給 JSContext 導入一個全局的 Object 接口對象。

那應該如何使用該 JSExport 協議呢?

首先定義須要 export 的 protocol,好比,這裏咱們直接定義一個分享協議接口:

@objc protocol WXShareProtocol: JSExport {
    
    // js調用App的微信分享功能 演示字典參數的使用
    func wxShare(callback:(share)->Void)
    
    // setShareInfo
    func wxSetShareMsg(dict: [String: AnyObject])

    // 調用系統的 alert 內容
    func showAlert(title: String,msg:String)
}

在 protocol 中定義的都是 public 方法,須要暴露給 JS 代碼直接使用的,沒有在 protocol 裏面聲明的都算是 私有 屬性。接着咱們定義一下具體 WXShareInface 的實現:

@objc class WXShareInterface: NSObject, WXShareProtocol {
    
    weak var controller: UIViewController?
    weak var jsContext: JSContext?
    var shareObj:[String:AnyObject]
    
    func wxShare(_ succ:()->{}) {
        // 調起微信分享邏輯
        //...

        // 成功分享回調
        succ()
    }

    func setShareMsg(dict:[String:AnyObject]){
        self.shareObj = ["name":dict.name,"msg":dict.msg]
        // ...
    }

    func showAlert(title: String, message: String) {
        
        let alert = AlertController(title: title, message: message, preferredStyle: .Alert)
        // 設置 alert 類型
        alert.addAction(AlertAction(title: "肯定", style: .Default, handler: nil))
        // 彈出消息
        self.controller?.presentViewController(alert, animated: true, completion: nil)
    }
    
    // 當用戶內容改變時,觸發 JS 中的 userInfoChange 方法。
    // 該方法是,swift 中私有的,不會保留給 JSExport
    func userChange(userInfo:[String:AnyObject]) {
        let jsHandlerFunc = self.jsContext?.objectForKeyedSubscript("\(userInfoChange)")
        let dict = ["name": userInfo.name, "age": userInfo.age]
        jsHandlerFunc?.callWithArguments([dict])
    }
}

類是已經定義好了,可是咱們須要將當前的類和 JSContext 進行綁定。具體步驟是將當前的 Class 轉換爲 Object 類型注入到 JSContext 中。

lazy var context: JSContext? = {

  let context = JSContext()
  let shareModel = WXShareInterface()

  do {
   
    // 注入 WXShare Class 對象,以後在 JSContext 就能夠直接經過 window.WXShare 調用 swift 裏面的對象
    context?.setObject(shareModel, forKeyedSubscript: "WXShare" as (NSCopying & NSObjectProtocol)!)
  } catch (let error) {
    print("Error while processing script file: \(error)")
  }

  return context
}()

這樣就完成了將 swift 類注入到 JSContext 的步驟,餘下的只是調用問題。這裏主要考慮到你 JS 執行的位置。好比,你能夠直接經過 JSCore 執行 JS,或者直接將 JSContext 和 webview 的 Context 綁定在一塊兒。

直接本地執行 JS 的話,咱們須要先加載本地的 js 文件,而後執行。如今本地有一個 share.js 文件:

// share.js 文件
WXShare.setShareMsg({
    name:"villainhr",
    msg:"Learn how to interact with JS in swift"
});

WXShare.wxShare(()=>{
    console.log("the sharing action has done");
})

而後,咱們須要像以前同樣加載它並執行:

// swift native 代碼
// swift 代碼
func init(){
    guard 
    let shareJSPath = Bundle.main.path(forResource:"common",ofType:"js") else{
        return
    }
    
    do{    
        // 加載當前 shareJS 並使用 JSCore 解析執行
        let shareJS = try String(contentsOfFile: shareJSPath, encoding: String.Encoding.utf8)
        self.context?.evaluateScript(shareJS)
    } catch(let error){
        print(error)
    }
    
}

若是你想直接將當前的 WXShareInterface 綁定到 Webview Context 中的話,前面實例的 Context 就須要直接修改成 webview 的 Context。對於 UIWebview 能夠直接得到當前 webview 的Context,可是 WKWebview 已經沒有了直接獲取 context 的接口,wkwebview 更推崇使用前文的 scriptMessageHandler 來作 jsbridge。固然,獲取 wkwebview 中的 context 也不是沒有辦法,能夠經過 KVO 的 trick 方式來拿到。

// 在 webview 加載完成時,注入相關的接口
func webViewDidFinishLoad(webView: UIWebView) {
    
    // 加載當前 View 中的 JSContext
    self.jsContext = webView.valueForKeyPath("documentView.webView.mainFrame.javaScriptContext") as! JSContext
    let model = WXShareInterface()
    model.controller = self
    model.jsContext = self.jsContext
    
    // 將 webview 的 jsContext 和 Interface  綁定
    self.jsContext.setObject(model, forKeyedSubscript: "WXShare")
    
    // 打開遠程 URL 網頁
    // guard let url = URL(string: "https://www.villainhr.com") else {
       // return 
    //}


    // 若是沒有加載遠程 URL,能夠直接加載
    // let request = URLRequest(url: url)
    // webView.load(request)

    // 在 jsContext 中直接以 html 的形式解析 js 代碼
    // let url = NSBundle.mainBundle().URLForResource("demo", withExtension: "html")
    // self.jsContext.evaluateScript(try? String(contentsOfURL: url!, encoding: NSUTF8StringEncoding))
    

    // 監聽當前 jsContext 的異常
    self.jsContext.exceptionHandler = { (context, exception) in
        print("exception:", exception)
    }
}

而後,咱們能夠直接經過上面的 share.js 調用 native 的接口。

原生組件的通訊

JSCore 實際上就是在 native 的一個線程中執行,它裏面沒有 DOM、BOM 等接口,它的執行和 nodeJS 的環境比較相似。簡單來講,它就是 ECMAJavaScript 的解析器,不涉及任何環境。

在 JSCore 中,和原生組件的通訊其實也就是 native 中兩個線程之間的通訊。對於一些高性能組件來講,這個通訊時延已經減小不少了。

那兩個之間通訊,是傳遞什麼呢?

就是 事件,DOM 操做等。在同層渲染中,這些信息其實都是內核在管理。因此,這裏的通訊架構其實就變爲:

通訊架構

Native Layer 在 Native 中,能夠經過一些手段可以在內核中設置 proxy,能很好的捕獲用戶在 UI 界面上觸發的事件,這裏因爲涉及太深的原生知識,我就不過多介紹了。簡單來講就是,用戶的一些 touch 事件,能夠直接經過 內核暴露的接口,在 Native Layer 中觸發對應的事件。這裏,咱們能夠大體理解內核和 Native Layer 之間的關係,可是實際渲染的 webview 和內核有是什麼關係呢?

在實際渲染的 webview 中,裏面的內容實際上是小程序的基礎庫 JS 和 HTML/CSS 文件。內核經過執行這些文件,會在內部本身維護一個渲染樹,這個渲染樹,其實和 webview 中 HTML 內容一一對應。上面也說過,Native Layer 也能夠和內核進行交互,但這裏就會存在一個 線程不安全的現象,有兩個線程同時操做一個內核,極可能會形成泄露。因此,這裏 Native Layer 也有一些限制,即,它不能直接操做頁面的渲染樹,只能在已有的渲染樹上去作節點類型的替換。

最後總結

這篇文章的主要目的,是讓你們更加了解一下小程序架構模式在開發者工具和手機端上的不一樣,更好的開發出一些高性能、優質的小程序應用。這也是小程序中心一直在作的事情。最後,總結一下前面將的幾個重要的點:

  • 開發者工具只有雙線程架構,經過 appservice_webview 和 content_webview 的通訊,實現小程序手機端的模擬。
  • 手機端上,會根據組件性能要求的不能對應優化使用不一樣的通訊架構。

    • 正常 div 渲染,使用 JSCore 和 webview 的雙線程通訊
    • video/map/canvas 等高階組件,一般是利用內核的接口,實現同層渲染。通訊模式就直接簡化爲 內核 <=> Native <=> appservice。(速度賊快)
因爲工做太忙,社區不多會上,這裏推薦你們,關注個人微信公衆號 《前端小吉米》,公衆號通常會及時更新

參考:

教程 | 《小程序開發指南》

相關文章
相關標籤/搜索