手机版

如何写好小程序的多终端架构文章

时间:2021-12-05 来源:互联网 编辑:宝哥软件园 浏览:

作为微信小程序底层API的维护者之一,我经历过风风雨雨,也经历过各种吐槽。为了让大家更好的写小程序,我特意梳理了一篇文章来介绍一下。如果你有什么想说的,请到https://developers.weixin.qq.开发者社区。

PS:的老板在找人,有自己实力的前端er可以直接把简历发到我的邮箱: villainthr@gmail.com。

简述小程序的通信体系

为了大家更好的开发一些高质量、高性能的小程序,在这里,我们带大家了解一下不同终端上小程序的架构体系的差异,让大家更好的了解一些小程序独特的代码编写方法。

整个小程序开发生态可以分为两部分:

最初桌面nwjs微信开发者工具(PC端)移动APP的官方运行环境是采用双线程模式,解决安全性和可控性问题。然而,随着开发复杂度的不断增加,原本耗时的双线程通信对于一些高性能的小程序来说,已经变得有些不可接受。即每次更新UI时,通过webview手动调用API实现更新。原始基础设施,可以参考官方地图:

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

但是上面这张图其实有点误导,因为webview渲染执行实际上是由手机上的内核来操作的,而webview只是内核暴露的DOM/BOM接口。所以这里有一个性能上的突破,那就是JSCore可以通过Native层直接获取内核的相关接口吗?答案是肯定的,所以其实上面的图可以简单的再划分一下,新的显示为:

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

简单地说,内核被改变,然后标准webview接口的副本被有选择地绘制到JsCore调用。但是有一个限制就是安卓端相对自由,可以通过V8提供的插件机制来完成。在IOS上,苹果爸爸是不允许的,除非你用的是IOS的原生组件,这会导致同层渲染逻辑。其实它们的底层内容是一样的。

为了更好地理解小程序具体开发过程中手机调试和开发者工具调试的一般区别,我们来分析一下它们各自的执行逻辑。

tl;dr

开发者工具通信系统(只能使用双向通信),即所有指令都通过appservice=nwjs中间层=webviewNative的通信系统运行:

小程序基础通信:双向通信-(core=webview=intermedia=app service)高层组件通信:单向通信系统(app service=Android/Swift=core)JSCore具体实现app service的逻辑内容

开发者工具的通信模式

出于安全可控的考虑,最初使用了多线程模型。简单地说,所有的JS执行都是在JSCore中完成的,不管绑定的事件、属性、DOM操作等等。

开发者工具主要运行在PC端,内部使用nwjs来做。不过,为了更好的理解,这里,直接按照nwjs的一般技术。开发人员使用的框架是基于nwjs管理一个webviewPool,通过webviewPool实现appservice_webview和content_webview。

因此,小程序和开发人员工具中的一些性能困难不会造成很大的问题。比如不会有任何画布元素不能放置div,视频元素不能设置自定义控件等。整个架构如图所示:

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

当您打开开发人员工具时,首先看到的实际上是appservice_webview中的Console内容。

)" data-bd-imgshare-binded="1" src="http://www.yiyongtong.com/uploads/allimg/181022/1121261462-3.png" />

content_webview 对外其实没必要暴露出来,因为里面执行的小程序底层的基础库和 开发者实际写的代码关系不大。大家理解的话,可以就把显示的 WXML 假想为 content_webview。

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

当你在实际预览页面执行逻辑时,都是通过 content_webview 把对应触发的信令事件传递给 service_webview。因为是双线程通信,这里只要涉及到 DOM 事件处理或者其他数据通信的都是异步的,这点在写代码的时候,其实非常重要。

如果在开发时,需要什么困难,欢迎联系:开发者专区 | 微信开放社区

IOS/Android 协议分析

前面简单了解了开发者工具上,小程序模拟的架构。而实际运行到手机上,里面的架构设计可能又会有所不同。主要的原因有:

  • IOS 和 Android 对于 webview 的渲染逻辑不同
  • 手机上性能瓶颈,JS 原始不适合高性能计算
  • video 等特殊元素上不能被其他 div 覆盖

一开始做小程序的双线程架构和开发者工具比较类似,content_webview 控制页面渲染,appservice 在手机上使用 JSCore 来进行执行。它的默认架构图其实就是这个:

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

但是,随着用户量的满满增多,对小程序的期望也就越高:

  • 小程序的性能是被狗吃了么?
  • 小程序打开速度能快一点么?
  • 小程序的包大小为什么这么小?

这些,我们都知道,所以都在慢慢一点一点的优化。考虑到原生 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 之间的类型转换。

大体内容可以参考这张架构图:

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

当然,除了正常的执行逻辑的上述是三个架构体外,还有提供接口协议的类架构。

  • JSExport: 它 是 JSCore 里面,用来暴露 native 接口的一个 protocol。简单来说,它会直接将 native 的相关属性和方法,直接转换成 prototype object 上的方法和属性。

简单执行 JS 脚本

使用 JSCore 可以在一个上下文环境中执行 JS 代码。首先你需要导入 JSCore:

import JavaScriptCore    //记得导入JavaScriptCore

然后利用 Context 挂载的 evaluateScript 方法,像 new Function(xxx) 一样传递字符串进行执行。

let contet:JSContext = JSContext() // 实例化 JSContextcontext.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 操作等。在同层渲染中,这些信息其实都是内核在管理。所以,这里的通信架构其实就变为:

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

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。(速度贼快)

版权声明:如何写好小程序的多终端架构文章是由宝哥软件园云端程序自动收集整理而来。如果本文侵犯了你的权益,请联系本站底部QQ或者邮箱删除。