如何写出一手好的小程序之多端架构篇

做为微信小程序底层 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。(速度贼快)
因为工做太忙,社区不多会上,这里推荐你们,关注个人微信公众号 《前端小吉米》,公众号通常会及时更新

参考:

教程 | 《小程序开发指南》

相关文章
相关标签/搜索