你真的了解JSBridge吗?&深度分析WebViewJavascriptBridge

2,342 阅读10分钟

JSBridge入门到精通

该文章的脉络结构是:首先介绍了什么是JSBridge,其次讲了JSBridge的基本使用,并详细介绍了开源JSB框架WebViewJavascriptBridge的实现原理和细节。

什么是JSBridge

JSBridge背景

首先我们先来看张图,了解一下传统的纯nativeAPP(iOS,Android)和webAPP(H5)的区别:

image.png

从上面的图中我们可以看到他们两者各有优缺点,这时候我们不得不进行思考,有没有办法能将两者的优势结合起来,扬长避短,将优点发挥到极致?
答案是肯定的,由于目前大多数APP都会自带浏览器内核,因此可以在APP中加载H5页面,然后根据每个页面的特点决定使用H5实现还是Native实现,这样因材施教~,因地制宜~将两者结合起来,这种混合模式开发既保证良好的用户体验,同时又兼顾热更新、跨平台的优势。

image.png

上面说的这种混合模式开发的APP即HybridAPP,简单来说就是一个APP的实现是由Native页面和H5页面共同组成的。但是随着H5页面引入Native中去,必然会带来两者之间通信的需求,H5有时需要调用native的功能,使用地址位置、摄像头、访问系统相册等,再比如Native需要获得H5页面的title或者向H5页面注入cookie等等。俗话说,哪里有需求哪里就有市场,JSBridge就是为了实现两者之间通信的需求而诞生的。

JSBridge定义

能够实现Native和H5之间通信的技术有很多,我们把但凡能够实现Native和H5两者之间通信的技术统称为JSBridge。

JSBridge常用实现技术

Native -----> H5
  • webView提供的API(webView就是Native加载H5的容器组件)
H5 -----> Native
  • 拦截scheme(H5发起的页面跳转能够被Native捕获)
  • 注入API(Native向JS执行环境注入对象)

JSBridge实现原理

当我介绍完上面常用实现技术之后,大家可能还会疑惑怎么就能通过这些方法实现JSBridge了,这些方法实现的底层依据又是什么?那接下来再详细介绍下:

As we all known,JS是一门脚本语言,必须借助JSEngine来解析和执行(例如谷歌V8引擎),而JSEngine在执行JS代码的时候,会构造出运行时环境(JSContext),其中JSContext内置了一个全局对象(比如window),window中又带有一些内置属性,比如Object、Function、Math等等。

如果JS应用程序只能操作这些内置属性,那么JS语言的应用范围将大打折扣。事实上,JSEngine通常不会独立存在,而是被集成到embedder中。最早的embedder就是浏览器了,为了让JS可以访问embedder的能力,JSEngine对外提供了扩展API。

JSEngine对外提供了扩展API,允许embedder向JS运行时环境注入其特有的原生能力(注入API),并且向emberdder暴露了一些hook接口,有的接口能够收到H5页面跳转请求(拦截scheme),能够从而打通了JS->Native的调用通道,配合JSEngine提供给emberdder的执行字符串形式的JS代码的API,就实现了JS<->Native的双向通信通道。

基本使用

Native调用JS

JavaScript是一种解释型语言,可以实现随时随地执行某一段代码,实时获取代码的执行结果。因此实现native调用H5通常来说是比较简单的。iOS可以通过如下方法执行调用:

-[WKWebView evaluateJavaScript:completionHandler:];

该方法有两个参数,前一个是要执行的代码块,后面是获得执行结果后要执行的回调。

具体实例:Native调用了JS的document.title方法,获取了标题,并且打印了出来。

image.png

JS调用Native

介绍一下上文提到的两种常用方式:

跳转拦截

image.png

跳转拦截是通过前端跳转一个特定标识的scheme。客户端可以通过代理方法对每一个跳转的scheme进行拦截,如果发现该scheme是事先约定好的,那么则会执行相关操作,从而达到调用客户端执行某一操作的目的,具体实现如下:

发生webView载入的类实现<WKNavigationDelegate>协议

实现协议如下方法,会拦截所有跳转,当监测到是相关的scheme时执行相关的操作
- (void)webView:(WKWebView *)webView 
    decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
    decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;

此方法存在一定的缺陷:

  1. 相对于JS方法注入,耗时较长,对安卓尤为明显
  2. 传入的scheme会有长度的限制隐患,不同浏览器内核长度限制不同,差别很大,出于兼容性考虑以最短为标准
  3. 对同一个IFrame在同一个eventLoop中,发送请求只有最后一个能到达Native 下面复现一下第3个缺陷:

JS代码,在call()方法中,通过setTimeout实现了在第一个eventLoop中发送了两个请求,在第二个eventLoop中发送了两个请求,第三个eventLoop中发送了一个请求

image.png

在Native端执行了一个打印操作,我们看下最终控制台上的输出是什么:

image.png

可以发现每个eventLoop中只有最后一个被Native捕获到。可能有人会想每发送一个请求就创建一个iFrame,这样就不会造成调用丢失,但是对于JS来说,频繁操作DOM是一种性能损耗十分高的操作,并且还有其他更优的方案,因此大可不必这样搞。

JS方法注入

在项目中,我们打开一个网页,通常是通过一个WKWebView的对象,因此我们在初始化这个对象之后可以向里面注入一个H5和native通用的标识。H5可以通过这个标识发送消息,而native收到H5发送来的的消息可以作出响应的操作,即实现了H5调用native的操作,具体实现如下

// 发生webview载入的类实现<WKScriptMessageHandler>协议
@interface WKWebVIewVC ()<WKScriptMessageHandler>

@implementation WKWebVIewVC

// 向webview的对象中注入通用的标识方法
- (void)viewDidLoad {
    [super viewDidLoad];

    WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init];
    configuration.userContentController = [[WKUserContentController alloc] init];
    WKUserContentController *userCC = configuration.userContentController;
    // 注入对象,前端调用其方法时,Native 可以捕获到
    [userCC addScriptMessageHandler:self name:@"XXX"];

    WKWebView wkWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];

    // TODO 显示 WebView
}

// 实现协议的required方法,当H5调用方法的时候,就会传到这个方法,因此可以在这个方法里面实现相应的逻辑操作
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
    if ([message.name isEqualToString:@"nativeBridge"]) {
        NSLog(@"前端传递的数据 %@: ",message.body);
        // Native 逻辑
    }
}

H5如果想要通过JS方法注入的方式调用native的功能,只需要按照如下操作即可

这个标识必须是经过native注入过的,否则无法传入到native中去
window.webkit.messageHandlers.XXX.postMessage(null);

JSBridge 如何引用

对于 JSBridge 的引用,有以下有两种方式,各有利弊,在实际开发中通常需要根据实际情况具体分析来决定具体采用哪种调用方式。

由 Native 端进行注入

注入方式和 Native 调用 JavaScript 类似,直接执行JSB的全部代码。 它的优点在于:JSB的版本很容易与 Native 保持一致,Native 端不用对不同版本的 JSBridge 进行兼容;与此同时,它的缺点是:注入时机不确定,需要实现注入失败后重试的机制,保证注入的成功率,同时 JavaScript 端在调用接口时,需要优先判断 JSBridge 是否已经注入成功。

由 JavaScript 端引用

将JSBridge的H5实现封装成一个npm包,需要使用时引入改包。与由 Native 端注入正好相反,它的优点在于:JavaScript 端可以确定 JSBridge 的存在,直接调用即可;缺点是:如果JSB的实现方式有更改,JSBridge 需要兼容多版本的 Native Bridge 或者 Native Bridge 兼容多版本的 JSBridge。

开源JSB框架WebViewJavascriptBridge

WebViewJavascriptBridge是一款比较经典的第三方开源JSBridge框架,下面详细介绍一下WebViewJavascriptBridge的具体实现。 WebViewJavascriptBridge采用的是请求拦截的方法。前面曾提到,请求拦截具有如下几个缺点:

  1. scheme长度受到限制,超出长度限制的部分会被丢弃
  2. 不能够连续发送请求,否则可能会导致请求的丢失

为此,采用了如下的解决方案: 前端维护一个数组队列,当H5要调用native方法时,将具体的参数以及callbackId放到这个队列数组中去,并且发送一个不带任何参数的scheme通知native来队列取出全部消息,native取出消息处理完成后将结果返回H5,H5再根据callbackId取出相应的回调方法,处理从native返回来的结果。这样就完成一次调用,避免了上述缺点。

核心方法和数据结构

window.WebViewJavascriptBridge = {
                registerHandler: registerHandler,
                callHandler: callHandler,
                _fetchQueue: _fetchQueue,
                _handleMessageFromObjC: _handleMessageFromObjC
};
        
var sendMessageQueue = [];
var messageHandlers = {};
var responseCallbacks = {};
  • registerHandler:

负责注册供native调用的方法,注册的方法保存在messageHandlers对象中,属性名为方法名称,值为具体的函数,负责对native传来的消息进行处理。

  • callHandler:

H5通过此方法调用native的相关功能。调用该方法时,首先会根据时间戳生成一个唯一的callbackId,为了能使native返回的消息能够正确处理,H5端会以callbackId为key,以相应的处理函数为value存入responseCallbacks对象中。当然callbackId也要一并存入message中,整个message对象作为调用native方法的参数一并存入sendMessageQueue中。 根据上面的描述,message 对象的参数已经非常明显:

  • handlerName:想要调用的native端的方法的名称
  • data:调用native方法所传的参数
  • callbackId:作为一次调用的唯一标识,通过此native将结果返回H5时,能够正确的找到回调函数处理返回的信息。
  • _fetchQueue:

供native调用,负责从sendMessageQueue中取出所有的message对象,转化为json字符串后全部返回给native。

  • _handleMessageFromObjC:

1.处理从native传过来的消息。传过来的消息分为两种:

native主动调用前端方法:消息格式为{data:XXX, callbackId: XXX, handlerName: XXX},其中callbackId不是必须的。首先根据handlerName取出相应的方法,如果没有callbackId,那么直接调用即可;否则,需要在调用完成后回调给native结果。

此情况下,message的消息格式如下:

  • handlerName:native想要调用的H5方法的名称
  • data:调用方法的参数
  • callbackId:调用的唯一标识,作用同上

2.前端调用native方法,native处理后的回调:消息格式为:{responseId:XXX, responseData:XXX}, 根据responseId从responseCallbacks中取出回调处理方法,然后传入responseData完成回调调用

此种情况下,message的消息格式如下:

  • responseData:native返回的消息
  • responseId:最开始调用native方法时生成的callbackId

具体交互逻辑图示

H5调用Native

image.png

Native调用H5

image.png

几个重要细节点

1.注入方式

此开源框架H5部分的JSBridge代码是存储在Native中的,由Native进行注入,注入时机是H5来控制的。当想要注入时,H5发送一个标识注入的scheme,Native拦截到调用消息,发现是注入的scheme,Native就直接调用执行JS代码的API,将存储在Native中的JSBridge的H5那部分JS代码传入执行。这样就完成了注入。

image.png

2.避免丢失调用

考虑这样一种情况,H5在发送完JSBridge注入请求后,但JSBridge还没有注入成功时(注入中),H5又发送了调用请求,这时这个调用请求如何保证不丢失?该框架是通过如下方式实现的:

image.png

具体实现方式是:每次调用都是通过上面的函数调用,函数入参(callback)为使用JSBridge功能的函数。该方法首先判断是否存在JSBridge(成功注入),直接调用传入的函数;如果存在一个callbacks数组(注入中),就把这次调用推进数组中;否则初始化这个callbacks数组,并发送一个JSBridge的注入请求。在注入的那部分JS代码的最后,会判断callbacks数组是否为null,如果不为null那么就将里面的调用全部取出并执行。