ObjC和JavaScript的交互,在恰當的時機注入對象
原文地址: http://www.jianshu.com/p/2e53d87c826b
* 警告:文章中提到的 *
- (void)webView:(id)unuse didCreateJavaScriptContext:(JSContext *)ctx forFrame:(id)frame;
方法涉及私有API,有網友反饋說審覈會被拒絕,希望看到的朋友們慎用
移動端項目開發中,免不了出現Native App(以下簡稱Native)和H5頁面(以下簡稱H5)的交互,網絡上有很多第三方框架,比較WebViewJavascriptBridge,對於一些小的項目需求來說,其實不用那麼麻煩,我們還是先從基礎着手。
先了解幾個基礎方法
網頁即將加載(最先執行的代理方法),在每次載入頁面的時候都會先走這個回調,可以在此做一些自己的操作,經常會在這兒攔截協議
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
// do something...
return YES;
}
網頁已經加載完成(最後執行的代理方法),執行到這個地方,網頁面已經加載完成,相關代碼也都執行完畢
- (void)webViewDidFinishLoad:(UIWebView *)webView {
// 加載完成 隱藏HUD
}
根據不同的場景,找一個最合適的方法
場景1
H5通信本地,告知本要做的事兒
H5 頁面在某個標籤點擊後,要關閉當前加載網頁的控制器VC
需求分析:
這應該不是最簡單的一個需求,最簡單的是本地通過url給H5頁面傳參數,告知H5要做的事。
這個需求中,H5頁面已經加載完畢,此時可以說H5頁面相關的Bug和UI缺陷都與本機無關,我每次都是這麼跟測試人員講,類似問題直接分配給他們。
功能實現:
對於這類比較簡單的需求,最常用的做法就是,通過攔截協議的方法,在點擊標籤的時候,可以調用自定義協議的超鏈接,比如定義一個yuhanle://action/close的鏈接,在頁面即將載入的時候,判斷url的協議,如果協議是yuhanle,就攔截掉這個請求,做自己的處理。
圖解:
!()[http://upload-images.jianshu.io/upload_images/545755-9a66b0a332873951.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240]
場景2
H5調用Native App的JS方法,包括同步和異步操作
H5 頁面在加載過程中,需要從Native 中取得部分數據,或調用某個功能,均包含同步
操作或異步操作,比如只是簡單的獲取token,則直接同步返回,如果需要Native 異
步拿到結果,Native 則需要考慮 JSExport 中的線程問題
需求分析:
這個需求中肯定需要Native注入JS方法,H5通過調用JS和Native通信,其中包括同步和異步兩種情況下的處理,需要注意的就是異步操作時,H5需要在調用App時傳入一個JS方法名,應用在拿到數據後可以回調H5的JS方法,在調用這個回調的時候,需要使用webView的currentThread,不然就會出現頁面卡死。
功能實現:
1-定義一個類,用於注入這個對象
// 此模型用於注入JS的模型,這樣就可以通過模型來調用方法。
@interface QWSJsObjCModel : NSObject <JavaScriptObjectiveCDelegate>
@property (nonatomic, weak) JSContext *jsContext;
@property (nonatomic, weak) UIWebView *webView;
@property (nonatomic, weak) G100WebViewController * webVc;
@end
2-聲明協議,實現和JS對應的方法**
#import <JavaScriptCore/JavaScriptCore.h>
@protocol JavaScriptObjectiveCDelegate <JSExport>
/**
* 獲取客戶端的token
*
* @param qwsKey 客戶端生成的密碼key
*
* @return 返回值token
*/
- (NSString *)getToken:(NSString *)qwsKey;
/**
* H5 傳遞key 獲取newToken 在調用其 callback 方法
*
* @param key qwskey
* @param callback 回調方法名
* @param property 方法參數
*/
- (void)getNewToken:(NSString *)key callback:(NSString *)callback property:(NSString *)property;
/**
* H5 在加載完成後 告訴客戶端在返回的時候調用該方法
*
* @param callback js 方法名
*/
- (void)getExitMsgCallback:(NSString *)callback;
3-我們需要在打開webView的時候,找到一個好的時機注入JS
// 首先拿到JSContext
self.jsContext = [_jsWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
// 通過模型調用方法,這種方式更好些。
QWSJsObjCModel *model = [[QWSJsObjCModel alloc] init];
self.jsContext[@"nativeObj"] = model;
model.jsContext = self.jsContext;
model.webView = _jsWebView;
self.jsContext[@"getUserinfo"] = ^(){
return @"1234";
};
self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
context.exception = exceptionValue;
NSLog(@"異常信息:%@", exceptionValue);
};
4-對應H5頁面的JS定義及調用
<!DOCTYPE html>
<html>
<head>
<title>測試IOS與JS之前的互調</title>
<style type="text/css">
* {
font-size: 40px;
}
</style>
<script type="text/javascript">
var jsFunc = function() {
alert('Objective-C call js to show alert');
}
var jsParamFunc = function(argument) {
document.getElementById('jsParamFuncSpan').innerHTML
= argument['name'];
}
</script>
</head>
<body>
<div style="margin-top: 100px">
<h1>Test how to use objective-c call js</h1>
<input type="button" value="getToken" onclick="alert(nativeObj.getToken())">
<input type="button" value="Call ObjC system alert" onclick="nativeObj.showAlertMsg('js title', 'js message')">
</div>
<div>
<input type="button" value="Call ObjC func with JSON " onclick="nativeObj.callWithDict({'name': 'testname', 'age': 10, 'height': 170})">
<input type="button" value="Call ObjC func with JSON and ObjC call js func to pass args." onclick="nativeObj.jsCallObjcAndObjcCallJsWithDict({'name': 'testname', 'age': 10, 'height': 170})">
</div>
<div>
<a href="test1.html">Click to next page</a>
</div>
<div>
<span id="jsParamFuncSpan" style="color: red; font-size: 50px;"></span>
</div>
</body>
</html>
按照以上的做法,就能達到本機和H5之間的相互通信,現在的問題是,在什麼時候注入JS對象,才能滿足H5頁面的需求,因爲實際情況中,H5頁面可能會隨時調用你的JS 。
需要注意的幾個問題
1-場景2中我們提到的,異步調用時的線程問題首先看下下面的代碼
- (void)getNewToken:(NSString *)key callback:(NSString *)callback property:(NSString *)property {
if (_webVc) {
if ([_webVc.qwsKey isEqualToString:key]) {
__block NSString * newToken = @"";
__block NSInteger result = 0;
[[UserManager shareManager] autoLoginWithComplete:^(NSInteger statusCode, ApiResponse *response, BOOL requestSuccess) {
if (requestSuccess) {
newToken = [[G100InfoHelper shareInstance] token];
}else{
newToken = @"error";
}
result = requestSuccess ? response.errCode : statusCode;
JSValue * function = self.jsContext[callback];
NSArray * params = @[@(result), newToken, property];
[function callWithArguments:params];
}];
}
}
}
這段代碼,就是想在H5頁面調用的時候,應用這邊自動登陸,重新獲取到最新的令牌,拿到結果以後並回調H5,整個過程上是異步的,看起來是沒問題的,但是一旦實際操作起來,會在這裏卡死。具體原因,我也不好解釋,解決辦法是有的,只能通過webView的currentThread來執行執行操作。
示例如下:
- (void)getNewToken:(NSString *)key callback:(NSString *)callback property:(NSString *)property {
if (_webVc) {
if ([_webVc.qwsKey isEqualToString:key]) {
__block NSString * newToken = @"";
__block NSInteger result = 0;
NSThread * webThread = [NSThread currentThread];
[[UserManager shareManager] autoLoginWithComplete:^(NSInteger statusCode, ApiResponse *response, BOOL requestSuccess) {
if (requestSuccess) {
newToken = [[G100InfoHelper shareInstance] token];
}else{
newToken = @"error";
}
result = requestSuccess ? response.errCode : statusCode;
// 這裏通過此方法 在當前線程操作纔不會造成卡死的現象
[self performSelector:@selector(callQWSJSWithArgument:) onThread:webThread withObject:@[callback, @(result), newToken, property] waitUntilDone:NO];
}];
}
}
}
- (void)callQWSJSWithArgument:(NSArray *)argument {
NSString * callback = argument[0];
JSValue * function = self.jsContext[callback];
NSMutableArray * params = [NSMutableArray arrayWithArray:argument];
// 移除第一個 方法名
[params removeObjectAtIndex:0];
[function callWithArguments:params];
}
2-同樣是場景2中的一個問題,什麼時候注入對象
需要總是虛無縹緲的,對於H5結合本機的開發結構中,本機始終扮演着服務和入口的角色,H5可能隨時都會主動和本機通信,但是本應該在這什麼時候準備好這些服務呢?
看到很多網上的資料,幾乎全部都是在頁面加載完成webViewDidFinishLoad這個回調中注入方法,但實際開發中,很多頁面在加載的時候就需要和本機通信,比如說拿到令牌,如果在這個時候才注入,肯定是來不及的,只能無功而返。
相信大多數人都沒有在這個問題,當然,如果強制讓H5的開發人員修改邏輯,將所有的通信都放在頁面加載完成以後在做,也沒問題,只不過對於用戶的體驗會變得糟糕。
深入研究官方文檔,就會發現,webView在加載過程中,會執行這麼一個方法,他的作用是
_:didCreateJavaScriptContext:for:
Notifies the delegate that a new JavaScript context has been created created
具體參見官方文檔說明didCreateJavaScriptContext
看到這裏,我們就能在收到這個消息的時候,拿到JSContext,然後注入我們的模型。
首先,新建一個NSObject的Catagory,在這個代理方法中發送一個通知
@implementation NSObject (JSTest)
- (void)webView:(id)unuse didCreateJavaScriptContext:(JSContext *)ctx forFrame:(id)frame {
[[NSNotificationCenter defaultCenter] postNotificationName:@"DidCreateContextNotification" object:ctx];
}
@end
然後,在webView的控制器中監聽這個消息
- (void)viewDidLoad {
[super viewDidLoad];
// 監聽可以注入js 方法的通知
[[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didCreateJSContext:) name:@"DidCreateContextNotification" object:nil];
}
實現@selector方法
#pragma mark - 可以注入js 的監聽
- (void)didCreateJSContext:(NSNotification *)notification {
NSString *indentifier = [NSString stringWithFormat:@"indentifier%lud", (unsigned long)self.webView.hash];
NSString *indentifierJS = [NSString stringWithFormat:@"var %@ = '%@'", indentifier, indentifier];
[self.webView stringByEvaluatingJavaScriptFromString:indentifierJS];
JSContext *context = notification.object;
if (![context[indentifier].toString isEqualToString:indentifier]) return;
self.jsContext = context;
// 通過模型調用方法,這種方式更好些。
QWSJsObjCModel *model = [[QWSJsObjCModel alloc] init];
self.jsContext[@"nativeObj"] = model;
model.jsContext = self.jsContext;
model.webView = self.webView;
model.webVc = self;
self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
context.exception = exceptionValue;
DLog(@"異常信息:%@", exceptionValue);
};
}