隨著H5技術的興起阅束,在iOS開發(fā)過程中,難免會遇到原生應用需要和H5頁面交互的問題股毫。其中會涉及方法調用及參數(shù)傳值等場景茴丰。
iOS原生應用和web頁面的交互大致上有這幾種方法:
1)URL攔截協(xié)議;(兼容iOS6及以下時可考慮姐刁,本文介紹)
2)第三方框架WebViewJavaScriptBridge芥牌;(本文不介紹)
3)iOS7之后的JavaScriptCore;(推薦聂使、本文重點介紹)
4)iOS8之后的WKWebView壁拉;(強烈推薦、本文重點介紹)
從使用場景上可分為OC調用JS柏靶、JS調用OC兩種形式弃理,下面分別討論之:
一、OC調用JS
JS 調用OC 方法后宿礁,有的操作可能需要將結果返回給JS案铺。這時候就是OC 調用JS 方法的場景。
方式一:使用UIWebView的方法
- (nullable NSString *)stringByEvaluatingJavaScriptFromString:(NSString *)script;
NSString *jsStr = [NSString stringWithFormat:@"showAlert('%@')",@"這是JS中alert彈出的message"];
[_webView stringByEvaluatingJavaScriptFromString:jsStr];
注意:這是一同步方法梆靖,可能會阻塞UI控汉,如果要執(zhí)行的js方法比較耗時,會造成界面卡頓返吻,比如姑子,js彈出alert后會阻塞UI界面,等待用戶操作響應测僵,而同時stringByEvaluatingJavaScriptFromString方法也會等待js執(zhí)行完畢后返回街佑。這就造成了死鎖谢翎。官方推薦使用WKWebView的evaluateJavaScript:completionHandler:代替這個方法。
還比如UIWebView的一些常用操作:
// 獲取當前頁面的title
NSString *title = [webview stringByEvaluatingJavaScriptFromString:@"document.title"];
// 獲取當前頁面的url
NSString *url = [webview stringByEvaluatingJavaScriptFromString:@"document.location.href"];
方式二沐旨、使用JavaScriptCore的方法(iOS7.0之后)
1森逮、使用JSContent的方法:- (JSValue *)evaluateScript:(NSString *)script;
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
NSString *textJS = @"showAlert('這是JS中alert彈出的message')";
[context evaluateScript:textJS];
//帶參數(shù)
NSString *jsStr = [NSString stringWithFormat:@"payResult('%@')",@"支付成功"];[[JSContext currentContext] evaluateScript:jsStr];
2、使用JSValue的方法:- (JSValue *)callWithArguments:(NSArray *)arguments;
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];[context[@"payResult"] callWithArguments:@[@"支付成功"]];
方式三磁携、使用WKWebView(iOS8之后)
使用WKWebView的evaluateJavaScript:completionHandler:方法褒侧,解決了使用UIWebView的stringByEvaluatingJavaScriptFromString方法易造成死鎖的問題。(官方推薦)
// 將分享結果返回給js
NSString *jsStr = [NSString stringWithFormat:@"shareResult('%@','%@','%@')",title,content,url];
[self.webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
? ? ?NSLog(@"%@----%@",result, error);
}];
說明:evaluateJavaScript:completionHandler:沒有返回值谊迄,JS 執(zhí)行成功還是失敗都會在completionHandler 中返回闷供。所以使用這個API 就可以避免執(zhí)行耗時的JS,或者alert 導致界面卡住的問題统诺。
二歪脏、JS調用OC
方式一、攔截URL
使用攔截URL方式又可細分為使用UIWebView攔截URL方式和使用WKWebView攔截URL方式粮呢。
1)使用UIWebView攔截URL:
UIWebView的代理方法shouldStartLoadWithRequest會攔截到每一個鏈接的Request婿失。當return YES時webView 就會加載這個鏈接;return NO時webView 就不會加載這個鏈接鬼贱,我們可以在該方法中根據(jù)scheme做不同處理移怯。
相關測試js代碼如下:(僅截取部分核心代碼)
function loadURL(url) {
var iFrame;
iFrame = document.createElement("iframe");
iFrame.setAttribute("src", url);
iFrame.setAttribute("style", "display:none;");
iFrame.setAttribute("height", "0px");
iFrame.setAttribute("width", "0px");
iFrame.setAttribute("frameborder", "0");
document.body.appendChild(iFrame);
// 發(fā)起請求后這個iFrame就沒用了,所以把它從dom上移除掉
iFrame.parentNode.removeChild(iFrame);
iFrame = null;
}
function payClick() {
loadURL("haleyAction://payAction?order_no=112233445566&channel=chris&amount=99&subject=測試支付");
}
function payResult(pay_no,pay_channel,pay_amount,pay_subject) {
var content = pay_no + ", " + pay_channel + ", " + pay_amount + ", " + pay_subject ;
asyncAlert(content);
document.getElementById("returnValue").value = content;
}
//設置一延遲方法这难,解決stringByEvaluatingJavaScriptFromString同步死鎖
function asyncAlert(content) {
setTimeout(function(){
alert(content);
},1);
}
<input type="button" value = "支付" onclick = "payClick()" />
在UIWebView的代理方法中舟误,攔截URL通過scheme做不同處理:
#pragma mark - UIWebViewDelegate
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType
{
NSURL *URL = request.URL;
NSString *scheme = [URL scheme];
if ([scheme isEqualToString:@"haleyaction"]) {
[self handleCustomAction:URL];
return NO;
}
return YES;
}
在此處,根據(jù)URL的不同host做響應不同處理姻乓。
- (void)handleCustomAction:(NSURL *)URL
{
NSString *host = [URL host];
if ([host isEqualToString:@"scanClick"]) {
? ? ?NSLog(@"掃一掃");
}??else if ([host isEqualToString:@"payAction"]) {
? ? [self payAction:URL];
}?
}
OC中的處理嵌溢,并將處理結果回傳至JS中(OC執(zhí)行js方法,以參數(shù)形式回傳結果)蹋岩。
- (void)payAction:(NSURL *)URL
{
NSArray *params =[URL.query componentsSeparatedByString:@"&"];
NSMutableDictionary *tempDic = [NSMutableDictionary dictionary];
for (NSString *paramStr in params) {
NSArray *dicArray = [paramStr componentsSeparatedByString:@"="];
if (dicArray.count > 1) {
NSString *decodeValue = [dicArray[1] stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
[tempDic setObject:decodeValue forKey:dicArray[0]];
}
}
NSString *orderNo = [tempDic objectForKey:@"order_no"];
long long amount = [[tempDic objectForKey:@"amount"] longLongValue];
NSString *subject = [tempDic objectForKey:@"subject"];
NSString *channel = [tempDic objectForKey:@"channel"];
// 支付操作
// 將支付結果返回給js(參數(shù)拼接赖草,當參數(shù)不是字符串時,不要加單引號'',如amount)
NSString *jsStr = [NSString stringWithFormat:@"payResult('%@', '%@', %lu, '%@')", orderNo, channel, (unsigned long)amount, subject];}
[self.webView stringByEvaluatingJavaScriptFromString:jsStr];
}
附注:js調用OC方法需要傳參數(shù)時剪个,可使用如下方法:
js中代碼:
function shareClick() {
loadURL("haleyAction://shareClick?title=測試分享的標題&content=測試分享的內容&url=http://www.baidu.com");
}
在OC方法中獲取參數(shù)秧骑,因所有的參數(shù)都在URL的query中,可先通過&將字符串拆分扣囊,再通過=把參數(shù)拆分成key和value乎折。
- (void)share:(NSURL *)URL
{
NSArray *params =[URL.query componentsSeparatedByString:@"&"];
NSMutableDictionary *tempDic = [NSMutableDictionary dictionary];
for (NSString *paramStr in params) {
? ? ? ?NSArray *dicArray = [paramStr componentsSeparatedByString:@"="];
? ? ? ?if (dicArray.count > 1) {
? ? ? ? ? ? NSString *decodeValue = [dicArray[1]
? ? ? ? ? ? ? ?stringByReplacingPercentEscapesUsingEncoding:NSUTF8StringEncoding];
? ? ? ? ? ?[tempDic setObject:decodeValue forKey:dicArray[0]];
? ? ? ?}
}
NSString *title = [tempDic objectForKey:@"title"];
NSString *content = [tempDic objectForKey:@"content"];
NSString *url = [tempDic objectForKey:@"url"];
// 在這里執(zhí)行分享的操作
// 將分享結果返回給js
NSString *jsStr = [NSString stringWithFormat:@"shareResult('%@','%@','%@')",title,content,url];
[self.webView stringByEvaluatingJavaScriptFromString:jsStr];
}
附注:以下代碼可以往HMTL的JS環(huán)境中插入全局變量、JS方法等
[webView stringByEvaluatingJavaScriptFromString:@"var arr = [3, 4, 'abc'];"];
2)使用WKWebView攔截URL(iOS8)(但更推薦使用下面介紹的方式三)
通過WKWebView的代理(WKNavigationDelegate)方法實現(xiàn)攔截URL(需要實現(xiàn)WKNavigationDelegate代理協(xié)議):
#pragma mark - WKNavigationDelegate
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler {
NSURL *URL = navigationAction.request.URL;
NSString *scheme = [URL scheme];
if ([scheme isEqualToString:@"haleyaction"]) {
? ? ?[self handleCustomAction:URL];
? ? ?decisionHandler(WKNavigationActionPolicyCancel);
? ? ?return;
}
decisionHandler(WKNavigationActionPolicyAllow);
}
說明:WKNavigationActionPolicyCancel代表取消加載侵歇,相當于UIWebView的代理方法return NO的情況骂澄;WKNavigationActionPolicyAllow代表允許加載,相當于UIWebView的代理方法中 return YES的情況惕虑。
關于[self handleCustomAction:URL];的具體實現(xiàn)同上面使用UIWebView攔截URL的情形坟冲,此不贅述磨镶。
注意:在WKWebView中要使用alert等彈窗操作,需要實現(xiàn)代理WKUIDelegate中相應方法健提,自己定義彈窗琳猫,否則alert彈不出。
#pragma mark - WKUIDelegate
- (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler
{
UIAlertController *alert = [UIAlertController alertControllerWithTitle:@"提醒" message:message preferredStyle:UIAlertControllerStyleAlert];
[alert addAction:[UIAlertAction actionWithTitle:@"知道了" style:UIAlertActionStyleCancel handler:^(UIAlertAction * _Nonnull action) {
completionHandler(); ?//此block一定得調用矩桂,否則alert彈不出沸移,調用位置隨意
}]];
[self presentViewController:alert animated:YES completion:nil];
}
方式二、使用JavaScriptCore
JavaScriptCore是iOS7開始引入的js框架侄榴。JavaScriptCore中主要的類有5個,下面分別介紹:1网沾、JSContext:
JSContext是為JavaScript的執(zhí)行提供運行環(huán)境癞蚕,所有的JavaScript的執(zhí)行都必須在JSContext環(huán)境中。常見的方法有:
//創(chuàng)建JSContext
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
// 運行一段js代碼辉哥,輸出結果為JSValue類型
- (JSValue *)evaluateScript:(NSString *)script;
// 獲取當前正在運行的JavaScript上下文環(huán)境
+ (JSContext *)currentContext;
// 獲取當前被調用方法的參數(shù)
+ (NSArray *)currentArguments;
2桦山、JSValue:
JavaScript中的變量和方法。JSValue都是通過JSContext返回或者創(chuàng)建的醋旦,并沒有構造方法恒水。JSValue包含了每一個JavaScript類型的值,通過JSValue可以將Objective-C中的類型轉換為JavaScript中的類型饲齐,也可以將JavaScript中的類型轉換為Objective-C中的類型钉凌。JSValue可以說是JavaScript和Object-C之間互換的橋梁。每個JSValue都和JSContext相關聯(lián)并且強引用context捂人,當與某JSContext對象關聯(lián)的所有JSValue釋放后御雕,JSContext也會被釋放。
//示例:在OC中往JS環(huán)境中添加一個變量(如一數(shù)組arr)
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
[context evaluateScript:@"var arr = [3, 4, 'abc'];"];
//取出arr變量
JSValue *jsArr = context[@"arr"];
//轉換成NSArray
NSArray *nsArr=[jsArr toArray];
NSLog(@"NSArray: %@", nsArr);
//常見其他轉換方法
- (int32_t)toInt32;
- (NSArray *)toArray;
- (NSString *)toString;
3滥搭、JSVirtualMachine:
JS運行的虛擬機酸纲,代表一個獨立的JavaScript對象空間,并為其執(zhí)行提供資源瑟匆。
4闽坡、JSManagedValue:
JS和OC對象的內存管理輔助對象,主要用來保存JSValue對象愁溜,解決OC對象中存儲js的值導致的循環(huán)引用問題疾嗅。
5、JSExport:
JS調用OC中的方法和屬性都寫在繼承自JSExport的協(xié)議當中祝谚,如此一來宪迟,這些方法和屬性會自動提供給JavaScript。
讓JSContext訪問我們的本地OC代碼的方式主要有兩種:block 和JSExport協(xié)議交惯,以下主要介紹block方式次泽。當一個 Objective-C block 被賦給JSContext里的一個標識符(如下面的@"share")穿仪,JavaScriptCore 會自動的把 block 封裝在 JavaScript 函數(shù)里。這使得在 JavaScript 中可以簡單的使用 Foundation 和 Cocoa 類意荤,所有的橋接都為你做好了啊片。
JS調用OC示例代碼:
//html中代碼變得更簡潔
function shareClick() {
? ? ? ?share('測試分享的標題','測試分享的內容','url=http://www.baidu.com');
}
在UIWebView代理方法中- (void)webViewDidFinishLoad:(UIWebView *)webView添加JS要調用的OC方法:
#pragma mark - UIWebViewDelegate
- (void)webViewDidFinishLoad:(UIWebView *)webView
{
NSLog(@"webViewDidFinishLoad");
[self addCustomActions]; ? //js要調用的oc方法
}
oc方法代碼:
- (void)addCustomActions
{
JSContext *context = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
[self addShareWithContext:context];
[self addPayActionWithContext:context];
//其他js要調用的方法...
}
具體實現(xiàn):(模塊獨立開,方便清晰定位)
- (void)addShareWithContext:(JSContext *)context {
__weak typeof(self) weakSelf = self;
context[@"share"] = ^() {
? ? NSArray *args = [JSContext currentArguments];
? ? if (args.count < 3) {
? ? return ;
? ? }
? ? NSString *title = [args[0] toString];
? ? NSString *content = [args[1] toString];
? ? NSString *url = [args[2] toString];
? ? // 在這里執(zhí)行分享的操作...
? ? // 將分享結果返回給js
? ? NSString *jsStr = [NSString ? ? ?stringWithFormat:@"shareResult('%@','%@','%@')",title,content,url];
? ? [[JSContext currentContext] evaluateScript:jsStr];
? };
}
注意:
1)獲取js運行環(huán)境JSContext的時機:一般在webViewDidFinishLoad中獲取
self.context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
2)線程問題:js調用OC代碼玖像,是在子線程紫谷。而更新OC中的UI(如彈出OC原生Alert),需要回到主線程捐寥。
3)循環(huán)引用問題:無論是把Block傳給JSContext對象讓其變成JavaScript方法笤昨,還是把它賦給exceptionHandler屬性,在Block內都不要直接使用其外部定義的JSContext對象或者JSValue握恳,應該將其當做參數(shù)傳入到Block中瞒窒,或者通過JSContext的類方法+ (JSContext *)currentContext;來獲得。否則會造成循環(huán)引用使得內存無法被正確釋放乡洼。
附加:
1)通過JSValue獲取JavaScript對象上的屬性崇裁。
JSValue會自動延展數(shù)組大小。并且通過JSValue還可以獲取JavaScript對象上的屬性束昵,比如例子中通過"length"就獲取到了JavaScript數(shù)組arr的長度(元素個數(shù))拔稳。
JSContext*context=[[JSContext alloc]init];
[context evaluateScript:@"var arr = [21, 7 , 'iderzheng.com'];"];
JSValue*jsArr=context[@"arr"];? // Get array from JSContext
NSLog(@"JS Array: %@;? ? Length: %@", jsArr, jsArr[@"length"]);
jsArr[1]=@"blog";? // Use JSValue as array
jsArr[7]=@7;
NSLog(@"JS Array: %@;? ? Length: %d", jsArr,[jsArr[@"length"]toInt32]);
NSArray *nsArr=[jsArr toArray];
NSLog(@"NSArray: %@", nsArr);
//Output:
//? JS Array: 21,7,iderzheng.com? ? Length: 3
//? JS Array: 21,blog,iderzheng.com,,,,,7? ? Length: 8
//? NSArray: (
//? 21,
//? blog,
//? "iderzheng.com",
//? "<null>",
//? "<null>",
//? "<null>",
//? "<null>",
//? 7
//? )
2)異常處理
在JSContext中執(zhí)行的JavaScript如果出現(xiàn)異常,只會被JSContext捕獲并存儲在exception屬性上锹雏,而不會向外拋出巴比。正確做法是:給JSContext對象設置exceptionHandler屬性,它接受的是^(JSContext *context, JSValue *exceptionValue)形式的Block逼侦。其默認值就是將傳入的exceptionValue賦給傳入的context的exception屬性匿辩。
JSContext *jsContext = [[JSContext alloc] init];
//捕獲運行js腳本的錯誤信息
jsContext.exceptionHandler = ^(JSContext *context, JSValue *exceptionValue) {
/*此處exceptionValue賦值給傳入的context的exception屬性,而不是賦給外部創(chuàng)建的 ?jsContext對象榛丢,這樣做是為了避免循環(huán)引用*/
? ? ? context.exception = exceptionValue;?
? ? ? NSLog(@"異常信息:%@", exceptionValue);
};
方式三铲球、使用WKWebView的addScriptMessageHandler
WKWebView是iOS8開始引入的UIWebView的增強版,無論是內存晰赞、性能還是使用靈活性上都要優(yōu)越于UIWebView稼病,是官方推出的UIWebView的替代品。
WKWebView->configuration(WKWebViewConfiguration)->userContentController(WKUserContentController)->addScriptMessageHandler:name:
WKWebView正是使用addScriptMessageHandler實現(xiàn)js調用原生OC方法掖鱼。而要使用此功能然走,必須實現(xiàn)WKScriptMessageHandler協(xié)議。該協(xié)議僅有一個@required方法:
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message;
其中戏挡,message.name即為消息發(fā)送者芍瑞,以指示發(fā)送的是哪個消息,可理解為js要調的oc方法名稱褐墅。
1)創(chuàng)建WKWebView:
WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
WKPreferences *preferences = [WKPreferences new];
preferences.javaScriptCanOpenWindowsAutomatically = YES;
preferences.minimumFontSize = 40.0;
configuration.preferences = preferences;
self.webView = [[WKWebView alloc] initWithFrame:self.view.frame configuration:configuration];
2)并配置js調用的oc方法(addScriptMessageHandler:name:)
- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"Share"];
}
為避免循環(huán)引用導致的控制器不能釋放問題拆檬,一般的洪己,在viewWillDisappear方法中需要移除相應的scriptMessageHandler:
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
// 移除scriptMessageHandler
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"Share"];
}
3)實現(xiàn)WKScriptMessageHandler協(xié)議方法
#pragma mark - WKScriptMessageHandler
- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message
{
if ([message.name isEqualToString:@"Share"]) {
? ? ?[self shareWithParams:message.body]; //js需要傳參數(shù)給oc
} else if ([message.name isEqualToString:@"Location"]) {
? ? ?[self getLocation]; ?//無參
}
}
說明:可根據(jù)message.name 來區(qū)分js要執(zhí)行的不同oc方法,而message.body 中存放著JS 要給OC 傳的參數(shù)竟贯。注意message.body允許的參數(shù)類型為:
Allowed types are ?NSNumber,NSString,NSDate,NSArray,NSDictionary, and?NSNull.
4)解析參數(shù)答捕,oc方法的具體實現(xiàn)
- (void)shareWithParams:(NSDictionary *)tempDic
{
if (![tempDic isKindOfClass:[NSDictionary class]]) {
? ? ?return;
}
NSString *title = [tempDic objectForKey:@"title"];
NSString *content = [tempDic objectForKey:@"content"];
NSString *url = [tempDic objectForKey:@"url"];
// 在這里執(zhí)行分享的操作
// 將分享結果返回給js
NSString *jsStr = [NSString stringWithFormat:@"shareResult('%@','%@','%@')",title,content,url];
[self.webView evaluateJavaScript:jsStr completionHandler:^(id _Nullable result, NSError * _Nullable error) {
? ? ? NSLog(@"%@----%@",result, error);
}];
}
5)js中的代碼
<input type = "button" value = "分享" onclick = "shareClick()" />
function shareClick() {
//此處傳遞的參數(shù)是一字典
window.webkit.messageHandlers.Share.postMessage({title:'測試分享的標題',content:'測試分享的內容',url:'http://m.rblcmall.com/share/openShare.htm?share_uuid=shdfxdfdsfsdfs&share_url=http://m.rblcmall.com/store_index_32787.htm&imagePath=http://c.hiphotos.baidu.com/image/pic/item/f3d3572c11dfa9ec78e256df60d0f703908fc12e.jpg'});
}
附加:WKWebView提供了estimatedProgress屬性代表當前網(wǎng)頁加載進度,可通過KVO監(jiān)聽此屬性屑那,實現(xiàn)進度計算拱镐。示例代碼如下:
- (void)observeValueForKeyPath:(NSString *)keyPath ofObject:(id)object change:(NSDictionary *)change context:(void *)context {
if (object == self.webView && [keyPath isEqualToString:@"estimatedProgress"]) {
CGFloat newprogress = [[change objectForKey:NSKeyValueChangeNewKey] doubleValue];
if (newprogress == 1) {
? ? [self.progressView setProgress:1.0 animated:YES];
? ? dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(0.7 * ? ? ? ?NSEC_PER_SEC)), dispatch_get_main_queue(), ^{
? ? self.progressView.hidden = YES;
? ? [self.progressView setProgress:0 animated:NO];
});
}else {
? ? self.progressView.hidden = NO;
? ? [self.progressView setProgress:newprogress animated:YES];
}
}
}