Hybrid通訊原理介紹包含兩個(gè)部分,分別對(duì)UIWebView和WKWebView的JS通訊部分進(jìn)行了介紹
Hybrid之UIWebView
- 最早期使用技術(shù)是通過iframe設(shè)置href來達(dá)到webview給native發(fā)送命令,其中又一個(gè)小問題需要注意,不能頻繁設(shè)置同樣的href,這樣會(huì)被webview丟棄,快速的設(shè)置也有可能造成命令丟失
- 通過NSURLProtocol攔截特定請(qǐng)求,可以使用img/script/iframe/ajax等任何element設(shè)置src進(jìn)行請(qǐng)求
- 通過使用(私有)API獲取webview的JSContext,然后給JSContext增加方法
- js代碼注入:經(jīng)常有需要在webview里面插入js代碼,一般在頁(yè)面加載完成后注入
例1:通過iframe地址攔截實(shí)現(xiàn),UIWebView
需求:需要有一個(gè)加密存儲(chǔ),存儲(chǔ)對(duì)象為鍵值對(duì),使用href傳送命令的方式大概如下:
//定義回調(diào)函數(shù),native處理完成會(huì)回調(diào)此函數(shù)把存儲(chǔ)的對(duì)象的值傳過來
//為了方便native回調(diào),這個(gè)函數(shù)必須是全局的
function getdataxxx(value) {
//得到value,保存在本地,或者進(jìn)行其它操作
}
native.secureStorage.getValue('key','getdataxxx')
native.secureStorage.setValue('key','value')
//propeerty相關(guān)的也需要通過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
}
通過上面的代碼,一套基本的webview bridge就建立起來了,native的代碼可以根據(jù)需要進(jìn)行合理派發(fā),也可以利用oc的動(dòng)態(tài)特性進(jìn)行動(dòng)態(tài)派發(fā)。
注入js代碼這里需要注意的是,注入之前api是不能使用的,如果需要知道什么時(shí)候api開始生效,需要有一個(gè)新的事件,比如addEventListener('nativeAPIReady', fn),另外一種解決方式就是把注入的js代碼放到調(diào)用方,這樣能解決這個(gè)問題,帶來的新問題就是可能造成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:;">
網(wǎng)頁(yè)中有上述的cors設(shè)置,這里需要前端和native上方配合可以更好的讓這個(gè)方案工作。這里可以選擇data作為scheme
改進(jìn)1:使用URLProtocol替代iframe
iframe方案本身存在一些問題,比如說頻繁發(fā)送命令,會(huì)有丟失的可能,必須通過定時(shí)器進(jìn)行解決等,并且需要手動(dòng)生成一個(gè)用來通訊的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消息是單向的,無法有返回值,必須通過回調(diào)才行,考慮ajax本身有同步模式,我們可以把耗時(shí)可以控制的api修改為同步的,參考上面的例子,理想的get方法應(yīng)該是
var value = native.secureStorage.get("key")
var value = native.secureStorage.count
實(shí)現(xiàn)這個(gè)的思路是通過同步的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)求來處理,這個(gè)首先有一個(gè)跨域訪問的問題,正常情況下ajax請(qǐng)求是有cors限制的,這里通過構(gòu)造一個(gè)相對(duì)路徑可以讓這個(gè)請(qǐng)求不存在cors問題,這樣就不再使用上面方案里的自定義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
- 同樣可以通過攔截請(qǐng)求來獲取webview發(fā)送的命令
- wkscripthandle可以設(shè)置一個(gè)對(duì)象供webview使用,這個(gè)和android有點(diǎn)類似
- 通過攔截alert/PROMPT/CONFIRM進(jìn)行攔截
- js代碼注入:可以在文檔加載前或加載后通過wkuserscript增加js片段
例2:通過wkscripthandler
WKScriptHandler可以注冊(cè)一個(gè)對(duì)象,接收來自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,通過這個(gè)技術(shù)可以實(shí)現(xiàn)各種需要的API。