在 App 混合開發中,app 層向 js 層提供接口有兩種方式,一種是同步接口,一種一異步接口(不清楚什麼是同步的請看這裏的討論)。爲了保證 web 流暢,大部分時候,咱們應該使用異步接口,可是某些狀況下,咱們可能更須要同步接口。同步接口的好處在於,首先 js 能夠經過返回值獲得執行結果;其次,在混合式開發中,app 層導出的某些 api 按照語義就應該是同步的,不然會很奇怪——一個可能在 for 循環中使用的,執行很是快的接口,好比讀寫某個配置項,設計成異步會很奇怪。javascript
那麼如何向 js 層導出同步接口呢?java
咱們知道,在 Android 框架中,經過 WebView.addJavascriptInterface() 這個函數,能夠將 java 接口導出到 js 層,而且這樣導出的接口是同步接口。可是在 iOS 的 Cocoa 框架中,想導出同步接口卻不容易,究其緣由,是由於 UIWebView 和 WKWebView 沒有 addJavascriptInterface 這樣的功能。同時,Android 這個功能爆出過安全漏洞,那麼,咱們有沒有別的方式實現同步調用呢?咱們以 iOS UIWebView 爲例提供一種實現,WKWebView 和 Android 也能夠參考。web
爲了找到問題的關鍵,咱們看一下 iOS 中實現 js 調用 app 的通行方法:ajax
首先,自定義 UIWebViewDelegate,在函數 shouldStartLoadWithRequest:navigationType: 中攔截請求。json
- (BOOL) webView:(UIWebView* _Nonnull)webView shouldStartLoadWithRequest:(NSURLRequest* _Nonnull)request navigationType:(UIWebViewNavigationType)navigationType { if ([request.HTTPMethod compare:@"GET" options:NSCaseInsensitiveSearch] != NSOrderedSame) { // 不處理非 get 請求 return YES; } NSURL* url = request.URL; if ([url.scheme isEqualToString:@'YourCustomProtocol']) { return [self onMyRequest:request]; } return YES; }
這種作法實質上就是將函數調用命令轉化爲 url,經過請求的方式通知 app 層,其中 onMyRequest: 是自定義的 request 響應函數。爲了發送請求,js 層要創建一個隱藏的 iframe 元素,每次發送請求時修改 iframe 元素的 src 屬性,app 便可攔截到相應請求。api
/** * js 向 native 傳遞消息 * @method js_sendMessageToNativeAsync * @memberof JSToNativeIOSPolyfill * @public * @param str {String} 消息字符串,由 HybridMessage 轉換而來 */ JSToNativeIOSPolyfill.prototype.js_sendMessageToNativeAsync = function (str) { if (!this.ifr_) { this._prepareIfr(); } this.ifr_.src = 'YourCustomProtocol://__message_send__?msg=' + encodeURIComponent(str); }
當 app 執行完 js 調用的功能,執行結果沒法直接返回,爲了返回結果,廣泛採用回調函數方式——js 層記錄一個 callback,app 經過 UIWebView 的 stringByEvaluatingJavaScriptFromString 函數調用這個 callback(相似 jsonp 的機制)。緩存
注意,這樣封裝的接口,自然是異步接口。由於 js_sendMessageToNativeAsync 這個函數會當即返回,不會等到執行結果發回來。安全
因此,咱們要想辦法把 js 代碼「阻塞」住。app
請回憶一下,js 中是用什麼方法能把 UI 線程代碼「阻塞」住,同時又不跑滿 CPU?框架
var async = false; var url = 'http://baidu.com'; var method = 'GET';
var req = new XMLHttpRequest();
req.open(method, url, async);
req.send(null);
「同步」ajax(其實沒這個詞,ajax 內涵異步的意思)能夠!在 baidu 的響應沒返回以前,這段代碼會一直阻塞。通常來講同步請求是不容許使用的,有致使 UI 卡頓的風險。可是在這裏由於咱們並不會真的去遠端請求內容,因此不妨一用。
至此實現方式已經比較清楚了,梳理一下思路:
那麼,如何攔截請求呢?你們知道,UIWebViewDelegate 是不會攔截 XMLHttpRequest 請求的,可是 iOS 至少給了咱們兩個位置攔截這類請求——NSURLCache 和 NSURLProtocol。
1、NSURLCache 是 iOS 中用來實現自定義緩存的類,當你建立了自定義的 NSURLCache 子類對象,並將其設置爲全局緩存管理器,全部的請求都會先到這裏檢查有無緩存(若是你沒禁掉緩存的話)。咱們能夠藉助這個性質攔截到接口調用請求,執行並返回數據。
- (NSCachedURLResponse*) cachedResponseForRequest:(NSURLRequest *)request { if ([request.HTTPMethod compare:@"GET" options:NSCaseInsensitiveSearch] != NSOrderedSame) { // 只對 get 請求作自定義處理 return [super cachedResponseForRequest:request]; } NSURL* url = request.URL; NSString* path = url.path; NSString* query = url.query; if (path == nil || query == nil) { return [super cachedResponseForRequest:request]; } LOGF(@"url = %@, path = %@, query = %@", url, path, query); if ([path isEqualToString:@"__env_get__"]) { // 讀環境變量 return [self getEnvValueByURL:url]; //* } else if ([path isEqualToString:@"__env_set__"]) { // 寫環境變量 return [self setEnvValueByURL:url]; } return [super cachedResponseForRequest:request]; }
注意註釋有 * 號的一行,便是執行 app 接口,返回結果。這裏的結果是一個 NSCachedResponse 對象,就不贅述了。
2、NSURLProtocol 是 Cocoa 中處理自定義 scheme 的類。這個類的使用更復雜一些,但它相比 NSURLCache 的好處是,可使用自定義協議 scheme,防止 URL 和真實 URL 混淆,而且自定義 scheme 在異步接口機制中也有使用,當你的 app 中同時存在兩種機制時,可使用 scheme 使得代碼更清晰。
+ (BOOL) canInitWithRequest:(NSURLRequest* _Nonnull)request { //只處理特定 scheme NSString* scheme = [[request URL] scheme]; if ([scheme compare:@"YourCustomProtocol"] == NSOrderedSame) { return YES; } return NO; } + (NSURLRequest* _Nonnull) canonicalRequestForRequest:(NSURLRequest* _Nonnull)request { return request; } - (BirdyURLProtocol* _Nonnull) initWithRequest:(NSURLRequest* _Nonnull)request cachedResponse:(NSCachedURLResponse* _Nullable)cachedResponse client:(id<NSURLProtocolClient> _Nullable)client { self = [super initWithRequest:request cachedResponse:cachedResponse client:client]; return self; } - (void) startLoading { NSURLRequest* connectionRequest = [self.request copy]; NSCachedURLResponse* cachedResponse = [[YourURLCache sharedURLCache] cachedResponseForRequest:connectionRequest]; if (cachedResponse != nil) { NSURLResponse* response = cachedResponse.response; NSData* data = cachedResponse.data; [[self client] URLProtocol:self didReceiveResponse:response cacheStoragePolicy:NSURLCacheStorageNotAllowed]; [[self client] URLProtocol:self didLoadData:data]; [[self client] URLProtocolDidFinishLoading:self]; } else { NSError* error = [NSError errorWithDomain:@"Bad Hybrid Request" code:400 userInfo:nil]; [[self client] URLProtocol:self didFailWithError:error]; } }
注意,以上代碼我借用了 YourURLCache 的實現,實際這是不必的。只是爲了方便演示。
以上即是實現 javascript 「同步」調用 app 代碼的方法,其核心就是使用同步 XMLHttpRequest 阻塞代碼,以及 app 層攔截請求。事實上,這個方法和操做系統以及開發框架無關,在 Android 系統中,也能夠實現這樣的機制。