UDN-企业互联网技术人气社区

板块导航

浏览  : 1343
回复  : 0

[讨论交流] ObjC&JavaScript交互,在恰当的时机注入对象

[复制链接]
开花包的头像 楼主
发表于 2016-12-12 13:20:47 | 显示全部楼层 |阅读模式
  移动端项目开发中,免不了出现 Native App (以下简称Native)和 H5 页面(以下简称H5)的交互,网络上有很多第三方框架,比如WebViewJavaScriptBridge,对于一些小的项目需求来说,其实不用那么麻烦,我们还是先从基础着手。

  先了解几个基础方法

  网页即将加载(最先执行的代理方法),在每次load 页面的时候都会先走这个回调,可以在此做一些自己的操作,经常会在这儿拦截协议

  1.   - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {

  2.   // do something...

  3.   return YES;

  4.   }
复制代码


  网页已经加载完成(最后执行的代理方法),执行到这个地方,web 页面已经加载完成,相关代码也都执行完毕

  1.   - (void)webViewDidFinishLoad:(UIWebView *)webView {

  2.   // 加载完成 隐藏HUD

  3.   }
复制代码


  根据不同的场景,找一个最合适的方法

  场景1


  
  1. (H5 通信 Native,告知Native 要做的事儿)
复制代码


  H5 页面在某个标签点击后,要关闭当前加载网页的控制器VC

  需求分析:

  这应该不是最简单的一个需求,最简单的是Native 通过url 给H5 页面传参数,告知H5 要做的事儿。

  这个需求中,H5 页面已经加载完毕,此时可以说H5 页面相关的Bug 和UI 缺陷都与Native 无关,我每次都是这么跟测试人员讲,类似问题直接assign 给他们。

  功能实现:

  对于这类比较简单的需求,最常用的做法就是,通过拦截协议的方法,在点击标签的时候,可以调用自定义协议的超链接,比如定义一个 yuhanle://action/close的链接,在页面即将load 的时候,判断url 的协议,如果协议是 yuhanle,就拦截掉这个请求,做自己的处理。

  图解:

f.png


  场景2

  1.   (H5 调用 Native App 的JS 方法,包括同步和异步操作)

  2.   H5 页面在加载过程中,需要从Native 中取得部分数据,或调用某个功能,均包含同步

  3.   操作或异步操作,比如只是简单的获取token,则直接同步返回,如果需要Native 异

  4.   步拿到结果,Native 则需要考虑 JSExport 中的线程问题
复制代码

  需求分析:

  这个需求中肯定需要Native 注入JS 方法,H5 通过调用JS 和Native 通信,其中包括同步和异步两种情况下的处理,需要注意的就是异步操作时,H5 需要在调用 App 时传入一个 JS 方法名,App 在拿到数据后可以回调 H5 的JS 方法,在调用这个回调的时候,需要使用webView 的currentThread,不然就会出现页面卡死。

  功能实现:

  1- 定义一个类,用于注入这个对象

  1.   // 此模型用于注入JS的模型,这样就可以通过模型来调用方法。

  2.   @interface QWSJsObjCModel : NSObject

  3.   @property (nonatomic, weak) JSContext *jsContext;

  4.   @property (nonatomic, weak) UIWebView *webView;

  5.   @property (nonatomic, weak) G100WebViewController * webVc;

  6.   @end
复制代码


  2- 声明协议,实现和JS 对应的方法

  1.   #import

  2.   @protocol JavaScriptObjectiveCDelegate

  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.   self.jsContext[@"getUserinfo"] = ^(){

  9.   return @"1234";

  10.   };

  11.   self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {

  12.   context.exception = exceptionValue;

  13.   NSLog(@"异常信息:%@", exceptionValue);

  14.   };
复制代码


  4- 对应H5 页面的JS 定义及调用

  1. <!DOCTYPE html>
  2. <html>
  3. <head>
  4. <title>测试IOS与JS之前的互调</title>
  5. <style type="text/css">
  6.    * {
  7.     font-size: 40px;
  8.    }
  9. </style>
  10.   <script type="text/JavaScript">

  11.   var jsFunc = function() {
  12.     alert('Objective-C call js to show alert');
  13.   }

  14.   var jsParamFunc = function(argument) {
  15.     document.getElementById('jsParamFuncSpan').innerHTML
  16.     = argument['name'];
  17.   }

  18.   </script>

  19. </head>

  20. <body>

  21. <div style="margin-top: 100px">
  22. <h1>Test how to use objective-c call js</h1>
  23. <input type="button" value="getToken" onclick="alert(nativeObj.getToken())">
  24. <input type="button" value="Call ObjC system alert" onclick="nativeObj.showAlertMsg('js title', 'js message')">
  25. </div>

  26. <div>
  27. <input type="button" value="Call ObjC func with JSON " onclick="nativeObj.callWithDict({'name': 'testname', 'age': 10, 'height': 170})">
  28. <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})">
  29. </div>
  30. <div>
  31.   <a href="test1.html">Click to next page</a>
  32. </div>

  33. <div>
  34. <span id="jsParamFuncSpan" style="color: red; font-size: 50px;"></span>
  35. </div>

  36. </body>
  37. </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.   __block NSString * newToken = @"";

  5.   __block NSInteger result = 0;

  6.   [[UserManager shareManager] autoLoginWithComplete:^(NSInteger statusCode, APIResponse *response, BOOL requestSuccess) {

  7.   if (requestSuccess) {

  8.   newToken = [[G100InfoHelper shareInstance] token];

  9.   }else{

  10.   newToken = @"error";

  11.   }

  12.   result = requestSuccess ? response.errCode : statusCode;

  13.   JSValue * function = self.jsContext[callback];

  14.   NSArray * params = @[@(result), newToken, property];

  15.   [function callWithArguments:params];

  16.   }];

  17.   }

  18.   }

  19.   }
复制代码


  这段代码,就是想在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.   __block NSString * newToken = @"";

  5.   __block NSInteger result = 0;

  6.   NSThread * webThread = [NSThread currentThread];

  7.   [[UserManager shareManager] autoLoginWithComplete:^(NSInteger statusCode, ApiResponse *response, BOOL requestSuccess) {

  8.   if (requestSuccess) {

  9.   newToken = [[G100InfoHelper shareInstance] token];

  10.   }else{

  11.   newToken = @"error";

  12.   }

  13.   result = requestSuccess ? response.errCode : statusCode;

  14.   // 这里通过此方法 在当前线程操作才不会造成卡死的现象

  15.   [self performSelector:@selector(callQWSJSWithArgument:) onThread:webThread withObject:@[callback, @(result), newToken, property] waitUntilDone:NO];

  16.   }];

  17.   }

  18.   }

  19.   }

  20.   - (void)callQWSJSWithArgument:(NSArray *)argument {

  21.   NSString * callback = argument[0];

  22.   JSValue * function = self.jsContext[callback];

  23.   NSMutableArray * params = [NSMutableArray arrayWithArray:argument];

  24.   // 移除第一个 方法名

  25.   [params removeObjectAtIndex:0];

  26.   [function callWithArguments:params];

  27.   }
复制代码


  2- 同样是场景2 中的一个问题,什么时候注入对象

  需求总是虚无缥缈的,对于H5 结合 Native 的开发结构中,Native 始终扮演着服务和入口的角色,H5 可能随时都会主动和Native 通信,但是Native 应该在什么时候准备好这些服务呢?

  看了很多网上的资料,几乎全部都是在页面加载完成 webViewDidFinishLoad 这个回调中注入方法,但实际开发中,很多页面在加载的时候就需要和Native 通信,比如说拿到token,如果在这个时候才注入,肯定是来不及的,只能无功而返。

  相信大多数人都没太在意这个问题,当然,如果强制让H5 的开发人员修改逻辑,将所有的通信都放在页面加载完成以后在做,也没问题,只不过对于用户的体验会变得糟糕。

  深入研究官方文档,就会发现,webView 在加载过程中,会执行这么一个方法,他的作用是

  1.   _:didCreateJavaScriptContext:for:

  2.   Notifies the delegate that a new JavaScript context has been created created.
复制代码

g.png


  具体参见官方文档说明 didCreateJavaScriptContext

  看到这里,我们就能在收到这个消息的时候,拿到JSContext,然后注入我们的Model。

  首先,新建一个NSObject 的Catagory,在这个代理方法中发送一个通知

  1.   @implementation NSObject (JSTest)

  2.   - (void)webView:(id)unuse didCreateJavaScriptContext:(JSContext *)ctx forFrame:(id)frame {

  3.   [[NSNotificationCenter defaultCenter] postNotificationName:@"DidCreateContextNotification" object:ctx];

  4.   }

  5.   @end
复制代码


  然后,在webView 的控制器中监听这个消息

  1.   - (void)viewDidLoad {

  2.   [super viewDidLoad];

  3.   // 监听可以注入js 方法的通知

  4.   [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(didCreateJSContext:) name:@"DidCreateContextNotification" object:nil];

  5.   }
复制代码


  实现@selector 方法

  1.   #pragma mark - 可以注入js 的监听

  2.   - (void)didCreateJSContext:(NSNotification *)notification {

  3.   NSString *indentifier = [NSString stringWithFormat:@"indentifier%lud", (unsigned long)self.webView.hash];

  4.   NSString *indentifierJS = [NSString stringWithFormat:@"var %@ = '%@'", indentifier, indentifier];

  5.   [self.webView stringByEvaluatingJavaScriptFromString:indentifierJS];

  6.   JSContext *context = notification.object;

  7.   if (![context[indentifier].toString isEqualToString:indentifier]) return;

  8.   self.jsContext = context;

  9.   // 通过模型调用方法,这种方式更好些。

  10.   QWSJsObjCModel *model = [[QWSJsObjCModel alloc] init];

  11.   self.jsContext[@"nativeObj"] = model;

  12.   model.jsContext = self.jsContext;

  13.   model.webView = self.webView;

  14.   model.webVc = self;

  15.   self.jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {

  16.   context.exception = exceptionValue;

  17.   DLog(@"异常信息:%@", exceptionValue);

  18.   };

  19.   }
复制代码


  如此,应该是一次比较完美的注入了~

原文作者:煜寒了  来源:开发者头条
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

关于我们
联系我们
  • 电话:010-86393388
  • 邮件:udn@yonyou.com
  • 地址:北京市海淀区北清路68号
移动客户端下载
关注我们
  • 微信公众号:yonyouudn
  • 扫描右侧二维码关注我们
  • 专注企业互联网的技术社区
版权所有:用友网络科技股份有限公司82041 京ICP备05007539号-11 京公网网备安1101080209224 Powered by Discuz!
快速回复 返回列表 返回顶部