正在開發(fā)的APP中一個特殊的需求,需要在APP內(nèi)嵌的webview中使用js調(diào)用一些原生的方法。通過對javascript的分析,發(fā)現(xiàn)能夠很好的解決這個問題,但是也發(fā)現(xiàn)了一些坑,與大家分享。
1.定義接口
通過protocol實(shí)現(xiàn),這一步大家都很清楚,告訴js那邊我有哪些接口。關(guān)鍵是JSExport,這個是我們接口必須遵循的協(xié)議,只有遵循這個協(xié)議,才能將方法爭取的注射到webview中。
@protocol WebViewBridgeDelegate <JSExport>
- (void)showURL:(NSString *)url;
- (NSString*)callDK:(NSString*)func function:(NSString*)param;
@end
使用中發(fā)現(xiàn),這些方法好像不能使用optional,沒有進(jìn)行嚴(yán)格的驗(yàn)證。
2.實(shí)現(xiàn)接口
只需要實(shí)現(xiàn)WebViewBridgeDelegate中的這些方法就可以了。
#import <UIKit/UIKit.h>
#import "WebViewBridgeDelegate.h"
@interface AppJSNative : NSObject<WebViewBridgeDelegate>
@property (nonatomic, weak) UINavigationController *navigationController;
@end
@implementation LoldkJSNative
- (void)showURL:(NSString *)url
{
NSLog(@"showURL:%@", url);
[JumpHandler jumpToURI:url navigationController:self.navigationController];
}
- (NSString*)callDK:(NSString*)func function:(NSString*)param
{
NSLog(@"%@", func);
if ([func isEqualToString:@"getToken"])
{
NSString *token =xxx;
NSMutableDictionary *result = [[NSMutableDictionary alloc]init];
if (!IsStrEmpty(token))
{
[result setObject:token forKey:@"token"];
[result setObject:@(YES) forKey:@"result"];
}
else
{
[result setObject:@(NO) forKey:@"result"];
}
NSLog(@"%@", token);
return [result JSONString];
}
NSMutableDictionary *result1 = [[NSMutableDictionary alloc]init];
[result1 setObject:@(NO) forKey:@"result"];
return [result1 JSONString];
}
@end
其中主要是實(shí)現(xiàn)了一些原生頁面跳轉(zhuǎn)的功能,以及獲取原生數(shù)據(jù)的的方法。
3. 注射到webview
- (void)webViewDidStartLoad:(UIWebView *)webView
{
self.jsContext = [_webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
AppJSNative *jsNative = [[AppJSNative alloc]init];
jsNative.navigationController = self.navigationController;
self.jsContext[@"iOS"] = jsNative;
self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exception) {
context.exception = exception;
NSLog(@"jsContext錯誤:%@",exception);
};
}
為什么我要在webViewDidStartLoad中進(jìn)行注射而不是webViewDidEndLoad中。因?yàn)樵趈s加載的時候就會調(diào)用原生方法,在webViewDidEndLoad中注射會導(dǎo)致JS找不到對應(yīng)的方法。
保存navigationController是為了方便進(jìn)行頁面跳轉(zhuǎn)。
4. JSContext生成時機(jī)的問題
? ? ? ?根據(jù)上一步的確能夠處理大部分問題,但是在實(shí)際使用過程中發(fā)現(xiàn),在windows.onload函數(shù)中調(diào)用的時候,發(fā)現(xiàn)原生的接口不存在。根據(jù)分析發(fā)現(xiàn)是因?yàn)镴SContext對象只會在onload函數(shù)之后才能獲取到。此處我們需要自己找到一種方法,能夠在正確的時機(jī)(越早越好)獲取到JSContext對象。
? ? ? ?基本原理是這樣的:WebKit用WebFrameLoadDelegate回調(diào)與客戶端進(jìn)行通訊就好像UIWebView傳達(dá)頁面加載事件通過他自己的UIWebViewDelegate。WebFrameLoadDelegate其中一個方法是webView:didCreateJavaScriptContext:forFrame:就像所有事件源,WebKit的代碼去檢測他的代理是否實(shí)現(xiàn)了回調(diào)方法,如果實(shí)現(xiàn)了就調(diào)用此方法。
證實(shí)在iOS,UIWebView內(nèi),不論任何對象實(shí)現(xiàn)WebKit的WebFrameLoadDelegate方法,并不是真的實(shí)現(xiàn)webView:didCreateJavaScriptContext:forFrame:所以WebKit從不會調(diào)用此方法。如果此方法存在于代理對象中,它將會被自動調(diào)用。
? ? ? ?既然如此,在OC中有很多的辦法給現(xiàn)有的類和對象動態(tài)的增添一個方法。最簡單的辦法就是通過擴(kuò)展。我給已有的類NSObject添加一個擴(kuò)展去實(shí)現(xiàn)webView:didCreateJavaScriptContext:forFrame:方法。
? ? ? ?的確,添加這個方法讓W(xué)ebKit開始調(diào)用它,因?yàn)槿魏螌ο?包括UIWebView中的一些sink object)都繼承自NSObject,現(xiàn)在都實(shí)現(xiàn)了webView:didCreateJavaScriptContext:forFrame:這個方法。如果未來UIWebView內(nèi)部的sink object實(shí)現(xiàn)了這個代理方法,那么這個途徑就是失效因?yàn)槲覀冏约簩?shí)現(xiàn)的分類永遠(yuǎn)不會被調(diào)用。
? ? ? ?當(dāng)我們的方法被WebKit調(diào)用的時候會傳給我們一個WebKit中的WebView(不是UIWebView),一個JavaScriptCore的JSContext對象和WebKit的WebFrame。因?yàn)闆]有一個公開的WebKit框架的頭文件提供給我們,所以WebView和WebFrame對我們來說非常透明。但是JSContext正是我們尋找的,通過JavaScriptCore框架對我們來說完全是適用的。(在實(shí)際中,我最終在WebFrame中調(diào)用方法,作為一個最佳狀態(tài))
? ? ? ?問題現(xiàn)在就變成怎樣根據(jù)JSContext反找到對應(yīng)的UIWebView。首先我嘗試使用WebView對象我們控制和沿著繼承的view去找到他擁有的UIWebView.但是后來證明這個對象是一些UIView的代理,并不是一個真正的UIView。并且因?yàn)樗麑ξ覀儊碚f是透明的,我也沒有打算使用它。
? ? ? ?我的解決方案是迭代所有在app中所創(chuàng)建的UIWebViews(參考代碼,我是怎么樣做的)并且使用stringByEvaluatingJavaScriptFromString:去儲存一個token"cookie"在JavaScriptContext中,然后我在JSContext中查找已經(jīng)存在的這個token,如果他存在這個UIWebView就是我所要找的。
? ? ? ?一旦我們有了JSContext我們就可以做一些很有趣的事情。我的測試App展示了我們怎樣映射ObjectiveC的blocks和對象到全局命名空間并且通過JavaScript訪問和調(diào)用它們。
5. JS調(diào)用
window.iOS. showURL("app://user?id=1");
var token = window.iOS. callDKFunction("token");
特別注意的是第二個方法,我們OC中實(shí)現(xiàn)的方法叫callDK, 但是由于多參數(shù)的問題,在JS中對應(yīng)的方法名不再是callDK,而是將后面的參數(shù)的名字加在了函數(shù)名后面組成了新的函數(shù)名callDKFunction,這個地方耗費(fèi)了我很久的時間。
6. 回調(diào)
我們也是可以在js中傳入函數(shù)當(dāng)做參數(shù)的,在OC中可以調(diào)用
- (void)processRequestWithData:(NSDictionary *)diconary callback:(JSValue *)value{
NSLog(@"%@",diconary);
NSMutableArray *array = [NSMutableArray array];
[array addObject:[NSArray arrayWithObjects:@{@"key1":@"value1"},@{@"key2":@"value2"},nil]];
[value callWithArguments:array];
}
其中value就是js傳入的一個回調(diào)函數(shù)。
總結(jié)
以上這些例子,包含了原生頁面調(diào)用、直接return值、回調(diào)。已經(jīng)涵蓋了絕大部分我們想通過js調(diào)用原生的場景。十分的方便。