Hybrid通信

10次阅读

共计 18381 个字符,预计需要花费 46 分钟才能阅读完成。

前言

相信很多人都在项目里熟练使用各种 Hybrid 技术,无论是使用了知名得 WebViewJavascriptBridge 框架来做自己的 Hybrid Web 容器,又或是自己从头着手写了一个满足自己业务需求的 bridge,从而构建起自己的 Hybrid Web 容器,也有的干脆直接使用了 cordova 这一大型 Hybrid 容器框架,cordova + ionic 来进行 Hybrid 的开发。

拆解学习框架源码是一个好事,但是在拆解优秀框架源码的背后,如何将多个优秀源码的精华打碎重塑,结合自己的产品业务需求重新组合成为适合自己的,并且扎实掌握可以灵活修改自如控制的代码,这也算是另一个层面的提升。

  • 选择合适的 JS 通信方案
  • 实现基本的 JSBridge 能力
  • 尝试拓展 JSBridge 的额外能力

这一篇先重点聊聊 JS 与 Native 通信的通信方案

几种 JS Native 相互通信方式的介绍

大家可能看了很多大框架源码,无论是 cordova 还是 WebViewJavascriptBridge 他们核心的通信方式就都是 假跳转请求拦截

但其实 JS 与 Native 通信并不止一种方式,还有很多种通信方式,尤为重要的是,不同的通信方式有着不同的特点,有的甚至虽然受限于安卓 / 苹果平台差异不通用,但独有的优点却是 假跳转请求拦截 无法比拟的

JS 调用 Native 的几种通信方案

  • 假跳转的请求拦截
  • 弹窗拦截

    alert()

    prompt()

    confirm()

  • JS 上下文注入

    苹果 JavaScriptCore 注入

    安卓 addJavascriptInterface 注入

    苹果 scriptMessageHandler 注入

Native 调用 JS 的几种通信方案

JS 是一个脚本语言,在设计之初就被设计的任何时候都可以执行一段字符串 js 代码,换句话说,任何一个 js 引擎都是可以在任意时机直接执行任意的 JS 代码,我们可以把任何 Native 想要传递的消息 / 数据直接写进 JS 代码里,这样就能传递给 JS 了

  • evaluatingJavaScript 直接注入执行 JS 代码

大家在 PC 上用电脑,用 Chrome 的时候都知道,可以直接用’javascript:xxxx’来简单的执行一些 JS 代码,弹个框,这个方法只有安卓可以用,因为 iOS 必须先将 url 字符串生成 Request 再交给 webview 去 load,这种’javascript:xxxx’生成 request 会失败

  • loadUrl 浏览器用 javascript:+JS 代码做跳转地址

WKWebView 官方提供了一个 Api,可以让 WebView 在加载页面的时候,自动执行注入一些预先准备好的 JS

  • WKUserScript WKWebView 的 addUserScript 方法,在加载时机注入

JS 调用 Native 的几种通信方案

假跳转的请求拦截

何谓 假跳转的请求拦截 就是由网页发出一条新的跳转请求,跳转的目的地是一个非法的压根就不存在的地址比如

// 常规的 Http 地址
https://wenku.baidu.com/xxxx?xx=xx

// 假的请求通信地址
wubacst://host/path?param=paramobj

看我下面写的那条假跳转地址,这么一条什么都不是的扯淡地址,直接放到浏览器里,直接扔到 webview 里,肯定是妥妥的什么都打不开的,而如果在经过我们改造过的 hybrid webview 里,进行拦截不进行跳转

url 地址分为这么几个部分

  • 协议:也就是 http/https/file 等,上面用了 wubacst
  • 域名:上面的 wenku.baidu.com 和 host
  • 路径:上面的 xxxx? 或 /path?
  • 参数:上面的 xx=xx 或 param=paramobj

如果我们构建一条假 url

  • 用协议与域名当做通信识别
  • 用路径当做指令识别
  • 用参数当做数据传递

客户端会无差别拦截所有请求,真正的 url 地址应该照常放过,只有协议域名匹配的 url 地址才应该被客户端拦截,拦截下来的 url 不会导致 webview 继续跳转错误地址,因此无感知,相反拦截下来的 url 我们可以读取其中路径当做指令,读取其中参数当做数据,从而根据约定调用对应的 native 原生代码

以上其实是一种 协议约定 只要 JS 侧按着这个约定协议生成假 url,native 按着约定协议拦截 / 读取假 url,整个流程就能跑通。

完全可以不用按着我写的这种方式约定协议,可以任意另行约定协议比如,协议当做通信识别,域名当做模块识别,路径当做指令识别,参数当做数据传递等等,协议协议,任何一种合理的约定都可以,都可以正常的让 JS 与 Native 进行通信

假跳转的请求拦截 -JS 发起调用

JS 其实有很多种方式发起假请求,跟发起一个新请求没啥两样,只要按着 协议约定 生成假请求地址,正常的发起跳转即可,任何一种方式都可以让客户端拦截住

  • A 标签跳转
// 在 HTML 中写上 A 标签直接填写假请求地址
<a href="wubacst: host="" path?param="paramobj"">
    A 标签 A 标签 A 标签 A 标签
</a href="wubacst:>
  • 原地跳转
// 在 JS 中用 location.href 跳转
location.href = 'wubacst://host/path?param=paramobj'
  • iframe 跳转
// 在 JS 中创建一个 iframe,然后插入 dom 之中进行跳转
$('body').append('<iframe src="' + 'wubacst: host="" path?param="paramobj' + '"style="display:none"">');
</iframe src="&#39; + &#39;wubacst:>

假跳转的请求拦截 - 客户端拦截

  • 安卓的拦截方式 shouldverrideUrlLoading
@Overridepublic boolean shouldOverrideUrlLoading(WebView view, String url) {    
    //1 根据 url,判断是否是所需要的拦截的调用 判断协议 / 域名    
    if(是){      
        //2 取出路径,确认要发起的 native 调用的指令是什么      
        //3 取出参数,拿到 JS 传过来的数据      
        //4 根据指令调用对应的 native 方法,传递数据      
        return true;    
    }    
    return super.shouldOverrideUrlLoading(view, url);
}
  • iOS 的 UIWebView 的拦截方式 webView:shouldStartLoadWithRequest:navigationType:
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{    
    //1 根据 url,判断是否是所需要的拦截的调用 判断协议 / 域名    
    if(是){      
        //2 取出路径,确认要发起的 native 调用的指令是什么      
        //3 取出参数,拿到 JS 传过来的数据      
        //4 根据指令调用对应的 native 方法,传递数据      
        return NO;      
        // 确认拦截,拒绝 WebView 继续发起请求    
    }        
    return YES;
}
  • iOS 的 WKWebView 的拦截方式 webView:decidePolicyForNavigationAction:decisionHandler:
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
    //1 根据 url,判断是否是所需要的拦截的调用 判断协议 / 域名
    if (是){
      //2 取出路径,确认要发起的 native 调用的指令是什么
      //3 取出参数,拿到 JS 传过来的数据
      //4 根据指令调用对应的 native 方法,传递数据

      // 确认拦截,拒绝 WebView 继续发起请求
        decisionHandler(WKNavigationActionPolicyCancel);
    }else{decisionHandler(WKNavigationActionPolicyAllow);
    }
    return YES;
}

弹窗拦截

前端可以发起很多种弹窗包含

  • alert() 弹出个提示框,只能点确认无回调
  • confirm() 弹出个确认框(确认,取消),可以回调
  • prompt() 弹出个输入框,让用户输入东西,可以回调

每种弹框都可以由 JS 发出一串字符串,用于展示在弹框之上,而此字符串恰巧就是可以用来传递数据,我们把所有要传递通讯的信息,都封装进入一个 js 对象,然后生成字典,最后序列化成 json 转成字符串

通过任意一种弹框将字符串传递出来,交给客户端就可以进行拦截,从而实现通信

弹窗拦截 – JS 发起调用

其实 alert/confirm/prompt 三种弹框使用上没任何区别和差异,这里只取其中一种举例,可以选一个不常用的当做管道进行 JS 通信,这里用 prompt 举例

var data = {
    action:'xxxx',
    params:'xxxx',
    callback:'xxxx',
};
var jsonData = JSON.stringify([data]);
// 发起弹框
prompt(jsonData);

弹窗拦截 – 客户端拦截

  • 安卓的拦截 onJsPrompt(其他的两个弹框也有)
@Override
public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) {
    //1 根据传来的字符串反解出数据,判断是否是所需要的拦截而非常规 H5 弹框
    if (是){
      //2 取出指令参数,确认要发起的 native 调用的指令是什么
      //3 取出数据参数,拿到 JS 传过来的数据
      //4 根据指令调用对应的 native 方法,传递数据
      return true;
    }
    return super.onJsPrompt(view, url, message, defaultValue, result);
}
  • iOS 的 WKWebView webView:runJavaScriptTextInputPanelWithPrompt:balbala
- (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(nullable NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString * _Nullable result))completionHandler{
    //1 根据传来的字符串反解出数据,判断是否是所需要的拦截而非常规 H5 弹框
    if (是){
        //2 取出指令参数,确认要发起的 native 调用的指令是什么
        //3 取出数据参数,拿到 JS 传过来的数据
        //4 根据指令调用对应的 native 方法,传递数据
        // 直接返回 JS 空字符串
        completionHandler(@"");
    }else{
        // 直接返回 JS 空字符串
        completionHandler(@"");
    }
}
  • iOS 的 UIWebView

UIWebView 不支持截获任何一种弹框,因此这条路走不通

JS 上下文注入

说道 JS 上下文注入,做 iOS 的都会了解到 iOS7 新增的一整个 JavaScriptCore 这个 framework,这个 framework 被广泛使用在了 JSPatch,RN 等上面,但这个东西一般用法都是完全脱离于 WebView,只有一个 JS 上下文,这个 JS 上下文里,没有 window 对象,没有 dom,严格意义上讲这个和我们所关注的依赖 WebView 的 Hybrid 框架是有很大差异的,就不在这篇文章里多说了

  • 苹果 UIWebview JavaScriptCore 注入
  • 安卓 addJavascriptInterface 注入
  • 苹果 WKWebView scriptMessageHandler 注入

虽然某种意义上讲上面三种方式,他们都可以被称作 JS 注入,他们都有一个共同的特点就是,不通过任何拦截的办法,而是直接将一个 native 对象(or 函数)注入到 JS 里面,可以由 web 的 js 代码直接调用,直接操作

但这三种注入方式都操作差异还是很大,并且各自的局限性各不相同,我们下面一一说明

苹果 UIWebview JavaScriptCore 注入

UIWebView 可以通过 KVC 的方法,直接拿到整个 WebView 当前所拥有的 JS 上下文

documentView.webView.mainFrame.javaScriptContext

拿到了 JSContext,一切的使用方式就和直接操作 JavaScriptCore 没啥区别了,我们可以把任何遵循 JSExport 协议的对象直接注入 JS,让 JS 能够直接控制和操作

所以在介绍如何 JS 与 Native 操作的时候换个顺序,先介绍客户端如何把 bridge 函数注入到 JS,在介绍 JS 如何使用

苹果 UIWebview JavaScriptCore 注入 – 客户端注入

// 拿到当前 WebView 的 JS 上下文
JSContext *context = [webview valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
// 给这个上下文注入 callNativeFunction 函数当做 JS 对象
context[@"callNativeFunction"] = ^(JSValue * data)
{
    //1 解读 JS 传过来的 JSValue  data 数据
    //2 取出指令参数,确认要发起的 native 调用的指令是什么
    //3 取出数据参数,拿到 JS 传过来的数据
    //4 根据指令调用对应的 native 方法,传递数据
    //5 此时还可以将客户端的数据同步返回!}

通过上面的方法可以拿到当前 WebView 的 JS 上下文 JSContext,然后就要准备往这个 JSContext 里面注入准备好的 block,而这个准备好的 block,负责解读 JS 传过来的数据,从而分发调用各种 native 函数指令

TIPS:

这种注入不止可以把 block 注入,在 JS 里成为一个 JS 函数,还可以把字符 / 数字 / 字典等数据直接注入到 JS 全局对象之中,可以让 JS 访问到 Native 才能获取的全局对象,甚至还可以注入任何 NSObject 对象,只要这个 NSObject 对象遵循 JSExportOC 的协议,相当于 JS 可以直接调用访问 OC 的内存对象

苹果 UIWebview JavaScriptCore 注入 – JS 调用

// 准备要传给 native 的数据,包括指令,数据,回调等
var data = {
    action:'xxxx',
    params:'xxxx',
    callback:'xxxx',
};
// 直接使用这个客户端注入的函数
callNativeFunction(data);

在没经过客户端注入的时候,直接使用调用 callNativeFunction() 会报 callNativeFunction is not defined 这个错误,说明此时 JS 上下全文全局,是没有这个函数的,调用无效

当执行完客户端注入的时候,此时 JS 上下文全局 global 下面,就拥有了这个 callNativeFunction 的函数对象,就可以正常调用,从而传递数据到 Native

安卓 addJavascriptInterface 注入

安卓的 WebView 有一个接口 addJavascriptInterface,可以在 loadUrl 之前提前准备一个对象,通过这个接口注入给 JS 上下文,从而让 JS 能够操作,这个操作方式很类似苹果 UIWebview JavaScriptCore 注入,整个机制也差别不离,但有个很重大的区别,后面在详述优缺点对比的时候,会重点描述

安卓 addJavascriptInterface 注入 – 客户端注入

使用安卓官方的 API 接口即可,并且可以在 loadUrl 之前 WebView 创建之后,即可配置相关注入功能,这个和 UIWebView-JSContext 的使用差异非常之大,后面会说

// 通过 addJavascriptInterface() 将 Java 对象映射到 JS 对象
// 参数 1:Javascript 对象名
// 参数 2:Java 对象名
mWebView.addJavascriptInterface(new AndroidtoJs(), "nativeObject");

其中 AndroidtoJs 这个是一个自定义的安卓对象,他们里面有个函数 callFunction,AndroidtoJs 这个对象的其他函数方法 JS 都可以调用

安卓 addJavascriptInterface 注入 – JS 调用

刚才注入的 js 对象叫 nativeObject,所以 JS 中可以在全局任意使用

`nativeObject.callFunction("js 调用了 android 中的 hello 方法");`

我不是很熟悉 android,以上很多安卓代码都取自 Android:你要的 WebView 与 JS 交互方式 都在这里了,后面也会纳入参考文献之中

苹果 WKWebView scriptMessageHandler 注入

苹果在开放 WKWebView 这个性能全方位碾压 UIWebView 的 web 组件后,也大幅更改了 JS 与 Native 交互的方式,提供了专有的交互 APIscriptMessageHandler

因为这是苹果的 API,使用方式搜一下一搜一大堆,我并不详细解释了,直接展示一下代码

苹果 WKWebView scriptMessageHandler 注入 – 客户端注入

// 配置对象注入
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"nativeObject"];
// 移除对象注入
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"nativeObject"];

需要说明一下,addScriptMessageHandler 就像安卓的 addJavascriptInterface 一样,可以在 WKWebView loadUrl 之前即可进行相关配置

但不一样的是,如果当前 WebView 没用了,需要销毁,需要先移除这个对象注入,否则会造成内存泄漏,WebView 和所在 VC 循环引用,无法销毁。

苹果 WKWebView scriptMessageHandler 注入 – JS 调用

刚才注入的 js 对象叫 nativeObject,但不像前边两个注入一样,直接注入到 JS 上下文全局 Global 对象里,addScriptMessageHandler 方法注入的对象被放到了,全局对象下一个 Webkit 对象下面,想要拿到这个对象需要这样拿

`window.webkit.messageHandlers.nativeObject`

并且和之前的两种注入也不同,前两种注入都可以让 js 任意操作所注入自定义对象的所有方法,而 addScriptMessageHandler 注入其实只给注入对象起了一个名字 nativeObject,但这个对象的能力是不能任意指定的,只有一个函数 postMessage,因此 JS 的调用方式也只能是

// 准备要传给 native 的数据,包括指令,数据,回调等
var data = {
    action:'xxxx',
    params:'xxxx',
    callback:'xxxx',
};
// 传递给客户端
window.webkit.messageHandlers.nativeObject.postMessage(data);

苹果 WKWebView scriptMessageHandler 注入 – 客户端接收调用

前两种注入方式,都是在注入的时候,就指定了对应的接收 JS 调用的 Native 函数,但是这次不是,在苹果的 API 设计里,当 JS 开始调用后,会调用到指定的 iOS 的 delegate 里

-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
    //1 解读 JS 传过来的 JSValue  data 数据
    NSDictionary *msgBody = message.body;
    //2 取出指令参数,确认要发起的 native 调用的指令是什么
    //3 取出数据参数,拿到 JS 传过来的数据
    //4 根据指令调用对应的 native 方法,传递数据
}

Native 调用 JS 的几种通信方案

说完了 JS 调用 Native,我们再聊聊 Native 发起调用 JS

evaluatingJavaScript 执行 JS 代码

上面也简单说了一下,JS 是一个脚本语言,可以在无需编译的情况下,直接输入字符串 JS 代码,直接运行执行看结果,这也是为什么在 Chrome 里,在网页运行的时候打开控制台,可以输入各种 JS 指令的看结果的。

也就是说当 Native 想要调用 JS 的时候,可以由 Native 把需要数据与调用的 JS 函数,通过字符串拼接成 JS 代码,交给 WebView 进行执行

说明一下,Android/iOS-UIWebView/iOS-WKWebView,都支持这种方法,这是目前最广泛运用的方法,甚至可以说,Chrome 的 DevTools 控制台也是用的同样的方式。

假如 JS 网页里已经有了这么一个函数

function calljs(data){console.log(JSON.parse(data)) 
    //1 识别客户端传来的数据
    //2 对数据进行分析,从而调用或执行其他逻辑  
}

那么客户端此时要调用他需要在客户端用 OC 拼接字符串,拼出一个 js 代码,传递的数据用 json

// 不展开了,data 是一个字典,把字典序列化
NSString *paramsString = [self _serializeMessageData:data];
NSString* javascriptCommand = [NSString stringWithFormat:@"calljs('%@');", paramsString];
// 要求必须在主线程执行 JS
if ([[NSThread currentThread] isMainThread]) {[self.webView evaluateJavaScript:javascriptCommand completionHandler:nil];
} else {__strong typeof(self)strongSelf = self;
    dispatch_sync(dispatch_get_main_queue(), ^{[strongSelf.webView evaluateJavaScript:javascriptCommand completionHandler:nil];
    });
}

其实我们拼接出来的 js 只是一行 js 代码,当然无论多长多复杂的 js 代码都可以用这个方式让 webview 执行

`calljs('{data:xxx,data2:xxx}');`

TIPS: 安卓 4.4 以上才可以使用 evaluatingJavaScript 这个 API

loadUrl 执行 JS 代码

安卓在 4.4 以前是不能用 evaluatingJavaScript 这个方法的,因此之前安卓都用的是 webview 直接 loadUrl,但是传入的 url 并不是一个链接,而是以 ”javascript:” 开头的 js 代码,从而达到让 webview 执行 js 代码的作用

其实这个过程和 evaluatingJavaScript 没啥差异

还按着刚才举例,假如 JS 网页里已经有了这么一个函数

function calljs(data){console.log(JSON.parse(data)) 
    //1 识别客户端传来的数据
    //2 对数据进行分析,从而调用或执行其他逻辑  
}

我不太熟悉安卓,就不写安卓的字典数据 json 序列化的逻辑了

`mWebView.loadUrl("javascript:callJS(\'{data:xxx,data2:xxx}\')");`

最终实际上相当于执行了一条 js 代码

`calljs('{data:xxx,data2:xxx}');`

WKUserScript 执行 JS 代码

对于 iOS 的 WKWebView,除了 evaluatingJavaScript,还有 WKUserScript 这个方式可以执行 JS 代码,他们之间是有区别的

  • evaluatingJavaScript 是在客户端执行这条代码的时候立刻去执行当条 JS 代码
  • WKUserScript 是预先准备好 JS 代码,当 WKWebView 加载 Dom 的时候,执行当条 JS 代码

很明显这个虽然是一种通信方式,但并不能随时随地进行通信,并不适合选则作为设计 bridge 的核心方案。但这里也简单介绍一下

// 在 loadurl 之前使用
//time 是一个时机参数,可选 dom 开始加载 /dom 加载完毕,2 个时机进行执行 JS
// 构建 userscript
WKUserScript *script = [[WKUserScript alloc]initWithSource:source injectionTime:time forMainFrameOnly:mainOnly];
WKUserContentController *userController = webView.userContentController;
// 配置 userscript
[userController addUserScript:script]

几种通信方式的优缺点对比

说完了 JS 主动调用 Native,也说完了 Native 主动调用 JS,有很多很多的方案我们来聊聊这么些个方案都有哪些局限性,是否值得我们选择

假请求的通信拦截的问题 – 当下最不该选择的通信方式

假通信拦截请求这种方式看起来是使用最广泛的,知名的 WebViewJavascriptBridge 和 cordova

为什么这些知名框架选用假请求通信拦截其实有很多原因,但我想说的是,基于眼下设计自己的 Hybrid 框架,最不应该选择的通信方式就是假请求通信拦截

先说说他为数不多的优点:

  • 版本兼容性好:iOS6 及以前只有这唯一的一种方式

cordova 的前身是 phonegap,随手搜一下大概能知道这个框架有多老,也可以看下 WebViewJavascriptBridge,最早第一次提交是在 5 年前,在没有 iOS7 的时候,有切只有这唯一的一种通信方式,因此他们都选用了他,但看一眼现在已经 iOS11 了,再看看 iOS6 及以下的占有度,呵呵,一到 iOS7 就有更好的全方位碾压的 bridge 方式了

  • webview 支持度好:简单地说框架的开发者容易偷懒

这是所有 JS call Native 的通信方式里,唯一同时支持安卓 webview/ 苹果 UIWebView/ 苹果 WKWebView 的一种通信方式,这也就是为什么 WebViewJavascriptBridge 在即便苹果已经推出了更好的 WKWebView 并且准备了专属的通信 APImessageHandler 的时候,还依然选择继续沿用假请求通信拦截的原因,代码不用重写了,适合写那种兼容 iOS7 以下的 UIWebView,在 iOS8 以上换 WKWebView 的代码,但看一眼现在的版本占有度?没有任何意义

多说两句:

即便是老项目还在使用 UIWebView,要计划升级到 WKWebView 的时候,既然是升级就应该全面升级到新的 WK 式通信,做什么妥协和折中方案?

而且最重要的一点,想要做到同时支持多个 WebView 兼容支持并不需要选择妥协方案,在开发框架的时候完全可以在框架侧解决。想要屏蔽这种 webview 通信差异,通过在 Hybrid 框架层设计,抽象统一的调用入口出口,把通信差异在内部消化,这样依然能做到统一对外业务代码流程和清晰的代码逻辑,想要做到代码统一不应该以功能上牺牲和妥协的方面去考虑。

要知道 cordova 都专门为 WKWebView 开发了独有的 cordova-plugin-wkwebview 插件来专门适配 WKWebView 的更优的官方通信 API,而不是像 WebViewJavascriptBridge 进行妥协,UI 与 WK 都采取同一种有功能性问题的通信方案

再说说他最严重的缺点:

  • 丢消息!一个通信方案,结果他最大的问题是丢失通信消息!
location.href = 'wubacst://wahahalalala/callNativeNslog?param=1111'
location.href = 'wubacst://wahahalalala/callNativeNslog?param=2222'

上面是一段 JS 调用 Native 的代码,可以靠字面意思猜一下,JS 此时的诉求是在同一个运行逻辑内,快速的连续发送出 2 个通信请求,用客户端本身 IDE 的 log,按顺序打印 111,222,那么实际结果是 222 的通信消息根本收不到,直接会被系统抛弃丢掉。

原因:因为假跳转的请求归根结底是一种模拟跳转,跳转这件事情上 webview 会有限制,当 JS 连续发送多条跳转的时候,webview 会直接过滤掉后发的跳转请求,因此第二个消息根本收不到,想要收到怎么办?JS 里将第二条消息延时一下

// 发第一条消息
location.href = 'wakaka://wahahalalala/callNativeNslog?param=1111'

// 延时发送第二条消息
setTimeout(500,function(){location.href = 'wakaka://wahahalalala/callNativeNslog?param=2222'})

这根本治标不治本好么,这种框架设计下决定了 JS 在任何通信逻辑都得考虑是否这个时间有其他的 JS 通信代码刚交互过,导致消息丢失?是否页面加载完毕的时候不能同时发送页面加载完毕和其他具体业务需要的 Native 消息,是否任何一个 AJax 网络请求回来后立刻发起的 Native 消息,都要谨慎考虑与此同时是否有别的 SetTimeout 也在发 Native 消息导致冲突?这 TM 根本是一个天坑,这么设计绝对是客户端开发舒坦省事写 bridge 框架代码,坑死天天做活动上线的前端同学的。

如果想继续使用假跳转请求,又不想换方案怎么办?前端同学在 JS 框架层包一层队列,所有 JS 代码调用消息都先进入队列并不立刻发送,然后前端会周期性比如 500 毫秒,清空 flush 一次队列,保证在很快的时间内绝对不会连续发 2 次假请求通信,这种通信队列的设计不光运用解决丢消息的问题,就连 RN 根本没丢消息问题的 JSCore 式的通信,也采用了这种方式,归根结底他能减少通信开销,但是!但是!给假通信请求做队列你将面临第二个根本没法解决的问题

  • URL 长度限制

假跳转请求归根结底他还是一个跳转,抛给客户端被拦截的时候都已经被封装成一个 request 了,那么如果 url 超长了呢?那么这个 request 里的 url 的内容还是你想要传递的原内容么?不会丢内容么?尤其是当你采用了队列控制,一次性发送的是多条消息组成的数组数据的时候。

假跳转是现在这个时候最不该使用的通信方式!!!

假跳转是现在这个时候最不该使用的通信方式!!!

假跳转是现在这个时候最不该使用的通信方式!!!

重要的事情说三遍

弹窗拦截

这个方式其实没啥不好的,而且 confirm 还可以用更简单的方式处理 callback 回调,因为 confirm 天然是需要返回 JS 内容的,但 callback 其实也可以用其他的方式实现,也许更好,因此这里按住不表,第二篇文章会整体聊聊,基于这么多种通信手段,如何设计一个自己的 Hybrid 框架

  • UIWebView 不支持,但没事 UIWebView 有更好的 JS 上下文注入的方式,JSContext 不仅支持直接传递对象无需 json 序列化,还支持传递 function 函数给客户端呢
  • 安卓一切正常,不会出现丢消息的情况
  • WKWebView 一切正常,也不会出现丢消息的情况,但其实 WKWebView 苹果给了更好的 API,何不用那个,至少用这个是可以直接传递对象无需进行 json 序列化的

唯一需要注意的一点,如果你的业务开发中经常希望在前端代码里使用系统 alert()/confirm()/prompt() 那么,你还是挑一个不常用的进行 hook,以免干扰常规业务

JS 上下文注入

JS 上下文注入其实一共 3 种情况,这 3 种情况每个情况都不同,我会一一进行优缺点说明

UIWebView 的 JSContext 注入

说实话这是我觉得最完美的一种交互方式了,苹果在 iOS7 开放了 JavaScriptCore 这个框架,支撑起了 RN,Weex 这么牛逼的摆脱了 WebView 的深度混合框架,他的能力是最完美的。

牛逼的优点:

  • 支持 JS 同步返回!

要知道我们看到的所有 JS 通信框架设计的都是异步返回,包括 RN(这有设计原因,但不代表 JSC 不支持同步返回),都是设计了一套 callback 机制,一条通信消息到达 Native 后,如果需要返回数据,需要调用这个 callback 接口由 Native 反向通知 JS,他们在 JS 侧写代码可是差异非常非常非常之大的!

// 同步 JS 调用 Native  JS 这边可以直接写 =  !!!
var nativeNetStatus = nativeObject.getNetStatus();
// 异步 JS 调用 Native JS 只能这么写
nativeObject.getNetSatus(callback(net){console.log(net)
})
  • 支持直接传递对象,无需通过字符串序列化

一个 JS 对象在 JS 代码中如果想通过假跳转 / 弹窗拦截等方式,那么必须把 JS 对象搞成 json,然后才能传递给端,端拿到后还要反解成字典对象,然后才能识别,但是 JS 上下文注入不需要(其实他本质上是框架层帮你做了这件事情,就是 JSValue 这个 iOS 类的能力)

  • 支持传递 JS 函数,客户端能够直接快速调用 callback

在 JS 里如果是一个 function,可以直接当做参数发送给客户端,在客户端得到一个 JSValue,可以通过 JSValue 的 callWithParmas 的方式直接当做函数去调用

  • 支持直接注入任意客户端类,客户端对象,JS 可以直接向调用客户端

JavaScriptCore 有一种使用方法,是可以让任意 iOS 对象,遵循协议,就可以直接把一整个 Native 对象直接注入,让 JS 可以直接操作这个对象,读取这个对象的属性,调用这个对象的方法

有点尴尬的缺点:

  • only UIWebView

这一点简直是最大的遗憾,只有 UIWebView 可以用 KVC 取到 JSContext,取到了 JSContext 才能发挥 JavaScriptCore 的牛逼能力,但是如果为了更好的性能升级到了 WKWebView,那就得忍痛,我依稀记得曾几何时我在哪看到过通过私有 API,让 WKWebView 也能获取 JSContext,但我找不到了,希望知道的同学能给我点指引。但我有一个看法 为了 WKWebView 的性能提升,舍弃 JSContext 的优点,值得!

  • JSContext 获取时机

UIWebView 的 JSContext 是通过 iOS 的 kvc 方法拿到,而非 UIWebView 的直接接口 API,因此 UIWebView-JSContext 注入使用上要非常注意注入时机

  • UIWebView-JSContext 在 loadUrl 之前注入无效
  • UIWebView-JSContext 在 FinishLoad 之后注入有效但有延迟

因为 WebView 每次加载一个新地址都会启用一个新的 JSContext,在 loadUrl 之前注入,会因为旧的 JSContext 已被舍弃导致注入无效,若在 WebView 触发 FinishLoad 事件的时候注入,又会导致在 FinishLoad 之前执行的 JS 代码,是无法调用 native 通信的

曾经写过一篇文章 UIWebView 代码注入时机与姿势,可以参考看看,有私有 API 解决办法,不在这里多言

如果你还在使用 UIWebView,真的应该彻底丢弃什么假跳转,直接使用这个方案(iOS7.0 现在已经不是门槛了吧),并且深度开发 JavaScriptCore 这么多牛逼优势所带来的一些黑科技(我感觉会在第三篇文章里提这个)

如果你还在使用 UIWebView,就用 JSContext 吧!不要犹豫!

如果你还在使用 UIWebView,就用 JSContext 吧!不要犹豫!

如果你还在使用 UIWebView,就用 JSContext 吧!不要犹豫!

安卓的 addJavascriptInterface 注入

我不太了解安卓,因此这粗略写一写,此处如果有错误非常希望大家帮我指出

安卓的 addJavascriptInterface 注入,其实原理机制几乎和 UIWebView 的 JSContext 注入一样,所以 UIWebView 的 JSContext 注入的有点他其实都有

  • 可以同步返回
  • 无需 json 化透传数据
  • 可以传递函数(不确定)
  • 可以注入 Native 对象

但是安卓的 addJavascriptInterface 没有注入时机这个缺点(类比 -UIWebView 的 JSContext 获取时机),原因是 UIWebView 缺失一个时机由内核通知外围,当前 JSContext 刚刚创建完毕,还未开始执行相关 JS,导致在 iOS 下无法在这个最应该进行注入的时机进行注入,除非通过私有 API,但安卓没事,安卓系统提供了个 API 来让外围获得这个最佳时机 onResourceloaded,详细说明见 UIWebView 代码注入时机与姿势

WKWebView 的 scriptMessageHandler 注入

苹果 iOS8 之后官方抓们推出的新一代 webview,号称全面优化,性能大幅度提升,是和 safari 一样的 web 内核引擎,带着光环出生,而 scriptMessageHandler 正是这个新 WKWebView 钦点的交互 API

优点:

  • 无需 json 化传递数据

是的,webkit.messageHandlers.xxx.postMessage() 是支持直接传递 json 数据,无需前端客户端字符串处理的

  • 不会丢消息

我们团队的以前老代码在丢消息上吃了无数的大亏,导致我对这个事情耿耿于怀,怨念极深!真是坑了好几代前端开发,叫苦不堪

缺点:

  • 版本要求 iOS8

我们舍弃了,不是问题

  • 不支持 JSContext 那样的同步返回

丧失了很多黑科技黑玩法的想象力!但我觉得还是有可能有办法哪怕用私有 API 的方式想办法找回来的,希望知道的朋友提供更多信息

如果你已经上了 WKWebView,就用它,不需要考虑

如果你已经上了 WKWebView,就用它,不需要考虑

如果你已经上了 WKWebView,就用它,不需要考虑

evaluatingJavaScript 直接执行 JS 代码

说完了 JS 主动调用 Native,我们再说说 Native 主动调用 JS,evaluatingJavaScript 是一个非常非常通用普遍的方式了,原因也在介绍里解释过,js 的脚本引擎天然支持,直接扔字符串进去,当做 js 代码开始执行

也没啥优缺点可以说的,除了有个特性需要在介绍 WKUserScript 的时候在多解释一下

安卓 /UIWebView/WKWebView 都支持

loadUrl 跳转 javascript 地址执行 JS 代码

具体的使用方式不详细介绍了,直说一个优点

  • 版本支持

在安卓 4.4 以前是没有 evaluatingJavaScript API 的,因此通过他来执行 JS 代码,但本质上和 evaluatingJavaScript 区别不大

WKUserScript 执行 JS 代码

这里要特别说明一下 WKUserScript 并不适合当做 Hybrid Bridge 的通信手段,原因是这种 Native 主动调用 JS,只能在 WebView 加载时期发起,并不能在任意时刻发起通信

WKUserScript 不能采用在 Hybrid 设计里当做通信手段

WKUserScript 不能采用在 Hybrid 设计里当做通信手段

WKUserScript 不能采用在 Hybrid 设计里当做通信手段

但 WKUserScript 却有一点值得说一下,上文也提到的 UIWebView 的注入时机,如果你想在恰当时机让 JS 上下文执行一段 JS 代码,在 UIWebView 你是找不到一个合适的加载时机的,除非你动用私有 API,但 WKWebView 解决了这个问题,在构造 WKUserScript 的时候可以选择 dom load start 的时候执行 JS,也可以选择在 dom load end 的时候执行 JS。但这个有点其实与设计 Hybrid 框架的核心通信方案,关系不大,但预加载 JS 预加载 CSS 也是一个 Hybrid 框架的扩展功能,后面第二篇会介绍的。

横向对比

如果我们要自主设计一个 Hybrid 框架,通信方案到底该如何取舍?

JS 主动调用 Native 的方案

Native 主动调用 JS 的方案

  • iOS: evaluatingJavaScript
  • 安卓: 其实 2 个区别不大,使用方法差异也不大

    4.4 以上 evaluatingJavaScript

  • 4.4 以下 loadUrl

这样对比优缺点,再根据自己项目需要支持的版本号,可以比较方便的选择合适的通信方案,进一步亲自设计一个 Hybrid 框架

一点个人看法

即便是老项目还在使用 UIWebView,要计划升级到 WKWebView 的时候,既然是升级就应该全面升级到新的 WK 式通信,做什么妥协和折中方案?

而且最重要的一点,想要做到同时支持多个 WebView 兼容支持并不需要选择妥协方案,在开发框架的时候完全可以在框架侧解决。想要屏蔽这种 webview 通信差异,通过在 Hybrid 框架层设计,抽象统一的调用入口出口,把通信差异在内部消化,这样依然能做到统一对外业务代码流程和清晰的代码逻辑,想要做到代码统一不应该以功能上牺牲和妥协的方面去考虑。

前面其实提到过这个看法不过说的还不彻底,可能有些人会觉得假跳转这个方案最大的好处是全平台全版本的适配与统一,甚至还可以统一安卓平台,可以保证代码一致性,但我认为这绝对不能建立在有严重功能短板导致开发中带来很严重问题的基础之上的,为了代码一致性,而妥协了框架的功能与能力

可能因为不同的平台 / 不同的版本 / 不同的 WebView 的使用与兼容,导致了我们需要在开发 Hybrid 框架的时候需要适配,但这一切都是可以通过设计良好的框架对外输入输出,把所有区别适配内部消化,从而做到在框架外层的业务代码依然保持代码一致性,保持干净整洁的。这里所说的框架绝不仅仅包括客户端这一侧,JS 侧也同理,谁说区分安卓和 IOS 平台来进行不同的通信方式代码就不整洁了,那是你框架层设计的不够优秀,合理框架层代码应该可以做到当新的系统组件出现,新的更优秀的通信方案出现的时候,能够立刻的支持和扩充,获得最新的能力和性能,但又在业务上层做到无感知,保持框架外围使用的一致性,这才是良好的设计。

所以我之前微博曾经说过一小段话:

就为了兼容从而选择放弃更合理的 WKWebview 官方注入 interface 方式,为了凑和 UIWebView 依然采用无论是 iframe 还是 location.href 的糊弄方式,这种我实在不觉得是美学,只是一种偷懒而已,抱着 UIWebview 时代的包袱不想丢还让 WKWebview 去迁就

没错,说的就是 WebViewJavascriptBridge

如果是你,你会怎么设计 Hybrid 框架

聊了这么多这个好那个不好,如果换做我们项目,我会选择啥?

  • iOS:MessageHandler 注入 (JSToNative) + evaluatingJavaScript(NativeToJS)

我们几个项目都已经只支持 iOS8 以上,甚至一个新项目只支持 iOS9 以上了

,旧项目确实在用 UIWebView 并且采用的是令我诟病的假跳转,但我们迁移新 WKWebView 的方案也绝不是将就 UIWebView,待迁移上线后是直接采用最合理的 MessageHandler 注入

  • 安卓: 拦截弹窗 (JSToNative)+loadUrl(NativeToJS)

我们安卓还需要支持更低的版本╮(╯_╰)╭

  • 同步通信:iOS-JSContext 以及安卓 -javascriptInterface

巨有巨大优势的 iOS-JSContext 以及安卓 -javascriptInterface,我们并不是完全舍弃,因为其同步传递特型真的有很大价值,所以在设计 Hybrid 框架 API 的时候,是留了 2 种 sendMessage 模式的,一种异步,一种同步,而这种同步作为额外功能对部分平台,部分 WebView 开放,WKWebView 很不幸在这种性能的选择下暂时丧失了同步通信能力,但还在想尽办法。

经过各种优缺点对比,我们确认了最核心的 JS 与 Native 通信方案,下一步就是亲自设计一个 Hybrid 框架了,这篇也太长了,挖个坑后面在写吧

正文完
 0