# ObjC和JavaScript的交互，在恰当的时机注入对象

## ObjC和JavaScript的交互，在恰当的时机注入对象

原文地址： <http://www.jianshu.com/p/2e53d87c826b>

&#x20;**警告：文章中提到的**&#x20;

```
- (void)webView:(id)unuse didCreateJavaScriptContext:(JSContext *)ctx forFrame:(id)frame;
```

方法涉及私有API，有网友反馈说审核会被拒绝，希望看到的朋友们慎用

移动端项目开发中，免不了出现Native App（以下简称Native）和H5页面（以下简称H5）的交互，网络上有很多第三方框架，比较[WebViewJavascriptBridge](https://github.com/marcuswestin/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|imageView2/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对应的方&#x6CD5;*\*\**

```
#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

![](http://upload-images.jianshu.io/upload_images/545755-08dab112b16f82f0.png?imageMogr2/auto-orient/strip|imageView2/2/w/1240) 具体参见官方文档说明[didCreateJavaScriptContext](https://developer.apple.com/documentation/webkit/webframeloaddelegate/1501462-webview?preferredLanguage=occ) 看到这里，我们就能在收到这个消息的时候，拿到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);
    };
}
```


---

# Agent Instructions: Querying This Documentation

If you need additional information that is not directly available in this page, you can query the documentation dynamically by asking a question.

Perform an HTTP GET request on the current page URL with the `ask` query parameter:

```
GET https://philm.gitbook.io/philm-ios-wiki/mei-zhou-yue-du/objc-he-javascript-de-jiao-hu-zai-qia-dang-de-shi-ji-zhu-ru-dui-xiang.md?ask=<question>
```

The question should be specific, self-contained, and written in natural language.
The response will contain a direct answer to the question and relevant excerpts and sources from the documentation.

Use this mechanism when the answer is not explicitly present in the current page, you need clarification or additional context, or you want to retrieve related documentation sections.
