本文主要總結(jié)一下JS與原生交互的幾種方式,其中包括UIWebview與WKWebView這兩個iOS端加載H5的控件。這類文章在網(wǎng)上其實也有很多,但是都只是介紹 iOS 這邊怎么處理的。這樣在和 H5 聯(lián)調(diào)時產(chǎn)生問題的時候就比較浪費時間,所以我這邊把H5端的代碼也奉獻出來希望對大家有幫助。
UIWebview
H5調(diào)用原生方法
1、通過攔截加載的URL。
相信iOS的小伙伴應該都知道Delegate這個玩意兒,在UIWebview中就存在UIWebViewDelegate這個玩意兒,攔截URL就是通過代理中的一個方法實現(xiàn)的。
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
就是上面這個玩意兒。這個方法就是當WebView在加載url之前會執(zhí)行。這時候我們就可以在加載他之前搞事情啦,具體的怎么玩兒,看代碼。
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
//獲取url里的相關參數(shù)(就是把URL里?后面的玩意兒取出來)
//如下:
//http://127.0.0.1:8080/index.html?message=ocuiwebviewurl攔截&flag=yjkwap://wjj.com&action=helloOC'
//這個玩意兒獲取到的 dict = {@"message":@"ocuiwebviewurl攔截", @"flag":@"yjkwap://wjj.com", @"action":@"helloOC"}
NSDictionary *dict = [YJKTool urlPramaDictionaryWithUrlString:request.URL.absoluteString];
//存在約定的規(guī)則就進行攔截(這里約定的規(guī)則就是flag=yjkwap://wjj.com)
if ([[dict valueForKey:@"flag"] isEqualToString:@"yjkwap://wjj.com"]) {
//執(zhí)行相關的動作(action代表要執(zhí)行的動作,就是要調(diào)用原生的啥方法)
if ([[dict valueForKey:@"action"] isEqualToString:@"helloOC"]) {
// message 就是H5給我們傳遞過來的參數(shù)
//在這里搞事情
// ... 此處省略十來行代碼 ...
// 最后要阻止頁面的加載
return NO;
}
}
return YES;
}
就是這么簡單,注釋寫的已經(jīng)很清楚了,應該是都可以看懂的哦。
既然原生這邊搞定了,還有一個問題就是H5那邊怎么寫呢? 就下面這句代碼,扔給你們H5。
location.;
2、系統(tǒng)自帶JavaScriptCore庫里的,JSContext類。
這個是系統(tǒng)類庫里的玩意兒,用起來也是比較簡單方便的,這個和上面方法的處理時機不太一樣,是在另一個代理方法里面實現(xiàn)的。
- (void)webViewDidFinishLoad:(UIWebView *)webView
這個方法就是當WebView在加載完成之后執(zhí)行的。
還有一點就是通過 JSContext 可以有兩種方式去實現(xiàn)交互的功能。
I、通過block回調(diào)
沒啥好說的,直接上代碼吧。
- (void)webViewDidFinishLoad:(UIWebView *)webView{
//獲取 JSContext 對象
JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//執(zhí)行約定的方法 通過block回調(diào)
__weak JSContext *weakContext = context;
__weak YJKWebViewVC *weakSelf = self;
// helloOC 就是提供給H5調(diào)用的方法。
context[@"helloOC"] = ^(){
//注意:這里面是子線程不能做刷新UI
// 獲取H5所傳遞的參數(shù)
NSArray *args = [JSContext currentArguments];
//這里是取出參數(shù)里面的地一項
JSValue *value = [args firstObject];
//這里是將 JSValue 對象轉(zhuǎn)化為字典,JSValue 的對象還可以轉(zhuǎn)化成NSString、NSNumber等,具體轉(zhuǎn)化為什么類型要看H5那邊所傳遞的參數(shù)類型是什么玩意兒
[value toDictionary]
// 搞事情
// ... 此處省略十來行代碼 ...
};
}
照例奉上H5代碼:
//單一參數(shù),參數(shù)為字典
window.helloOC({"message":"OC UIWebview JSContext"})
//多參數(shù),使用逗號分割就好。參數(shù)分別為 NSString、Int
window.helloOC('123',123)
注意:這里的 block 是在子線程回調(diào)的,不能進行UI操作。
II、掛載在其他對象下
這個和上面實質(zhì)是一樣的,這樣做就是為了將交互方法統(tǒng)一管理,使代碼更加清晰明了。
- (void)webViewDidFinishLoad:(UIWebView *)webView{
//SwiftJavaScriptModel 就是我新弄的一個類
//掛載對象
[[SwiftJavaScriptModel alloc] initWithJsContext:context];
}
import UIKit
// 定義協(xié)議SwiftJavaScriptDelegate 該協(xié)議必須遵守JSExport協(xié)議
@objc protocol SwiftJavaScriptDelegate: JSExport {
//給H5調(diào)用的方法
func helloOC() -> Void
}
@objc class SwiftJavaScriptModel: NSObject, SwiftJavaScriptDelegate {
@discardableResult @objc convenience init(jsContext: JSContext) {
self.init()
//這一步是將SwiftJavaScriptModel模型注入到JS中,在JS就可以通過YJKJSContextObj調(diào)用我們暴露的方法了。
jsContext.setObject(self, forKeyedSubscript: "YJKJSContextObj" as NSCopying & NSObjectProtocol)
}
//這個方法的實現(xiàn)邏輯和通過 Block 回調(diào)的方式是一樣的,就沒多些注釋哦。
func helloOC() -> Void {
let paramArray = JSContext.currentArguments()
let value:JSValue? = paramArray?.first as? JSValue
if value != nil {
let dict = value!.toDictionary()
print(dict!)
}
}
}
萬眾期待的H5代碼:
//是不是和上面的很像額,他就是在 window 下弄啦個 YJKJSContextObj 去處理和原生的交互。
window.YJKJSContextObj.helloOC({"message":"OC UIWebview JSContext"})
3、iOS同行小伙伴封裝的WebViewJavascriptBridge庫。
這個庫就很優(yōu)秀了,有時間的同學可以去看看源碼,它的實現(xiàn)原理就是通過攔截URL,真的要膜拜大神,腦子真好使,封裝出來這么一個小可愛。
到底怎么使用呢?廢話不多說,直接上代碼。
__weak YJKWebViewVC *weakSelf = self;
//弄一個 WebViewJavascriptBridge 的對象
self.bridge = [WebViewJavascriptBridge bridgeForWebView:self.uiWebView];
//處理 WebView 回調(diào)用的
[self.bridge setWebViewDelegate:self];
//注冊方法給H5用 方法名就是helloOC
[self.bridge registerHandler:@"helloOC" handler:^(id data, WVJBResponseCallback responseCallback) {
//data 是H5傳過來的參數(shù)
//搞事情
//搞完事情,通過回調(diào)返回相應的參數(shù)給到H5,比如它調(diào)用你的 1+1 的方法,你計算完之后要把 2 給到H5。
responseCallback(@"1111")
}];
注意 1:WebViewJavascriptBridge 的對象不能弄成局部變量,不然他會釋放掉,導致 webview 的無法執(zhí)行。
注意 2:當H5頁面剛打開時就要調(diào)用原生方法時(比如H5調(diào)用原生的數(shù)據(jù)請求),這時候需要做個延遲處理。(我這邊分析的原因可能是在H5調(diào)用原生方法時,原生這邊的方法還沒注冊上,因此可能是H5調(diào)用方法的時機不太對,有哪位小伙伴知道怎么處理希望可以告訴我哦)
到這里我們這邊的事情已經(jīng)搞定啦,又是無奈的H5教學時間。
首先要在 .JS 文件中添加一下方法,并且調(diào)用一下,這里是準備工作。
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'https://__bridge_loaded__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
setupWebViewJavascriptBridge((bridge) => {});
然后就只需要在調(diào)用原生方法的時候?qū)懗鲆幌麓a就好啦
//調(diào)用原生注冊的 helloOC 方法,參數(shù)為 {"message":"11111"}
window.WebViewJavascriptBridge.callHandler('helloOC',{"message":"11111"}, function responseCallback(responseData) {
//這邊原生回調(diào)信息(responseData),處理自己的邏輯
})
這邊的具體原理就不說啦,有時間會寫另外一篇文章來詳細說明的。
到這里,在 UIWebview 中 H5 調(diào)用原生方法的處理方式就總結(jié)完成啦,有啥不明白的可以評論或私信哦。
原生調(diào)用H5方法
1、UIWebView 執(zhí)行 JavaScriptString。
原生調(diào)用H5就更簡單啦,一句話搞定
// webView對象在合適的時機,調(diào)用這個方法就行啦。
//入?yún)⒕褪且粋€JavaScriptString。
//changeString('haahhahah') 他的意思就是調(diào)用H5的 changeString 方法,傳入?yún)?shù)'haahhahah'。
[webView stringByEvaluatingJavaScriptFromString:@"changeString('haahhahah')"];
H5要為我們做的事情
//我這邊使用的是Vue,其他框架大致相同。
mounted:function(){
// 將changeString方法綁定到window下面,提供給外部調(diào)用
window['changeString'] = (result) => {
this.aaaa = result
}
}
2、JSContext 執(zhí)行 JavaScriptString。
// context 這個玩意兒還記得么?就是在頁面加載完成時,通過
//JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//代碼獲取到的
[self.context evaluateScript:@"changeString('haahhahah')"];
H5那邊要做的事情和方法一樣的哦。
3、iOS同行小伙伴封裝的WebViewJavascriptBridge庫。
別人的庫總是那么好使。
//調(diào)用H5 helloJS 方法,參數(shù)為nil
[self.bridge callHandler:@"helloJS" data:nil responseCallback:^(id responseData) {
//等待H5的回調(diào)然后做事情
NSLog(@"%@",responseData);
}];
H5的小哥哥要為我們做的事情
mounted:function(){
//注冊方法給原生調(diào)用哦(寫在這我也不知道好使不... 自己的demo沒驗證這個)
window.WebViewJavascriptBridge.registerHandler('helloJS', function(data, responseCallback) {
responseCallback("123")
})
},
關于 UIWebview 的總結(jié)就是這些咯,下面要說大家期待的 WKWebView。
WKWebView
從 iOS 8 開始呢,就可以用 WKWebView 了,好處有很多,坑呢也不少。這些網(wǎng)上文章也多,這里就不介紹這些東西了。接下來進入我們這個文章的主題
H5調(diào)用原生方法
1、通過攔截加載的URL。
這個和UIWebview那邊區(qū)別不大,就是攔截URL的代理變掉了。通過下面代碼就可以發(fā)現(xiàn)嘍,代理方法里面執(zhí)行的邏輯和UIWebview一眼的額。
-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
//獲取url里的相關參數(shù)(就是把URL里?后面的玩意兒取出來)
//如下:
//http://127.0.0.1:8080/index.html?message=ocuiwebviewurl攔截&flag=yjkwap://wjj.com&action=helloOC'
//這個玩意兒獲取到的 dict = {@"message":@"ocuiwebviewurl攔截", @"flag":@"yjkwap://wjj.com", @"action":@"helloOC"}
NSDictionary *dict = [YJKTool urlPramaDictionaryWithUrlString:request.URL.absoluteString];
//存在約定的規(guī)則就進行攔截(這里約定的規(guī)則就是flag=yjkwap://wjj.com)
if ([[dict valueForKey:@"flag"] isEqualToString:@"yjkwap://wjj.com"]) {
//執(zhí)行相關的動作(action代表要執(zhí)行的動作,就是要調(diào)用原生的啥方法)
if ([[dict valueForKey:@"action"] isEqualToString:@"helloOC"]) {
// message 就是H5給我們傳遞過來的參數(shù)
//在這里搞事情
// ... 此處省略十來行代碼 ...
// 最后要阻止頁面的加載
decisionHandler(WKNavigationActionPolicyCancel);
}
}
decisionHandler(WKNavigationActionPolicyAllow);
}
H5小伙伴寫的東西有完全沒變化,參照 UIWebview 那邊的代碼好啦。
2、WKScriptMessageHandler。
先初始化 WKWebView 對象
//弄一個 WKWebViewConfiguration 對象
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
//然后 configuration.userContentController 對象初始化一下哦
configuration.userContentController = [[WKUserContentController alloc] init];
//添加 helloOC 方法給H5小伙伴調(diào)用哦
[configuration.userContentController addScriptMessageHandler:self name:@"helloOC"];
//下面就是創(chuàng)建 WKWebView 對象的相關代碼,沒啥好看的哦
self.wkWebView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:configuration];
self.wkWebView.navigationDelegate = self;
[self.wkWebView loadRequest:[NSURLRequest requestWithURL:[NSURL URLWithString:@"http://192.168.2.50:8080/index.html"]]];
[self.view addSubview:self.wkWebView];
helloOC 方法的具體實現(xiàn)使用下面的代理就好啦
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message{
// message.name 獲取到對應的方法名
if ([message.name isEqualToString:@"helloOC"]) {
// message.body H5傳過來的參數(shù),具體啥類型要看H5所傳參數(shù)是啥類型哦
NSDictionary *dict = message.body;
}
}
請教H5的伙伴
//很簡單的代碼哦
// window.webkit.messageHandlers 這個對象,調(diào)用 helloOC 方法,傳遞相關參數(shù)({"message":"OC WKWebview WKScriptMessageHandler"})
window.webkit.messageHandlers.helloOC.postMessage({"message":"OC WKWebview WKScriptMessageHandler"})
注意:這里方法會導致頁面的循環(huán)引用。解決方法有以下兩個哦,喜歡那個就用那個嘍
iOS WKWebView導致ViewController不調(diào)用dealloc方法
使用WKWebView時,ViewController不走dealloc方法的問題解決方法
3、iOS同行小伙伴封裝的WebViewJavascriptBridge庫。
這里沒有任何變化,大神已做兼容,參照 UIWebview 就好。
原生調(diào)用H5方法
1、WKWebView 執(zhí)行 JavaScriptString。
原生要做的就是下面這句話,H5的做法和 UIWebview 那邊一樣的。
[webView stringByEvaluatingJavaScriptFromString:@"changeString('haahhahah')"];
2、iOS同行小伙伴封裝的WebViewJavascriptBridge庫。
這里沒有任何變化,大神已做兼容,參照 UIWebview 就好。
到這里就全部總結(jié)完成啦,剛開始和H5做交互的時候不是H5調(diào)不到原生,就是原生調(diào)不到H5,當時大家都是蒙蔽的,浪費很多時間去排查問題。經(jīng)過這一總結(jié),以后出問題就可以快速定位啦。
希望可以給閱讀者提供幫助,有什么問題可以評論或私信,demo寫的太亂就不貼鏈接了,有需要可以找我要。
下面是我對 WebViewJavascriptBridge 源碼的學習,有興趣的同學可以看看哦。
WebViewJavascriptBridge 源碼學習--了解其實現(xiàn)原理