基于JSBridge的前端与客户端交互探索实践

6,377 阅读6分钟

要解决什么问题?

移动应用开发中为了兼具H5和Native App的优点,通常会采用Hybrid的形式进行混合应用开发,使用JSBridge作为JS和Native之间通信的双向通道,JS可以通过JSBridge调用Native的能力。

在实际开发中前端和客户端合作模式通常是由客户端封装方法和控件UI后由前端直接调用,对于前端代码来说做到了跨平台跨系统,多个平台只需要维护一套前端代码,上线灵活版本迭代快。但是在业务需要客户端频繁迭代的场景下就需要走应用商店的审核流程,应用的迭代速度优势就大打折扣了。

客户端功能迭代后在前端JS中需要对这些功能做版本控制,因为用户手上的旧版本的App没有客户端新的功能,不做版本控制会导致bug,并且随着版本迭代次数越来越多JS中的版本区分会越来越臃肿,同一份代码里可能充满了对不同App不同版本号的区分判断,代码会变得越来越不可维护。

分析问题

在H5和Native都迭代了非常多版本的情况下,没有办法使用RN、Weex这样的方案去完全重构Native,成本太高,但是可以借鉴一下它们的思路。

以RN为例,RN使用ReactJS执行js逻辑代码、描述和管理VirtualDom,通过Bridge将ReactJS翻译为绘制指令给Native进行绘制,Native通过Bridge将原生接收到的用户事件反馈给ReactJS。

那我们用JSBridge也能完成吗?想要用JS生成Native控件关键是JS要将绘制指令传递到Native,Native将用户事件反馈给JS。用JSBridge我们可以完成前端和客户端之间的数据通信,使用JSON作为数据载体,前端代码翻译成绘制指令的工作放在前端来做,用户事件反馈由Native通过JSBridge通知前端。 这种方案的好处是成本低,前端和客户端改动代码都不大,可以实现基本的客户端控件绘制,用户事件回调,具有一定的客户端控件热更新能力。缺点是通过JSBridge传递数据没有JSCore直接解析JS执行效率高,在一些频繁触发更改的场景下力不从心(例如客户端控件跟随网页滚动,通过JS监听滚动再传递修改数值给客户端)

实现idea

接下来以前端的视角介绍一下这个idea的实现

数据载体

作为数据载体的JSON,可以定义如下结构,target描述操作的元素,action描述对当前对象的操作,params描述当前操作需要携带的参数:

[
    {
        "target": "xxx",
        "action": "init",
        "params": {
            "key": "value",
        }
    }
]

将对Native控件操作的一系列指令放在JSON数组里,Native接收后依次取出指令执行。

target

定义了几种最基本的元素:UICanvas、UIButton、UIInput、UIImage、UIIput,可以理解为H5的div、button、span、img、input,复杂的元素可以由简单的元素拼凑出来。

action

action是对target的操作,最基本的操作有:

action说明
init初始化一个基本元素
set设置一个基本元素的属性
get获取一个基本元素的属性
delloc销毁一个基本元素

params

params是当前操作需要的参数,例如init时,params需要传递控件的坐标和宽高等信息。

JSON生成

显然,在JS里直接书写上面提到的JSON数组是不靠谱的,难以阅读也难以维护。需要一个sdk工具将前端代码翻译成JSON数组,这样在前端就能以比较自然的面向对象的形式书写生成客户端控件的代码。

例如下面这段json,创建了一个名为view的native矩形画布,并添加到webview坐标(0, 0)处

[
    {
        "target": "UICanvas",
        "action": "init",
        "params": {
            "name": "view",
            "width": 100,
            "height": 100,
        }
    }, {
        "target": "view",
        "action": "addToWebview",
        "params": {}
    }
]

期望的OOP书写方式:

const view = new UICanvas({
    name: 'view',
    width: 100,
    height: 100,
})
view.addToWebview()
view.getCode() // 获取生成的JSON数组,准备通过JSBridge传给客户端

前端sdk需要实现以下两个功能:

  1. 实现和客户端相同的类、抽象类、接口、类的方法,并且有相同的继承和实现结构,组件类的方法主要作用是生成json。
  2. 实现一个方法装饰器collector,装饰每个类方法,调用时自动收集返回的json数组到sdk中,最后通过尾调用给出json,清空sdk数组。

例如刚才提到的addToWebview方法在sdk中实现如下:

    /**
     * 添加视图到webview上(会盖住Webview)
     */
    @collector() // 装饰器,将返回值收集到sdk的json数组中
    public addToWebView(): WebKitCode {
        return {
            target: this.instanceName, // 调用的实例名
            action: 'addToWebView',
        }
    }

执行:

view.addToWebview()
const arr = view.getCode() // [{"target": "view", "action": "addToWebview", "params": {}}]

OK,这样就能在前端使用面向对象的形式书写客户端控件的生成规则了,通过JSBridge传递给native,native解析json数组,使用真正的UICanvas在webview上绘制view这个控件。

以上只是创建一个最基本的一个矩形,在这个矩形之上还可以添加子视图,添加按钮、图片、文字、绑定点击事件、添加动画、控制样式。理论上可以实现任何客户端控件。

在实际应用中,我用这套方案实现了一个native的文章页底栏,每个按钮可以直接绑定客户端的事件,或者由客户端通过JSBridge通知前端自行执行对应方法,实现了前端对客户端底栏的完全自治,迭代周期和版本控制问题迎刃而解。

扩展

这套方案也可以应用在native本身的控件上,对于一些频繁迭代的app控件,客户端可以直接将sdk和实现模板预载,在一个shadow webview上执行生成json,调用控件时就能直接通过json生成控件,也能拥有一定程度的热更新能力。

结语

这套方案是长久以来受客户端发版周期、版本控制困扰后总结出来的低成本解决方案,刚起步肯定会有一些不足之处,如果大家有自己的想法和思考欢迎在评论区分享讨论。

我是suhangdev,欢迎与我交流前端相关话题, 邮箱17816876697@163.com,如果文章对你有帮助,请点赞支持噢,谢谢!