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"
,
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;
|