ObjC & JavaScript 交互,在恰當的時機注入對象
移動端項目開發中,免不了出現 Native App (以下簡稱Native)和 H5 頁面(以下簡稱H5)的交互,網絡上有很多第三方框架,比如 WebViewJavascriptBridge ,對于一些小的項目需求來說,其實不用那么麻煩,我們還是<!–more–>先從基礎著手。
先了解幾個基礎方法
- 網頁即將加載(最先執行的代理方法),在每次load 頁面的時候都會先走這個回調,可以在此做一些自己的操作,經常會在這兒攔截協議
- (BOOL)webView:(UIWebView *)webViewshouldStartLoadWithRequest:(NSURLRequest *)requestnavigationType:(UIWebViewNavigationType)navigationType {
// do something...
return YES;
}
- 網頁已經加載完成(最后執行的代理方法),執行到這個地方,web 頁面已經加載完成,相關代碼也都執行完畢
- (void)webViewDidFinishLoad:(UIWebView *)webView {
// 加載完成 隱藏HUD
}
根據不同的場景,找一個最合適的方法
場景1
(H5 通信 Native,告知Native 要做的事兒)
H5 頁面在某個標簽點擊后,要關閉當前加載網頁的控制器VC
需求分析:
這應該不是最簡單的一個需求,最簡單的是Native 通過url 給H5 頁面傳參數,告知H5 要做的事兒。
這個需求中,H5 頁面已經加載完畢,此時可以說H5 頁面相關的Bug 和UI 缺陷都與Native 無關,我每次都是這么跟測試人員講,類似問題直接assign 給他們。
功能實現:
對于這類比較簡單的需求,最常用的做法就是,通過攔截協議的方法,在點擊標簽的時候,可以調用自定義協議的超鏈接,比如定義一個 yuhanle://action/close 的鏈接,在頁面即將load 的時候,判斷url 的協議,如果協議是 yuhanle ,就攔截掉這個請求,做自己的處理。
圖解:
場景2
(H5 調用 Native App 的JS 方法,包括同步和異步操作)
H5 頁面在加載過程中,需要從Native 中取得部分數據,或調用某個功能,均包含同步
操作或異步操作,比如只是簡單的獲取token,則直接同步返回,如果需要Native 異
步拿到結果,Native 則需要考慮 JSExport 中的線程問題
需求分析:
這個需求中肯定需要Native 注入JS 方法,H5 通過調用JS 和Native 通信,其中包括同步和異步兩種情況下的處理,需要注意的就是異步操作時,H5 需要在調用 App 時傳入一個 JS 方法名,App 在拿到數據后可以回調 H5 的JS 方法,在調用這個回調的時候,需要使用webView 的currentThread,不然就會出現頁面卡死。
功能實現:
1- 定義一個類,用于注入這個對象
// 此模型用于注入JS的模型,這樣就可以通過模型來調用方法。
@interface QWSJsObjCModel : NSObject
@property (nonatomic, weak) JSContext *jsContext;
@property (nonatomic, weak) UIWebView *webView;
@property (nonatomic, weak) G100WebViewController * webVc;
@end
2- 聲明協議,實現和JS 對應的方法
#import
@protocolJavaScriptObjectiveCDelegate
/**
* 獲取客戶端的token
*
* @param qwsKey 客戶端生成的密碼key
*
* @return 返回值token
*/
- (NSString *)getToken:(NSString *)qwsKey;
/**
* H5 傳遞key 獲取newToken 在調用其 callback 方法
*
* @param key qwskey
* @param callback 回調方法名
* @param property 方法參數
*/
- (void)getNewToken:(NSString *)keycallback:(NSString *)callbackproperty:(NSString *)property;
/**
* H5 在加載完成后 告訴客戶端在返回的時候調用該方法
*
* @param callback js 方法名
*/
- (void)getExitMsgCallback:(NSString *)callback;
3- 我們需要在打開webView 的時候,找到一個好的時機注入 JS
// 首先拿到JSContext
self.jsContext = [_jsWebViewvalueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
// 通過模型調用方法,這種方式更好些。
QWSJsObjCModel *model = [[QWSJsObjCModelalloc] 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>
<divstyle="margin-top: 100px">
<h1>Test how to use objective-c call js</h1>
<inputtype="button" value="getToken" onclick="alert(nativeObj.getToken())">
<inputtype="button" value="Call ObjC system alert" onclick="nativeObj.showAlertMsg('js title', 'js message')">
</div>
<div>
<inputtype="button" value="Call ObjC func with JSON " onclick="nativeObj.callWithDict({'name': 'testname', 'age': 10, 'height': 170})">
<inputtype="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>
<ahref="test1.html">Click to next page</a>
</div>
<div>
<spanid="jsParamFuncSpan" style="color: red; font-size: 50px;"></span>
</div>
</body>
</html>
Test how to use objective-c call js
按照以上的做法,就能達到Native 和H5 之間的相互通信,現在的問題是,在什么時候注入JS 對象,才能滿足H5 頁面的需求,因為實際情況中,H5 頁面可能會隨時調用你的JS。
需要注意的幾個問題
1- 場景2 中我們提到的,異步調用時的線程問題 首先看下下面的代碼
- (void)getNewToken:(NSString *)keycallback:(NSString *)callbackproperty:(NSString *)property {
if (_webVc) {
if ([_webVc.qwsKeyisEqualToString:key]) {
__blockNSString * newToken = @"";
__blockNSIntegerresult = 0;
[[UserManagershareManager] autoLoginWithComplete:^(NSIntegerstatusCode, ApiResponse *response, BOOL requestSuccess) {
if (requestSuccess) {
newToken = [[G100InfoHelpershareInstance] token];
}else{
newToken = @"error";
}
result = requestSuccess ? response.errCode : statusCode;
JSValue * function = self.jsContext[callback];
NSArray * params = @[@(result), newToken, property];
[function callWithArguments:params];
}];
}
}
}
這段代碼,就是想在H5 頁面調用的時候,App 這邊自動登陸,重新獲取到最新的token,拿到結果以后并回調H5,整個過程上是異步的,看起來是沒問題的,但是一旦實際操作起來,會在這里卡死。具體原因,我也不好解釋,解決辦法是有的,只能通過webView 的currentThread 來執行perform 操作。
示例如下:
- (void)getNewToken:(NSString *)keycallback:(NSString *)callbackproperty:(NSString *)property {
if (_webVc) {
if ([_webVc.qwsKeyisEqualToString:key]) {
__blockNSString * newToken = @"";
__blockNSIntegerresult = 0;
NSThread * webThread = [NSThreadcurrentThread];
[[UserManagershareManager] autoLoginWithComplete:^(NSIntegerstatusCode, ApiResponse *response, BOOL requestSuccess) {
if (requestSuccess) {
newToken = [[G100InfoHelpershareInstance] token];
}else{
newToken = @"error";
}
result = requestSuccess ? response.errCode : statusCode;
// 這里通過此方法 在當前線程操作才不會造成卡死的現象
[self performSelector:@selector(callQWSJSWithArgument:) onThread:webThreadwithObject:@[callback, @(result), newToken, property] waitUntilDone:NO];
}];
}
}
}
- (void)callQWSJSWithArgument:(NSArray *)argument {
NSString * callback = argument[0];
JSValue * function = self.jsContext[callback];
NSMutableArray * params = [NSMutableArrayarrayWithArray:argument];
// 移除第一個 方法名
[paramsremoveObjectAtIndex:0];
[function callWithArguments:params];
}
2- 同樣是場景2 中的一個問題,什么時候注入對象
需求總是虛無縹緲的,對于H5 結合 Native 的開發結構中,Native 始終扮演著服務和入口的角色,H5 可能隨時都會主動和Native 通信,但是Native 應該在什么時候準備好這些服務呢?
看了很多網上的資料,幾乎全部都是在頁面加載完成 webViewDidFinishLoad 這個回調中注入方法,但實際開發中,很多頁面在加載的時候就需要和Native 通信,比如說拿到token,如果在這個時候才注入,肯定是來不及的,只能無功而返。
相信大多數人都沒太在意這個問題,當然,如果強制讓H5 的開發人員修改邏輯,將所有的通信都放在頁面加載完成以后在做,也沒問題,只不過對于用戶的體驗會變得糟糕。
深入研究官方文檔,就會發現,webView 在加載過程中,會執行這么一個方法,他的作用是
_:didCreateJavaScriptContext:for:
Notifiesthedelegatethat a new JavaScriptcontexthasbeencreatedcreated.
具體參見官方文檔說明 didCreateJavaScriptContext
看到這里,我們就能在收到這個消息的時候,拿到JSContext,然后注入我們的Model。
首先,新建一個NSObject 的Catagory,在這個代理方法中發送一個通知
@implementationNSObject (JSTest)
- (void)webView:(id)unusedidCreateJavaScriptContext:(JSContext *)ctxforFrame:(id)frame {
[[NSNotificationCenterdefaultCenter] postNotificationName:@"DidCreateContextNotification" object:ctx];
}
@end
然后,在webView 的控制器中監聽這個消息
- (void)viewDidLoad {
[super viewDidLoad];
// 監聽可以注入js 方法的通知
[[NSNotificationCenterdefaultCenter] addObserver:selfselector:@selector(didCreateJSContext:) name:@"DidCreateContextNotification" object:nil];
}
實現@selector 方法
#pragma mark - 可以注入js 的監聽
- (void)didCreateJSContext:(NSNotification *)notification {
NSString *indentifier = [NSStringstringWithFormat:@"indentifier%lud", (unsigned long)self.webView.hash];
NSString *indentifierJS = [NSStringstringWithFormat:@"var %@ = '%@'", indentifier, indentifier];
[self.webViewstringByEvaluatingJavaScriptFromString:indentifierJS];
JSContext *context = notification.object;
if (![context[indentifier].toStringisEqualToString:indentifier]) return;
self.jsContext = context;
// 通過模型調用方法,這種方式更好些。
QWSJsObjCModel *model = [[QWSJsObjCModelalloc] 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);
};
}
如此,應該是一次比較完美的注入了~
來自:http://ios.jobbole.com/91223/