如何实现 javascript “同步”调用 app 代码

在 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 卡顿的风险。可是在这里由于咱们并不会真的去远端请求内容,因此不妨一用。

至此实现方式已经比较清楚了,梳理一下思路:

  1. 使用同步 XMLHttpRequest 配合特殊构造的 URL 通知 app层。
  2. app 层拦截请求执行功能,将结果做为 Response 返回。
  3. XMLHttpRequest.send() 返回,经过 status 和 responseText 获得结果。

那么,如何拦截请求呢?你们知道,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 系统中,也能够实现这样的机制。

相关文章
相关标签/搜索