参考文章WKWebView官方文档javascript
相信不少公司的app都有网页的嵌入吧,原生APP和JS交互的方式有UIWebView、WKWebView、Cordava、Weex、Flutter、Reactive Native等,咱们目前比较经常使用的是WKWebView,可是本篇文章我准备讲解一下UIWebView和WKWebView和JS的交互。html
UIWebView继承自UIView,是iOS内置的浏览器控件,能够浏览网页、打开文档等。可以加载html、htm、pdf、docx、txt等格式的文件。 iOS8,苹果新推出了WebKit,用WKWebView代替UIWebView和WebView。相关的使用和特性能够细读。性能、稳定性、功能大幅度提高 容许JavaScript的Nitro库加载并使用(UIWebView中限制)、支持了更多的HTML5特性、高达60fps的滚动刷新率以及内置手势、GPU硬件加速、KVO、重构UIWebView成14类与3个协议。java
WKWebView是现代WebKit API在iOS8和OS X Yosemite应用中的核心部分。它代替了UIKit的UIWebView和APPKit中的WebView,提供了统一的跨双平台API,目前主要使用WKWebView。ios
初始化一个UIWebView,并调用UIWebView网页加载展现的方法,并实现UIWebView的代理方法,基本上就能够实现网页加载的功能了。git
一、//使用 NSURLRequest 的方式加载网页
- (void)loadRequest:(NSURLRequest *)request;
二、/*
功能:加载HTML字符串
string为要加载的本地HTML字符串
baseURL用来肯定htmlString的基准地址,至关于HTML的<base>标签的做用,定义页面中全部连接的默认地址
*/
- (void)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
三、- (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType
textEncodingName:(NSString *)textEncodingName baseURL:(NSURL *)baseURL;
复制代码
//是否容许加载网页,也可获取js要打开的url,经过截取此url可与js交互
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request
navigationType:(UIWebViewNavigationType)navigationType;
//开始加载网页
- (void)webViewDidStartLoad:(UIWebView *)webView;
//网页加载完成
- (void)webViewDidFinishLoad:(UIWebView *)webView;
//网页加载错误
- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error;
复制代码
是一个比较经常使用的方法,使用起来比较简单直接,直接调用- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;就能够了,示例以下:github
self.title = [self.webView stringByEvaluatingJavaScriptFromString:@"document.title"];
复制代码
这个方法虽然简单,可是有不少缺点:web
对于以上的缺点,能够经过使用JavaScriptCore(iOS 7.0 +)来解决。ajax
JSPatch 最近被苹果禁止在appStore中的应用中使用, JSPatch中最核心的是JavaScriptCore,由于JavaScriptCore的JS到OC的映射,能够将js方法替换成为oc方法,因此其动态性(配合runtime的不安全性)也就成为了JSPatch被Apple禁掉的最主要缘由。这里讲下UIWebView经过JavaScriptCore来实现OC调用JS。其实WebKit都有一个内嵌的js环境,通常咱们在页面加载完成以后,获取js上下文,而后经过JSContext的evaluateScript:方法来获取返回值。由于该方法获得的是一个JSValue对象,因此支持JavaScript的Array、对象等数据类型。macos
用法以下:数组
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
JSValue *value = [context evaluateScript:@"document.title"];
self.title = value.toString;
复制代码
假设咱们执行了一个不存在的方法的话,会出现什么样的状况呢?好比getSize方法
[self.context evaluateScript:@"document.getSize"];
复制代码
结果能够知道程序报错,咱们能够经过@property (copy) void(^exceptionHandler)(JSContext *context, JSValue *exception);,设置exceptionHandler来获取异常。这个方法也很好的解决了程序出现异常以后捕获不到异常信息的状况,用法以下:
//在调用不存在的方法getSize前,先设置异常回调
[self.context setExceptionHandler:^(JSContext *context, JSValue *exception){
NSLog(@"程序异常为:%@", exception);
}];
//执行getSize方法
JSValue *value = [self.context evaluateScript:@"document.getSize"];
复制代码
假若有一个使用短信验证码登陆的功能,html或者js中的方法名为mobileCode,点击使用短信验证码登陆按钮的时候会捕捉下面的连接,解析出所需的参数,从而实现JS 调用OC。
<a href="mobileCode://smsLogin?username=13678946758&code=122786">使用短信验证码登陆</a>
复制代码
OC代码中,当打开了一个连接,webView会经过代理方法捕捉到连接,而且返回NO,从而能够实现咱们的OC方法。捕捉不到的话就返回YES,继续跳转到html页面。
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSURL *URL = request.URL;
if ([URL.scheme isEqualToString:@"mobileCode"]) {
if ([URL.host isEqualToString:@"smsLogin"]) {
NSLog(@"使用短信验证码登陆,参数为 %@", URL.query);
return NO;
}
}
return YES;
}
复制代码
首先咱们在js文件中定义了一个share方法
function share (title, content, imageUrl, url) {
//使用WKWebView测试
window.webkit.messageHandlers.share.postMessage({title: title, content: content, imageUrl: imageUrl, url: url});
//OC实现代码
}
复制代码
html中实现一个a标签调用share方法
<a href="javascript:void(0);" class="sharebtn" onclick="share('领取话费','分享连接给你的微信号又或者qq好友,便可领取1元话费' 'https://timgsa.baidu.com/timg?image&quality=80&size=b9999_10000&sec=1566203866173&di=7a3035ce1c25fb6f1003ca2eeca7f2cd&imgtype=0&src=http%3A%2F%2Fimg1.juimg.com%2F180405%2F355858-1P40511025273.jpg', location.href)">分享领话费</a>
复制代码
咱们在OC中的实现以下
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
// self.title = [self.title stringByAppendingString:[webView stringByEvaluatingJavaScriptFromString:@"document.title"]];
//获取该UIWebView的javascript上下文
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//这也是一种获取标题的方法。
JSValue *value = [context evaluateScript:@"document.title"];
//更新标题
self.title = value.toString;
[self convertJSToOCMethod];
}
#pragma mark - 将JS的函数转换成OC的方法
- (void)convertJSToOCMethod
{
//获取该UIWebview的javascript上下文
//self持有context
//@property (nonatomic, strong) context *context;
self.context = [self.webView valueForKeyPath:@"context"];
//context oc调用js
//JSValue *value = [self.context evaluateScript:@"document.title"];
//js调用oc
//其中share就是js的方法名称,赋给是一个block,block中是oc代码
//此方法最终将打印出全部接收到的参数,js参数是不固定的
self.context[@"share"] = ^() {
//获取到share方法里的全部参数array
NSArray *array = [JSContext currentArguments];
//array中的元素JSValue对象转换为OC对象
NSMutableArray *messages = [NSMutableArray array];
for (JSValue *value in array) {
[messages addObject:[value toObject]];
}
NSLog(@"点击分享按钮js传回的参数以下:\n%@", messages);
};
复制代码
点击html中的分享领话费按钮会在控制台打印出传递参数
Cookie,有时也用其复数形式 Cookies,指某些网站为了辨别用户身份、进行 session 跟踪而储存在用户本地终端上的数据(一般通过加密)。 UIWebView的cookie通常是由[NSHTTPCookieStorage sharedHTTPCookieStorage]这个单例来管理的,UIWebView会自动同步单例中的Cookie。特殊状况要经过添加Cookie区分的时候能够经过如下几种方式来实现
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.LynkCo.com"]];
[request addValue:@"cookitnmae=78965420;" forHTTPHeaderField:@"Set-Cookie"];
[self.webView loadRequest:request];
复制代码
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:@{
NSHTTPCookieName: @"cookieNmae",
NSHTTPCookieDomain: @".LynkCo.com",
NSHTTPCookiePath: @"/"
NSHTTPCookieValue: @"78965420",
}];
//Cookie存在则覆盖,不存在添加
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];
复制代码
NSArray *cookies = [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies;
//Cookies数组转换为requestHeaderFields
NSDictionary *requestHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
//设置请求头
request.allHTTPHeaderFields = requestHeaderFields;
复制代码
总体的初始化示例
- (void)createWebView
{
WKWebViewConfiguration *config = [[WKWebViewConfiguration alloc] init]
WKUserContentController *controller = [[WKUserContentController alloc] init];
config.userContentController = controller;
// 根据须要去设置对应的属性
WKWebView *webView = [[WKWebView alloc] initWithFrame:self.view.bounds configuration:config];
webView.navigationDelegate = self;
[self.view addSubview:webView];
NSURL *url = [NSURL URLWithString:self.strURL];
[self loadWebViewWithURL:url]; // JS调用OC 添加处理脚本
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"Share"];
}
复制代码
经常使用建立方法
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration NS_DESIGNATED_INITIALIZER;
- (nullable instancetype)initWithCoder:(NSCoder *)coder NS_DESIGNATED_INITIALIZER;
复制代码
介绍如下WKWebViewConfiguration中的两个比较重要的属性
WKWebView的一些缓存存储在websiteDataStore中,修改缓存能够经过WKWebsiteDataStore.h中提供的方法,事实上咱们用的比较少,通常状况下清除缓存能够经过删除沙盒目录中的Cache文件。
js和oc的交互以及动态注入js会用到这个属性。
// 导航代理
@property (nullable, nonatomic, weak) id <WKNavigationDelegate> navigationDelegate;
// UI代理
@property (nullable, nonatomic, weak) id <WKUIDelegate> UIDelegate;
// 页面标题, 通常使用KVO动态获取
@property (nullable, nonatomic, readonly, copy) NSString *title;
// 页面加载进度, 通常使用KVO动态获取
@property (nonatomic, readonly) double estimatedProgress;
// 可返回的页面列表, 已打开过的网页, 有点相似于navigationController的viewControllers属性
@property (nonatomic, readonly, strong) WKBackForwardList *backForwardList;
// 页面url
@property (nullable, nonatomic, readonly, copy) NSURL *URL;
// 页面是否在加载中
@property (nonatomic, readonly, getter=isLoading) BOOL loading;
// 是否可返回
@property (nonatomic, readonly) BOOL canGoBack;
// 是否可向前
@property (nonatomic, readonly) BOOL canGoForward;
// WKWebView继承自UIView, 因此若是想设置scrollView的一些属性, 须要对此属性进行配置
@property (nonatomic, readonly, strong) UIScrollView *scrollView;
// 是否容许手势左滑返回上一级, 相似导航控制的左滑返回
@property (nonatomic) BOOL allowsBackForwardNavigationGestures;
//自定义UserAgent, 会覆盖默认的值 ,iOS 9以后有效
@property (nullable, nonatomic, copy) NSString *customUserAgent
复制代码
// 带配置信息的初始化方法
// configuration 配置信息
- (instancetype)initWithFrame:(CGRect)frame configuration:(WKWebViewConfiguration *)configuration
// 加载请求
- (nullable WKNavigation *)loadRequest:(NSURLRequest *)request;
// 加载HTML
- (nullable WKNavigation *)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
// 返回上一级
- (nullable WKNavigation *)goBack;
// 前进下一级, 须要曾经打开过, 才能前进
- (nullable WKNavigation *)goForward;
// 刷新页面
- (nullable WKNavigation *)reload;
// 根据缓存有效期来刷新页面
- (nullable WKNavigation *)reloadFromOrigin;
// 中止加载页面
- (void)stopLoading;
// 执行JavaScript代码
- (void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id, NSError * _Nullable error))completionHandler;
复制代码
示例代码以下
/* 第一步:经过给userContentController添加WKUserScript,能够实现动态注入js。好比我先注入一个脚本,给每一个页面添加一个Cookie */
//添加自定义的cookie
WKUserScript *newCookieScript = [[WKUserScript alloc] initWithSource:@" document.cookie = 'LynkcoCookie=Lynkco;'" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:NO];
//添加脚本
[controller addUserScript:newCookieScript];
/* 第二步骤:注入一个脚本,每当页面加载,就会alert当前页面cookie,在OC中的实现 */
//建立脚本
WKUserScript *cookieScript = [[WKUserScript alloc] initWithSource:@"alert(document.cookie);" injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO];
//添加脚本
[controller addUserScript:script];
复制代码
注入的js 资源能够是js字符串,也能够是js文件,好比咱们要注入一个js文件ImageClickEvent
/**
页面中的全部img标签添加点击事件
*/
- (void)imgAddClickEvent
{
//防止频繁IO操做,形成性能影响
static NSString *jsSource;
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
jsSource = [NSString stringWithContentsOfFile:[[NSBundle mainBundle] pathForResource:@"ImageClickEvent" ofType:@"js"] encoding:NSUTF8StringEncoding error:nil];
});
//添加自定义的脚本
WKUserScript *js = [[WKUserScript alloc] initWithSource:jsSource injectionTime:WKUserScriptInjectionTimeAtDocumentEnd forMainFrameOnly:NO];
[self.webView.configuration.userContentController addUserScript:js];
//注册回调
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"imageDidClick"];
}
复制代码
加载的方法一般有如下几种
[self.webView loadRequest:[NSURLRequest requestWithURL:[NSURL fileURLWithPath:[[NSBundle mainBundle] pathForResource:@"test" ofType:@"html"]]]];
@protocol WKNavigationDelegate; //相似于UIWebView的加载成功、失败、是否容许跳转等
@protocol WKUIDelegate; //主要是一些alert、打开新窗口之类的
如下是WKNavigationDelgate的一些协议方法
//下面这2个方法共同对应了UIWebView的 - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
//先:针对一次action来决定是否容许跳转,action中能够获取request,容许与否都须要调用decisionHandler,好比decisionHandler(WKNavigationActionPolicyCancel);
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
//后:根据response来决定,是否容许跳转,容许与否都须要调用decisionHandler,如decisionHandler(WKNavigationResponsePolicyAllow);
- (void)webView:(WKWebView *)webView decidePolicyForNavigationResponse:(WKNavigationResponse *)navigationResponse decisionHandler:(void (^)(WKNavigationResponsePolicy))decisionHandler;
//开始加载,对应UIWebView的- (void)webViewDidStartLoad:(UIWebView *)webView;
- (void)webView:(WKWebView *)webView didStartProvisionalNavigation:(null_unspecified WKNavigation *)navigation;
//加载成功,对应UIWebView的- (void)webViewDidFinishLoad:(UIWebView *)webView;
- (void)webView:(WKWebView *)webView didFinishNavigation:(null_unspecified WKNavigation *)navigation;
//加载失败,对应UIWebView的- (void)webView:(UIWebView *)webView didFailLoadWithError:(NSError *)error;
- (void)webView:(WKWebView *)webView didFailNavigation:(null_unspecified WKNavigation *)navigation withError:(NSError *)error;
复制代码
该方法很好的解决了UIWebView使用stringByEvaluatingJavaScriptFromString:方法的两个缺点(1. 返回值只能是NSString。2. 报错没法捕获)。好比说要获取webView的title除了self.webView.title,还能够经过如下方法
-(void)evaluateJavaScript:(NSString *)javaScriptString completionHandler:(void (^ _Nullable)(_Nullable id result, NSError * _Nullable error))completionHandler; 用法示例
[self.webView evaluateJavaScript:@"document.title" completionHandler:^(id _Nullable title, NSError * _Nullable error) {
NSLog(@"调用evaluateJavaScript异步获取title:%@", title);
}];
复制代码
和UIWebView的拦截方式一致,具体可参照本文3.1,在此就不作赘述。
在OC中添加一个scriptMessageHandler,则会在all frames中添加一个js的function: window.webkit.messageHandlers..postMessage() ,涉及到的方法:
第一步:在OC中注册一个handler回调
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"choosePhoneContact"];
复制代码
第二步:js中调用方法
window.webkit.messageHandlers.choosePhoneContact.postMessage(param);
复制代码
第三步:oc中实现WKScriptMessageHandler回调
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
if ([message.name isEqualToString:@"choosePhoneContact"]) {
[self selectContactCompletion:^(NSString *name, NSString *phone) {
NSLog(@"选择完成");
//读取js function的字符串
NSString *jsFunctionString = message.body[@"completion"];
//拼接调用该方法的js字符串
NSString *callbackJs = [NSString stringWithFormat:@"(%@)({name: '%@', mobile: '%@'});", jsFunctionString, name, phone];
//执行回调
[self.webView evaluateJavaScript:callbackJs completionHandler:^(id _Nullable result, NSError * _Nullable error) {
}];
}];
}
}
复制代码
第四步:调用removeScriptMessageHandler移除回调
- (void)dealloc {
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"choosePhoneContact"];
}
复制代码
加载Cookie的时候的几个注意事项
在请求头中添加cookie,这样的话只要保证[NSHTTPCookieStorage sharedHTTPCookieStorage]中存在你的cookie,第一次请求就不会有问题了。
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.LynkCo.com"]];
NSArray *cookies = [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies;
//Cookies数组转换为requestHeaderFields
NSDictionary *requestHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
//设置请求头
request.allHTTPHeaderFields = requestHeaderFields;
[self.webView loadRequest:request];
复制代码
只须要经过添加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丢失问题
@param request 请求
@return 一个fixedRequest
*/
- (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;
}
复制代码
在iOS开发中,H5的嵌入能够经过UIWebView或者WKWebView。这两个都是继承UIView,来加载web数据的类。UIWebView是在iOS2的时候开始使用的。特色是加载速度慢,占用内存多,优化艰难。目前UIWebView在iOS12.0以后已被废弃,WKWebView是在iOS8苹果新推出的,加载速度快,占用内存较少,是一个不错的选择。
可是目前WKWebView依然存在不少坑,好比
-(void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler;
[self.webView setValue:[NSValue valueWithUIEdgeInsets:self.webView.scrollView.contentInset] forKey:@"_obscuredInsets"]。