在hybrid iOS app中,为了达到有些页面能够方便的通过服务器配置内容更改客户端界面,而不需要等待苹果的一个审核周期,一种简单的办法就是把这样的界面用UIWebView通过读取服务端的H5来展示。如果除去简单的展示功能,还希望能做更复杂的事情,调用一些native的模块完成一些操作,甚至进而将操作结果展示在H5页面上,就需要借助webview javascript bridge的帮助了。
最近借用了这个bridge完成了一个H5页面上的图片全屏浏览,左右滑动,长按保存的功能。
两个基础API
WebviewJavascriptBridge是github上的开源库,它可以做到js与native互相发送消息并且返回结果。它主要是基于iOS UIWebView的两个方法来完成js和native之间的通信的,这两个API是整个通信机制的基石:
//UIWebView
func stringByEvaluatingJavaScriptFromString(script:String)->String
//UIWebViewDelegate
func webView(_ webView:UIWebView, shouldStartLoadWithRequest request:NSURLRequest, navigationType navigationType:UIWebViewNavigationType) -> Bool
通过第一个API,native可以向js发消息,这很简单而js端要想向native发消息,就必须借助一点黑魔法。这里js和native端约定好一个fake protocol,在js中创建一个空的iframe,这个frame通过设置src来读取这个fake address,而这个读取请求则会被UIWebViewDelegate的代理方法截获,并且当成发送给native端的消息去解析。
var CUSTOM_PROTOCOL_SCHEME = 'wvjbscheme'
function _createQueueReadyIframe(doc) {
messagingIframe = doc.createElement('iframe')
messagingIframe.style.display = 'none'
messagingIframe.src = CUSTOM_PROTOCOL_SCHEME + '://' + QUEUE_HAS_MESSAGE
doc.documentElement.appendChild(messagingIframe)
}
此后每次js想要通知native端接收消息,都会设置这个messagingIframe的src。
js向native发消息的过程
native端初始化
为了响应js端发来的消息,native端在初始化加载H5的UIWebView的时候需要做一些准备工作:
- 初始化一个WebviewJavascriptBridge实例,这里需要注册一个默认的响应函数,还可以用registerHandler方法以key-block的形式注册若干handler,其本质类似于向js端暴露若干函数,key相当于函数的声明,block相当于函数的实现。这里我们注册了一个browseImages的方法,block中就是调用图片浏览的模块用paging的UIScrollView去展示图片。
- UIWebView可以去加载H5啦
- 在UIWebView加载完毕的delegate method回调中,注入js,包括WebviewJavascriptBridge的js和我们自己定义的js。以我们的需求为例,在这里需要给所有的image节点加上listener,当图片上发生点击事件的时候,去通过bridge向native端发送browseImages的消息。
实际流程
虽然上面说明了如果在js端如何向native端发送消息,但是仍有两个问题需要解决:
- 参数,尤其是复杂的参数如何传递
- 回调,如果能够异步处理另一端的处理结果
第一个问题的答案是js端将调用函数,参数,回调block都保存到了sendMessageQueue中,native端收到了消息后,会主动来取参数。
第二个问题的答案是为回调block生成callbackid,保存callbackid和回调block到字典中,在回调消息中去和responseid做匹配。
那么,2还带来了一个延伸问题,虽然有以上两个API保证了互相传递的通道,但是如何区分是另一方主动发来的调用还是回调的通知呢?
这就是通过判断是否存在responseid这个字段来判定的。
详细流程如下,左侧代表js端,右侧代表native端:
native端向js端发消息
总体来说流程是完全类似的,这次用文字来描述:
- js端首先注册若干handler方法,保存在messageHandlers字典中
- native端发起对js的调用:将调用的js handler名称,参数放到message字典中
- 如果需要回调,则为回调方法生成唯一的callbackId,将callbackId和回调block保存在native端的responseCallbacks字典中
- 通过UIWebView.stringByEvaluatingJavascript...调用js端处理message函数
- js端检查发现message中不包含responseId,确认不是js调用native后的回调,则在自己的messageHandlers中找到对应的handler
- 如果message中包含callbackId,则生成回调native的block,callbackId在回调message中的key是responseId
- handler运行完成,如果需要回调native端,则将生成的数据加入回调message字典中。(现在字典中包含两个key:responseId和responseData)
- 将message放到sendMessageQueue中,然后用预定义的protocol设置空iframe的src
- native端收到shouldStartLoad.....调用,通过evaluateJavascript...获取sendMessageQueue中的message
- 发现message中包含responseId,确认为自己主动发起的调用的回调
- 从responseCallbacks字典中根据responseId找到对应的block,用message中的responseData调用,整个流程完成。
end
整个bridge的库,基于底层两个互相通信的方法,实现了一整套优雅的机制,写的真是太棒了!