原文鏈接:
iOS中UIWebView與WKWebView、JavaScript與OC交互、Cookie管理看我就夠(上)
iOS中UIWebView與WKWebView、JavaScript與OC交互、Cookie管理看我就夠(中)
iOS中UIWebView與WKWebView、JavaScript與OC交互、Cookie管理看我就夠(下)
一、前言
iOS開發中,用來顯示一個html頁、H5頁,經常會用的一個控件是WebView。說到WebView,你知道多少呢?是簡單的展示,還是要和OC交互實現比較復雜的功能呢?本文將為您介紹iOS中的WebView,并且由淺到深,一步步帶你了解并掌握WebView的用法,JavaScript與Objective的交互,以及Cookie的管理、js的調試等。
文章因涉及到的內容較多,因此拆分成以下幾部分:
- iOS中UIWebView與WKWebView、JavaScript與OC交互、Cookie管理看我就夠(上)
- iOS中UIWebView與WKWebView、JavaScript與OC交互、Cookie管理看我就夠(中)
- iOS中UIWebView與WKWebView、JavaScript與OC交互、Cookie管理看我就夠(下)
關于文中提到的一些內容,這里我準備了個Demo,有需要的小伙伴可以下載。
二、UIWebView
1. UIWebView基本用法
首先要介紹的就是我們的老朋友UIWebView
。相信對大多數小伙伴兒而言,UIWebView
和UILabel
一樣,都是最早接觸的控件了,其實UIWebView
用法比較簡單(功能基本能滿足需求),簡單的創建,并且調用
- (void)loadRequest:(NSURLRequest *)request;
- (void)loadHTMLString:(NSString *)string baseURL:(nullable NSURL *)baseURL;
- (void)loadData:(NSData *)data MIMEType:(NSString *)MIMEType textEncodingName:(NSString *)textEncodingName baseURL:(NSURL *)baseURL;
這些方法,加載就可以了。
當然,如果需要監聽頁面加載的結果,或者需要判斷是否允許打開某個URL,那需要設置UIWebView
的delegate
,代理只需要遵循<UIWebViewDelegate>
協議,并且在代理中實現下面的這些可選方法就可以:
__TVOS_PROHIBITED @protocol UIWebViewDelegate <NSObject>
@optional
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
- (void)webViewDidStartLoad:(UIWebView *)webView;
- (void)webViewDidFinishLoad:(UIWebView *)webView;
- (void)webView:(UIWebView *)webView didFailLoadWithError:(nullable NSError *)error;
@end
關于UIWebView中JavaScript與Objective的交互
這里不詳細討論一些很好的第三方實現,比如WebViewJavascriptBridge,單純的講講native端JS與OC的交互實現方式,讀完了下面的部分,相信你也會實現一個簡單的bridge
了。
2. UIWebView:OC調用JS
1. stringByEvaluatingJavaScriptFromString:
最常用的方法,很簡單,只要調用- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
就可以了,如:
self.navigationItem.title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
雖然比較方便,但是缺點也有:
- 該方法不能判斷調用了一個js方法之后,是否發生了錯誤。當錯誤發生時,返回值為nil,而當調用一個方法本身沒有返回值時,返回值也為nil,所以無法判斷是否調用成功了。
- 返回值類型為
nullable NSString *
,就意味著當調用的js方法有返回值時,都以字符串返回,不夠靈活。當返回值是一個js的Array時,還需要解析字符串,比較麻煩。
對于上述缺點,可以通過使用JavaScriptCore(iOS 7.0 +)來解決。
2. JavaScriptCore(iOS 7.0 +)
想必大家不會陌生吧,前些日子弄的沸沸揚揚的JSPatch
被禁事件中,最核心的就是它了。因為JavaScriptCore
的JS到OC的映射,可以替換各種js方法成oc方法,所以其動態性(配合runtime的不安全性)也就成為了JSPatch
被Apple禁掉的最主要原因。這里講下UIWebView
通過JavaScriptCore
來實現OC->JS。
其實WebKit都有一個內嵌的js環境,一般我們在頁面加載完成之后,獲取js上下文,然后通過JSContext
的evaluateScript:
方法來獲取返回值。因為該方法得到的是一個JSValue
對象,所以支持JavaScript的Array、Number、String、對象等數據類型。
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
//更新標題,這是上面的講過的方法
//self.navigationItem.title = [webView stringByEvaluatingJavaScriptFromString:@"document.title"];
//獲取該UIWebView的javascript上下文
JSContext *jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//這也是一種獲取標題的方法。
JSValue *value = [self.jsContext evaluateScript:@"document.title"];
//更新標題
self.navigationItem.title = value.toString;
}
該方法解決了stringByEvaluatingJavaScriptFromString:
返回值只是NSString
的問題。
那么如果我執行了一個不存在的方法,比如
[self.jsContext evaluateScript:@"document.titlexxxx"];
那么必然會報錯,報錯了,可以通過@property (copy) void(^exceptionHandler)(JSContext *context, JSValue *exception);
,設置該block來獲取異常。
//在調用前,設置異常回調
[self.jsContext setExceptionHandler:^(JSContext *context, JSValue *exception){
NSLog(@"%@", exception);
}];
//執行方法
JSValue *value = [self.jsContext evaluateScript:@"document.titlexxxx"];
該方法,也很好的解決了stringByEvaluatingJavaScriptFromString:
調用js方法后,出現錯誤卻捕獲不到的缺點。
3. UIWebView:JS調用OC
1. Custom URL Scheme(攔截URL)
比如darkangel://
,方法是在html或者js中,點擊某個按鈕觸發事件時,跳轉到自定義URL Scheme構成的鏈接,而Objective-C中捕獲該鏈接,從中解析必要的參數,實現JS到OC的一次交互。比如頁面中一個a標簽,鏈接如下:
<a href="darkangel://smsLogin?username=12323123&code=892845">短信驗證登錄</a>
而在Objective-C中,只要遵循了UIWebViewDelegate
協議,那么每次打開一個鏈接之前,都會觸發方法
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
在該方法中,捕獲該鏈接,并且返回NO(阻止本次跳轉),從而執行對應的OC方法。
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
//標準的URL包含scheme、host、port、path、query、fragment等
NSURL *URL = request.URL;
if ([URL.scheme isEqualToString:@"darkangel"]) {
if ([URL.host isEqualToString:@"smsLogin"]) {
NSLog(@"短信驗證碼登錄,參數為 %@", URL.query);
return NO;
}
}
return YES;
}
當用戶點擊短信驗證登錄時,控制臺會輸出短信驗證碼登錄,參數為 username=12323123&code=892845
。參數可以是一個json格式并且URLEncode過的字符串,這樣就可以實現復雜參數的傳遞(比如WebViewJavascriptBridge)。
優點:泛用性強,可以配合h5實現頁面動態化。比如頁面中一個活動鏈接到活動詳情頁,當native尚未開發完畢時,鏈接可以是一個h5鏈接,等到native開發完畢時,可以通過該方法跳轉到native頁面,實現頁面動態化。且該方案適用于Android和iOS,泛用性很強。
缺點:無法直接獲取本次交互的返回值,比較適合單向傳參,且不關心回調的情景,比如h5頁面跳轉到native頁面等。
其實,WebViewJavascriptBridge使用的方案就是攔截URL,為了解決無法直接獲取返回值的缺點,它采用了將一個名為callback
的function
作為參數,通過一些封裝,傳遞到OC(js->oc 傳遞參數和callbackId),然后在OC端執行完畢,再通過block
來回調callback(oc->js,傳遞返回值參數),實現異步獲取返回值,比如在js端調用
//JS調用OC的分享方法(當然需要OC提前注冊)share為方法名,shareData為參數,后面的為回調function
WebViewJavascriptBridge.callHandler('share', shareData, function(response) {
//OC端通過block回調分享成功或者失敗的結果
alert(response);
});
具體的可以看下它的源碼,還是很值得學習的。
2. JavaScriptCore(iOS 7.0 +)
除了攔截URL的方法,還可以利用上面提到的JavaScriptCore
。它十分強大,強大在哪里呢?下面我們來一探究竟。
當然,還是需要在頁面加載完成時,先獲取js上下文。獲取到之后,我們就可以進行強大的方法映射了。
比如js中我定義了一個分享的方法
function share(title, imgUrl, link) {
//這里需要OC實現
}
在OC中實現如下
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
//將js的function映射到OC的方法
[self convertJSFunctionsToOCMethods];
}
- (void)convertJSFunctionsToOCMethods
{
//獲取該UIWebview的javascript上下文
//self持有jsContext
//@property (nonatomic, strong) JSContext *jsContext;
self.jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
//js調用oc
//其中share就是js的方法名稱,賦給是一個block 里面是oc代碼
//此方法最終將打印出所有接收到的參數,js參數是不固定的
self.jsContext[@"share"] = ^() {
NSArray *args = [JSContext currentArguments];//獲取到share里的所有參數
//args中的元素是JSValue,需要轉成OC的對象
NSMutableArray *messages = [NSMutableArray array];
for (JSValue *obj in args) {
[messages addObject:[obj toObject]];
}
NSLog(@"點擊分享js傳回的參數:\n%@", messages);
};
}
在html或者js的某處,點擊a標簽調用這個share方法,并傳參,如
<a href="javascript:void(0);" class="sharebtn" onclick="share('分享標題', 'http://cc.cocimg.com/api/uploads/170425/b2d6e7ea5b3172e6c39120b7bfd662fb.jpg', location.href)">分享活動,領30元紅包</a>
此時,如果用戶點擊了 ‘分享活動,領30元紅包’ 這個標簽,那么在控制臺會打印出所有參數
上面的代碼實現了OC方法替換JS實現。它十分靈活,主要依賴這些Api:
@interface JSContext (SubscriptSupport)
/*!
@method
@abstract Get a particular property on the global object.
@result The JSValue for the global object's property.
*/
- (JSValue *)objectForKeyedSubscript:(id)key;
/*!
@method
@abstract Set a particular property on the global object.
*/
- (void)setObject:(id)object forKeyedSubscript:(NSObject <NSCopying> *)key;
self.jsContext[@"yourMethodName"] = your block;
這樣寫不僅可以在有yourMethodName
方法時替換該JS方法為OC實現,還會在g該方法沒有時,添加方法。簡而言之,有則替換,無則添加。
那如果我想寫一個有兩個參數,一個返回值的js方法,oc應該怎么替換呢?
js中
//該方法傳入兩個整數,求和,并返回結果
function testAddMethod(a, b) {
//需要OC實現a+b,并返回
return a + b;
}
//js調用
console.log(testAddMethod(1, 5)); //output 6
oc直接替換該方法
self.jsContext[@"testAddMethod"] = ^NSInteger(NSInteger a, NSInteger b) {
return a + b;
};
那么當在js調用
//js調用
console.log(testAddMethod(1, 5)); //output 6, 方法為 a + b
如果oc替換該方法為兩數相乘
self.jsContext[@"testAddMethod"] = ^NSInteger(NSInteger a, NSInteger b) {
return a * b;
};
再次調用js
console.log(testAddMethod(1, 5)); //output 5,該方法變為了 a * b。
舉一反三,調用方法原實現,并且在原結果上乘以10。
//調用方法的本來實現,給原結果乘以10
JSValue *value = self.jsContext[@"testAddMethod"];
self.jsContext[@"testAddMethod"] = ^NSInteger(NSInteger a, NSInteger b) {
JSValue *resultValue = [value callWithArguments:[JSContext currentArguments]];
return resultValue.toInt32 * 10;
};
再次調用js
console.log(testAddMethod(1, 5)); //output 60,該方法變為了(a + b) * 10
上面的方法,都是同步函數,如果我想實現JS調用OC的方法,并且異步接收回調,那么該怎么做呢?比如h5中有一個分享按鈕,用戶點擊之后,調用native分享(微信分享、微博分享等),在native分享成功或者失敗時,回調h5頁面,告訴其分享結果,h5頁面刷新對應的UI,顯示分享成功或者失敗。
這個問題,需要對js有一定了解。下面上js代碼。
//聲明
function share(shareData) {
var title = shareData.title;
var imgUrl = shareData.imgUrl;
var link = shareData.link;
var result = shareData.result;
//do something
//這里模擬異步操作
setTimeout(function(){
//2s之后,回調true分享成功
result(true);
}, 2000);
}
//調用的時候需要這么寫
share({
title: "title",
imgUrl: "http://img.dd.com/xxx.png",
link: location.href,
result: function(res) { //函數作為參數
console.log(res ? "success" : "failure");
}
});
從封裝的角度上講,js的share
方法的參數是一個對象
,該對象包含了幾個必要的字段,以及一個回調函數,這個回調函數有點像oc的block
,調用者把一個function
傳入一個function
當作參數,在適當時候,方法內實現者調用該function
,實現對調用者的異步回調。那么如果此時OC來實現share
方法,該怎么做呢?其實大概是這樣的:
//異步回調
self.jsContext[@"share"] = ^(JSValue *shareData) { //首先這里要注意,回調的參數不能直接寫NSDictionary類型,為何呢?
//仔細看,打印出的確實是一個NSDictionary,但是result字段對應的不是block而是一個NSDictionary
NSLog(@"%@", [shareData toObject]);
//獲取shareData對象的result屬性,這個JSValue對應的其實是一個javascript的function。
JSValue *resultFunction = [shareData valueForProperty:@"result"];
//回調block,將js的function轉換為OC的block
void (^result)(BOOL) = ^(BOOL isSuccess) {
[resultFunction callWithArguments:@[@(isSuccess)]];
};
//模擬異步回調
dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(3 * NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
NSLog(@"回調分享成功");
result(YES);
});
};
其中一些坑,已經在代碼的注釋寫的比較清楚了,這里要注意JavaScript
的function
和Objective-C
的block
的轉換。
從上面的一些探討和嘗試來看,足以證明JavaScriptCore
的強大,這里不再展開,小伙伴們可以自行探索。
4. UIWebView的Cookie管理
1. Cookie簡介
說到Cookie
,或許有些小伙伴會比較陌生,有些小伙伴會比較熟悉。如果項目中,所有頁面都是純原生來實現的話,一般Cookie
這個東西或許我們永遠也不會接觸到。但是,這里還是要說一下Cookie
,因為它真的很重要,由它產生的一些坑也很多。
Cookie
在Web利用的最多的地方,是用來記錄各種狀態。比如你在Safari
中打開百度,然后登陸自己的賬號,之后打開所有百度相關的頁面,都會是登陸狀態,而且當你關了電腦,下次開機再次打開Safari
打開百度,會發現還是登陸狀態,其實這個就利用了Cookie
。Cookie
中記錄了你百度賬號的一些信息、有效期等,也維持了跨域請求時登錄狀態的統計性。
可以看到Cookie
的域各不相同,有效期也各不相同,一般.baidu.com
這樣的域的Cookie
就是為了跨域時,可以維持一些狀態。
那么在App中,Cookie最常用的就是維持登錄狀態了。一般Native端都有自己的一套完整登錄注冊邏輯,一般大部分頁面都是原生實現的。當然,也會有一些頁面是h5來實現的,雖然h5頁面在App中通過WebView
加載或多或少都會有點性能問題,感覺不流暢或者體驗不好,但是它的靈活性是Native App無法比擬的。那么由此,便產生了一種需求,當Native端用戶是登錄狀態的,打開一個h5頁面,h5也要維持用戶的登錄狀態。
這個需求看似簡單,如何實現呢?一般的解決方案是Native保存登錄狀態的Cookie,在打開h5頁面中,把Cookie添加上,以此來維持登錄狀態。其實坑還是有很多的,比如用戶登錄或者退出了,h5頁面的登錄狀態也變了,需要刷新,什么時候刷新?WKWebView
中Cookie
丟失問題?這里簡單說下UIWebView
的Cookie
管理,后面的章節再介紹WKWebView
。
2. Cookie管理
UIWebView
的Cookie
管理很簡單,一般不需要我們手動操作Cookie
,因為所有Cookie
都會被[NSHTTPCookieStorage sharedHTTPCookieStorage]
這個單例管理,而且UIWebView
會自動同步CookieStorage
中的Cookie,所以只要我們在Native端,正常登陸退出,h5在適當時候刷新,就可以正確的維持登錄狀態,不需要做多余的操作。
可能有一些情況下,我們需要在訪問某個鏈接時,添加一個固定Cookie
用來做區分,那么就可以通過header
來實現
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@"http://www.baidu.com"]];
[request addValue:@"customCookieName=1314521;" forHTTPHeaderField:@"Set-Cookie"];
[self.webView loadRequest:request];
也可以主動操作NSHTTPCookieStorage
,添加一個自定義Cookie
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:@{
NSHTTPCookieName: @"customCookieName",
NSHTTPCookieValue: @"1314521",
NSHTTPCookieDomain: @".baidu.com",
NSHTTPCookiePath: @"/"
}];
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie]; //Cookie存在則覆蓋,不存在添加
還有一些常用的方法,如讀取所有Cookie
NSArray *cookies = [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies;
Cookie轉換成HTTPHeaderFields,并添加到request的header中
//Cookies數組轉換為requestHeaderFields
NSDictionary *requestHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
//設置請求頭
request.allHTTPHeaderFields = requestHeaderFields;
整體來說UIWebView
的Cookie
管理比較簡單,小伙伴們可以自己寫個demo
測試一下,發揮你們的想象。
三、未完待續
關于UIWebView
的介紹,以及使用UIWebView
進行JS與OC的交互,Cookie
的管理,就先簡單介紹到這里。如果有小伙伴對于WebViewJavascriptBridge比較感興趣,可以留言,根據留言我考慮一下寫一篇文章,分析它的詳細實現。
另外,后續將為您介紹WKWebView
的用法,一些OC與JS交互,Cookie管理、如何在Safari
中調試以及一些不為人知的坑等,敬請期待~
下篇文章:iOS中UIWebView與WKWebView、JavaScript與OC交互、Cookie管理看我就夠(中)