JSBridge原理与实现

1,038 阅读5分钟

一、JSBridge的基本概念

JSBridge主要是给 JavaScript 提供调用 Native 功能的接口,让混合开发中的前端部分可以方便地使用 Native 的功能(例如:地址位置、摄像头)。它的核心是构建 Native 和非 Native 间消息通信的通道,而且这个通信的通道是双向的。

那么要构建这个双向通道有四要素:

1.通讯触发行为:

能被原生监听并捕获拦截的H5行为。 包括有console.log,alert,confirm,prompt,location.href等,为了不影响原h5页面的流程,一般采用创建一个iframe发起伪请求协议,并在调用结束后将iframe删除实现调用全过程。

2.通讯协议:

自定义的伪协议,一般会定义成与http协议类似的格式: - 协议名://接口路径?参数1=XXX&参数2=XXX&参数3=XXX#callback

  • 其中:
  • a、协议名:app自定义的协议名,用于H5触发行为的监控捕获,如使用jsbridge://;
  • b、接口路径:原生具体能力路径,不同原生能力路径不同;
  • c、参数1=XXX&参数2=XXX&参数3=XXX#callback:H5传参与回调方法标识; 根据通讯协议规范,即可针对不同的原生能力给H5提供不同的调用地址,如: jsbridge://method?a=2&b=3#h5MethodTag
3.通讯行为—调用:

原生终端根据H5传过来的内容,解析匹配后会路由到具体处理方法,执行原生能力逻辑。

4.通讯行为—回调:

原生根据H5传过来的内容,捕获js回调函数方法名,在原生逻辑执行结束后,将执行结果带到回调函数中并执行js回调函数。通过在第三步“调用”执行完后,iOS会调用js回调函数H5MethodTag。

graph LR
前端 --通讯触发--> 原生:根据通讯协议完成功能调用 --回调--> 前端

二、JSBridge 的实现前提

JavaScript 运行在一个单独的 JS Context(JS运行环境) 中(例如,Webkit引擎 的 WKWebView与UIWebView、JSCore),在不同的平台(iOS、Android等)使用不同的Context都有提供相应的与原生通信方法,我们需要对这些方法进行统一封装与实现。

三、JSBridge 的具体实现

在四要素中,第二点通讯协议与第三点原生具体处理方法都是需要丰满与扩展的部分,第一点通讯触发与第四点原生回调是整个JSBridge的骨架,我们来讲解一下这两点。

1.前端调用

主要有原生注入API拦截URL SCHEME

1.1 原生注入API

原生注入 API 方式的主要原理是,通过 WebView 提供的接口,向 JavaScript 的 Context(window)中注入对象或者方法,让 JavaScript 调用时,直接执行相应的 Native 代码逻辑,达到 JavaScript 调用 Native 的目的。 对于 iOS 的UIWebView 示例如下:

// 原生方法注册
JSContext *context = [uiWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext”];

// 注入方法对象postBridgeMessage
context[@"postBridgeMessage"] = ^(NSArray<NSArray *> *calls) { 
    // Native 逻辑 
    ...
}; 

// 前端调用方式
window.postBridgeMessage(message); 

对于 iOS 的 WKWebView 示例如下:

// 原生方法注册
@interface WKWebVIewVC ()<WKScriptMessageHandler> 
@implementation WKWebVIewVC 
- (void)viewDidLoad { 
    [super viewDidLoad];
    WKWebViewConfiguration* configuration = [[WKWebViewConfiguration alloc] init];
    configuration.userContentController = [[WKUserContentController alloc] init];
    WKUserContentController *userCC = configuration.userContentController; 
    // 注入对象,前端调用其方法时,Native 可以捕获到 [userCC addScriptMessageHandler:self name:@"nativeBridge”];
    WKWebView wkWebView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];
    // TODO 显示 WebView 
 } 
 - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { 
 if ([message.name isEqualToString:@"nativeBridge"]) { 
     NSLog(@"前端传递的数据 %@: ",message.body); 
     // Native 逻辑 
     ...
  } 
}

// 前端调用方式
window.webkit.messageHandlers.nativeBridge.postMessage(message); 

参考文档 blog.csdn.net/yuzhengfei7…

1.2 拦截 URL SCHEME

主要流程是:Web 端通过某种方式(例如 iframe.src)发送 URL Scheme 请求,之后 Native 拦截到请求并根据 URL SCHEME(包括所带的参数)进行相关操作。 在使用过程中,这种方式有一定的缺陷: 使用 iframe.src 发送 URL SCHEME 会有 url 长度的隐患。 创建请求,需要一定的耗时,比注入 API 的方式调用同样的功能,耗时会较长。

// 原生监听url变化
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType { 
    NSURL *URL = request.URL;
    NSString *scheme = [URL scheme];
    if ([scheme isEqualToString:@"协议名"]) {
        [self handleCustomAction:URL];
        return NO; 
    } 
  return YES; 
} 

// 前端调用方式 
function loadURL(url) { 
    var iFrame; iFrame = document.createElement("iframe");
    iFrame.setAttribute("src", url); iFrame.setAttribute("style", "display:none;"); 
    iFrame.setAttribute("height", "0px"); iFrame.setAttribute("width", "0px"); 
    iFrame.setAttribute("frameborder", "0");
    document.body.appendChild(iFrame); 
    // 发起请求后这个iFrame就没用了,所以把它从dom上移除掉 
    iFrame.parentNode.removeChild(iFrame); iFrame = null; 
} 
loadURL("esales://ZTHShowPicker"); 

参考文档 www.cnblogs.com/dianming/p/…

2.Native回调JavaScript 的方式

相比于 JavaScript 调用 Native, Native 调用 JavaScript 较为简单,直接执行拼接好的 JavaScript 代码即可。 从外部调用 JavaScript 中的方法,因此 JavaScript 的方法必须在全局的 window 上。

对于 iOS 的 UIWebView,示例如下:

result = [uiWebview stringByEvaluatingJavaScriptFromString:javaScriptString];
* javaScriptString为JavaScript 代码串 

对于 iOS 的 WKWebView,示例如下:

[wkWebView evaluateJavaScript:javaScriptString completionHandler:completionHandler]; 

四、JSBridge协议解析

整体流程: 在 Native 端配合实现 JSBridge 的 JavaScript 调用 Native 逻辑也很简单,主要的代码逻辑是:接收到 JavaScript 消息 => 解析参数,拿到 bridgeName、data 和 callbackId => 根据 bridgeName 找到功能方法,以 data 为参数执行 => 执行返回值和 callbackId 一起回传前端。 Native调用 JavaScript 也同样简单,直接自动生成一个唯一的 ResponseId,并存储句柄,然后和 data 一起发送给前端即可。

graph LR
javascript --bridgeName://data#callbackId--> native:根据bridgeName找到功能方法,以data为参数执行 --callbackId,data--> javascript

扩展:拦截 URL SCHEME之JSBridge前端引用方式

因此整个JSBridge分为两个部分,前端与原生端,原生端方法没有异议只能是在原生端进行代码实现,那么前端代码可以有两种方式进行引入。

1.由 Native 端进行注入

注入方式和 Native 调用 JavaScript 类似,直接执行桥的全部代码。

它的优点是:

桥的版本很容易与 Native 保持一致,Native 端不用对不同版本的 JSBridge 进行兼容。

它的缺点是:

注入时机不确定,需要实现注入失败后重试的机制,保证注入的成功率,同时 JavaScript 端在调用接口时,需要优先判断 JSBridge 是否已经注入成功。

2.由 JavaScript 端引用

它的优点是:

JavaScript 端可以确定 JSBridge 的存在,直接调用即可。

它的缺点是:

如果桥的实现方式有更改,JSBridge 需要兼容多版本的 Native Bridge 或者 Native Bridge 兼容多版本的 JSBridge。