ObjC & JavaScript 交互,在恰當的時機注入對象

njzt 8年前發布 | 6K 次閱讀 iOS開發 移動開發 JavaScript Objective-C

移動端項目開發中,免不了出現 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

Click to next page

按照以上的做法,就能達到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/

 

 本文由用戶 njzt 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!