Hybrid Js<—>Native通訊原理

Hybrid通訊原理介紹包含兩個(gè)部分冯丙,分別對(duì)UIWebView和WKWebView的JS通訊部分進(jìn)行了介紹

Hybrid之UIWebView

  • 最早期使用技術(shù)是通過(guò)iframe設(shè)置href來(lái)達(dá)到webview給native發(fā)送命令协饲,其中又一個(gè)小問(wèn)題需要注意货邓,不能頻繁設(shè)置同樣的href昨悼,這樣會(huì)被webview丟棄捉腥,快速的設(shè)置也有可能造成命令丟失
  • 通過(guò)NSURLProtocol攔截特定請(qǐng)求歼捐,可以使用img/script/iframe/ajax等任何element設(shè)置src進(jìn)行請(qǐng)求
  • 通過(guò)使用(私有)API獲取webview的JSContext何陆,然后給JSContext增加方法
  • js代碼注入:經(jīng)常有需要在webview里面插入js代碼,一般在頁(yè)面加載完成后注入

例1:通過(guò)iframe地址攔截實(shí)現(xiàn)豹储,UIWebView

需求:需要有一個(gè)加密存儲(chǔ)贷盲,存儲(chǔ)對(duì)象為鍵值對(duì),使用href傳送命令的方式大概如下:

//定義回調(diào)函數(shù)剥扣,native處理完成會(huì)回調(diào)此函數(shù)把存儲(chǔ)的對(duì)象的值傳過(guò)來(lái)
//為了方便native回調(diào)巩剖,這個(gè)函數(shù)必須是全局的
function getdataxxx(value) {
    //得到value慨灭,保存在本地,或者進(jìn)行其它操作
}
native.secureStorage.getValue('key','getdataxxx')
native.secureStorage.setValue('key','value')
//propeerty相關(guān)的也需要通過(guò)get/set方法
native.secureStorage.getCount('getCountCallback')
native.secureStorage.setMethod('EC')

因?yàn)樗械拿钚枰D(zhuǎn)換成url地址進(jìn)行發(fā)送球及,調(diào)用最終會(huì)轉(zhuǎn)換成:

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"

為了完成這個(gè)轉(zhuǎn)換氧骤,我們必須有一段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})}
  }
}

把上面的代碼存儲(chǔ)在bundle中吃引,在webview加載完成后進(jìn)行注入

//注入js代碼
func webViewDidFinishLoad(_ webView: UIWebView) {
    //load js from bundle
    let js = loadJsFromBundle()
    webView.stringByEvaluatingJavaScript(from: js)
}
//攔截請(qǐng)求
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
}

通過(guò)上面的代碼筹陵,一套基本的webview bridge就建立起來(lái)了,native的代碼可以根據(jù)需要進(jìn)行合理派發(fā)镊尺,也可以利用oc的動(dòng)態(tài)特性進(jìn)行動(dòng)態(tài)派發(fā)朦佩。

注入js代碼這里需要注意的是,注入之前api是不能使用的庐氮,如果需要知道什么時(shí)候api開(kāi)始生效语稠,需要有一個(gè)新的事件,比如addEventListener('nativeAPIReady', fn)弄砍,另外一種解決方式就是把注入的js代碼放到調(diào)用方仙畦,這樣能解決這個(gè)問(wèn)題,帶來(lái)的新問(wèn)題就是可能造成API版本不匹配音婶。

第一次失斂:cors問(wèn)題

<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:;">

網(wǎng)頁(yè)中有上述的cors設(shè)置,這里需要前端和native上方配合可以更好的讓這個(gè)方案工作衣式。這里可以選擇data作為scheme

改進(jìn)1:使用URLProtocol替代iframe

iframe方案本身存在一些問(wèn)題寸士,比如說(shuō)頻繁發(fā)送命令,會(huì)有丟失的可能碴卧,必須通過(guò)定時(shí)器進(jìn)行解決等弱卡,并且需要手動(dòng)生成一個(gè)用來(lái)通訊的iframe,使用URLProtocol配合<img>就可以解決住册。這里是新實(shí)現(xiàn)的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攔截這個(gè)請(qǐng)求婶博,其他的邏輯都和之前的一樣
}

改進(jìn)2:利用ajax修改為同步調(diào)用

之前的方案,js到native消息是單向的界弧,無(wú)法有返回值凡蜻,必須通過(guò)回調(diào)才行,考慮ajax本身有同步模式垢箕,我們可以把耗時(shí)可以控制的api修改為同步的划栓,參考上面的例子,理想的get方法應(yīng)該是

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

實(shí)現(xiàn)這個(gè)的思路是通過(guò)同步的ajax請(qǐng)求把內(nèi)容放到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})
    }
  }
}

相對(duì)應(yīng)iOS端的處理就是把這個(gè)請(qǐng)求當(dāng)作是真正的請(qǐng)求來(lái)處理,這個(gè)首先有一個(gè)跨域訪問(wèn)的問(wèn)題,正常情況下ajax請(qǐng)求是有cors限制的委煤,這里通過(guò)構(gòu)造一個(gè)相對(duì)路徑可以讓這個(gè)請(qǐng)求不存在cors問(wèn)題堂油,這樣就不再使用上面方案里的自定義scheme,為了讓urlprotocol能夠處理請(qǐng)求碧绞,加入了自定義的header府框。下面是一個(gè)urlprotocol的簡(jiǎn)單實(shí)現(xiàn)

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

  • 同樣可以通過(guò)攔截請(qǐng)求來(lái)獲取webview發(fā)送的命令
  • wkscripthandle可以設(shè)置一個(gè)對(duì)象供webview使用,這個(gè)和android有點(diǎn)類似
  • 通過(guò)攔截alert/PROMPT/CONFIRM進(jìn)行攔截
  • js代碼注入:可以在文檔加載前或加載后通過(guò)wkuserscript增加js片段

例2:通過(guò)wkscripthandler

WKScriptHandler可以注冊(cè)一個(gè)對(duì)象讥邻,接收來(lái)自js的消息迫靖,簡(jiǎn)單的代碼如下:

//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.

這種方法同樣不能進(jìn)行同步API調(diào)用,同時(shí)使用WKWebView需要注意一點(diǎn)兴使,我們需要實(shí)現(xiàn)uiDelegate才能讓webview進(jìn)行alert/prompt/confirm系宜,參考http://stackoverflow.com/questions/26898941/ios-wkwebview-not-showing-javascript-alert-dialog

改進(jìn)1:同步API實(shí)現(xiàn)

熟悉H5的同學(xué)應(yīng)該知道prompt的用途是在網(wǎng)頁(yè)上彈一個(gè)輸入框,然后獲取用戶輸入的文本作為返回值发魄,這里我們就可以利用這個(gè)特性盹牧,當(dāng)prompt的時(shí)候檢測(cè)是不是api調(diào)用,如果是的話励幼,就進(jìn)行api處理汰寓,這里返回值只能是字符串,所以需要進(jìn)行一些雙方協(xié)定赏淌,和uiwebview類似踩寇,先注入一段js代碼啄清,WKWebView提供了WKUserScript可以非常方便的進(jìn)行代碼注入

//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)

代碼注入完成后六水,我們簡(jiǎn)單處理一下prompt的處理就可以正確進(jìn)行同步API的調(diào)用了,代碼大概如下:

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
  //荣茫。想帅。。
 }

這樣就實(shí)現(xiàn)了一個(gè)簡(jiǎn)單回傳ECHO的API啡莉,通過(guò)這個(gè)技術(shù)可以實(shí)現(xiàn)各種需要的API港准。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市咧欣,隨后出現(xiàn)的幾起案子浅缸,更是在濱河造成了極大的恐慌,老刑警劉巖魄咕,帶你破解...
    沈念sama閱讀 212,454評(píng)論 6 493
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件衩椒,死亡現(xiàn)場(chǎng)離奇詭異,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)毛萌,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 90,553評(píng)論 3 385
  • 文/潘曉璐 我一進(jìn)店門苟弛,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人阁将,你說(shuō)我怎么就攤上這事膏秫。” “怎么了做盅?”我有些...
    開(kāi)封第一講書人閱讀 157,921評(píng)論 0 348
  • 文/不壞的土叔 我叫張陵荔睹,是天一觀的道長(zhǎng)。 經(jīng)常有香客問(wèn)我言蛇,道長(zhǎng)僻他,這世上最難降的妖魔是什么? 我笑而不...
    開(kāi)封第一講書人閱讀 56,648評(píng)論 1 284
  • 正文 為了忘掉前任腊尚,我火速辦了婚禮吨拗,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘婿斥。我一直安慰自己劝篷,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 65,770評(píng)論 6 386
  • 文/花漫 我一把揭開(kāi)白布民宿。 她就那樣靜靜地躺著娇妓,像睡著了一般。 火紅的嫁衣襯著肌膚如雪活鹰。 梳的紋絲不亂的頭發(fā)上哈恰,一...
    開(kāi)封第一講書人閱讀 49,950評(píng)論 1 291
  • 那天,我揣著相機(jī)與錄音志群,去河邊找鬼着绷。 笑死,一個(gè)胖子當(dāng)著我的面吹牛锌云,可吹牛的內(nèi)容都是我干的荠医。 我是一名探鬼主播,決...
    沈念sama閱讀 39,090評(píng)論 3 410
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼桑涎,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼彬向!你這毒婦竟也來(lái)了?” 一聲冷哼從身側(cè)響起攻冷,我...
    開(kāi)封第一講書人閱讀 37,817評(píng)論 0 268
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤娃胆,失蹤者是張志新(化名)和其女友劉穎,沒(méi)想到半個(gè)月后讲衫,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體缕棵,經(jīng)...
    沈念sama閱讀 44,275評(píng)論 1 303
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡孵班,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 36,592評(píng)論 2 327
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了招驴。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片篙程。...
    茶點(diǎn)故事閱讀 38,724評(píng)論 1 341
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖别厘,靈堂內(nèi)的尸體忽然破棺而出虱饿,到底是詐尸還是另有隱情,我是刑警寧澤触趴,帶...
    沈念sama閱讀 34,409評(píng)論 4 333
  • 正文 年R本政府宣布氮发,位于F島的核電站,受9級(jí)特大地震影響冗懦,放射性物質(zhì)發(fā)生泄漏爽冕。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 40,052評(píng)論 3 316
  • 文/蒙蒙 一披蕉、第九天 我趴在偏房一處隱蔽的房頂上張望颈畸。 院中可真熱鬧,春花似錦没讲、人聲如沸眯娱。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 30,815評(píng)論 0 21
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)徙缴。三九已至,卻和暖如春嘁信,著一層夾襖步出監(jiān)牢的瞬間于样,已是汗流浹背。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 32,043評(píng)論 1 266
  • 我被黑心中介騙來(lái)泰國(guó)打工吱抚, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留百宇,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 46,503評(píng)論 2 361
  • 正文 我出身青樓秘豹,卻偏偏與公主長(zhǎng)得像,于是被迫代替她去往敵國(guó)和親昌粤。 傳聞我的和親對(duì)象是個(gè)殘疾皇子既绕,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 43,627評(píng)論 2 350

推薦閱讀更多精彩內(nèi)容