如何实现javascript“同步”调用app代码在App混合开发中,app层向js层提供接口有两种方式,一种是同步接口,一种一异步接口(不清楚什么是同步的请看这里的讨论)。为了保证web流畅,大部分时候,我们应该使用异步接口,但是某些情况下,我们可能更需要同步接口。同步接口的好处在于,首先js可以通过返回值得到执行结果;其次,在混合式开发中,app层导出的某些api按照语义就应该是同步的,否则会很奇怪——一个可能在for循环中使用的,执行非常快的接口,比如读写某个配置项,设计成异步会很奇怪。那么如何向js层导出同步接口呢?我们知道,在Android框架中,通过WebView.addJavascriptInterface()这个函数,可以将java接口导出到js层,并且这样导出的接口是同步接口。但是在iOS的Cocoa框架中,想导出同步接口却不容易,究其原因,是因为UIWebView和WKWebView没有addJavascriptInterface这样的功能。同时,Android这个功能爆出过安全漏洞,那么,我们有没有别的方式实现同步调用呢?我们以iOSUIWebView为例提供一种实现,WKWebView和Android也可以参考。为了找到问题的关键,我们看一下iOS中实现js调用app的通行方法:首先,自定义UIWebViewDelegate,在函数shouldStartLoadWithRequest:navigationType:中拦截请求。-(BOOL)webView:(UIWebView*_Nonnull)webViewshouldStartLoadWithRequest:(NSURLRequest*_Nonnull)requestnavigationType:(UIWebViewNavigationType)navigationType{if([request.HTTPMethodcompare:@GEToptions:NSCaseInsensitiveSearch]!=NSOrderedSame){//不处理非get请求returnYES;}NSURL*url=request.URL;if([url.schemeisEqualToString:@'YourCustomProtocol']){return[selfonMyRequest:request];}returnYES;}这种做法实质上就是将函数调用命令转化为url,通过请求的方式通知app层,其中onMyRequest:是自定义的request响应函数。为了发送请求,js层要建立一个隐藏的iframe元素,每次发送请求时修改iframe元素的src属性,app即可拦截到相应请求。/***js向native传递消息*@methodjs_sendMessageToNativeAsync*@memberofJSToNativeIOSPolyfill*@public*@paramstr{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代码“阻塞”住。请回忆一下,js中是用什么方法能把UI线程代码“阻塞”住,同时又不跑满CPU?varasync=false;varurl='='GET';brvarreq=newXMLHttpRequest();brreq.open(method,url,async);brreq.send(null);“同步”ajax(其实没这个词,ajax内涵异步的意思)可以!在baidu的响应没返回之前,这段代码会一直阻塞。一般来说同步请求是不允许使用的,有导致UI卡顿的风险。但是在这里因为我们并不会真的去远端请求内容,所以不妨一用。至此实现方式已经比较清楚了,梳理一下思路:使用同步XMLHttpRequest配合特殊构造的URL通知app层。app层拦截请求执行功能,将结果作为Response返回。XMLHttpRequest.send()返回,通过status和responseText得到结果。那么,如何拦截请求呢?大家知道,UIWebViewDelegate是不会拦截XMLHttpRequest请求的,但是iOS至少给了我们两个位置拦截这类请求——NSURLCache和NSURLProtocol。一、NSURLCache是iOS中用来实现自定义缓存的类,当你创建了自定义的NSURLCache子类对象,并将其设置为全局缓存管理器,所有的请求都会先到这里检查有无缓存(如果你没禁掉缓存的话)。我们可以借助这个性质拦截到接口调用请求,执行并返回数据。-(NSCachedURLResponse*)cachedResponseForRequest:(NSURLRequest*)request{if([request.HTTPMethodcompare:@GEToptions:NSCaseInsensitiveSearch]!=NSOrderedSame){//只对get请求做自定义处理return[supercachedResponseForRequest:request];}NSURL*url=request.URL;NSString*path=url.path;NSString*query=url.query;if(path==nil||query==nil){return[supercachedResponseForRequest:request];}LOGF(@url=%@,path=%@,query=%@,url,path,query);if([pathisEqualToString:@__env_get__]){//读环境变量return[selfgetEnvValueByURL:url];//*}elseif([pathisEqualToString:@__env_set__]){//写环境变量return[selfsetEnvValueByURL:url];}return[supercachedResponseForRequest:request];}注意注释有*号的一行,即是执行app接口,返回结果。这里的结果是一个NSCachedResponse对象,就不赘述了。二、NSURLProtocol是Cocoa中处理自定义scheme的类。这个类的使用更复杂一些,但它相比NSURLCache的好处是,可以使用自定义协议scheme,防止URL和真实URL混淆,并且自定义scheme在异步接口机制中也有使用,当你的app中同时存在两种机制时,可以使用scheme使得代码更清晰。+(BOOL)canInitWithRequest:(NSURLRequest*_Nonnull)request{//只处理特定schemeNSString*scheme=[[requestURL]scheme];if([schemecompare:@YourCustomProtocol]==NSOrderedSame){returnYES;}returnNO;}+(NSURLRequest*_Nonnull)canonicalRequestForRequest:(NSURLRequest*_Nonnull)request{returnrequest;}-(BirdyURLProtocol*_Nonnull)initWithRequest:(NSURLRequest*_Nonnull)requestcachedResponse:(NSCachedURLResponse*_Nullable)cachedResponseclient:(idNSURLProtocolClient_Nullable)client{self=[superinitWithRequest:requestcachedResponse:cachedResponseclient:client];returnself;}-(void)startLoading{NSURLRequest*connectionRequest=[self.requestcopy];NSCachedURLResponse*cachedResponse=[[YourURLCachesharedURLCache]cachedResponseForRequest:connectionRequest];if(cachedResponse!=nil){NSURLResponse*response=cachedResponse.response;NSData*data=cachedResponse.data;[[selfclient]URLProtocol:selfdidReceiveResponse:responsecacheStoragePolicy:NSURLCacheStorageNotAllowed];[[selfclient]URLProtocol:];[[selfclient]URLProtocolDidFinishLoading:self];}else{NSError*error=[NSErrorerrorWithDomain:@BadHybridRequestcode:400userInfo:nil];[[selfclient]URLProtocol:selfdidFailWithError:error];}}注意,以上代码我借用了YourURLCache的实现,实际这是没必要的。只是为了方便演示。以上便是实现javascript“同步”调用app代码的方法,其核心就是使用同步XMLHttpRequest阻塞代码,以及app层拦截请求。事实上,这个方法和操作系统以及开发框架无关,在Android系统中,也可以实现这样的机制。