UIWebView與WKWebView、JavaScript與OC交互


UIWebView OC調用JS

1. stringByEvaluatingJavaScriptFromString:

最常用的方法,很簡單,只要調用- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;就可以了,如:

1
     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、對象等數據類型。

1
2
3
4
5
6
7
8
9
10
11
12
13
- (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的問題。

那么如果我執行了一個不存在的方法,比如

1
[self.jsContext evaluateScript:@ "document.titlexxxx" ];

那么必然會報錯,報錯了,可以通過@property (copy) void(^exceptionHandler)(JSContext *context, JSValue *exception);,設置該block來獲取異常。

1
2
3
4
5
6
//在調用前,設置異常回調
[self.jsContext setExceptionHandler:^(JSContext *context, JSValue *exception){
         NSLog(@ "%@" , exception);
}];
//執行方法
JSValue *value = [self.jsContext evaluateScript:@ "document.titlexxxx" ];

該方法,也很好的解決了stringByEvaluatingJavaScriptFromString:調用js方法后,出現錯誤卻捕獲不到的缺點。

UIWebView JS調用OC

1. Custom URL Scheme(攔截URL)

比如darkangel://。方法是在html或者js中,點擊某個按鈕觸發事件時,跳轉到自定義URL Scheme構成的鏈接,而Objective-C中捕獲該鏈接,從中解析必要的參數,實現JS到OC的一次交互。比如頁面中一個a標簽,鏈接如下:

1
短信驗證登錄

而在Objective-C中,只要遵循了UIWebViewDelegate協議,那么每次打開一個鏈接之前,都會觸發方法

1
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;

在該方法中,捕獲該鏈接,並且返回NO(阻止本次跳轉),從而執行對應的OC方法。

1
2
3
4
5
6
7
8
9
10
11
12
- (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端調用

1
2
3
4
5
//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中我定義了一個分享的方法

1
2
3
function  share(title, imgUrl, link) {
      //這里需要OC實現
}

在OC中實現如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
- (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方法,並傳參,如

 

 

上面的代碼實現了OC方法替換JS實現。它十分靈活,主要依賴這些Api。

 

1
2
3
4
5
6
7
8
9
10
11
12
@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*)key;

self.jsContext[@"yourMethodName"] = your block;這樣寫不僅可以在有yourMethodName方法時替換該JS方法為OC實現,還會在該方法沒有時,添加方法。簡而言之,有則替換,無則添加

那如果我想寫一個有兩個參數,一個返回值的js方法,oc應該怎么替換呢?

js中

1
2
3
4
5
6
7
//該方法傳入兩個整數,求和,並返回結果
function  testAddMethod(a, b) {
      //需要OC實現a+b,並返回
       return  a + b;
}
//js調用
console.log(testAddMethod(1, 5));     //output  6

oc直接替換該方法

1
2
3
self.jsContext[@ "testAddMethod" ] = ^NSInteger(NSInteger a, NSInteger b) {
       return  a + b;
};

那么當在js調用

1
2
//js調用
console.log(testAddMethod(1, 5));     //output  6, 方法為 a + b

如果oc替換該方法為兩數相乘

1
2
3
self.jsContext[@ "testAddMethod" ] = ^NSInteger(NSInteger a, NSInteger b) {
       return  a * b;
};

再次調用js

1
console.log(testAddMethod(1, 5));     //output  5,該方法變為了 a * b。

舉一反三,調用方法原實現,並且在原結果上乘以10。

//調用方法的本來實現,給原結果乘以10

1
2
3
4
5
JSValue *value = self.jsContext[@ "testAddMethod" ];
self.jsContext[@ "testAddMethod" ] = ^NSInteger(NSInteger a, NSInteger b) {
     JSValue *resultValue = [value callWithArguments:[JSContext currentArguments]];
     return  resultValue.toInt32 * 10;
};

再次調用js

1
console.log(testAddMethod(1, 5));     //output  60,該方法變為了(a + b) * 10

上面的方法,都是同步函數,如果我想實現JS調用OC的方法,並且異步接收回調,那么該怎么做呢?比如h5中有一個分享按鈕,用戶點擊之后,調用native分享(微信分享、微博分享等),在native分享成功或者失敗時,回調h5頁面,告訴其分享結果,h5頁面刷新對應的UI,顯示分享成功或者失敗。

這個問題,需要對js有一定了解。下面上js代碼。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//聲明
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方法,該怎么做呢?其實大概是這樣的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//異步回調
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的強大,這里不再展開,小伙伴們可以自行探索。

UIWebView的Cookie管理

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。

Cookie管理

UIWebView的Cookie管理很簡單,一般不需要我們手動操作Cookie,因為所有Cookie都會被[NSHTTPCookieStorage sharedHTTPCookieStorage]這個單例管理,而且UIWebView會自動同步CookieStorage中的Cookie,所以只要我們在Native端,正常登陸退出,h5在適當時候刷新,就可以正確的維持登錄狀態,不需要做多余的操作。

可能有一些情況下,我們需要在訪問某個鏈接時,添加一個固定Cookie用來做區分,那么就可以通過header來實現

1
2
3
NSMutableURLRequest *request = [NSMutableURLRequest requestWithURL:[NSURL URLWithString:@ "http://www.baidu.com" ]];
[request addValue:@ "customCookieName=1314521;"  forHTTPHeaderField:@ "Set-Cookie" ];
[self.webView loadRequest:request];

也可以主動操作NSHTTPCookieStorage,添加一個自定義Cookie

1
2
3
4
5
6
7
NSHTTPCookie *cookie = [NSHTTPCookie cookieWithProperties:@{
     NSHTTPCookieName: @ "customCookieName"
     NSHTTPCookieValue: @ "1314521"
     NSHTTPCookieDomain: @ ".baidu.com" ,
     NSHTTPCookiePath: @ "/"
}];
[[NSHTTPCookieStorage sharedHTTPCookieStorage] setCookie:cookie];     //Cookie存在則覆蓋,不存在添加

還有一些常用的方法,如讀取所有Cookie

1
NSArray *cookies = [NSHTTPCookieStorage sharedHTTPCookieStorage].cookies;

Cookie轉換成HTTPHeaderFields,並添加到request的header中

1
2
3
4
//Cookies數組轉換為requestHeaderFields
NSDictionary *requestHeaderFields = [NSHTTPCookie requestHeaderFieldsWithCookies:cookies];
//設置請求頭
request.allHTTPHeaderFields = requestHeaderFields;


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM