相信很多人都在项目里熟练使用各种Hybrid技术,无论是使用了知名得 WebViewJavascriptBridge 框架来做自己的Hybrid Web容器,又或是自己从头着手写了一个满足自己业务需求的bridge,从而构建起自己的Hybrid Web容器,也有的干脆直接使用了cordova 这一大型Hybrid容器框架,cordova + ionic 来进行Hybrid的开发
拆解学习框架源码是一个好事,但是在拆解优秀框架源码的背后,如何将多个优秀源码的精华打碎重塑,结合自己的产品业务需求重新组合成为适合自己的,并且扎实掌握可以灵活修改自如控制的代码,这也算是另一个层面的提升。
- 选择合适的JS通信方案(第一篇)
- 实现基本的WebView容器能力(第二篇 待续)
- 尝试拓展WebView容器的额外能力(第三篇 待续)
这系列文章我想表达的并不是在推广什么我自己的新Bridge轮子,也不是针对某个开源Bridge框架进行深度的源码分析。我们从看开源框架轮子如何设计,如何使用,源码如何工作的思维方式中跳出来
换一种模式去从目的出发,从需求出发,思考当你什么都没有的时候,你要从零思考构建一个hybrid框架的时候,你都要考虑哪些方面?这些方面采用怎样的设计思想能做到未来在使用中灵活自如,不至于面临局限
这一篇先重点聊聊 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 的几种通信方案
假跳转的请求拦截
弹窗拦截
以上两种这里不做说明,主要介绍下面的常用的。
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上下文
1 | documentView.webView.mainFrame.javaScriptContext |
拿到了JSContext,一切的使用方式就和直接操作JavaScriptCore没啥区别了,我们可以把任何遵循JSExport协议的对象直接注入JS,让JS能够直接控制和操作
所以在介绍如何JS与Native操作的时候换个顺序,先介绍客户端如何把bridge函数注入到JS,在介绍JS如何使用
苹果UIWebview JavaScriptCore注入 - 客户端注入
1 | //拿到当前WebView的JS上下文 |
通过上面的方法可以拿到当前WebView的JS上下文JSContext,然后就要准备往这个JSContext里面注入准备好的block,而这个准备好的block,负责解读JS传过来的数据,从而分发调用各种native函数指令
TIPS:
这种注入不止可以把block注入,在JS里成为一个JS函数,还可以把字符/数字/字典等数据直接注入到JS全局对象之中,可以让JS访问到Native才能获取的全局对象,甚至还可以注入任何NSObject对象,只要这个NSObject对象遵循JSExportOC的协议,相当于JS可以直接调用访问OC的内存对象
苹果UIWebview JavaScriptCore注入 - JS调用
1 | //准备要传给native的数据,包括指令,数据,回调等 |
在没经过客户端注入的时候,直接使用调用callNativeFunction()会报 callNativeFunction is not defined这个错误,说明此时JS上下全文全局,是没有这个函数的,调用无效
当执行完客户端注入的时候,此时JS上下文全局global下面,就拥有了这个callNativeFunction的函数对象,就可以正常调用,从而传递数据到Native
安卓addJavascriptInterface注入
安卓的WebView有一个接口addJavascriptInterface,可以在loadUrl之前提前准备一个对象,通过这个接口注入给JS上下文,从而让JS能够操作,这个操作方式很类似苹果UIWebview JavaScriptCore注入,整个机制也差别不离,但有个很重大的区别,后面在详述优缺点对比的时候,会重点描述
安卓addJavascriptInterface注入 - 客户端注入
使用安卓官方的API接口即可,并且可以在loadUrl之前WebView创建之后,即可配置相关注入功能,这个和UIWebView-JSContext的使用差异非常之大,后面会说
1 | // 通过addJavascriptInterface()将Java对象映射到JS对象 |
其中AndroidtoJs这个是一个自定义的安卓对象,他们里面有个函数callFunction,AndroidtoJs这个对象的其他函数方法JS都可以调用
安卓addJavascriptInterface注入 - JS调用
刚才注入的js对象叫nativeObject,所以JS中可以在全局任意使用
1 | nativeObject.callFunction("js调用了android中的hello方法"); |
我不是很熟悉android,以上很多安卓代码都取自 Android:你要的WebView与 JS 交互方式 都在这里了,后面也会纳入参考文献之中
苹果WKWebView scriptMessageHandler注入
苹果在开放WKWebView这个性能全方位碾压UIWebView的web组件后,也大幅更改了JS与Native交互的方式,提供了专有的交互APIscriptMessageHandler
因为这是苹果的API,使用方式搜一下一搜一大堆,我并不详细解释了,直接展示一下代码
苹果WKWebView scriptMessageHandler注入 - 客户端注入
1 | //配置对象注入 |
需要说明一下,addScriptMessageHandler就像安卓的addJavascriptInterface一样,可以在WKWebView loadUrl之前即可进行相关配置
但不一样的是,如果当前WebView没用了,需要销毁,需要先移除这个对象注入,否则会造成内存泄漏,WebView和所在VC循环引用,无法销毁。
苹果WKWebView scriptMessageHandler注入 - JS调用
刚才注入的js对象叫nativeObject,但不像前边两个注入一样,直接注入到JS上下文全局Global对象里,addScriptMessageHandler方法注入的对象被放到了,全局对象下一个Webkit对象下面,想要拿到这个对象需要这样拿
1 | window.webkit.messageHandlers.nativeObject |
并且和之前的两种注入也不同,前两种注入都可以让js任意操作所注入自定义对象的所有方法,而addScriptMessageHandler注入其实只给注入对象起了一个名字nativeObject,但这个对象的能力是不能任意指定的,只有一个函数postMessage,因此JS的调用方式也只能是
1 | //准备要传给native的数据,包括指令,数据,回调等 |
苹果WKWebView scriptMessageHandler注入 - 客户端接收调用
前两种注入方式,都是在注入的时候,就指定了对应的接收JS调用的Native函数,但是这次不是,在苹果的API设计里,当JS开始调用后,会调用到指定的iOS的delegate里
1 | -(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{ |
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网页里已经有了这么一个函数
1 | function calljs(data){ |
那么客户端此时要调用他需要在客户端用OC拼接字符串,拼出一个js代码,传递的数据用json
1 | //不展开了,data是一个字典,把字典序列化 |
其实我们拼接出来的js只是一行js代码,当然无论多长多复杂的js代码都可以用这个方式让webview执行
1 | calljs('{data:xxx,data2:xxx}'); |
TIPS:安卓4.4以上才可以使用evaluatingJavaScript这个API
WKUserScript 执行JS代码
对于iOS的WKWebView,除了evaluatingJavaScript,还有WKUserScript这个方式可以执行JS代码,他们之间是有区别的
evaluatingJavaScript 是在客户端执行这条代码的时候立刻去执行当条JS代码
WKUserScript 是预先准备好JS代码,当WKWebView加载Dom的时候,执行当条JS代码
很明显这个虽然是一种通信方式,但并不能随时随地进行通信,并不适合选则作为设计bridge的核心方案。但这里也简单介绍一下
1 | //在loadurl之前使用 |
优缺点分析
JS上下文注入
JS上下文注入其实一共3种情况,这3种情况每个情况都不同,我会一一进行优缺点说明
UIWebView的JSContext注入
说实话这是我觉得最完美的一种交互方式了,苹果在iOS7开放了JavaScriptCore这个框架,支撑起了RN,Weex这么牛逼的摆脱了WebView的深度混合框架,他的能力是最完美的.
牛逼的优点:
- 支持JS同步返回!
要知道我们看到的所有JS通信框架设计的都是异步返回,包括RN(这有设计原因,但不代表JSC不支持同步返回),都是设计了一套callback机制,一条通信消息到达Native后,如果需要返回数据,需要调用这个callback接口由Native反向通知JS,他们在JS侧写代码可是差异非常非常非常之大的!
1 | //同步JS调用Native JS这边可以直接写= !!! |
- 支持直接传递对象,无需通过字符串序列化
一个JS对象在JS代码中如果想通过假跳转/弹窗拦截等方式,那么必须把JS对象搞成json,然后才能传递给端,端拿到后还要反解成字典对象,然后才能识别,但是JS上下文注入不需要(其实他本质上是框架层帮你做了这件事情,就是JSValue这个iOS类的能力)
- 支持传递JS函数,客户端能够直接快速调用callback
在JS里如果是一个function,可以直接当做参数发送给客户端,在客户端得到一个JSValue,可以通过JSValue的callWithParmas的方式直接当做函数去调用
- 支持直接注入任意客户端类,客户端对象,JS可以直接向调用客户端
JavaScriptCore有一种使用方法,是可以让任意iOS对象,遵循
有点尴尬的缺点:
- 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都支持
一点个人看法
即便是老项目还在使用UIWebView,要计划升级到WKWebView的时候,既然是升级就应该全面升级到新的WK式通信,做什么妥协和折中方案?
而且最重要的一点,想要做到同时支持多个WebView兼容支持并不需要选择妥协方案,在开发框架的时候完全可以在框架侧解决。想要屏蔽这种webview通信差异,通过在Hybrid框架层设计,抽象统一的调用入口出口,把通信差异在内部消化,这样依然能做到统一对外业务代码流程和清晰的代码逻辑,想要做到代码统一不应该以功能上牺牲和妥协的方面去考虑。
前面其实提到过这个看法不过说的还不彻底,可能有些人会觉得假跳转这个方案最大的好处是全平台全版本的适配与统一,甚至还可以统一安卓平台,可以保证代码一致性,但我认为这绝对不能建立在有严重功能短板导致开发中带来很严重问题的基础之上的,为了代码一致性,而妥协了框架的功能与能力
可能因为不同的平台/不同的版本/不同的WebView的使用与兼容,导致了我们需要在开发Hybrid框架的时候需要适配,但这一切都是可以通过设计良好的框架对外输入输出,把所有区别适配内部消化,从而做到在框架外层的业务代码依然保持代码一致性,保持干净整洁的。这里所说的框架绝不仅仅包括客户端这一侧,JS侧也同理,谁说区分安卓和IOS平台来进行不同的通信方式代码就不整洁了,那是你框架层设计的不够优秀,合理框架层代码应该可以做到当新的系统组件出现,新的更优秀的通信方案出现的时候,能够立刻的支持和扩充,获得最新的能力和性能,但又在业务上层做到无感知,保持框架外围使用的一致性,这才是良好的设计。
所以我之前微博曾经说过一小段话:
就为了兼容从而选择放弃更合理的WKWebview 官方注入interface方式,为了凑和UIWebView依然采用无论是iframe还是location.href的糊弄方式,这种我实在不觉得是美学,只是一种偷懒而已,抱着UIWebview时代的包袱不想丢还让WKWebview去迁就
没错,说的就是WebViewJavascriptBridge
转自 折腾范儿の味精