H5與App原生交互,一般會(huì)是前端頁(yè)面中的JavaScript與App使用的原生開發(fā)語(yǔ)言的交互,。技術(shù)方案應(yīng)能達(dá)到以下要求:
- 在js與原生進(jìn)行交互的時(shí)候能保證正常的正向調(diào)用邏輯返回,,反向可以處理異步回調(diào),因?yàn)閷?duì)js來(lái)說,,大部分邏輯都是回調(diào)與監(jiān)聽,。
- 要保證H5與Native App通訊效率高、安全性強(qiáng),,能有效防止通過H5頁(yè)面進(jìn)行App注入、中間人攻擊或者釣魚,。
- 方便測(cè)試階段,,H5嵌入到App當(dāng)中,開發(fā)人員方便調(diào)試與Debug,。
目前主流的技術(shù)方案:
1.在iOS7以前,,在UIWebView中實(shí)現(xiàn)一些代理方法攔截帶有約定好的protocol的Url,從Url上獲取get方式的參數(shù)傳遞,,映射成本地原生方法,,如下:
-(BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType{
NSString *urlString = request.URL.absoluteString;
if ([urlString rangeOfString:@"js-call://"].location != NSNotFound) {
NSString * host = [self sliceHost:urlString];
NSDictionary * params = [self sliceParams:urlString];
if ([host isEqualToString:@"openOrderDetail"]) {
[self openOrderDetail:params];
}
return NO;
}
return YES;
}
這僅僅解決了js調(diào)用原生方法的問題,,至于調(diào)用的結(jié)果與調(diào)用完之后要進(jìn)行一些頁(yè)面的回調(diào),在這個(gè)攔截的過程中根本沒有辦法進(jìn)行,,不過有一些蹩腳的補(bǔ)償措施,,如下:
-(void)webViewDidFinishLoad:(UIWebView *)webView
{
self.orderDetailCallBackFuncName = [webView stringByEvaluatingJavaScriptFromString:@"orderCallbackfuncName()"];
}
會(huì)在頁(yè)面加載完畢后主動(dòng)去取頁(yè)面上設(shè)置的回調(diào)方法的名稱,然后在原生方法中處理完邏輯再進(jìn)行回調(diào),。
-(void)OpenOrderDetail:(NSDictionary *)params{
//do someting
[self.webView stringByEvaluatingJavaScriptFromString:[NSString stringWithFormat:@"%@()",self.orderDetailCallBackFuncName ]];
}
2.iOS7以后,,大家都使用JavaScriptCore這個(gè)官方的WebKit 的 JavaScript 引擎,實(shí)現(xiàn)oc與JavaScript的語(yǔ)言穿梭,。
-(void)configJsCallBack{
WeakSelf;
self.jsContext = [self.webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
self.jsContext.exceptionHandler = ^(JSContext * con,JSValue * exception){
NSLog(@"JS Error:%@",exception);
};
Coordinator * coordinator = [[Coordinator alloc]init];
self.jsContext[@"mobileCoordinator"] = coordinator;
self.jsContext[@"console"] = coordinator;
}
不太了解JavaScriptCore的同學(xué)可以自己查閱一下官方文檔或者學(xué)習(xí)資料,。這里我們使用一些技巧,將所有的App開放給js的方法都由一個(gè)叫Coordinator的調(diào)度器來(lái)調(diào)度,,而這個(gè)調(diào)度器實(shí)現(xiàn)了JSExport協(xié)議:
#import <Foundation/Foundation.h>
#import <JavaScriptCore/JavaScriptCore.h>
@protocol CoordinatorExport <JSExport>
-(void)log:(NSString *)msg;
-(BOOL)callNativeModule:(NSString *)url;
/*
js調(diào)用原生分享
shareOpinion為json對(duì)象
{
"type":"share",
"title":"share title",
"content":"share content",
"imgUrl":"",
"clickUrl":""
}
其中類型type有以下幾種:
share(只有朋友圈和微信好友),doubleShare(包含所有分享渠道),allShare(分享全部渠道)
*/
JSExportAs(showShareMenu, -(BOOL)showShareMenu:(NSString *)url opinion:(NSString *)opinion);
@end
@interface Coordinator : NSObject< CoordinatorExport >
@property(nonatomic,copy)BOOL (^openShareCallBack)(NSDictionary * opinion);
@end
上面的做法就是我們會(huì)在合適的實(shí)際向JavaScript的運(yùn)行的環(huán)境中注入一個(gè)叫做mobileCoordinator的對(duì)象,,這個(gè)對(duì)象會(huì)注入到JavaScript環(huán)境中的window對(duì)象上,全局可用,。為什么要封裝到一個(gè)對(duì)象上,,是因?yàn)閖s沒有命名空間的概念并且有變量提升向上查找,會(huì)引起命名沖突,,所以我們把對(duì)外暴露的方法都進(jìn)行一個(gè)對(duì)象封裝,。還有一個(gè)好處就是JavaScript的開發(fā)者與app的開發(fā)者都會(huì)像編寫各自語(yǔ)言的代碼一樣書寫代碼,沒有語(yǔ)法損失,,js同步調(diào)用原生方法,,原生實(shí)現(xiàn)的時(shí)候具備返回值,js的調(diào)用者就可以獲取返回值,,如果是異步回調(diào),,那可以對(duì)外暴露方法的時(shí)候提供一個(gè)callback的入?yún)ⅲ诋惒酵瓿珊筮M(jìn)行回調(diào),。
3.其他方案例如JavaScriptBridge等與第二種方案類似,。
方案比較:
方案1的流程如下:
交互方式為單向
H5調(diào)用Native:
H5頁(yè)面 —>發(fā)起Url Redirect(Url上攜帶帶有動(dòng)作語(yǔ)義的參數(shù))->Native App->攔截Url Redirect->解析動(dòng)作語(yǔ)義參數(shù)->調(diào)用相關(guān)Native代碼
Native調(diào)用H5頁(yè)面:
Native App—>獲取頁(yè)面上預(yù)留參數(shù)和解析動(dòng)作語(yǔ)義參數(shù)->調(diào)用相關(guān)JavaScript代碼
這樣使得一個(gè)簡(jiǎn)單的方法調(diào)用變得非常割裂,而且雙端維護(hù)成本非常高,,不易debug,。
方案2的流程如下:
交互為雙向:
H5頁(yè)面(Native App)<->調(diào)用Native代碼(調(diào)用JavaScript代碼)<->Native App執(zhí)行被調(diào)用Native代碼返回調(diào)用結(jié)果(H5頁(yè)面執(zhí)行被調(diào)用JavaScript代碼并返回調(diào)用結(jié)果)
方案2優(yōu)勢(shì)比較明顯,一般會(huì)采用第二種,。
實(shí)現(xiàn)細(xì)節(jié)
細(xì)節(jié)上有一些需要注意的東西:
1.oc方法是帶有參數(shù)標(biāo)簽的,,js的方法并沒有,注意使用JSExportAs這個(gè)宏來(lái)將oc原生語(yǔ)言轉(zhuǎn)換為js語(yǔ)法風(fēng)格的代碼,。
2.注意獲取jscontext上下文并注入方法與對(duì)象的時(shí)機(jī),,這取決于H5頁(yè)面上的js引用時(shí)機(jī),如果H5頁(yè)面上使用require來(lái)進(jìn)行順序引用,就不會(huì)出現(xiàn)問題,,如果與原生交互的js的代碼加載與原生注入的注入順序混亂,,則調(diào)用不到原生暴露的方法會(huì)引起js執(zhí)行異常。建議結(jié)合攔截url的方式讓H5決定何時(shí)注入,,或者是前端工程師梳理規(guī)范,,在H5引用js的時(shí)候做順序控制。
防止注入與釣魚
其實(shí)這個(gè)不太算是技術(shù)方案,,不過可以提一下,。有時(shí)候手機(jī)在危險(xiǎn)的網(wǎng)絡(luò)環(huán)境中比方說鏈接在不安全的路由器中,DNS進(jìn)行惡意中轉(zhuǎn)到釣魚網(wǎng)站上,,如果頁(yè)面調(diào)用已知的原生暴露出來(lái)的方法,,同步數(shù)據(jù)或者是調(diào)用關(guān)鍵業(yè)務(wù),就會(huì)有注入攻擊的風(fēng)險(xiǎn),。一般需要做的是,,H5在調(diào)用app原生關(guān)鍵業(yè)務(wù)的時(shí)候,需要在調(diào)用原生方法的時(shí)候傳入票據(jù),,原生通過服務(wù)端的認(rèn)證中心驗(yàn)證票據(jù),,通過才可以處理頁(yè)面調(diào)用請(qǐng)求,在同步數(shù)據(jù)與狀態(tài)的時(shí)候,,比方說將app中的用戶登錄狀態(tài)同步到H5頁(yè)面上,,一般app會(huì)同步cookie,不過這種方式維護(hù)成本較高,。對(duì)于同步狀態(tài)與數(shù)據(jù),,app應(yīng)該使用業(yè)務(wù)票據(jù)來(lái)傳遞給H5,H5通過票據(jù)中心置換出真正的用戶狀態(tài)或者是關(guān)鍵業(yè)務(wù)數(shù)據(jù),。更高級(jí)別的方案還有H5與App臨時(shí)握手等,。
H5在WebView中的Debug
這個(gè)是一個(gè)比較惡心的事情,不過我們可以替換js的window對(duì)象上的console對(duì)象,,將log函數(shù)轉(zhuǎn)接到原生,,再通過一些其他方式進(jìn)行輸出,JavaScriptCore中提供了exceptionHandler
context.exceptionHandler = ^(JSContext *context, JSValue *exception) { NSLog(@"JS Error: %@", exception);};
下一篇文章將會(huì)介紹一下用websocket協(xié)議,,將app中的WebView的調(diào)試信息輸出到指定IP的電腦上,,方便開發(fā)調(diào)試,這樣就能減少溝通與配合聯(lián)調(diào),,提高開發(fā)效率,。
文/Neo_joke(簡(jiǎn)書作者)
原文鏈接:http://www.jianshu.com/p/0c49a584ffce
著作權(quán)歸作者所有,轉(zhuǎn)載請(qǐng)聯(lián)系作者獲得授權(quán),,并標(biāo)注“簡(jiǎn)書作者”。
|