本章主要介绍oc与js交互,分为UIWebView和WKWebView–案例demo
UIWebView
UIWebView是比较早的webView了,在ios8推出的WKWebView来代替它,且流畅度要高一些,且ios12已经抛弃了UIWebView,因此现在使用webView直接使用WKWebView即可,可是一些老代码的需要适配到新的,或者仍然在ios12之前继续沿用UIWebView的,可以看下面的UIWebView介绍
可以参考demo中的WebViewController文件
UIWebView的基本功能
通过下面几行代码就可以加载一个webView了
self.webView = [[UIWebView alloc] initWithFrame:self.view.frame];
NSURL *url = [[NSBundle mainBundle] URLForResource:@"index.html" withExtension:nil];
[self.webView loadRequest:[NSURLRequest requestWithURL:url]];
self.webView.delegate = self;
[self.view addSubview:self.webView];
复制代码
UIWebView协议与js、OC互相调用
通过实现webView的协议UIWebViewDelegate,可以检测和拦截webView的一些事件
shouldStartLoadWithRequest方法可以直接拦截路由,当点击路由事件的时候可以直接拦截,并在oc中执行oc的函数方法,即可以实现用js路由调用oc方法,如下所示
路由的如下所示,可以参考demo文件中的html文件
<a href="marshal://getSum/helloword/js">点击跳转效应OC方法</a>
//是否加载相应内容,可以在里面进行拦截相关信息
- (BOOL)webView:(UIWebView *)webView
shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType {
NSLog(@"%@", request);
NSURL *url = request.URL; //url
NSArray *pathComponents = url.pathComponents; //地址集合
NSString *scheme = url.scheme; //scheme 开头的标识
if ([scheme isEqualToString:@"marshal"]) {
//可以设置路由,根据路由响应拦截内容
//这样就实现了js调用oc方法,传递参数可以写到后面
//NSLog(@"%@", url.port);
if ([url.host isEqualToString:@"getSum"]) {
//通过拦截路由调用oc方法
[self getSum];
}else if ([url.host isEqualToString:@"getMessage"]) {
[self getMessage:webView];
}
return NO;
}
return true;
}
- (void)getSum {
NSLog(@"getSum被调用了");
}
- (void)getMessage:(UIWebView *)webView {
NSLog(@"getMessage被调用了");
通过oc调用js的方法
[webView stringByEvaluatingJavaScriptFromString:@"handleMessage()"];
}
//开始加载
- (void)webViewDidStartLoad:(UIWebView *)webView {
NSLog(@"开始加载");
}
//加载完毕,可以在加载成功后计算网页高度,等
- (void)webViewDidFinishLoad:(UIWebView *)webView {
NSLog(@"加载完毕");
// [self callJSMethod: webView];
[self callJSMethodByJSCore];
}
//加载失败
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error {
NSLog(@"加载失败");
}
复制代码
而OC调用js的方法也很简单,通过webView调用stringByEvaluatingJavaScriptFromString即可
[webView stringByEvaluatingJavaScriptFromString:@"handleMessage()"];
复制代码
JavaScriptCore实现js和oc交互
使用JavaScriptCore框架,可以获取到JSContext对象
获取后,可以使用JSContext对象的evaluateScript方法直接调用js函数或者执行一些js脚本代码(例如:声明一些基本对象)
通过JSContext对象,通过下面方法jsContext[@”showMessage”]这种方式,可以直接声明出js可以直接调用的方法,可以理解为于在js中声明了该方法,在js中可以将showMessage当做其函数直接调用
- (void)callJSMethodByJSCore {
//获取网页标题
NSString *naviTitle = [self.webView
stringByEvaluatingJavaScriptFromString:@"document.title"];
NSLog(@"naviTitle : %@", naviTitle);
//获取js的上下文对象
JSContext *jsContext = [self.webView
valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//根据上下文对象执行js代码,声明数组对象(var变量提升)
[jsContext evaluateScript:@"var arr = ['帅仔', '靓仔', '酷仔'];"];
//直接调用js中的方法
//[jsContext evaluateScript:@"showAlert('测试')('帅呆了')"];
//桥接js,保存handleMessage函数
//注意:如果功能不同,不要和js中的方法名字一样,会覆盖js中的方法(这里会覆盖掉js中的showMessage方法)
jsContext[@"showMessage"] = ^(id value){
NSLog(@"展示完毕信息了: %@", value);
//可以查看变量参数
NSArray *args = [JSContext currentArguments];
NSLog(@"args = %@",args);
};
}
复制代码
以上便实现了js和oc之间的交互,WebViewJavascriptBridge后续在介绍
WKWebView
WKWebView作为现在的主流WebView显示工具,我们的交互离不开他,下面介绍一下他的基本使用,以及与js的交互
可以参考demo中的TestWKWebViewController文件
**注意:**WKWebView会出现cookie丢失问题,可以参考cookie解决方案
WKWebView的基本功能
WKWebView和UIWebView不一样,如果不加上面前的一段脚本,则页面大小会非常难受(字体等很小),加上后和UIWebView一样了
//不加上webView显示大小有问题
NSString *jScript = @"var meta = document.createElement('meta'); meta.setAttribute('name', 'viewport'); meta.setAttribute('content', 'width=device-width'); document.getElementsByTagName('head')[0].appendChild(meta);";
WKUserScript *wkUScript = [[WKUserScript alloc] initWithSource:jScript injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:YES];
WKUserContentController *wkUController = [[WKUserContentController alloc] init];
[wkUController addUserScript:wkUScript];
WKWebViewConfiguration *wkWebConfig = [[WKWebViewConfiguration alloc] init];
wkWebConfig.userContentController = wkUController;
_wkWebView = [[WKWebView alloc]initWithFrame:self.view.frame configuration:wkWebConfig];
_wkWebView.UIDelegate = self;
_wkWebView.navigationDelegate = self;
//使用kvo监听进度
// [_wkWebView addObserver:self forKeyPath:NSStringFromSelector(@selector(estimatedProgress)) options:NSKeyValueObservingOptionNew context:nil];
//手势触摸滑动
_wkWebView.allowsBackForwardNavigationGestures = YES;
NSURL *url = [[NSBundle mainBundle] URLForResource:@"index.html" withExtension:nil];
NSURLRequest *request = [NSURLRequest requestWithURL:url cachePolicy:NSURLRequestUseProtocolCachePolicy timeoutInterval:15];
[_wkWebView loadRequest:request];
[self.view addSubview:_wkWebView];
复制代码
WKWebView协议
若想很好的使用WKWebView,则需要实现WKUIDelegate, WKNavigationDelegate两个协议
注意:在WKWebView中WKUIDelegate中下面的协议如果不实现,则js无法alert默认的弹窗
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提醒" message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"知道了" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
completionHandler();
}]];
[self presentViewController:alert animated:YES completion:nil];
}
复制代码
NaivigationDelegate协议
这个协议和UIWebView的协议差不多,使用起来也类似,如下所示
//请求之前,决定是否要跳转:用户点击网页上的链接,需要打开新页面时,将先调用这个方法,跟webView的拦截一样
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSURL *url = navigationAction.request.URL; //url
NSArray *pathComponents = url.pathComponents; //地址集合
NSString *scheme = url.scheme; //scheme 开头的标识
if ([scheme isEqualToString:@"marshal"]) {
//可以设置路由,根据路由响应拦截内容
//这样就实现了js调用oc方法,传递参数可以写到后面
//NSLog(@"%@", url.port);
if ([url.host isEqualToString:@"getSum"]) {
[self getSum];
}else if ([url.host isEqualToString:@"getMessage"]) {
}else if ([url.host isEqualToString:@"enterWk"]) {
}
decisionHandler(WKNavigationActionPolicyCancel);
}else {
decisionHandler(WKNavigationActionPolicyAllow);
}
}
复制代码
下面的协议使用场景和UIWebView差不多,可以根据实际使用场景实现调用,用的比较多的可就是finish和fail
//接收到相应数据后,决定是否跳转
//- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
////页面开始加载时调用
//- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
//// 主机地址被重定向时调用
//- (void)webView:(WKWebView *)webView didReceiveServerRedirectForProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
//// 页面加载失败时调用
//- (void)webView:(WKWebView *)webView didFailProvisionalNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
//// 当内容开始返回时调用
//- (void)webView:(WKWebView *)webView didCommitNavigation:(null_unspecified WKNavigation *)navigation;
//// 页面加载完毕时调用
//- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;
////跳转失败时调用
//- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
//// 如果需要证书验证,与使用AFN进行HTTPS证书验证是一样的
//- (void)webView:(WKWebView *)webView didReceiveAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge completionHandler:(void (^)(NSURLSessionAuthChallengeDisposition disposition, NSURLCredential *__nullable credential))completionHandler;
////9.0才能使用,web内容处理中断时会触发
//- (void)webViewWebContentProcessDidTerminate:(WKWebView *)webView NS_AVAILABLE(10_11, 9_0);
复制代码
WKWebView实现oc和js交互
默认js调用oc和webView相似,在NaivigationDelegate中的协议中根据路由拦截方法,从而调用oc的方法即可,参考上面的decidePolicyForNavigationAction协议
而oc调动js调用了下面的方法即可,可以直接调用函数和声明变量等
[self.wkWebView evaluateJavaScript:@"var arr = ['帅仔', '靓仔', '酷仔'];"
completionHandler:^(id _Nullable returnObject, NSError * _Nullable error) {
//returnObject 返回值内容
if (error) {
NSLog(@"%@", error.localizedDescription); //错误信息
}
}];
[self.wkWebView evaluateJavaScript:@"showAlert('你真的')('帅呆了')"
completionHandler:^(id _Nullable returnObject, NSError * _Nullable error) {
//returnObject 返回值内容
if (error) {
NSLog(@"%@", error.localizedDescription); //错误信息
}
}];
复制代码
通过MessageHandler来实现oc和js交互
使用MessageHandle的时候,需要注册Messagehandle,需要合适的时机移除掉,和使用Timer类似
//在oc中注册MessageHandle
[self.wkWebView.configuration.userContentController
addScriptMessageHandler:self name:@"OCHandleMessage"];
注意添加和移除handle需要成对出现,不然会出现内存泄漏
//移除handler,不移除会内存泄露,可以找一个合适的时机直接移除掉handle
[self.wkWebView.configuration.userContentController
removeScriptMessageHandlerForName:@"OCHandleMessage"];
复制代码
oc调用js的方法,如下所示
[self.wkWebView evaluateJavaScript:@"showMessage()"
completionHandler:^(id _Nullable returnObject, NSError * _Nullable error) {
}];
复制代码
还需要实现WKScriptMessageHandler的协议,来实现交互
//可以在这里接收js通过handle传递过来的数据
- (void)userContentController:(WKUserContentController *)userContentController
didReceiveScriptMessage:(WKScriptMessage *)message {
// NSLog(@"%@ -- %@", message.name, message.body);
//如果执行完毕要反馈可以反调用js中的方法
[self.wkWebView evaluateJavaScript:@"showMessage()"
completionHandler:^(id _Nullable returnObject, NSError * _Nullable error) {
}];
}
复制代码
如上所示,通过addScriptMessageHandler方法设置了handler,名字为OCHandleMessage(也可以设置为其他的,就是一个交互设置的中介对象名字,可以设置多个)
设置完成之后,可以在js中通过这个名字直接往oc中传递消息,如果想区分消息,可以通过切换Messagehandle或者添加标签来实现消息区分
通过下面的方法传递过来的参数,会走到上面的userContentController协议中,内容在message中
//js中的方法调用
function messageHandle(){
//通过handle往oc中发送消息,OCHandleMessage是设置的handle名字
window.webkit.messageHandlers.OCHandleMessage.postMessage("messageHandle消息");
}
复制代码
这样就实现了oc和js的交互
使用WebViewJavascriptBridge实现oc和js交互
WebViewJavascriptBridge为桥接UIWebView和WKWebView交互的一个组件,在使用上非常方便,当然现在只使用wkWebView的可以直接抛弃之以减少代码大小,使用习惯或觉得好用可以继续沿用
其原理就是WebViewJavascriptBridge作为中间的桥接者,负责桥接翻译两部分的数据交互
直接用podfile中加入WebViewJavascriptBridge,install即可
pod 'WebViewJavascriptBridge'
复制代码
使用前在oc中声明桥接对象,然后桥接webView(UIWebView和wkWebView),类似于WkWekView总的Messagehandler
@property (nonatomic, strong) WebViewJavascriptBridge *wjb;
self.wjb = [WebViewJavascriptBridge bridgeForWebView:self.webView];
// 如果你要在VC中实现UIWebView的代理方法,就设置代理,否则省略
[self.wjb setWebViewDelegate:self];
[self.wjb registerHandler:@"jsCallsOC" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"data == %@ -- %@",data,responseCallback);
}];
复制代码
使用WebViewJavascriptBridge注册函数和调用js中的函数如下所示
//设置给js调用的handler,方便调用
- (void)registerJShandle {
[self.wjb registerHandler:@"jsCallsOC" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"data == %@ -- %@",data,responseCallback);
responseCallback(@"测试数据"); //回调数据response
}];
[self.wjb registerHandler:@"jsCallsOC1" handler:^(id data, WVJBResponseCallback responseCallback) {
NSLog(@"data == %@ -- %@",data,responseCallback);
responseCallback(@"测试数据"); //回调数据response
}];
}
//调用js中的函数中注册的handle
- (void)callJSHandle {
[self.wjb callHandler:@"OCCallJS" data:@"测试数据哈" responseCallback:^(id responseData) {
NSLog(@"responseData == %@",responseData);
}];
[self.wjb callHandler:@"OCCallJS1" data:@"测试数据哈" responseCallback:^(id responseData) {
NSLog(@"responseData == %@",responseData);
}];
}
复制代码
而js中类似,需要实现如下代码即可
js中通过bridge注册和调用handler来实现交互,且不用区分UIWebView和WKWebView
//互相调用要加入这个方法
function setupWebViewJavascriptBridge(callback) {
if (window.WebViewJavascriptBridge) { return callback(WebViewJavascriptBridge); }
if (window.WVJBCallbacks) { return window.WVJBCallbacks.push(callback); }
window.WVJBCallbacks = [callback];
var WVJBIframe = document.createElement('iframe');
WVJBIframe.style.display = 'none';
WVJBIframe.src = 'wvjbscheme://__BRIDGE_LOADED__';
document.documentElement.appendChild(WVJBIframe);
setTimeout(function() { document.documentElement.removeChild(WVJBIframe) }, 0)
}
setupWebViewJavascriptBridge(function(bridge) {
// 注册JS被调用的handle,用于oc调用
bridge.registerHandler('OCCallJS', function(data, responseCallback) {
alert('OCCallJS方法被调用:'+data);
responseCallback('js执行过了');
})
bridge.registerHandler('OCCallJS1', function(data, responseCallback) {
alert('OCCallJS1方法被调用:'+data);
responseCallback('js执行过了');
})
//oc调用js中注册的handle,可以传递接收参数
bridge.callHandler('jsCallsOC', @"测试数据哈", function(response) {
alert(response);
})
bridge.callHandler('jsCallsOC1', @"测试数据哈", function(response) {
alert(response);
})
})
复制代码
WebView中的cookie问题
下面的cookie介绍,参考自此文章
Cookie,有时也用其复数形式 Cookies,指某些网站为了辨别用户身份、进行 session 跟踪而储存在用户本地终端上的数据(通常经过加密)。
UIWebView的cookie
UIWebView的cookie一般是由[NSHTTPCookieStorage sharedHTTPCookieStorage]这个单例来管理的,UIWebView会自动同步单例中的Cookie。特殊情况要通过添加Cookie区分的时候可以通过以下几种方式来实现
添加header实现
NSMutableURLRequest *request = [NSMutableURLRequest
requestWithURL:[NSURL URLWithString:@"http://www.LynkCo.com"]];
[request addValue:@"cookieName=123456;" forHTTPHeaderField:@"Set-Cookie"];
[self.webView loadRequest:request];
复制代码
通过操作NSHTTPCookieStorage添加一个自定义的Cookie
NSHTTPCookie *cookie = [NSHTTPCookie
cookieWithProperties:@{
NSHTTPCookieName: @"cookieName",
NSHTTPCookieDomain: @".juejin.cn",
NSHTTPCookiePath: @"/"
NSHTTPCookieValue: @"123456",
}];
Cookie会覆盖,不覆盖则可以
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
复制代码
读取所有Cookie,Cookie转换成HTTPHeaderFields,并添加到request的header中
NSArray *cookies = [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies;
//Cookies数组转换为requestHeaderFields
NSDictionary *requestHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
//设置请求头
request.allHTTPHeaderFields = requestHeaderFields
复制代码
WKWebView的cookie丢失问题
WKWebView加载网页得到的Cookie会同步到NSHTTPCookieStorage中。
WKWebView加载请求时,不会同步NSHTTPCookieStorage中已有的Cookie。
通过共用一个WKProcessPool并不能解决Cookie同步问题,且可能会造成Cookie丢失。
WLWebViKw首次加载Cookie不成功的问题
在请求头中添加cookie,这样的话只要保证 [NSHTTPCookieStorage sharedHTTPCookieStorage] 中存在你的cookie,第一次请求就不会有问题了。
NSMutableURLRequest *request = [NSMutableURLRequest
requestWithURL:[NSURL URLWithString:@"http://www.juejin.cn"]];
NSArray *cookies = [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies;
//Cookies数组转换为requestHeaderFields
NSDictionary *requestHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
//设置请求头
request.allHTTPHeaderFields = requestHeaderFields;
[self.webView loadRequest:request];
复制代码
ajax请求cookie丢失的问题
只需要通过添加WKUserScript就可以了,只要保证sharedHTTPCookieStorage中你的Cookie存在,后续Ajax请求就不会有问题。
更新webView的cookie
- (void)updateWebViewCookie
{
WKUserScript * cookieScript = [[WKUserScript alloc] initWithSource:[self cookieString]
injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
//添加Cookie
[self.configuration.userContentController addUserScript:cookieScript];
}
- (NSString *)cookieString
{
NSMutableString *script = [NSMutableString string];
[script appendString:
@"var cookieNames = document.cookie.split('; ').map(function(cookie) { return cookie.split('=')[0] } );\n"];
for (NSHTTPCookie *cookie in [[NSHTTPCookieStorage sharedHTTPCookieStorage] cookies]) {
// Skip cookies that will break our script
if ([cookie.value rangeOfString:@"'"].location != NSNotFound) {
continue;
}
// Create a line that appends this cookie to the web view's document's cookies
[script appendFormat:
@"if (cookieNames.indexOf('%@') == -1) { document.cookie='%@'; };\n",
cookie.name, cookie.da_javascriptString];
}
return script;
}
复制代码
跳转页面cookie丢失问题
修复打开链接Cookie丢失问题
- (NSURLRequest *)fixRequest:(NSURLRequest *)request
{
NSMutableURLRequest *fixedRequest;
if ([request isKindOfClass:[NSMutableURLRequest class]]) {
fixedRequest = (NSMutableURLRequest *)request;
} else {
fixedRequest = request.mutableCopy;
}
//防止Cookie丢失
NSDictionary *dict = [NSHTTPCookie
requestHeaderFieldsWithCookies:[
NSHTTPCookieStorage sharedHTTPCookieStorage].cookies];
if (dict.count) {
NSMutableDictionary *mDict = request.allHTTPHeaderFields.mutableCopy;
[mDict setValuesForKeysWithDictionary:dict];
fixedRequest.allHTTPHeaderFields = mDict;
}
return fixedRequest;
}
#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView
decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction
decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
#warning important 这里很重要
//解决Cookie丢失问题
NSURLRequest *originalRequest = navigationAction.request;
[self fixRequest:originalRequest];
//如果originalRequest就是NSMutableURLRequest, originalRequest中已添加必要的Cookie,可以跳转
//允许跳转
decisionHandler(WKNavigationActionPolicyAllow);
//可能有小伙伴,会说如果originalRequest是NSURLRequest,不可变,那不就添加不了Cookie了,是的,我们不能因为这个问题,不允许跳转,也不能在不允许跳转之后用loadRequest加载fixedRequest,否则会出现死循环,具体的,可以用本地的html测试下
NSLog(@"%@", NSStringFromSelector(_cmd));
}
#pragma mark - WKUIDelegate
- (WKWebView *)webView:(WKWebView *)webView
createWebViewWithConfiguration:(WKWebViewConfiguration *)configuration
forNavigationAction:(WKNavigationAction *)navigationAction
windowFeatures:(WKWindowFeatures *)windowFeatures {
#warning important 这里也很重要
//这里不打开新窗口
[self.webView loadRequest:[self fixRequest:navigationAction.request]];
return nil;
}
复制代码