小程序模板網(wǎng)

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

發(fā)布時間:2018-10-22 11:21 所屬欄目:小程序開發(fā)教程

作為微信小程序底層 API 維護者之一,經(jīng)歷了風風雨雨、各種各樣的吐槽。為了讓大家能更好的寫一手小程序,特地梳理一篇文章介紹。如果有什么吐槽的地方,歡迎去 https://developers.weixin.qq.... 開發(fā)者社區(qū)吐槽。

PS: 老板要找人,對自己有實力的前端er,可以直接發(fā)簡歷到我的郵箱: villainthr@gmail.com

簡述小程序的通信體系

為了大家能更好的開發(fā)出一些高質(zhì)量、高性能的小程序,這里帶大家理解一下小程序在不同端上架構體系的區(qū)分,更好的讓大家理解小程序一些特有的代碼寫作方式。

整個小程序開發(fā)生態(tài)主要可以分為兩部分:

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

一開始的考慮是使用雙線程模型來解決安全和可控性問題。不過,隨著開發(fā)的復雜度提升,原有的雙線程通信耗時對于一些高性能的小程序來說,變得有些不可接受。也就是每次更新 UI 都是通過 webview 來手動調(diào)用 API 實現(xiàn)更新。原始的基礎架構,可以參考官方圖:

不過上面那張圖其實有點誤導行為,因為,webview 渲染執(zhí)行在手機端上其實是內(nèi)核來操作的,webview 只是內(nèi)核暴露的一下 DOM/BOM 接口而已。所以,這里就有一個性能突破點就是,JSCore 能否通過 Native 層直接拿到內(nèi)核的相關接口?答案是可以的,所以上面那種圖其實可以簡單的再進行一下相關劃分,新的如圖所示:

簡單來說就是,內(nèi)核改改,然后將規(guī)范的 webview 接口,選擇性的抽一份給 JsCore 調(diào)用。但是,有個限制是 Android 端比較自由,通過 V8 提供 plugin 機制可以這么做,而 IOS 上,蘋果爸爸是不允許的,除非你用的是 IOS 原生組件,這樣的話就會扯到同層渲染這個邏輯。其實他們的底層內(nèi)容都是一致的。

后面為了大家能更好理解在小程序具體開發(fā)過程中,手機端調(diào)試和在開發(fā)者工具調(diào)試的大致區(qū)分,下面我們來分析一下兩者各自的執(zhí)行邏輯。

tl;dr

  • 開發(fā)者工具 通信體系 (只能采用雙向通信) 即,所有指令都是通過 appservice <=> nwjs 中間層 <=> webview
  • Native 端運行的通信體系:

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

開發(fā)者工具的通信模式

一開始考慮到安全可控的原因使用的是雙線程模型,簡單來說你的所有 JS 執(zhí)行都是在 JSCore 中完成的,無論是綁定的事件、屬性、DOM操作等,都是。

開發(fā)者工具,主要是運行在 PC 端,它內(nèi)部是使用 nwjs 來做,不過為了更好的理解,這里,直接按照 nwjs 的大致技術來講。開發(fā)者工具使用的架構是 基于 nwjs 來管理一個 webviewPool,通過 webviewPool 中,實現(xiàn) appservice_webview 和 content_webview。

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

當你打開開發(fā)者工具時,你第一眼看見的其實是 appservice_webview 中的 Console 內(nèi)容。

content_webview 對外其實沒必要暴露出來,因為里面執(zhí)行的小程序底層的基礎庫和 開發(fā)者實際寫的代碼關系不大。大家理解的話,可以就把顯示的 WXML 假想為 content_webview。

當你在實際預覽頁面執(zhí)行邏輯時,都是通過 content_webview 把對應觸發(fā)的信令事件傳遞給 service_webview。因為是雙線程通信,這里只要涉及到 DOM 事件處理或者其他數(shù)據(jù)通信的都是異步的,這點在寫代碼的時候,其實非常重要。

如果在開發(fā)時,需要什么困難,歡迎聯(lián)系: 開發(fā)者專區(qū) | 微信開放社區(qū)

IOS/Android 協(xié)議分析

前面簡單了解了開發(fā)者工具上,小程序模擬的架構。而實際運行到手機上,里面的架構設計可能又會有所不同。主要的原因有:

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

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

但是,隨著用戶量的滿滿增多,對小程序的期望也就越高:

  • 小程序的性能是被狗吃了么?
  • 小程序打開速度能快一點么?
  • 小程序的包大小為什么這么?。?/li>

這些,我們都知道,所以都在慢慢一點一點的優(yōu)化??紤]到原生 webview 的渲染性能很差,組內(nèi)大神 rex 提出了使用同層渲染來解決性能問題。這個辦法,不僅搞定了 video 上不能覆蓋其他元素,也提高了一下組件渲染的性能。

開發(fā)者在手機上具體開發(fā)時,對于某些 高階組件,像 video、canvas 之類的,需要注意它們的通信架構和上面的雙線程通信來說,有了一些本質(zhì)上的區(qū)別。為了性能,這里底層使用的是原生組件來進行渲染。這里的通信成本其實就回歸到 native 和 appservice 的通信。

為了大家更好的理解 appservice 和 native 的關系,這里順便簡單介紹一下 JSCore 的相關執(zhí)行方法。

JSCore 深入淺出

在 IOS 和 Android 上,都提供了 JSCore 這項工程技術,目的是為了獨立運行 JS 代碼,而且還提供了 JSCore 和 Native 通信的接口。這就意味著,通過 Native 調(diào)起一個 JSCore,可以很好的實現(xiàn) Native 邏輯代碼的日常變更,而不需要過分的依靠發(fā)版本來解決對應的問題,其實如果不是特別嚴謹,也可以直接說是一種 "熱更新" 機制。

在 Android 和 IOS 平臺都提供了各自運行的 JSCore,在國內(nèi)大環(huán)境下運行的工程庫為:

  • Anroid: 國內(nèi)平臺較為分裂,不過由于其使用的都是 Google 的 Android 平臺,所以,大部分都是基于 chromium 內(nèi)核基礎上,加上中間層來實現(xiàn)的。在騰訊內(nèi)部通常使用的是 V8 JSCore。
  • IOS: 在 IOS 平臺上,由于是一整個生態(tài)閉源,在使用時,只能是基于系統(tǒng)內(nèi)嵌的 webkit 引擎來執(zhí)行,提供 webkit-JavaScriptCore 來完成。

這里我們主要以具有官方文檔的 webkit-JavaScriptCore 來進行講解。

JSCore 核心基礎

普遍意義上的 JSCore 執(zhí)行架構可以分為三部分 JSVirtualMachine、JSContext、JSValue。由這三者構成了 JSCore 的執(zhí)行內(nèi)容。具體解釋參考如下:

  • JSVirtualMachine: 它通過實例化一個 VM 環(huán)境來執(zhí)行 js 代碼,如果你有多個 js 需要執(zhí)行,就需要實例化多個 VM。并且需要注意這幾個 VM 之間是不能相互交互的,因為容易出現(xiàn) GC 問題。
  • JSContext: jsContext 是 js代碼執(zhí)行的上下文對象,相當于一個 webview 中的 window 對象。在同一個 VM 中,你可以傳遞不同的 Context。
  • JSValue: 和 WASM 類似,JsValue 主要就是為了解決 JS 數(shù)據(jù)類型和 swift 數(shù)據(jù)類型之間的相互映射。也就是說任何掛載在 jsContext 的內(nèi)容都是 JSValue 類型,swift 在內(nèi)部自動實現(xiàn)了和 JS 之間的類型轉(zhuǎn)換。

大體內(nèi)容可以參考這張架構圖:

當然,除了正常的執(zhí)行邏輯的上述是三個架構體外,還有提供接口協(xié)議的類架構。

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

簡單執(zhí)行 JS 腳本

使用 JSCore 可以在一個上下文環(huán)境中執(zhí)行 JS 代碼。首先你需要導入 JSCore:

import JavaScriptCore    //記得導入JavaScriptCore

然后利用 Context 掛載的 evaluateScript 方法,像 new Function(xxx) 一樣傳遞字符串進行執(zhí)行。

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")

// 傳入?yún)?shù)調(diào)用:
// 因為 function 傳入?yún)?shù)實際上就是一個 arguemnts[fake Array],在 swift 中就需要寫成 Array 的形式
let name2 = combine.callWithArguments(["jimmy","tian"]).toString() 
print(name2)  // jimmytian

如果你想執(zhí)行一個本地打進去 JS 文件的話,則需要在 swift 里面解析出 JS 文件的路徑,并轉(zhuǎn)換為 String 對象。這里可以直接使用 swift 提供的系統(tǒng)接口,Bundle 和 String 對象來對文件進行轉(zhuǎn)換。

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

JSExport 接口的暴露

JSExport 是 JSCore 里面,用來暴露 native 接口的一個 protocol,能夠使 JS 代碼直接調(diào)用 native 的接口。簡單來說,它會直接將 native 的相關屬性和方法,直接轉(zhuǎn)換成 prototype object 上的方法和屬性。

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

那應該如何使用該 JSExport 協(xié)議呢?

首先定義需要 export 的 protocol,比如,這里我們直接定義一個分享協(xié)議接口:

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

    // 調(diào)用系統(tǒng)的 alert 內(nèi)容
    func showAlert(title: String,msg:String)
}

在 protocol 中定義的都是 public 方法,需要暴露給 JS 代碼直接使用的,沒有在 protocol 里面聲明的都算是 私有 屬性。接著我們定義一下具體 WXShareInface 的實現(xiàn):

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

        // 成功分享回調(diào)
        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)
    }
    
    // 當用戶內(nèi)容改變時,觸發(fā) 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])
    }
}

類是已經(jīng)定義好了,但是我們需要將當前的類和 JSContext 進行綁定。具體步驟是將當前的 Class 轉(zhuǎn)換為 Object 類型注入到 JSContext 中。

lazy var context: JSContext? = {

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

  do {
   
    // 注入 WXShare Class 對象,之后在 JSContext 就可以直接通過 window.WXShare 調(diào)用 swift 里面的對象
    context?.setObject(shareModel, forKeyedSubscript: "WXShare" as (NSCopying & NSObjectProtocol)!)
  } catch (let error) {
    print("Error while processing script file: \(error)")
  }

  return context
}()

這樣就完成了將 swift 類注入到 JSContext 的步驟,余下的只是調(diào)用問題。這里主要考慮到你 JS 執(zhí)行的位置。比如,你可以直接通過 JSCore 執(zhí)行 JS,或者直接將 JSContext 和 webview 的 Context 綁定在一起。

直接本地執(zhí)行 JS 的話,我們需要先加載本地的 js 文件,然后執(zhí)行。現(xiàn)在本地有一個 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");
})

然后,我們需要像之前一樣加載它并執(zhí)行:

// swift native 代碼
// swift 代碼
func init(){
    guard 
    let shareJSPath = Bundle.main.path(forResource:"common",ofType:"js") else{
        return
    }
    
    do{    
        // 加載當前 shareJS 并使用 JSCore 解析執(zhí)行
        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 已經(jīng)沒有了直接獲取 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 網(wǎng)頁
    // 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))
    

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

然后,我們可以直接通過上面的 share.js 調(diào)用 native 的接口。

原生組件的通信

JSCore 實際上就是在 native 的一個線程中執(zhí)行,它里面沒有 DOM、BOM 等接口,它的執(zhí)行和 nodeJS 的環(huán)境比較類似。簡單來說,它就是 ECMAJavaScript 的解析器,不涉及任何環(huán)境。

在 JSCore 中,和原生組件的通信其實也就是 native 中兩個線程之間的通信。對于一些高性能組件來說,這個通信時延已經(jīng)減少很多了。

那兩個之間通信,是傳遞什么呢?

就是 事件,DOM 操作等。在同層渲染中,這些信息其實都是內(nèi)核在管理。所以,這里的通信架構其實就變?yōu)椋?/p>

Native Layer 在 Native 中,可以通過一些手段能夠在內(nèi)核中設置 proxy,能很好的捕獲用戶在 UI 界面上觸發(fā)的事件,這里由于涉及太深的原生知識,我就不過多介紹了。簡單來說就是,用戶的一些 touch 事件,可以直接通過 內(nèi)核暴露的接口,在 Native Layer 中觸發(fā)對應的事件。這里,我們可以大致理解內(nèi)核和 Native Layer 之間的關系,但是實際渲染的 webview 和內(nèi)核有是什么關系呢?

在實際渲染的 webview 中,里面的內(nèi)容其實是小程序的基礎庫 JS 和 HTML/CSS 文件。內(nèi)核通過執(zhí)行這些文件,會在內(nèi)部自己維護一個渲染樹,這個渲染樹,其實和 webview 中 HTML 內(nèi)容一一對應。上面也說過,Native Layer 也可以和內(nèi)核進行交互,但這里就會存在一個 線程不安全的現(xiàn)象,有兩個線程同時操作一個內(nèi)核,很可能會造成泄露。所以,這里 Native Layer 也有一些限制,即,它不能直接操作頁面的渲染樹,只能在已有的渲染樹上去做節(jié)點類型的替換。

最后總結

這篇文章的主要目的,是讓大家更加了解一下小程序架構模式在開發(fā)者工具和手機端上的不同,更好的開發(fā)出一些高性能、優(yōu)質(zhì)的小程序應用。這也是小程序中心一直在做的事情。最后,總結一下前面將的幾個重要的點:

  • 開發(fā)者工具只有雙線程架構,通過 appservice_webview 和 content_webview 的通信,實現(xiàn)小程序手機端的模擬。
  • 手機端上,會根據(jù)組件性能要求的不能對應優(yōu)化使用不同的通信架構。

    • 正常 div 渲染,使用 JSCore 和 webview 的雙線程通信
    • video/map/canvas 等高階組件,通常是利用內(nèi)核的接口,實現(xiàn)同層渲染。通信模式就直接簡化為 內(nèi)核 <=> Native <=> appservice。(速度賊快)


易優(yōu)小程序(企業(yè)版)+靈活api+前后代碼開源 碼云倉庫:starfork
本文地址:http://22321a.com/wxmini/doc/course/24905.html 復制鏈接 如需定制請聯(lián)系易優(yōu)客服咨詢:800182392 點擊咨詢
QQ在線咨詢