请选择 进入手机版 | 继续访问电脑版

技术控

    今日:106| 主题:57238
收藏本版 (1)
最新软件应用技术尽在掌握

[其他] ObjC&JavaScript 交互,在恰当的时机注入对象

[复制链接]
腾讯是企鹅 发表于 2016-12-1 13:25:53
167 3
移动端项目开发中,免不了出现 Native App (以下简称Native)和 H5 页面(以下简称H5)的交互,网络上有很多第三方框架,比如 WebViewJavascriptBridge ,对于一些小的项目需求来说,其实不用那么麻烦,我们还是先从基础着手。
   

ObjC&JavaScript 交互,在恰当的时机注入对象

ObjC&JavaScript 交互,在恰当的时机注入对象-1-技术控-方法,调用,Native,加载,NSString

  先了解几个基础方法

  
       
  • 网页即将加载(最先执行的代理方法),在每次load 页面的时候都会先走这个回调,可以在此做一些自己的操作,经常会在这儿拦截协议  
        
  1. - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
  2.     // do something...
  3.    
  4.     return YES;
  5. }
复制代码
      
       
  • 网页已经加载完成(最后执行的代理方法),执行到这个地方,web 页面已经加载完成,相关代码也都执行完毕  
        
  1. - (void)webViewDidFinishLoad:(UIWebView *)webView {
  2.     // 加载完成 隐藏HUD
  3.    
  4. }
复制代码
      根据不同的场景,找一个最合适的方法

  场景1

  (H5 通信 Native,告知Native 要做的事儿)
        
  1. H5 页面在某个标签点击后,要关闭当前加载网页的控制器VC
复制代码
      需求分析:
  这应该不是最简单的一个需求,最简单的是Native 通过url 给H5 页面传参数,告知H5 要做的事儿。
  这个需求中,H5 页面已经加载完毕,此时可以说H5 页面相关的Bug 和UI 缺陷都与Native 无关,我每次都是这么跟测试人员讲,类似问题直接assign 给他们。
  功能实现:
   对于这类比较简单的需求,最常用的做法就是,通过拦截协议的方法,在点击标签的时候,可以调用自定义协议的超链接,比如定义一个 yuhanle://action/close 的链接,在页面即将load 的时候,判断url 的协议,如果协议是 yuhanle ,就拦截掉这个请求,做自己的处理。
  图解:
   

ObjC&JavaScript 交互,在恰当的时机注入对象

ObjC&JavaScript 交互,在恰当的时机注入对象-2-技术控-方法,调用,Native,加载,NSString

  场景2

  (H5 调用 Native App 的JS 方法,包括同步和异步操作)
        
  1. H5 页面在加载过程中,需要从Native 中取得部分数据,或调用某个功能,均包含同步
  2. 操作或异步操作,比如只是简单的获取token,则直接同步返回,如果需要Native 异
  3. 步拿到结果,Native 则需要考虑 JSExport 中的线程问题
复制代码
      需求分析:
  这个需求中肯定需要Native 注入JS 方法,H5 通过调用JS 和Native 通信,其中包括同步和异步两种情况下的处理,需要注意的就是异步操作时,H5 需要在调用 App 时传入一个 JS 方法名,App 在拿到数据后可以回调 H5 的JS 方法,在调用这个回调的时候,需要使用webView 的currentThread,不然就会出现页面卡死。
  功能实现:
  1- 定义一个类,用于注入这个对象
        
  1. // 此模型用于注入JS的模型,这样就可以通过模型来调用方法。
  2. @interface QWSJsObjCModel : NSObject <JavaScriptObjectiveCDelegate>
  3. @property (nonatomic, weak) JSContext *jsContext;
  4. @property (nonatomic, weak) UIWebView *webView;
  5. @property (nonatomic, weak) G100WebViewController * webVc;
  6. @end
复制代码
      2- 声明协议,实现和JS 对应的方法
        
  1. #import <JavaScriptCore/JavaScriptCore.h>
  2. @protocol JavaScriptObjectiveCDelegate <JSExport>
  3. /**
  4. *  获取客户端的token
  5. *
  6. *  @param qwsKey 客户端生成的密码key
  7. *
  8. *  @return 返回值token
  9. */
  10. - (NSString *)getToken:(NSString *)qwsKey;
  11. /**
  12. *  H5 传递key 获取newToken 在调用其 callback 方法
  13. *
  14. *  @param key      qwskey
  15. *  @param callback 回调方法名
  16. *  @param property 方法参数
  17. */
  18. - (void)getNewToken:(NSString *)key callback:(NSString *)callback property:(NSString *)property;
  19. /**
  20. *  H5 在加载完成后 告诉客户端在返回的时候调用该方法
  21. *
  22. *  @param callback js 方法名
  23. */
  24. - (void)getExitMsgCallback:(NSString *)callback;
复制代码
      3- 我们需要在打开webView 的时候,找到一个好的时机注入 JS
        
  1. // 首先拿到JSContext
  2. self.jsContext = [_jsWebView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
  3.    // 通过模型调用方法,这种方式更好些。
  4.    QWSJsObjCModel *model  = [[QWSJsObjCModel alloc] init];
  5.    self.jsContext[@"nativeObj"] = model;
  6.    model.jsContext = self.jsContext;
  7.    model.webView = _jsWebView;
  8.    
  9.    self.jsContext[@"getUserinfo"] = ^(){
  10.        return @"1234";
  11.    };
  12.    
  13.    self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
  14.        context.exception = exceptionValue;
  15.        NSLog(@"异常信息:%@", exceptionValue);
  16.    };
复制代码
      4- 对应H5 页面的JS 定义及调用
        
  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. true<title>测试IOS与JS之前的互调</title>
  5. true<style type="text/css">
  6. true  * {
  7. true          font-size: 40px;
  8. true  }
  9. true</style>
  10.   <script type="text/javascript">
  11.   var jsFunc = function() {
  12.     alert('Objective-C call js to show alert');
  13.   }
  14.   
  15.   var jsParamFunc = function(argument) {
  16.     document.getElementById('jsParamFuncSpan').innerHTML
  17.     = argument['name'];
  18.   }
  19.   
  20.   </script>
  21.   
  22. </head>
  23. <body>
  24.   
  25. <div style="margin-top: 100px">
  26. true<h1>Test how to use objective-c call js</h1>
  27. true<input type="button" value="getToken" onclick="alert(nativeObj.getToken())">
  28. true<input type="button" value="Call ObjC system alert" onclick="nativeObj.showAlertMsg('js title', 'js message')">
  29. </div>
  30. <div>
  31. true<input type="button" value="Call ObjC func with JSON " onclick="nativeObj.callWithDict({'name': 'testname', 'age': 10, 'height': 170})">
  32. true<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})">
  33. </div>
  34. <div>
  35.   <a href="test1.html">Click to next page</a>
  36. </div>
  37. <div>
  38. true<span id="jsParamFuncSpan" style="color: red; font-size: 50px;"></span>
  39. </div>
  40. </body>
  41. </html>
复制代码
      按照以上的做法,就能达到Native 和H5 之间的相互通信,现在的问题是,在什么时候注入JS 对象,才能满足H5 页面的需求,因为实际情况中,H5 页面可能会随时调用你的JS。
  需要注意的几个问题

  1- 场景2 中我们提到的,异步调用时的线程问题 首先看下下面的代码
        
  1. - (void)getNewToken:(NSString *)key callback:(NSString *)callback property:(NSString *)property {
  2.     if (_webVc) {
  3.         if ([_webVc.qwsKey isEqualToString:key]) {
  4.             
  5.             __block NSString * newToken = @"";
  6.             __block NSInteger result = 0;
  7.             
  8.             [[UserManager shareManager] autoLoginWithComplete:^(NSInteger statusCode, ApiResponse *response, BOOL requestSuccess) {
  9.                 if (requestSuccess) {
  10.                     newToken = [[G100InfoHelper shareInstance] token];
  11.                 }else{
  12.                     newToken = @"error";
  13.                 }
  14.                
  15.                 result = requestSuccess ? response.errCode : statusCode;
  16.                
  17.                 JSValue * function = self.jsContext[callback];
  18.                 NSArray * params = @[@(result), newToken, property];
  19.                 [function callWithArguments:params];
  20.             }];
  21.         }
  22.     }
  23. }
复制代码
      这段代码,就是想在H5 页面调用的时候,App 这边自动登陆,重新获取到最新的token,拿到结果以后并回调H5,整个过程上是异步的,看起来是没问题的,但是一旦实际操作起来,会在这里卡死。具体原因,我也不好解释,解决办法是有的,只能通过webView 的currentThread 来执行perform 操作。
  示例如下:
        
  1. - (void)getNewToken:(NSString *)key callback:(NSString *)callback property:(NSString *)property {
  2.     if (_webVc) {
  3.         if ([_webVc.qwsKey isEqualToString:key]) {
  4.             
  5.             __block NSString * newToken = @"";
  6.             __block NSInteger result = 0;
  7.             NSThread * webThread = [NSThread currentThread];
  8.             
  9.             [[UserManager shareManager] autoLoginWithComplete:^(NSInteger statusCode, ApiResponse *response, BOOL requestSuccess) {
  10.                 if (requestSuccess) {
  11.                     newToken = [[G100InfoHelper shareInstance] token];
  12.                 }else{
  13.                     newToken = @"error";
  14.                 }
  15.                
  16.                 result = requestSuccess ? response.errCode : statusCode;
  17.                 // 这里通过此方法 在当前线程操作才不会造成卡死的现象
  18.                 [self performSelector:@selector(callQWSJSWithArgument:) onThread:webThread withObject:@[callback, @(result), newToken, property] waitUntilDone:NO];
  19.             }];
  20.         }
  21.     }
  22. }
  23. - (void)callQWSJSWithArgument:(NSArray *)argument {
  24.     NSString * callback = argument[0];
  25.     JSValue * function = self.jsContext[callback];
  26.    
  27.     NSMutableArray * params = [NSMutableArray arrayWithArray:argument];
  28.     // 移除第一个 方法名
  29.     [params removeObjectAtIndex:0];
  30.     [function callWithArguments:params];
  31. }
复制代码
      2- 同样是场景2 中的一个问题,什么时候注入对象
  需求总是虚无缥缈的,对于H5 结合 Native 的开发结构中,Native 始终扮演着服务和入口的角色,H5 可能随时都会主动和Native 通信,但是Native 应该在什么时候准备好这些服务呢?
  看了很多网上的资料,几乎全部都是在页面加载完成 webViewDidFinishLoad 这个回调中注入方法,但实际开发中,很多页面在加载的时候就需要和Native 通信,比如说拿到token,如果在这个时候才注入,肯定是来不及的,只能无功而返。
  相信大多数人都没太在意这个问题,当然,如果强制让H5 的开发人员修改逻辑,将所有的通信都放在页面加载完成以后在做,也没问题,只不过对于用户的体验会变得糟糕。
  深入研究官方文档,就会发现,webView 在加载过程中,会执行这么一个方法,他的作用是
        
  1. - (void)webViewDidFinishLoad:(UIWebView *)webView {
  2.     // 加载完成 隐藏HUD
  3.    
  4. }
  5. 0
复制代码
      

ObjC&JavaScript 交互,在恰当的时机注入对象

ObjC&JavaScript 交互,在恰当的时机注入对象-3-技术控-方法,调用,Native,加载,NSString

   具体参见官方文档说明 didCreateJavaScriptContext
  看到这里,我们就能在收到这个消息的时候,拿到JSContext,然后注入我们的Model。
  首先,新建一个NSObject 的Catagory,在这个代理方法中发送一个通知
        
  1. - (void)webViewDidFinishLoad:(UIWebView *)webView {
  2.     // 加载完成 隐藏HUD
  3.    
  4. }
  5. 1
复制代码
      然后,在webView 的控制器中监听这个消息
        
  1. - (void)webViewDidFinishLoad:(UIWebView *)webView {
  2.     // 加载完成 隐藏HUD
  3.    
  4. }
  5. 2
复制代码
      [email protected] 方法
        
  1. - (void)webViewDidFinishLoad:(UIWebView *)webView {
  2.     // 加载完成 隐藏HUD
  3.    
  4. }
  5. 3
复制代码
      如此,应该是一次比较完美的注入了~



上一篇:Image zoom with pure JavaScript
下一篇:Hitchhiker&#x27;s Guide to the GDB
商装魅 发表于 2016-12-1 16:33:28
我了个去,顶了
回复 支持 反对

使用道具 举报

mcming92 发表于 2016-12-1 17:30:16
鄙视楼下的顶帖没我快,哈哈
回复 支持 反对

使用道具 举报

xq9012 发表于 2016-12-7 16:49:04
我可以轻视你,鄙视你,小看你,不看你.
回复 支持 反对

使用道具 举报

我要投稿

回页顶回复上一篇下一篇回列表
手机版/CoLaBug.com ( 粤ICP备05003221号 | 文网文[2010]257号 | 粤公网安备 44010402000842号 )

© 2001-2017 Comsenz Inc.

返回顶部 返回列表