參考文章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"]。