Hybrid Js<—>Native通讯原理

Hybrid通讯原理介绍包含两个部分,分别对UIWebView和WKWebView的JS通讯部分进行了介绍

Hybrid之UIWebView

  • 最早期使用技术是通过iframe设置href来达到webview给native发送命令,其中又一个小问题需要注意,不能频繁设置同样的href,这样会被webview丢弃,快速的设置也有可能造成命令丢失
  • 通过NSURLProtocol拦截特定请求,可以使用img/script/iframe/ajax等任何element设置src进行请求
  • 通过使用(私有)API获取webview的JSContext,然后给JSContext增加方法
  • js代码注入:经常有需要在webview里面插入js代码,一般在页面加载完成后注入

例1:通过iframe地址拦截实现,UIWebView

需求:需要有一个加密存储,存储对象为键值对,使用href传送命令的方式大概如下:

//定义回调函数,native处理完成会回调此函数把存储的对象的值传过来
//为了方便native回调,这个函数必须是全局的
function getdataxxx(value) {
    //得到value,保存在本地,或者进行其它操作
}
native.secureStorage.getValue('key','getdataxxx')
native.secureStorage.setValue('key','value')
//propeerty相关的也需要通过get/set方法
native.secureStorage.getCount('getCountCallback')
native.secureStorage.setMethod('EC')

因为所有的命令需要转换成url地址进行发送,调用最终会转换成:

iframe.location.href = "cmd://secureStorage/getValue?key=key&callback=getdataxxx"
iframe.location.href = "cmd://secureStorage/setValue?key=key&value=value"
iframe.location.href = "cmd://secureStorage/getCount?callback=getCountCallback"
iframe.location.href = "cmd://secureStorage/setMethod?value=EC"

为了完成这个转换,我们必须有一段js代码注入到webview中,大致如此:

function sendMessage(path, parameter) {
  var url = "cmd://"+path
  var sep = "?"
  for (key in parameter) {
    url += sep
    url += (key+"="+parameter[key])
    sep = "&"
  }
  iframe.location.href = url
}
var native = {
  secureStorage:{
    setValue:function(key,value){
      sendMessage("secureStorage/setValue",{key:key, value:value})},
    getValue:function(key,callback){
      sendMessage("secureStorage/getValue",{key:key, callback:callback})}
  }
}

把上面的代码存储在bundle中,在webview加载完成后进行注入

//注入js代码
func webViewDidFinishLoad(_ webView: UIWebView) {
    //load js from bundle
    let js = loadJsFromBundle()
    webView.stringByEvaluatingJavaScript(from: js)
}
//拦截请求
func webView(_ webView: UIWebView, shouldStartLoadWith request: URLRequest, navigationType: UIWebViewNavigationType) -> Bool {
    if request.url?.scheme == "cmd" {
        //cmd received, use a worker thread to execute the cmd
        //NATIVE的处理这里不做介绍
        getdataxxx('test data')
        return false
    }
    return true
}

通过上面的代码,一套基本的webview bridge就建立起来了,native的代码可以根据需要进行合理派发,也可以利用oc的动态特性进行动态派发。

注入js代码这里需要注意的是,注入之前api是不能使用的,如果需要知道什么时候api开始生效,需要有一个新的事件,比如addEventListener('nativeAPIReady', fn),另外一种解决方式就是把注入的js代码放到调用方,这样能解决这个问题,带来的新问题就是可能造成API版本不匹配。

第一次失败:cors问题

<meta http-equiv="Content-Security-Policy" content="default-src wx.qlogo.cn *.tanx.com *.mmstat.com *.meituan.com  *.wandafilm.com https://i.meituan.com/ https://ms0.meituan.com https://mc0.meituan.com https://mpay.meituan.com 192.168.4.223:9999 *.maoyan.com https://*.meituan.com https://*.meituan.net http://*.meituan.net www.google-analytics.com wvjbscheme://* imeituan://* *.dianping.com *.dpfile.com *.51ping.com 'self' 'unsafe-inline' 'unsafe-eval' blob: data:;">

网页中有上述的cors设置,这里需要前端和native上方配合可以更好的让这个方案工作。这里可以选择data作为scheme

改进1:使用URLProtocol替代iframe

iframe方案本身存在一些问题,比如说频繁发送命令,会有丢失的可能,必须通过定时器进行解决等,并且需要手动生成一个用来通讯的iframe,使用URLProtocol配合<img>就可以解决。这里是新实现的sendMessage

function buildUrl(path, parameter) {
  var url = "cmd://"+path
  var sep = "?"
  for (key in parameter) {
    url += sep
    url += (key+"="+parameter[key])
    sep = "&"
  }
  return url
}
function sendMessage(path, parameter) {
  (new Image).src = buildUrl//urlprotocol拦截这个请求,其他的逻辑都和之前的一样
}

改进2:利用ajax修改为同步调用

之前的方案,js到native消息是单向的,无法有返回值,必须通过回调才行,考虑ajax本身有同步模式,我们可以把耗时可以控制的api修改为同步的,参考上面的例子,理想的get方法应该是

var value = native.secureStorage.get("key")
var value = native.secureStorage.count

实现这个的思路是通过同步的ajax请求把内容放到response里面,然后解析response获取,修改后的sendMessage如下

function buildUrl(path, parameter) {  
  var url = path
  var sep = '?'   
  for( key in parameter ){
    url += sep
    url += (key+'='+parameter[key])
    sep = '&'
  }
  return url
} 
function sendMessage(path, parameter){
  var ajax = new XMLHttpRequest
  ajax.open('GET', buildUrl(path, parameter), false)
  ajax.setRequestHeader('ajaxHead','\(ajaxHead)')
  ajax.send()
  return ajax.responseText
}
var native = {
  secureStorage:{
    setValue:function(key,value){
      sendMessage('secureStorage/setValue',{key:key, value:value})
    },
    getValue:function(key,callback){
      return sendMessage('secureStorage/getValue',{key:key})
    }
  }
}

相对应iOS端的处理就是把这个请求当作是真正的请求来处理,这个首先有一个跨域访问的问题,正常情况下ajax请求是有cors限制的,这里通过构造一个相对路径可以让这个请求不存在cors问题,这样就不再使用上面方案里的自定义scheme,为了让urlprotocol能够处理请求,加入了自定义的header。下面是一个urlprotocol的简单实现

override class func canInit(with request: URLRequest) -> Bool {
  return request.allHTTPHeaderFields?["ajaxHead"] == ajaxHead 
}
    
override func startLoading() {
  let response = HTTPURLResponse(url: self.request.url!, statusCode: 200, httpVersion: "1.1", headerFields: nil)
  self.client?.urlProtocol(self, didReceive: response!, cacheStoragePolicy: .notAllowed)
  var key : String?
  var value : String?
  let component = URLComponents(url: request.url!, resolvingAgainstBaseURL: false)
  for query in (component?.queryItems)! {
    if query.name == "key" {
      key = query.value
    }
    else if query.name == "value" {
      value = query.value
    }
  }
  switch request.url!.lastPathComponent {
  case "setValue":
    if let key = key, let value = value {
      AJaxProtocol.store[key] = value
    }
  case "getValue":
    if let key = key {
      if let value = AJaxProtocol.store[key] {
        self.client?.urlProtocol(self, didLoad: value.data(using: .utf8)!)
      }
    }
  default:break;
  }
  self.client?.urlProtocolDidFinishLoading(self)
}

Hybrid之WKWebView

  • 同样可以通过拦截请求来获取webview发送的命令
  • wkscripthandle可以设置一个对象供webview使用,这个和android有点类似
  • 通过拦截alert/PROMPT/CONFIRM进行拦截
  • js代码注入:可以在文档加载前或加载后通过wkuserscript增加js片段

例2:通过wkscripthandler

WKScriptHandler可以注册一个对象,接收来自js的消息,简单的代码如下:

//JS:webkit.messageHandlers.<name>.postMessage(<messageBody>) 
//sample: webkit.messageHandlers.secureStorage.postMessage("a message body")
wkConfig.userContentController.add(self, name: "secureStorage")
extension WKFirstViewController : WKScriptMessageHandler {
    func userContentController(_ userContentController: WKUserContentController, didReceive message: WKScriptMessage) {
        print(message.name)
        print(message.body)
        //do something... and then send message to webview
        message.webView?.evaluateJavaScript("alert(1)", completionHandler: nil)
    }
}

body allowed types are NSNumber, NSString, NSDate, NSArray,NSDictionary, and NSNull.

这种方法同样不能进行同步API调用,同时使用WKWebView需要注意一点,我们需要实现uiDelegate才能让webview进行alert/prompt/confirm,参考http://stackoverflow.com/questions/26898941/ios-wkwebview-not-showing-javascript-alert-dialog

改进1:同步API实现

熟悉H5的同学应该知道prompt的用途是在网页上弹一个输入框,然后获取用户输入的文本作为返回值,这里我们就可以利用这个特性,当prompt的时候检测是不是api调用,如果是的话,就进行api处理,这里返回值只能是字符串,所以需要进行一些双方协定,和uiwebview类似,先注入一段js代码,WKWebView提供了WKUserScript可以非常方便的进行代码注入

//JS 代码
function buildCmd(path, parameter) {
  var url = path;    
  var sep = '?';    
  for( key in parameter ){
    url += sep;    
    url += (key+'='+parameter[key]);   
    sep = '&'
  }
  return url
}
var native = { secureStorage:{ 
  getValue:function(key){
    return sendMessage('secureStorage/getValue', {key:key})}, 
  setValue: function(key, value){
    sendMessage('secureStorage/setValue',{key:key,value:value})
  }}
} 
function sendMessage(path, paras){return prompt('wkbridge',buildCmd(path, paras))}

//SWIFT 代码
let wkConfig = WKWebViewConfiguration()
//inject script
let jsInject = loadJS()
wkConfig.userContentController.addUserScript(WKUserScript(source: jsInject, injectionTime: .atDocumentStart, forMainFrameOnly: false))
let webview = WKWebView(frame: view.frame, configuration: wkConfig)

代码注入完成后,我们简单处理一下prompt的处理就可以正确进行同步API的调用了,代码大概如下:

func webView(_ webView: WKWebView, runJavaScriptTextInputPanelWithPrompt prompt: String, defaultText: String?, initiatedByFrame frame: WKFrameInfo,
                 completionHandler: @escaping (String?) -> Void) {
  if prompt == "wkbridge" && defaultText != nil {
    completionHandler(defaultText)
    return
  }
  //。。。do right thing for prompt
  //。。。
 }

这样就实现了一个简单回传ECHO的API,通过这个技术可以实现各种需要的API。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,324评论 5 476
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,303评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,192评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,555评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,569评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,566评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,927评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,583评论 0 257
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,827评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,590评论 2 320
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,669评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,365评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,941评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,928评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,159评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 42,880评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,399评论 2 342

推荐阅读更多精彩内容