从零收拾一个hybrid框架(一)-- 从选择JS通讯方案开始

相信不少人都在项目里熟练使用各类Hybrid技术,不管是使用了知名得 WebViewJavascriptBridge 框架来作本身的Hybrid Web容器,又或是本身从头着手写了一个知足本身业务需求的bridge,从而构建起本身的Hybrid Web容器,也有的干脆直接使用了cordova 这一大型Hybrid容器框架,cordova + ionic 来进行Hybrid的开发javascript

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

  • 选择合适的JS通讯方案(第一篇)
  • 实现基本的WebView容器能力(第二篇 待续)
  • 尝试拓展WebView容器的额外能力(第三篇 待续)

这系列文章我想表达的并非在推广什么我本身的新Bridge轮子,也不是针对某个开源Bridge框架进行深度的源码分析。咱们从看开源框架轮子如何设计,如何使用,源码如何工做的思惟方式中跳出来前端

换一种模式去从目的出发,从需求出发,思考当你什么都没有的时候,你要从零思考构建一个hybrid框架的时候,你都要考虑哪些方面?这些方面采用怎样的设计思想能作到将来在使用中灵活自如,不至于面临局限java

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

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

你们可能看了不少大框架源码,不管是cordova仍是WebViewJavascriptBridge他们核心的通讯方式就都是 假跳转请求拦截git

但其实JS与Native通讯并不止一种方式,还有不少种通讯方式,尤其重要的是,不一样的通讯方式有着不一样的特色,有的甚至虽然受限于安卓/苹果平台差别不通用,但独有的优势倒是 假跳转请求拦截 没法比拟的github

JS 调用 Native 的几种通讯方案

  • 假跳转的请求拦截
  • 弹窗拦截
    • alert()
    • prompt()
    • confirm()
  • JS上下文注入
    • 苹果JavaScriptCore注入
    • 安卓addJavascriptInterface注入
    • 苹果scriptMessageHandler注入

Native 调用 JS 的几种通讯方案

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

  • evaluatingJavaScript 直接注入执行JS代码

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

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

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

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

JS 调用 Native 的几种通讯方案

假跳转的请求拦截

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

//常规的Http地址
https://wenku.baidu.com/xxxx?xx=xx
//假的请求通讯地址
wakaka://wahahalalala/action?param=paramobj
复制代码

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

url地址分为这么几个部分

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

若是咱们构建一条假url

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

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

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

彻底能够不用按着我写的这种方式约定协议,能够任意另行约定协议好比,协议当作通讯识别,域名当作模块识别,路径当作指令识别,参数当作数据传递等等,协议协议,任何一种合理的约定均可以,均可以正常的让JS与Native进行通讯

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

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

  • A标签跳转
//在HTML中写上A标签直接填写假请求地址
<a href="wakaka://wahahalalala/action?param=paramobj">A标签A标签A标签A标签</a>
复制代码
  • 原地跳转
//在JS中用location.href跳转
location.href = 'wakaka://wahahalalala/action?param=paramobj'
复制代码
  • iframe跳转
//在JS中建立一个iframe,而后插入dom之中进行跳转
$('body').append('<iframe src="' + 'wakaka://wahahalalala/action?param=paramobj' + '" style="display:none"></iframe>');
复制代码

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

  • 安卓的拦截方式 shouldOverrideUrlLoading
@Override
public 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不支持截获任何一种弹框,所以这条路走不通

通过好心人提醒,UIWebView也存在一种利用Undocumented API(只是未公开API,可是否处于被禁止的私有API不必定)的方式来拦截弹框。

原理是能够自行建立一个categroy,在里面实现一个未出如今任何UIWebView头文件里的delegate,就能拦截弹框了(这个Undocumented的delegate长得和WKWebView的拦截delegate一个样子)

iOS--UIWebView 屏蔽 alert警告框

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,有不少不少的方案咱们来聊聊这么些个方案都有哪些局限性,是否值得咱们选择

假请求的通讯拦截的问题 -- 当下最不应选择的通讯方式

假通讯拦截请求这种方式看起来是使用最普遍的,知名的WebViewJavascriptBridgecordova

为何这些知名框架选用假请求通讯拦截其实有不少缘由,但我想说的是,基于眼下设计本身的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 = 'wakaka://wahahalalala/callNativeNslog?param=1111'

location.href = 'wakaka://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函数给客户端呢(借助隐藏的API也能够支持)
  • 安卓一切正常,不会出现丢消息的状况
  • WKWebView一切正常,也不会出现丢消息的状况,但其实WKWebView苹果给了更好的API,何不用那个,至少用这个是能够直接传递对象无需进行json序列化的

惟一须要注意的一点,若是你的业务开发中常常但愿在前端代码里使用系统alert()/confirm()/prompt()那么,你仍是挑一个不经常使用的进行hook,以避免干扰常规业务

修订补充优势!

弹窗拦截也能够支持同步返回!

prompt( ) 拦截在客户端须要执行confirm(data)从而用同步的方式给客户端返回数据到JS

//同步JS调用Native JS这边能够直接写 = !!!
var nativeNetStatus = nativeObject.getNetStatus();
//异步JS调用Native JS只能这么写
nativeObject.getNetSatus(callback(net){
    console.log(net)
})
复制代码

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对象,遵循<JSExport>协议,就能够直接把一整个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的方案

通讯方案 版本支持 丢消息 支持同步返回 传递对象 注入原生对象 数据长度限制
假跳转 全版本全平台 会丢失 不支持 不支持 不支持 有限制
弹窗拦截 UIWebView不支持 不丢失 支持 不支持 不支持 无限制
JSContext注入 只有UIWebView支持 不丢失 支持 支持 支持 无限制
安卓interface注入 安卓全版本 不丢失 支持 支持 支持 无限制
MessageHandler注入 只有WKWebView支持 不丢失 不支持 不支持 不支持 无限制

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注入/Prompt弹框拦截(JSToNative) + evaluatingJavaScript (NativeToJS)

通过修正,异步返回采用MessageHandler 同步返回采用Prompt弹框拦截(JSToNative)

其实同步/异步在iOS上均可以采用 Prompt弹框拦截(JSToNative) 但毕竟MessageHandler是系统钦定API,而且拥有直接传递JSON对象,无需手动序列化这一优点,因此咱们依然选择2个方案都用,一个用来异步,一个用来同步

但其实,你也能够同步/异步都使用 Prompt弹框拦截

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

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

安卓就直接把 Prompt弹框拦截 当作同步/异步都选择的通讯方式

以上在设计Hybrid框架API的时候,都考虑了2种sendMessage模式的,一种异步,一种同步

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

本篇参考文献

因为我不是很懂安卓,本篇不少安卓的信息来自我和同事之间的探讨以及这篇文章

Android:你要的WebView与 JS 交互方式 都在这里了

另外聊到UIWebView的JSContext ,扯了好多JS上下文时机的事情,详细介绍在我本身的另外一篇文章里

UIWebView代码注入时机与姿式

系列相关文章

从零收拾一个hybrid框架(一)-- 从选择JS通讯方案开始

从零收拾一个hybrid框架(二)-- WebView容器基础功能设计思路

从零收拾一个Hybrid框架(三)-- WebView 容器的一些脑洞方案思路探讨 (挖坑ing)

相关文章
相关标签/搜索