如何写出一手好的小程序代码,从架构说起

作为微信小程序底层 API 维护者之一,经历了风风雨雨、各种各样的吐槽。为了让大家能更好的写一手小程序,特地梳理一篇文章介绍。如果有什么吐槽的地方,欢迎去 developers.weixin.qq.com/ 开发者社区吐槽。

01

简述小程序的通信体系

为了大家能更好的开发出一些高质量、高性能的小程序,这里带大家理解一下小程序在不同端上架构体系的区分,更好的让大家理解小程序一些特有的代码写作方式。

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

· 桌面 nwjs 的微信开发者工具(PC 端)

· 移动 APP 的正式运行环境

一开始的考虑是使用双线程模型来解决安全和可控性问题。不过,随着开发的复杂度提升,原有的双线程通信耗时对于一些高性能的小程序来说,变得有些不可接受。也就是每次更新 UI 都是通过 webview 来手动调用 API 实现更新。原始的基础架构,可以参考官方图:

不过上面那张图其实有点误导行为,因为,webview 渲染执行在手机端上其实是内核来操作的,webview 只是内核暴露的一下 DOM/BOM 接口而已。所以,这里就有一个性能突破点就是,JSCore 能否通过 Native 层直接拿到内核的相关接口?

答案是可以的,所以上面那种图其实可以简单的再进行一下相关划分,新的如图所示:

简单来说就是,内核改改,然后将规范的 webview 接口,选择性的抽一份给 JsCore 调用。但是,有个限制是 Android 端比较自由,通过 V8 提供 plugin 机制可以这么做,而 IOS 上,苹果爸爸是不允许的,除非你用的是 IOS 原生组件,这样的话就会扯到同层渲染这个逻辑。其实他们的底层内容都是一致的。

后面为了大家能更好理解在小程序具体开发过程中,手机端调试和在开发者工具调试的大致区分,下面我们来分析一下两者各自的执行逻辑。

tl;dr

· 开发者工具 通信体系 (只能采用双向通信) 即,所有指令都是通过 appservice <=> nwjs 中间层 <=> webview

· Native 端运行的通信体系:

· 小程序基础通信:双向通信-- ( core <=> webview <=> intermedia <=> appservice )

· 高阶组件通信:单向通信体系 ( appservice <= android/Swift => core)

JSCore 具体执行 appservice 的逻辑内容

02

开发者工具的通信模式

一开始考虑到安全可控的原因使用的是双线程模型,简单来说你的所有 JS 执行都是在 JSCore 中完成的,无论是绑定的事件、属性、DOM操作等,都是。

开发者工具,主要是运行在 PC 端,它内部是使用 nwjs 来做,不过为了更好的理解,这里,直接按照 nwjs 的大致技术来讲。开发者工具使用的架构是 基于 nwjs 来管理一个 webviewPool,通过 webviewPool 中,实现 appservicewebview 和 contentwebview。

所以在小程序上的一些性能难点,开发者工具上并不会构成很大的问题。

比如说,不会有 canvas 元素上不能放置 div,video 元素不能设置自定义控件等。整个架构如图:

当你打开开发者工具时,你第一眼看见的其实是 appservice_webview 中的 Console 内容。

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

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

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

IOS/Android 协议分析

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

主要的原因有:

· IOS 和 Android 对于 webview 的渲染逻辑不同

· 手机上性能瓶颈,JS 原始不适合高性能计算

· video 等特殊元素上不能被其他 div 覆盖

· ……

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

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

小程序的性能是被狗吃了么?

小程序打开速度能快一点么?

小程序的包大小为什么这么小?

这些,我们都知道,所以都在慢慢一点一点的优化。考虑到原生 webview 的渲染性能很差,组内大神 rex 提出了使用同层渲染来解决性能问题。这个办法,不仅搞定了 video 上不能覆盖其他元素,也提高了一下组件渲染的性能。

开发者在手机上具体开发时,对于某些 高阶组件,像 video、canvas 之类的,需要注意它们的通信架构和上面的双线程通信来说,有了一些本质上的区别。为了性能,这里底层使用的是原生组件来进行渲染。这里的通信成本其实就回归到 native 和 appservice 的通信。

为了大家更好的理解 appservice 和 native 的关系,这里顺便简单介绍一下 JSCore 的相关执行方法。

03

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 之间的类型转换。

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

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

· 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 也有一些限制,即,它不能直接操作页面的渲染树,只能在已有的渲染树上去做节点类型的替换。

04

最后的总结

这篇文章的主要目的,是让大家更加了解一下小程序架构模式在开发者工具和手机端上的不同,更好的开发出一些高性能、优质的小程序应用。这也是小程序中心一直在做的事情。

最后,总结一下前面将的几个重要的点:

· 开发者工具只有双线程架构,通过 appservicewebview 和 contentwebview 的通信,实现小程序手机端的模拟。

· 手机端上,会根据组件性能要求的不能对应优化使用不同的通信架构。

· 正常 div 渲染,使用 JSCore 和 webview 的双线程通信

· video/map/canvas 等高阶组件,通常是利用内核的接口,实现同层渲染。通信模式就直接简化为 内核 <=> Native <=> appservice。(速度贼快)

小程序开发有许多便捷的第三方工具,

比如

腾讯云小程序音视频解决方案

开发者们可以练练手,阅读原文,简易开发

零基础快速对接

专为小程序打造的一站式音视频解决方案,无需具备音视频基础知识,简单易用,功能强大

极卓越的音视频品质

端到端延时小于400ms,抗800ms的网络抖动,抗30%网络丢包,自适应网络变化,智能无回声降噪处理。

跨平台互通

支持微信小程序与Android、iOS、PC、Web主流平台互通;满足一对一、一对多的实时音视频通话场景需要。

丰富的场景化DEMO

将实时音视频融入到各业务场景中,提供不同业务场景的Demo,帮助用户高效完成小程序开发。

技术树洞

关注“腾讯云视频”

聊天窗口关键字“技术支持”,我们将会为您解答海外云服务问题。

音视频交流群友,正在召唤你

扫一扫下方二维码,加小编微信,进群交流

(由于群友已超100人,需加成员拉进群)

扫一扫下方二维码,关注“腾讯云视频”公众号

获取更多视频技术服务

点击“阅读原文”

了解小程序音视频

原文发布于微信公众号 - 腾讯云视频(txvideocloud)

原文发表时间:2018-10-24

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏青青天空树

node.js+vue.js搭建程序设计类课程教学辅助系统

  毕业才刚刚两个多月而已,现在想想大学生活是那么的遥不可及,感觉已经过了好久好久,社会了两个月才明白学校的好啊。。。额,扯远了,自从毕业开始就想找个时间写下毕...

4472
来自专栏雪胖纸的玩蛇日常

django2用模板代码图标字体丢失报404 cJZKeOuBrn4kERxqtaUH3T8E0i7KZn-EPnyo3HZu7kw.woff

1503
来自专栏哲学驱动设计

产品前端重构(TypeScript、MVC框架设计)

最近两周完成了对公司某一产品的前端重构,本文记录重构的主要思路及相关的设计内容。 公司期望把某一管理类信息系统从项目代码中抽取、重构为一个可复用的产品。该系统的...

2558
来自专栏大数据挖掘DT机器学习

Python实现爬取知乎神回复

这篇文章主要介绍了Python实现爬取知乎神回复简单爬虫代码分享,本文实现了爬取知乎的“如何正确地吐槽”收藏夹,是对个人的一个兴趣实现,需要的朋友可以参考下。 ...

3425
来自专栏ionic3+

【技巧】ionic3自动聚焦暴力实现

很早前和群里的人探讨过自动聚焦,在android上可以,但是在ios上失败,后来在网上看到这个:

822
来自专栏恰同学骚年

ASP.Net WebForm温故知新学习笔记:二、ViewState与UpdatePanel探秘

开篇:经历了上一篇《aspx与服务器控件探秘》后,我们了解了aspx和服务器控件背后的故事。这篇我们开始走进WebForm状态保持的一大法宝—ViewState...

1593
来自专栏腾讯技术工程官方号的专栏

免费开放阅读 | 数据库管理系统的事务原理(上)

作者介绍: 那海蓝蓝,腾讯技术工程事业群计费平台部金融云TDSQL数据库T4级专家,熟悉PostgreSQL、MySQL、Informix等数据库内核技术,著有...

5328
来自专栏向治洪

android开发性能分析

1 背景 其实有点不想写这篇文章的,但是又想写,有些矛盾。不想写的原因是随便上网一搜一堆关于性能的建议,感觉大家你一总结、我一总结的都说到了很多优化注意事项...

2525
来自专栏杨建荣的学习笔记

system表空间不足的问题分析(r6笔记第66天)

很多事情见多了也就有了麻木的感觉,报警短信就是如此,每天总能收到不少的报警短信,可能很多时候就扫一眼,如果没有严重的问题自己是不会情愿打开电脑处理的。 对于此,...

2744
来自专栏FSociety

Python爬取猫眼「碟中谍」全部评论

评论算保存完了,近期会再做一个关于此次数据的可视化分析。另外阿汤哥真心太帅了,全程打肾上腺素,各位还没去看的赶紧~

760

扫码关注云+社区

领取腾讯云代金券