js ios 交互

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

此篇文章的逻辑图前端

1192353-fd26211d54aea8a9.png

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

概述git

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

Objective-C执行JavaScript代码web

相关方法缓存

 

1微信

2app

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吧。

相关文章
相关标签/搜索