正在加载今日诗词....
5 min read

iOS原生与JS的交互

iOS原生与JS的交互

目的:

  • 交互<1> 原生调用JS
  • 交互<2> JS调用原生

方式:

  • 拦截协议
  • JavaScriptCore库
  • WKWebView
  • 自定义NSURLProtocol拦截
  • WebViewJavascriptBridge

UIWebview 适配iOS7

HTML 网页的源代码

var res = { 
"title":" 端午礼包提回去,丈母娘不满意来找我!!!",
"url":"http:\/\/app.xxx.com\/news\/rd\/content_wap_39865.shtml",
"image":"http:\/\/www.xxx.com\/v\/b\/images\/2016\/5\/30\/20165301464599344828_167.jpg",
"summary":" 端午礼包提回去,丈母娘不满意来找我!!!"
};

// js方法
function item_info(){
	return JSON.stringify(res);
}

交互<1> 原生调用JS

方式一: 在UIWebView的代理方法 -(void) webViewDidFinishLoad:(UIWebView *)webView

// 使用 OC原生的stringByEvaluatingJavaScriptFromString 方法调用js方法

NSString *info = [webView stringByEvaluatingJavaScriptFromString:@"item_info()"];

NSDictionary *shareData = [NSJSONSerialization JSONObjectWithData:[info dataUsingEncoding:NSUnicodeStringEncoding] options:NSJSONReadingAllowFragments error:nil];

方式二: JSContent

JSContext *context = [[JSContext alloc] init];
JSValue *jsVal = [context evaluateScript:@"21+7"];
int iVal = [jsVal toInt32];
NSLog(@"JSValue: %@, int: %d", jsVal, iVal);

evaluateScript后面跟 JS语句,就可以调用JS了

交互<2> JS调用原生

方式一: 使用 自定义协议头 (原生和H5定义好scheme)

在 web 中的按钮 或者 点击链接的onclick方法中 发起一个虚假的请求.
格式为
'协议头://路径?参数1=值1&参数2=值2'

yourscheme://callfunction/parameter1/parameter2?parameter3=value

// 1.请求发起方式①
window.location.href='playVideo://tide?url=http://123.125.148.27:96/video/gesila.mp4';

// 2.请求发起方式②
function execute(url) 
{
  var iframe = document.createElement("IFRAME");
  iframe.setAttribute("src", url);
  document.documentElement.appendChild(iframe);
  iframe.parentNode.removeChild(iframe);
  iframe = null;
}

其中 playVideo 为自定义的协议头scheme

然后客户端 原生代码中,使用 webView 的代理方法 即 重定义方法

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

 NSURL *url = request.URL;
    if ([url.scheme isEqualToString:@"playvideo"]) {
        NSArray *querys = [url.query componentsSeparatedByString:@"&"];
        NSString *videoUrl = nil;
        for (NSString *query in querys) {
            if ([query rangeOfString:@"url="].location != NSNotFound) {
                videoUrl = [query substringFromIndex:4];
                break;
            }
        }
  //使用 & 分割 想要传递的数据参数 就可以了
}

这种方式实现简单,但是写法太死板,所有的请求都在一个代理方法中,有太多的判断和重复代码.
而且有可能会影响到H5正常的网络请求.
比如H5连续两次发起此种网络请求的话,结果就是只能截获后面那次请求.
注意点:

  • 关于参数也可以将Json数据进行Base64 编码,以保证 url 中不会出现一些非法的字符。

  • js call native 是异步,native call js 是同步

  • 在开发中发现,当在 native 层调用stringByEvaluatingJavaScriptFromString 方法时,可能由于 javascript 是单线程的原因,会阻塞原有 js 代码的执行。这里解决办法是在 js 端用 defer 将 iframe 的插入延后执行

  • UIWebView 的stringByEvaluatingJavaScriptFromString 方法必须是主线程中执行,而主线程的执行时间过长就会 block UI 的更新。所以我们应该尽量让stringByEvaluatingJavaScriptFromString方法执行的时间短

方式二: - (void)webViewDidFinishLoad:(UIWebView *)webView
使用JSContent 通过在原生中捕获H5的上下文来做到

JSContext *context = [[JSContext alloc] init];
context[@"log"] = ^() {
NSLog(@"+++++++Begin Log+++++++");
 // 获取JS的log函数中传入的参数
NSArray *args = [JSContext currentArguments];
for (JSValue *jsVal in args) {
NSLog(@"%@", jsVal);
}
 
JSValue *this = [JSContext currentThis];
NSLog(@"this: %@",this);
NSLog(@"-------End Log-------");
};
 // JS执行的log方法
[context evaluateScript:@"log('ider', [7, 21], { hello:'world', js:100 });"];
 
//Output:
// +++++++Begin Log+++++++
// ider
// 7,21
// [object Object]
// this: [object GlobalObject]
// -------End Log-------

注意事项:
Block在JavaScriptObjective-C之间的转换 建立起更多的桥梁,让互通更方便。但是要注意的是无论是把Block传给JSContext对象让其变成JavaScript方法,还是把它赋给exceptionHandler属性,在Block内都不要直接使用其外部定义的JSContext对象或者JSValue,应该将其当做参数传入到Block中,或者通过JSContext的类方法+ (JSContext *)currentContext;来获得。否则会造成循环引用使得内存无法被正确释放。

WKWebview 推荐

如果不需要适配iOS8之前的版本,强烈建议替换成iOS的新框架WebKit
注意WKWebView只能够使用代码创建

交互<1> 原生调用JS

//直接调用js
    webView.evaluateJavaScript("hi()", completionHandler: nil)
//调用js带参数
    webView.evaluateJavaScript("hello('liuyanwei')", completionHandler: nil)
// 调用js获取返回值
   webView.evaluateJavaScript("getName()") { (any,error) -> Void in
        NSLog("%@", any as! String)
    }

交互<2> JS调用原生

方式一:
经过以下步骤:
WKScriptMessageHandler的注册

let config = WKWebViewConfiguration()
//注册js方法  注意 webViewApp 为JS和原生都定义好的name,类似拦截代理的协议头
config.userContentController.addScriptMessageHandler(self, name: "webViewApp")
// 初始化
let webView = WKWebView(frame: self.webWrap.frame, configuration: config)

注意先注册,后初始化WKWebView

② 处理 WKScriptMessageHandler的回调
遵守WKScriptMessageHandler的代理

extention ViewController:WKScriptMessageHandler
    
  //实现js调用ios的handle委托
  func userContentController(userContentController: WKUserContentController, didReceiveScriptMessage message: WKScriptMessage) {
     //接受JS传过来的消息 从而决定app调用的方法
      if let dict = message.body as? [String: Any],
      let method = dict["method"] as? String,
      let param1 = dict["param1"] as? String,
      if method == "hello"{
          hello(param1)
     }
}

③ 截获js的调用方法,通过WKScriptMessageHandlername

H5的源码

var message = {
                 'method' : 'Hello',
                 'param1' : 'Word',
                 };
 window.webkit.messageHandlers.webViewApp.postMessage(message);

方式二: 拦截代理
WKWebView的代理方法中拦截

-(void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler
{
    NSString *url = navigationAction.request.URL.absoluteString;
    NSLog(@"%@",url);

    if (navigationAction.navigationType == WKNavigationTypeLinkActivated  && [url rangeOfString:@"playVideo://"].location != NSNotFound) {
    // 取消虚假的请求
    decisionHandler(WKNavigationActionPolicyCancel);

        // url的协议头是playVideo ,客户端处理自己的请求
        NSLog(@"playVideo");
    } else {
        decisionHandler(WKNavigationActionPolicyAllow);
    }

}

注意这里POST请求这儿还有一个坑。加载POST请求的时候,会丢失HTTPBody。解决办法是在网页上开一个JavaScript方法,在请求POST的时候去调用JavaScript这个方法,从而完成POST请求

参考链接