js ios 交互

最近公司的运营瞎搞了个活动,其活动要服务端提供数据支持,web前端在微信公众账号内作为主要的运营阵地,而iOS、Android要提供相应的入口及页面进行配合。一个活动,动用了各个端的程序猿。而在这里面技术方面主要就是涉及到web端和服务端的交互,web前端和iOS、Android的交互。本人作为一个iOS开发者,今天就聊聊web、iOS、Android三端的交互,其实在说明白一点就是方法的互相调用而已。这里主要讲解iOS。Android会稍微提一下,仅作参考。

此篇文章的逻辑图

1192353-fd26211d54aea8a9.png

图0-0 此篇文章的逻辑图

概述

iOS原生应用和web页面的交互大致上有这几种方法iOS7之后的JavaScriptCore、拦截协议、第三方框架WebViewJavaScriptBridge、iOS8之后的WKWebView在这里主要讲解JavaScriptCore和拦截协议这两种办法。WebViewJavaScriptBridge是基于拦截协议进行的封装。学习成本相对JavaScriptCore较高,使用也不如JavaScriptCore方便本文不做叙述。WKWebView是iOS8之后推出的,还没有成为主流使用,所以本篇文章也不做详细叙述。

Objective-C执行JavaScript代码

相关方法

1

2

3

4

5

6

// UIWebView的方法

- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;

// JavaScriptCore中JSContext的方法

- (JSValue *)evaluateScript:(NSString *)script;

- (JSValue *)evaluateScript:(NSString *)script withSourceURL:(NSURL *)sourceURL

相关应用

用这些方法去执行大段的JavaScript代码是没什么必要的,但是有些小场景用起来还是比较顺手和实用的,列举两个例子作为参考:

1

2

3

4

5

// 获取当前页面的title

NSString *title = [webview stringByEvaluatingJavaScriptFromString:@"document.title"];

// 获取当前页面的url

NSString *url = [webview stringByEvaluatingJavaScriptFromString:@"document.location.href"];

JavaScriptCore

iOS7之后苹果推出了JavaScriptCore这个框架,从而让web页面和本地原生应用交互起来非常方便,而且使用此框架可以做到Android那边和iOS相对统一,web前端写一套代码就可以适配客户端的两个平台,从而减少了web前端的工作量。

web前端

在三端交互中,web前端要强势一些,一切传值、方法命名都按web前端开发人员来定义,让另外两端去做适配。在这里以调用摄像头和分享为例来详细讲解,测试网页代码取名为test.html,其代码内容如下:

test.html代码内容(因识别问题,用方括号替换了代码中的尖括号)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

[!DOCTYPE html]

[html]

[head]

    [meta charset="UTF-8"]

[/head]

[body]

    [div style="margin-top: 100px"]

        [h1>Objective-C和JavaScript交互的那些事[/h1]

        [input type="button" value="CallCamera" onclick="Toyun.callCamera()"]

    [/div]       

    [div]

        [input type="button" value="Share" onclick="callShare()"]

    [/div]

     

[script]

    var callShare = function() {

        var shareInfo = JSON.stringify({"title""标题""desc""内容""shareUrl""http://www.jianshu.com/p/f896d73c670a",

        "shareIco":"http://upload-images.jianshu.io/upload_images/1192353-fd26211d54aea8a9.png?imageMogr2/auto-orient/strip%7CimageView2/2/w/1240"});

        Toyun.share(shareInfo);

    }

     

    var picCallback = function(photos) {

        alert(photos);

    }

     

    var shareCallback = function(){

        alert('success');

    }

[/script]

[/body]

[/html]

test.html代码解释

可能有些同学对web前端的一些知识不太熟悉,稍微对这段代码做下解释,先说Toyun是iOS和Android这两边在本地要注入的一个对象【参考下面iOS的代码更容易明白】,充当原生应用和web页面之间的一个桥梁。页面上定义了两个按钮名字分别为CallCamera和Share。点击CallCamera会通过Toyun这个桥梁调用本地应用的方法- (void)callCamera,没有传参;而点击Share会先调用本文件中的JavaScript方法callShare这里将要分享的内容格式转成JSON字符串格式(这样做是为了适配Android,iOS可以直接接受JSON对象)然后再通过Toyun这个桥梁去调用原生应用的- (void)share:(NSString *)shareInfo方法这个是有传参的,参数为shareInfo。而下面的两个方法为原生方法调用后的回调方法,其中picCallback为获取图片成功的回调方法,并且传回拿到的图片photos;shareCallback为分享成功的回调方法。

iOS

iOS这边根据前端定义的方法名来写代码,但是有些时候web前端会让我们定义,但是我们定义好之后他又要修改,这时候就会很烦啊。所以碰到三端交互的时候最好就是让web前端去定义方法名,iOS和Android根据web前端定义好的去写代码。JavaScriptCore中web页面调用原生应用的方法可以用Delegate或Block两种方法,此文以按Delegate讲解。

JavaScriptCore中类及协议:

  • JSContext:给JavaScript提供运行的上下文环境

  • JSValue:JavaScript和Objective-C数据和方法的桥梁

  • JSManagedValue:管理数据和方法的类

  • JSVirtualMachine:处理线程相关,使用较少

  • JSExport:这是一个协议,如果采用协议的方法交互,自己定义的协议必须遵守此协议

ViewController中的代码

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

47

48

49

50

51

52

53

54

55

56

#import "ViewController.h"

#import [JavaScriptCore/JavaScriptCore.h](此处为尖括号)

@protocol JSObjcDelegate [JSExport](此处为尖括号)

- (void)callCamera;

- (void)share:(NSString *)shareString;

@end

@interface ViewController () [UIWebViewDelegate, JSObjcDelegate](此处为尖括号)

@property (nonatomic, strong) JSContext *jsContext;

@property (weak, nonatomic) IBOutlet UIWebView *webView;

@end

@implementation ViewController

#pragma mark - Life Circle

- (void)viewDidLoad {

    [super viewDidLoad];

     

    NSURL *url = [[NSBundle mainBundle] URLForResource:@"test" withExtension:@"html"];

    [self.webView loadRequest:[[NSURLRequest alloc] initWithURL:url]];

}

#pragma mark - UIWebViewDelegate

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

    self.jsContext = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];

    self.jsContext[@"Toyun"] = self;

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

        context.exception = exceptionValue;

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

    };

}

#pragma mark - JSObjcDelegate

- (void)callCamera {

    NSLog(@"callCamera");

    // 获取到照片之后在回调js的方法picCallback把图片传出去

    JSValue *picCallback = self.jsContext[@"picCallback"];

    [picCallback callWithArguments:@[@"photos"]];

}

- (void)share:(NSString *)shareString {

    NSLog(@"share:%@", shareString);

    // 分享成功回调js的方法shareCallback

    JSValue *shareCallback = self.jsContext[@"shareCallback"];

    [shareCallback callWithArguments:nil];

}

@end

ViewController中的代码解释

自定义JSObjcDelegate协议,而且此协议必须遵守JSExport这个协议,自定义协议中的方法就是暴露给web页面的方法。在webView加载完毕的时候获取JavaScript运行的上下文环境,然后再注入桥梁对象名为Toyun,承载的对象为self即为此控制器,控制器遵守此自定义协议实现协议中对应的方法。在JavaStript调用完本地应用的方法做完相对应的事情之后,又回调了JavaStript中对应的方法,从而实现了web页面和本地应用之间的通讯。

JavaScriptCore使用注意

JavaStript调用本地方法是在子线程中执行的,这里要根据实际情况考虑线程之间的切换,而在回调JavaScript方法的时候最好是在刚开始调用此方法的线程中去执行那段JavaStript方法的代码,我在实际运用中开始没注意,就被坑惨了啊。什么,说的太绕,看下面的代码解释:

1

2

3

4

5

6

7

8

9

//  假设此方法是在子线程中执行的,线程名sub-thread

- (void)callCamera {     

    // 这句假设要在主线程中执行,线程名main-thread

    NSLog(@"callCamera");

       

    // 下面这两句代码最好还是要在子线程sub-thread中执行啊

    JSValue *picCallback = self.jsContext[@"picCallback"];

    [picCallback callWithArguments:@[@"photos"]];

}

运行效果

运行效果如图3-1所示

1192353-c7968c7ff587cf91.jpg

图3-1 运行效果

拦截协议

拦截协议这个适合一些比较简单的一些情况,不需要引入什么框架,只需要web前端配合一下就好。但是在具体调用哪一个方法上,以及在传值的时候可能会有些不方便,而且调用完后无法在回调JavaScript的方法。

web前端

test.html中的代码(因识别问题,用方括号替换了代码中的尖括号)

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

[!DOCTYPE html]

[html]

[head]

    [meta charset="UTF-8"]

[/head]

[body]

    [div]

        [input type="button" value="CallCamera" onclick="callCamera()"]

    [/div]

     

[script]

    function callCamera() {

        window.location.href = 'toyun://callCamera';

    }

[/script]

[/body]

[/html]

test.html中的代码解释

这段代码相比上面的那段测试代码是很简单的,同样有一个按钮,名字为CallCamera点击之后调用自己的callCamera方法,window.location.href这里是改变主窗口的指向从而马上发出一个链接为Toyun://callCamera请求,而想要传给原生应用的参数也可已包含到此请求中,而在iOS方法中我们要拦截这个请求,根据请求内容去判断JavaStript想要做的事情,从而实现web页面和本地应用之间的交互。

iOS

iOS对应的代码

1

2

3

4

5

6

7

8

9

10

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

{

    NSString *url = request.URL.absoluteString;

    if ([url rangeOfString:@"toyun://"].location != NSNotFound) { 

        // url的协议头是Toyun

        NSLog(@"callCamera");

        return NO;

    }

    return YES;

}

iOS对应的代码的解释

在webView的代理方法中去拦截自定义的协议Toyun://如果是此协议则据此判断JavaStript想要做的事情,调用原生应用的方法,这些都是提前约定好的,同时阻止此链接的跳转。

总结

随着手机硬件的配置越来越强大和HTML5的兴起,一个App完全可以由web页面来写。现在已经有部分应用这么干了,我是遇见过的,如古诗文网。尽管比较少但是web页面和本地应用的交互不论是iOS还是Android都是会有遇到的。iOS我还是比较推荐JavaScriptCore,这样三端可以相对统一起来,写的时候都比较简单。随着时间的推移iOS8推出的WKWebView会逐渐成为主流,这个的功能更强大。拦截协议也只能说用到比较简单的一些情况吧,复杂的情况处理相互之间参数的传递还是比较麻烦的,而且这个不能回调JavaScript的方法,确实喜欢拦截协议的同学可以研究WebViewJavaScriptBridge这个第三方库。对于Android本人也就是略知皮毛而已,就不班门弄斧了,对于一些Android开发者来说,可以看地第一段的test.html这个页面的写法完全是可以适配Android的。

WKWebView使用及注意点(keng)

144 作者 TIME_for 关注

2016.11.19 16:31* 字数 2498 阅读 4996评论 54喜欢 97

iOS8之后,苹果推出了WebKit这个框架,用来替换原有的UIWebView,新的控件优点多多,不一一叙述。由于一直在适配iOS7,就没有去替换,现在仍掉了iOS7,以为很简单的就替换过来了,然而在替换的过程中,却遇到了很多坑。还有一点就是原来写过一篇文章 Objective-C与JavaScript交互的那些事以为年代久远的UIWebView已经作古,可这篇文章现在依然有一定的阅读量。所以在决定在续一篇此文,以引导大家转向WKWebView,并指出自己踩过的坑,让大家少走弯路。

此篇文章的逻辑图

此篇文章的逻辑图

WKWebView使用

WKWebView简单介绍

使用及注意点

WKWebView只能用代码创建,而且自身就支持了右滑返回手势allowsBackForwardNavigationGestures和加载进度estimatedProgress等一些UIWebView不具备却非常好用的属性。在创建的时候,指定初始化方法中要求传入一个WKWebViewConfiguration对象,一般我们使用默认配置就好,但是有些地方是要根据自己的情况去做更改。比如,配置中的allowsInlineMediaPlayback这个属性,默认为NO,如果不做更改,网页中内嵌的视频就无法正常播放。

更改User-Agent

有时我们需要在User-Agent添加一些额外的信息,这时就要更改默认的User-Agent在使用UIWebView的时候,可用如下代码(在使用UIWebView之前执行)全局更改User-Agent

// 获取默认User-Agent
UIWebView *webView = [[UIWebView alloc] initWithFrame:CGRectZero];
NSString *oldAgent = [webView stringByEvaluatingJavaScriptFromString:@"navigator.userAgent"];

// 给User-Agent添加额外的信息
NSString *newAgent = [NSString stringWithFormat:@"%@;%@", oldAgent, @"extra_user_agent"];

// 设置global User-Agent
NSDictionary *dictionnary = [[NSDictionary alloc] initWithObjectsAndKeys:newAgent, @"UserAgent", nil];
[[NSUserDefaults standardUserDefaults] registerDefaults:dictionnary];

以上代码是全局更改User-Agent,也就是说,App内所有的Web请求的User-Agent都被修改。替换为WKWebView后更改全局User-Agent可以继续使用上面的一段代码,或者改为用WKWebView获取默认的User-Agent,代码如下:

self.wkWebView = [[WKWebView alloc] initWithFrame:CGRectZero];

// 获取默认User-Agent
[self.wkWebView evaluateJavaScript:@"navigator.userAgent" completionHandler:^(id result, NSError *error) {
    NSString *oldAgent = result;

    // 给User-Agent添加额外的信息
    NSString *newAgent = [NSString stringWithFormat:@"%@;%@", oldAgent, @"extra_user_agent"];

    // 设置global User-Agent
    NSDictionary *dictionary = [NSDictionary dictionaryWithObjectsAndKeys:newAgent, @"UserAgent", nil];
    [[NSUserDefaults standardUserDefaults] registerDefaults:dictionary];
}];

对比发现,这两种方法并没有本质的区别,一点小区别在于一个是用UIWebView获取的默认User-Agent,一个是用WKWebView获取的默认User-Agent。上面方法的缺点也是很明显的,就是App内所有Web请求的User-Agent全部被修改。

iOS9WKWebView提供了一个非常便捷的属性去更改User-Agent,就是customUserAgent属性。这样使用起来不仅方便,也不会全局更改User-Agent,可惜的是iOS9才有,如果适配iOS8,还是要使用上面的方法。

WKWebView的相关的代理方法

WKWebView的相关的代理方法分别在WKNavigationDelegateWKUIDelegate以及WKScriptMessageHandler这个与JavaScript交互相关的代理方法。

  • WKNavigationDelegate: 此代理方法中除了原有的UIWebView的四个代理方法,还增加了其他的一些方法,具体可参考我下面给出的Demo
  • WKUIDelegate: 此代理方法在使用中最好实现,否则遇到网页alert的时候,如果此代理方法没有实现,则不会出现弹框提示。
  • WKScriptMessageHandler: 此代理方法就是和JavaScript交互相关,具体介绍参考下面的专门讲解。

WKWebView使用过程中的坑

WKWebView下面添加自定义View

因为我们有个需求是在网页下面在添加一个View,用来展示此链接内容的相关评论。在使用UIWebView的时候,做法非常简单粗暴,在UIWebViewScrollView后面添加一个自定义View,然后根据View的高度,在改变一下scrollViewcontentSize属性。以为WKWebView也可以这样简单粗暴的去搞一下,结果却并不是这样。

首先改变WKWebViewscrollViewcontentSize属性,系统会在下一次帧率刷新的时候,再给你改变回原有的,这样这条路就行不通了。我马上想到了另一个办法,改变scrollViewcontentInset这个系统倒不会在变化回原来的,自以为完事大吉。后来过了两天,发现有些页面的部分区域的点击事件无法响应,百思不得其解,最后想到可能是设置的contentInset对其有了影响,事实上正是如此。查来查去,最后找到了一个解决办法是,就是当页面加载完成时,在网页下面拼一个空白的div,高度就是你添加的View的高度,让网页多出一个空白区域,自定义的View就添加在这个空白的区域上面。这样就完美解决了此问题。具体可参考Demo所写,核心代码如下:

self.addView = [[UIView alloc] initWithFrame:CGRectMake(0, 0, ScreenWidth, addViewHeight)];
self.addView.backgroundColor = [UIColor redColor];
[self.webView.scrollView addSubview:self.addView];

NSString *js = [NSString stringWithFormat:@"\
                        var appendDiv = document.getElementById(\"AppAppendDIV\");\
                        if (appendDiv) {\
                        appendDiv.style.height = %@+\"px\";\
                        } else {\
                        var appendDiv = document.createElement(\"div\");\
                        appendDiv.setAttribute(\"id\",\"AppAppendDIV\");\
                        appendDiv.style.width=%@+\"px\";\
                        appendDiv.style.height=%@+\"px\";\
                        document.body.appendChild(appendDiv);\
                        }\
                        ", @(addViewHeight), @(self.webView.scrollView.contentSize.width), @(addViewHeight)];

[self.webView evaluateJavaScript:js completionHandler:nil];

WKWebView加载HTTPS的链接

HTTPS已经越来越被重视,前面我也写过一系列的HTTPS的相关文章HTTPS从原理到应用(四):iOS中HTTPS实际使用当加载一些HTTPS的页面的时候,如果此网站使用的根证书已经内置到了手机中这些HTTPS的链接可以正常的通过验证并正常加载。但是如果使用的证书(一般为自建证书)的根证书并没有内置到手机中,这时是链接是无法正常加载的,必须要做一个权限认证。开始在UIWebView的时候,是把请求存储下来然后使用NSURLConnection去重新发起请求,然后走NSURLConnection的权限认证通道,认证通过后,在使用UIWebView去加载这个请求。

WKWebView中,WKNavigationDelegate中提供了一个权限认证的代理方法,这是权限认证更为便捷。代理方法如下:

- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *credential))completionHandler {
    if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
        if ([challenge previousFailureCount] == 0) {
            NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
            completionHandler(NSURLSessionAuthChallengeUseCredential, credential);
        } else {
            completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
        }
    } else {
        completionHandler(NSURLSessionAuthChallengeCancelAuthenticationChallenge, nil);
    }
}

这个方法比原来UIWebView的认证简单的多。但是使用中却发现了一个很蛋疼的问题,iOS8系统下,自建证书的HTTPS链接,不调用此代理方法。查来查去,原来是一个bug,在iOS9中已经修复,这明显就是不管iOS8的情况了,而且此方法也没有标记在iOS9中使用,这点让我感到有点失望。这样我就又想到了换回原来UIWebView的权限认证方式,但是试来试去,发现也不能使用了。所以关于自建证书的HTTPS链接在iOS8下面使用WKWebView加载,我没有找到很好的办法去解决此问题。这样我不得已有些链接换回了HTTP,或者在iOS8下面在换回UIWebView。如果你有解决办法,也欢迎私信我,感激不尽。

WKWebView加载POST请求

非常感谢@e231e1ff5f8b的指出,原来POST请求这儿还有一个坑。自己项目中并没有这块需求,也就没有发现。加载POST请求的时候,会丢失HTTPBody。解决办法是在网页上开一个JavaScript方法,在请求POST的时候去调用JavaScript这个方法,从而完成POST请求。调用JavaScript方法参考下面交互这一章节。

WKWebView和JavaScript交互

WKWebViewJavaScript交互,在WKUserContentController.h这个头文件中- (void)addScriptMessageHandler:(id <WKScriptMessageHandler>)scriptMessageHandler name:(NSString *)name;这个方法的注释中已经明确给出了交互办法。使用起来倒是非常的简单。创建WKWebView的时候添加交互对象,并让交互对象实现WKScriptMessageHandler中的唯一的一个代理方法。具体的方式参考Demo中的使用。

// 添加交互对象
[config.userContentController addScriptMessageHandler:(id)self.ocjsHelper name:@"timefor"];

/** 此点后来更新,如果不移除交互对象,则导致交互对象内存常驻(2016.12.17) */
// VC销毁时,移除交互对象
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"timefor"];

// 代理方法
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;

JavaScript调用Objective-C的时候,使用window.webkit.messageHandlers.timefor.postMessage({code: '0001', functionName: 'getdevideId'}); Objective-C自动对交互参数包装成了WKScriptMessage对象,其属性body则为传送过来的参数,name为添加交互对象的时候设置的名字,以此名字可以过滤掉不属于自己的交互方法。其中body可以为NSNumber, NSString, NSDate, NSArray, NSDictionary, and NSNull。

Objective-C在回调JavaScript的时候,不能像我原来在 Objective-C与JavaScript交互的那些事这篇文章中写的那样,JavaScript传过来一个匿名函数,Objective-C这边直接调用一下就完事。WKWebView没有办法传过来一个匿名函数,所以回调方式,要么执行一段JavaScript代码,或者就是调用JavaScript那边的一个全局函数。一般是采用后者,至于Web端虽说暴露了一个全局函数,同样可以把这一点代码处理的很优雅。Objective-C传给JavaScript的参数,可以为Number, String, and Object。参考如下:

// 数字
NSString *js = [NSString stringWithFormat:@"globalCallback(%@)", number];
[self.webView evaluateJavaScript:js completionHandler:nil];
// 字符串
NSString *js = [NSString stringWithFormat:@"globalCallback(\'%@\')", string];
[self.webView evaluateJavaScript:js completionHandler:nil];
// 对象
NSString *js = [NSString stringWithFormat:@"globalCallback(%@)", @{@"name" : @"timefor"}];
[self.webView evaluateJavaScript:js completionHandler:nil];
// 带返回值的JS函数
[self.webView evaluateJavaScript:@"globalCallback()" completionHandler:^(id result, NSError * _Nullable error) {
    // 接受返回的参数,result中
}];

总结

此文主要介绍了WKWebView使用中的注意点,一般也都是常用的,还有缓存等一些不是太常用的就没有具体介绍。如果在其他方面遇到问题,也欢迎你私信我共同探讨进步。WKWebView确实比UIWebView有些地方好用不少,但是一些bug至今也没解决,权限挑战是在iOS9解决的,POST请求则至今没有解决,而改变contentInset导致的点击事件不准确,同样是没有解决。这些问题让开发者使用起来,有诸多不便啊。
此文的Demo地址:WKWebViewDemo 如果此文对你有所帮助,请给个star吧。

猜你喜欢

转载自my.oschina.net/u/2329800/blog/1021780