iOS中UIWebView與WKWebView、JavaScript與OC交互、Cookie管理看我就夠(上)

原文鏈接:

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的調試等。

文章因涉及到的內容較多,因此拆分成以下幾部分:

關于文中提到的一些內容,這里我準備了個Demo,有需要的小伙伴可以下載。

二、UIWebView

1. UIWebView基本用法

首先要介紹的就是我們的老朋友UIWebView。相信對大多數小伙伴兒而言,UIWebViewUILabel一樣,都是最早接觸的控件了,其實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,那需要設置UIWebViewdelegate,代理只需要遵循<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"];

雖然比較方便,但是缺點也有:

  1. 該方法不能判斷調用了一個js方法之后,是否發生了錯誤。當錯誤發生時,返回值為nil,而當調用一個方法本身沒有返回值時,返回值也為nil,所以無法判斷是否調用成功了。
  2. 返回值類型為nullable NSString *,就意味著當調用的js方法有返回值時,都以字符串返回,不夠靈活。當返回值是一個js的Array時,還需要解析字符串,比較麻煩。

對于上述缺點,可以通過使用JavaScriptCore(iOS 7.0 +)來解決。

2. JavaScriptCore(iOS 7.0 +)

想必大家不會陌生吧,前些日子弄的沸沸揚揚的JSPatch被禁事件中,最核心的就是它了。因為JavaScriptCore的JS到OC的映射,可以替換各種js方法成oc方法,所以其動態性(配合runtime的不安全性)也就成為了JSPatchApple禁掉的最主要原因。這里講下UIWebView通過JavaScriptCore來實現OC->JS。

其實WebKit都有一個內嵌的js環境,一般我們在頁面加載完成之后,獲取js上下文,然后通過JSContextevaluateScript:方法來獲取返回值。因為該方法得到的是一個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,為了解決無法直接獲取返回值的缺點,它采用了將一個名為callbackfunction作為參數,通過一些封裝,傳遞到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元紅包’ 這個標簽,那么在控制臺會打印出所有參數

image

上面的代碼實現了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);
    });
};

其中一些坑,已經在代碼的注釋寫的比較清楚了,這里要注意JavaScriptfunctionObjective-Cblock的轉換。

從上面的一些探討和嘗試來看,足以證明JavaScriptCore的強大,這里不再展開,小伙伴們可以自行探索。

4. UIWebView的Cookie管理

1. Cookie簡介

說到Cookie,或許有些小伙伴會比較陌生,有些小伙伴會比較熟悉。如果項目中,所有頁面都是純原生來實現的話,一般Cookie這個東西或許我們永遠也不會接觸到。但是,這里還是要說一下Cookie,因為它真的很重要,由它產生的一些坑也很多。

Cookie在Web利用的最多的地方,是用來記錄各種狀態。比如你在Safari中打開百度,然后登陸自己的賬號,之后打開所有百度相關的頁面,都會是登陸狀態,而且當你關了電腦,下次開機再次打開Safari打開百度,會發現還是登陸狀態,其實這個就利用了CookieCookie中記錄了你百度賬號的一些信息、有效期等,也維持了跨域請求時登錄狀態的統計性。

image

可以看到Cookie的域各不相同,有效期也各不相同,一般.baidu.com這樣的域的Cookie就是為了跨域時,可以維持一些狀態。

那么在App中,Cookie最常用的就是維持登錄狀態了。一般Native端都有自己的一套完整登錄注冊邏輯,一般大部分頁面都是原生實現的。當然,也會有一些頁面是h5來實現的,雖然h5頁面在App中通過WebView加載或多或少都會有點性能問題,感覺不流暢或者體驗不好,但是它的靈活性是Native App無法比擬的。那么由此,便產生了一種需求,當Native端用戶是登錄狀態的,打開一個h5頁面,h5也要維持用戶的登錄狀態。

這個需求看似簡單,如何實現呢?一般的解決方案是Native保存登錄狀態的Cookie,在打開h5頁面中,把Cookie添加上,以此來維持登錄狀態。其實坑還是有很多的,比如用戶登錄或者退出了,h5頁面的登錄狀態也變了,需要刷新,什么時候刷新?WKWebViewCookie丟失問題?這里簡單說下UIWebViewCookie管理,后面的章節再介紹WKWebView。

2. Cookie管理

UIWebViewCookie管理很簡單,一般不需要我們手動操作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;

整體來說UIWebViewCookie管理比較簡單,小伙伴們可以自己寫個demo測試一下,發揮你們的想象。

三、未完待續

關于UIWebView的介紹,以及使用UIWebView進行JS與OC的交互,Cookie的管理,就先簡單介紹到這里。如果有小伙伴對于WebViewJavascriptBridge比較感興趣,可以留言,根據留言我考慮一下寫一篇文章,分析它的詳細實現。

另外,后續將為您介紹WKWebView的用法,一些OC與JS交互,Cookie管理、如何在Safari中調試以及一些不為人知的坑等,敬請期待~

下篇文章:iOS中UIWebView與WKWebView、JavaScript與OC交互、Cookie管理看我就夠(中)

最后編輯于
?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 228,412評論 6 532
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 98,514評論 3 416
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 176,373評論 0 374
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 62,975評論 1 312
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 71,743評論 6 410
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 55,199評論 1 324
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 43,262評論 3 441
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 42,414評論 0 288
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 48,951評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 40,780評論 3 354
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 42,983評論 1 369
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 38,527評論 5 359
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,218評論 3 347
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 34,649評論 0 26
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 35,889評論 1 286
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 51,673評論 3 391
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 47,967評論 2 374

推薦閱讀更多精彩內容