JSBridge总结

由于Webview内嵌H5的性能/功能各种受限,于是有了各种的混合开发解决方案,例如Hybrid、RN、WEEX、Flutter、小程序、快应用等等。

React Native 至今没有推出1.0版本,由于各种可能的坑,一些hold不住的团队可能会放弃。
Flutter 是否可替代RN,真正实现两端统一,拭目以待,他从头到尾重写一套跨平台的UI框架,包括UI控件、渲染逻辑甚至开发语言。我本人之后会关注学习一下。
小程序 不用说太多了,大家都很熟悉了;微信、支付宝、百度都在用。除了第一次需要花点时间下载,体验上可以说是很不错了,但是封闭性是他很大的一个缺点。
快应用 目标是很好的,统一API,但是还是要看各厂家的执行力度。

现在来总结一下我们团队目前使用的Hybrid方案。算是回顾一下,巩固基础,好记性不如烂笔头。

一、Hybrid简介

Hybrid可以说是上面提到的几种里最古老,最成熟的解决方案了。

缺点是明显的:H5有的缺点他几乎都有,比如性能差、JS执行效率低等等。

但是优点也很显著:随时发版,不受应用市场审核限制(当然这个前提是Hybrid对应Native的功能都已准备就绪);拥有几乎和Native一样的能力,eg:拍照、存储、加日历等等...

基本原理

Hybrid利用JSBridge进行通信的基本原理网上一搜一大把,简单记录一下。

Native => JS
两端都有现成方法。谁让都在别人的地盘下面玩呢,Native当然有办法来执行JS方法。
iOS

// Swift
webview.stringByEvaluatingJavaScriptFromString("Math.random()")
// OC
[webView stringByEvaluatingJavaScriptFromString:@"Math.random();"];

Android

mWebView.evaluateJavascript("javascript: 方法名('参数,需要转为字符串')", new ValueCallback() {
        @Override
        public void onReceiveValue(String value) {
            //这里的value即为对应JS方法的返回值
        }
});

JS => Native
对于Webview中发起的网络请求,Native都有能力去捕获/截取/干预。所以JSBridge的核心就是设计一套url方案,让Native可以识别,从而做出响应,执行对应的操作就完事。
例如,正常的网络请求可能是: https://img.alicdn.com/tps/TB17ghmIFXXXXXAXFXXXXXXXXXX.png
我们可以自定义协议,改成jsbridge://methodName?param1=value1&param2=value2
Native拦截jsbridge开头的网络请求,做出对应的动作。
最常见的做法就是创建一个隐藏的iframe来实现通信。

二、现成的解决方案

iOS WebViewJavascriptBridge
Android JsBridge

基本原理都相同,项目的设计就决定了一个它的可扩展性&可维护性。良好的可扩展性&可维护性对于JSBridge尤为重要,他是后面一切业务的基石。

基础库简析

(下面都以Android为例)

1、 初始化

类似写普通H5页面需要监听DOMContentLoaded或者onLoad来决定开始执行脚本一样,JSBridge需要一个契机去告诉JS,我准备好了,你可以来调用我的方法了。

[前端] 执行监听 && 检测

    if (window.WebViewJavascriptBridge) {
        //do your work here
    } else {
        document.addEventListener(
            'WebViewJavascriptBridgeReady'
            , function() {
                //do your work here
            },
            false
        );
    }

[Native (埋在端里的JS)] dispatchEvent触发

    var WebViewJavascriptBridge = window.WebViewJavascriptBridge = {
        init: init,
        send: send,
        registerHandler: registerHandler,
        callHandler: callHandler,
        _fetchQueue: _fetchQueue,
        _handleMessageFromNative: _handleMessageFromNative
    };
    
    var readyEvent = doc.createEvent('Events');
    readyEvent.initEvent('WebViewJavascriptBridgeReady');
    readyEvent.bridge = WebViewJavascriptBridge;
    doc.dispatchEvent(readyEvent);
2、JS调Native方法

先上代码,下面是埋在端内的,JSBridge.callHandler,用来实现JS调用Native。

    // 调用线程
    function callHandler(handlerName, data, responseCallback) {
        _doSend({
            handlerName: handlerName,
            data: data
        }, responseCallback);
    }

    //sendMessage add message, 触发native处理 sendMessage
    function _doSend(message, responseCallback) {
        if (responseCallback) {
            var callbackId = 'cb_' + (uniqueId++) + '_' + new Date().getTime();
            responseCallbacks[callbackId] = responseCallback;
            message.callbackId = callbackId;
        }

        sendMessageQueue.push(message);
        messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE;
    }

jsbridge.callHandler是JS调Native方法的核心。
handlerName是前端与Native协商好的方法名称
data 参数
responseCallback 回调
回调函数绑在了一个内部对象中var responseCallbacks = {},发送给Native的消息message中只包含了这个回调函数对应的id,端上处理完成之后触发&销毁。

这个方法并不直接把消息全部推送走,而是存在一个队列中sendMessageQueue。同时通知Native,有新数据(message)需要处理。即上面代码的最后一行,他利用iframe的src通知端上的信息如下:

    var CUSTOM_PROTOCOL_SCHEME = 'sn'
    var QUEUE_HAS_MESSAGE = '__sn__queue_message__'

上面提到的,JS只是通知了端上有新消息,Native调用获取时机暂时不考虑,就假设他收到一条就处理一次,极端高频情况下,两三条处理一次。Native通过_fetchQueue统一处理存储在sendMessageQueue中的数据:

    // 提供给native调用,该函数作用:获取sendMessageQueue返回给native,由于android不能直接获取返回的内容,所以使用url shouldOverrideUrlLoading 的方式返回内容
    function _fetchQueue() {
        var messageQueueString = JSON.stringify(sendMessageQueue);
        sendMessageQueue = [];
        //android can't read directly the return data, so we can reload iframe src to communicate with java
        if (messageQueueString !== '[]') {
            bizMessagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://return/_fetchQueue/' + encodeURIComponent(messageQueueString);
        }
    }

这些基本就是JS主动调用Native的流程,关于回调方法,下面统一说。

3、Native调JS方法

虽说Native可以随意执行JS,但是总是需要知道哪些JS方法是可执行的吧。registerHandler就是用来执行注册。
registerHandler在Native端定义(是JSBridge对象的一个方法),由前端来注册。

    // 注册线程 往数组里面添加值
    function registerHandler(handlerName, handler) {
        messageHandlers[handlerName] = handler;
    }

Native主动调用。
Native主动调用分两种情况,1是Native主动触发前端事件,例如通知前端页面可视状态变化。2是前端调用Native的回调。JSBridge是天生异步的,所以回调和主动调用归结到一类里面了。
如果是前端主动调用的方法,有responseId,即有回调,直接调用执行即可。
否则就去注册的messageHandlers中寻找方法,调用。

    //提供给native使用,
    function _dispatchMessageFromNative(messageJSON) {
        setTimeout(function() {
            var message = JSON.parse(messageJSON);
            var responseCallback;
            //java call finished, now need to call js callback function
            // 前端主动调用的Callback
            if (message.responseId) {
                responseCallback = responseCallbacks[message.responseId];
                if (!responseCallback) {
                    return;
                }
                responseCallback(message.responseData);
                delete responseCallbacks[message.responseId];
            } else {
                // Native主动调用
                //直接发送
                if (message.callbackId) {
                    var callbackResponseId = message.callbackId;
                    responseCallback = function(responseData) {
                        _doSend({
                            responseId: callbackResponseId,
                            responseData: responseData
                        });
                    };
                }

                var handler = WebViewJavascriptBridge._messageHandler;
                if (message.handlerName) {
                    handler = messageHandlers[message.handlerName];
                }
                //查找指定handler
                try {
                    handler(message.data, responseCallback);
                } catch (exception) {
                    if (typeof console != 'undefined') {
                        console.log("WebViewJavascriptBridge: WARNING: javascript handler threw.", message, exception);
                    }
                }
            }
        });
    }

代码分析基本就到这里,盗一张图(地址放在最后了),把流程都画了出来,个人感觉没啥问题

三、业务封装

直接使用前面的库可以完成功能,但是不够优雅,代码不经过良好的设计可能会变得牵一发动全身,可维护性差。下面说说我们的设计,可能不是最好的,但是是很符合我们业务场景的。

  1. 事件基础类 EventClass
    处理事件广播、订阅。
  2. 连接基础类 ConnectClass
/**
 * 创建和获取 jsbridge 基础类
 * @class ConnectClass
 * @extends EventsClass
 */
class ConnectClass extends EventsClass {

  /**
   * 获取jsbridge实例,注入到sncClass上的bridge属性 `this.bridge`
   */
  connect() {
     // 事件广播,通知开始建立连接,统计使用
     // 建立JSBridge
     // 建立JSBridge.then  1.注册Native主动调用的事件,对应上面的bridge.registerHandler;2.广播 建立完成,统计使用
  }

  // ... 其他的一些方法
 // eg: 分平台初始化JSBridge,处理差异性
 // eg: bridge.registerHandler 回调的封装一层的统一处理函数

}

关于注册Native主动调用的事件(和下面会提到的JS主动调用事件),实现插件化,并同一封装。好处是可以明确代码执行步骤、方便业务同学调试(这不是我的锅,我已经执行调用了...)、方便性能统计。

  1. 业务类
class SncClass extends ConnectClass {
  constructor(option){
    // 监听connect,监听首屏数据
    // 建立连接 this.connect
    // 挂载必备API
  }

  // 初始化,根据参数决定挂载哪些api
  init(apis){
    this.mountApi(apis);
  }

  /**
   * 挂载 api
   * @param  {Object} apis api 对象集合
   */
  mountApi(apis) {
    // 1.  错误处理
    // 2. 检测是否已经jsb建立连接 已连接则 直接执行真正挂载函数 return
    // 3. bridge 未初始化时,定义方法预声明。执行的方法将会被储存在缓存队列里在 bridge 初始化后调用
    // 4. 监听连接事件,执行真正挂载 loadMethods
  }
}

 /**
   * 加载 API 到实例属性,标志着 api 的真正挂载
   */
  loadMethods(apis) {
      // 1.  防止重复挂载 api,
      // 2. 给插件初始化方法注入ctx,让插件得以调用库内真正的初始化函数,即封装一层的上面提到的 callHandler
  }

// ... 其他实例方法,比如 extend,得以在业务中和Native互相约定新的非通用JSB,方便扩展
  1. 初始化
    导出单例appSNC,拥有的方法都在appApis中定义,如果有新的业务需求直接扩展此文件夹中内容即可。
import * as apis from '../appApis'; // 方法集合
import SNC from './sdk';  // 上面的 SncClass
const option = {} ; // 一些配置
const appSNC = new SNC(option);

export default appSNC.init(apis);

以上就是我们正在使用的方案,总结一下,不断积累。

参考链接

JSBridge深度剖析

WebViewJavascriptBridge

JsBridge

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 197,368评论 5 462
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 82,941评论 2 374
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 144,369评论 0 326
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 52,848评论 1 267
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 61,719评论 5 358
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 46,505评论 1 275
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 36,904评论 3 388
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 35,528评论 0 254
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 39,819评论 1 293
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 34,848评论 2 314
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 36,652评论 1 328
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 32,468评论 3 316
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 37,912评论 3 300
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,095评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 30,389评论 1 255
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 41,906评论 2 343
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 41,120评论 2 339

推荐阅读更多精彩内容