1 背景
APP 开发过程中,为了追求开发效率、更新成本、性能和交互体验的平衡,经常会采取 Hybrid 的 APP 端架构。用基于 HTML5 的 WEB APP 实现易变的业务部分,用原生代码实现对效率、权限、数据交换等有要求的功能部分,然后通过 JS Bridge 打通两者,实现 JS 与 原生代码的相互调用,完成整个产品功能。
但谈到 APP 开发,大家都知道至少存在两个平台,那就是 Android 和 iOS。这两个系统采取不同的原生开发语言,也有不同的 Webview 浏览器环境。但 WEB APP 是跨平台的,所以跨浏览器的调用总归需要在一个层面上得到统一,这样才不需要专门针对两个平台开发不同的 WEB APP。
下面先对在目前的技术框架下有哪些 JS - NA 相互调用方式做一下综合介绍,然后基于上述技术提出几种跨平台 JS Bridge API 统一设计思路,最后扩展讨论下 JS Bridge 设计中的一些值得注意的点。
2 在原生代码中调用 JS 代码
2.1 Android Platform
loadUrl 方法
Android Webview 的 loadUrl 接口,可以直接在 Java 代码中执行 Javascript 脚本。在 API 23(Android 6.0)及之前,这里的 Javascript 脚本能够获取当前加载页面的变量,甚至执行当前加载页面里定义好的函数。也就是说,传入的 JS 脚本是在当前加载页面的上下文中执行的。
// Javascript: 在网页内定义一个函数,显示一个消息,返回一个内容 function propose(msg) { alert(msg); return "Yes!"; } // Java: 执行当前加载页面中定义好的一个函数 propose() webView.loadUrl("javascript:propose('Will you merry me?');");
可惜的是,这种方法:
- 只能执行 JS,无法获取返回结果,需要用其它的方式(下文介绍)获取返回结果;
- 而且会触发一次页面的刷新,可能会导致焦点丢失,软键盘消失之类的问题;
- 在 Android 7.0 以后,存在兼容性问题;
evaluateJavascript 方法
不过,如果 APP 适配的版本在 API 19(Android 4.4)以后,也可以使用 Webview 的 evaluateJavascript 接口 。这也是更为推荐的做法,因为避免了上面 loadUrl 的问题。
// Java: 执行当前加载页面中定义好的一个函数 propose() webView.evaluateJavascript("propose('Will you merry me?')", new ValueCallback() { @Override public void onReceiveValue(String answer) { // 拿到 answer 是 "Yes!" } });
间接方法:Web Event 分发
这种方法很少有人提到,因为它是一种间接的调用方法。Web Event 接口提供了一种在 DOM 里进行广播的机制,那也就意味着原生代码可以不知晓 JS 的函数名,而只是广播一个事件,由页面内的 JS 决定是否处理这个 Event。这能够避免 JS 代码执行的异常,更常用于原生代码主动通知页面某些信息更新的场景。
// Javascript: 在网页内定义一个函数,显示一个消息,返回一个内容 function propose(e) { alert(e.msg); return "Yes!"; } // 注册 WebDomEvent handler window.addEventListener("propose_event", propose);
// Java: webView.evaluateJavascript("var e=new Event('propose_event'); e.msg='Will you merry me?'; window.dispatchEvent(e);", new ValueCallback() { @Override public void onReceiveValue(String answer) { // nothing } });
这种方法也存在无法获取返回结果的问题,也需要用其它的方式(下文介绍)获取返回结果。不过在使用到 Event 通知的场景下,我们一般也不需要返回。
2.2 iOS Platform
讲到 iOS,必须提到两个不同的 WebView,一个是过时但广泛使用的 UIWebView,另一个是建议且逐渐流行的 WKWebView。
UIWebView: stringByEvaluatingJavaScriptFromString 方法
UIWebView 提供了 stringByEvaluatingJavaScriptFromString 接口 ,并且能够获得返回结果。
// OC: 执行当前加载页面中定义好的一个函数 propose() [_webView stringByEvaluatingJavaScriptFromString:@"propose('Will you merry me?')"];
这个方法的主要问题在于,它是一个同步的方法。它可能会阻塞 UI 线程,不太适合执行复杂的调用。
UIWebView: JavaScriptCore
在 iOS 7 之后,苹果提供了一个获取 UIWebView 中 JSContext 的方法,直接将 JS 执行环境暴露给原生代码。这样就可以在原生代码中任意执行 JS 代码了。同时,这个接口也可以用于 JS 调用原生代码的能力,下文中会介绍。
// OC: 获取 JSContext JSContext *context = [_webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"] [context evaluateScript:@"propose('Will you merry me?')"];
WKWebView: evaluateJavaScript 方法
可以看到,JavaScriptCore 使用起来极其方便,但在 WKWebView 中我们享受不到这种方便了。因为 WKWebView 的页面渲染是在独立的进程中,在当前进程无法直接拿到 JSContext。
不过 WKWebView 提供了一个更好的 evaluateJavaScript 接口 ,可以传入一个回调函数,实现了 JS 的异步调用。
// OC: 执行当前加载页面中定义好的一个函数 propose() [_webView evaluateJavaScript:@"propose('Will you merry me?')" completionHandler:^(id _Nullable result, NSError * _Nullable error) { // 拿到 result 是 "Yes!", error 是 nil }];
可以看到,evaluateJavaScript 接口与上文 Android evaluateJavaScript 接口极为类似。
间接方法:Web Event 分发
当然,由于 Event 接口是 WEB 标准,iOS 上也可以同样进行 Event 分发。场景和作用请看上文,不再赘述,简单代码如下:
// Javascript: 在网页内定义一个函数,显示一个消息,返回一个内容 function propose(e) { alert(e.msg); return "Yes!"; } // 注册 WebDomEvent handler window.addEventListener("propose_event", propose);
// OC: 执行当前加载页面中定义好的一个函数 propose() [_webView evaluateJavaScript:@"var e=new Event('propose_event'); e.msg='Will you merry me?'; window.dispatchEvent(e);" completionHandler:^(id _Nullable result, NSError * _Nullable error) { // nothing }];
3 在 JS 代码中调用原生代码
3.1 Android Platform
addJavascriptInterface 方法
Android 从 API 1 就开始提供了 addJavascriptInterface 接口,用这个接口可以很方便地把原生的方法注入到 JS 上下文中,可以说比 iOS 做得好很多。
// Java: 定义一个类,提供一个接口,返回一个内容 class NativeApis { @JavascriptInterface public String propose(String msg) { return "Yes!"; } } webView.addJavascriptInterface(new NativeApis(), "Bridge");
// Javascript: 执行一个 native 的方法 alert(window.Bridge.propose("Will you merry me?"));
但问题在于在 API 17 (Android 4.2) 之前这个方法存在安全漏洞,攻击者可以执行任意代码。在 API 17 及以后,通过显式地给出 @JavascriptInterface 限定暴露的接口,避免了安全漏洞。但在 API 17 以前,不建议使用此方法,可以考虑下面的 work around。
URL 拦截:shouldOverrideUrlLoading
这是一种曲线救国的方式,那就是通过加载非标准 Scheme( 非 http/s, 非 ftp)的 URL,用一个非法(或者叫自定义)的 URL 传递参数。当页面中的 Javascript 动态插入一个 iframe 元素时,iframe 的 url 会被 WebView 通过 shouldOverrideUrlLoading 方法传给 WebViewClient 判断是否需要加载该 URL。在这里可以拦截自定义的 URL Scheme,通过 encode 到 URL 中的信息传递参数。
// Java: 解析要加载的 URL,如果是自定义 scheme,调用相关的函数 class MyWebViewClient extends WebViewClient { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { if (url.startsWith("bridge://")) { // 解析 // 后面的 action 和参数,调用相关的函数 } return true; } } webView.setWebViewClient(new MyWebViewClient());
// Javascript: 用不可见 iframe 打开一个自定义 URL,参数需要 urlencode bridgeFrame = document.createElement('iframe'); bridgeFrame.style.display = 'none'; bridgeFrame.src = 'bridge://propose?msg=Will%20you%20merry%20me%3F'; document.documentElement.appendChild(bridgeFrame);
URL 拦截的问题也是无法直接拿到原生代码的返回结果,需要用 URL 字符串参数传入一个回调函数,然后用上文讲到的原生代码调用 JS 的方式回调传回结果。
弹出框拦截
Android Webview 可以定义一些接口,重载 onJsAlert()、onJsConfirm()、onJsPrompt() 这些回调方法。当 JS 控制弹出框时,这些回调会被调用,进而可以通过约定的特殊内容格式判断是真正的弹出框,还是 JS 到 NA 的调用。由于 onJsPrompt 可以返回结果,所以更合适一些。
// Java: 重载 onJsPrompt 方法,提取 prompt 内容判断是否需要拦截 class MyWebViewClient extends WebChromeClient { @Override public boolean onJsPrompt(WebView view, String url, String message, String defaultValue, JsPromptResult result) { if (message.startsWith("bridge://")) { // 解析 // 后面的 action 和参数,调用相关的函数 result.confirm("Yes!"); } return true; } } webView.setWebViewClient(new MyWebViewClient());
// Javascript: 调用 prompt 弹框,使用特定内容格式以利于拦截 alert(window.prompt('bridge://propose?msg=Will%20you%20merry%20me%3F'));
Local Server
APP 可以在手机的本地地址 127.0.0.1 上启动一个 HTTP/WebSocket 服务,浏览器内的 JS 可以通过本地回环网络连接到这个服务,把 APP 视为一个服务端,进行正常的 B/S 通信,也可以实现在 JS 中调用原生代码。
使用这种方式时,额外注意一点是要进行有效地鉴权。因为除了 APP 内的 WebView,手机内其它的 APP 也可以访问这个服务,很可能会造成一些安全问题。所以可能需要 NA 在加载 Webview 的时候,通过 Cookie/URL参数/JS 上下文环境传入合法的 Token,才能保证其安全性。
还有一点是,如果不幸出现了端口冲突,需要有办法去解决。
3.2 iOS Platform
URL 拦截:shouldStartLoadWithRequest
UIWebView 原生并没有提供任何可以在 JS 代码中调用 NA 方法的 API,但 UIWebView 也可以通过与 Android 相同的方式进行 URL 拦截,进而间接实现 JS 到 NA 的传参。
// UIWebView - (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType;
这个方式在 WKWebView 上,依然有效,只是叫做 decidePolicyForNavigationAction
- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler;
UIWebview: JavaScriptCore
大概苹果官方也觉得这种方式太 ugly,所以后来在 iOS 7 以后,提供了一个好一些的接口,就是 JavaScriptCore。在页面加载完后,可以获取当前加载页面的 JavaScript 上下文执行环境 JSContext。然后可以把一些原生方法注入到 JSConext 中,这样页面内的 JS 就可以直接调用到这些注入的方法了。
// OC: 获取 JSContext,将原生方法注入进去 JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; context[@"propose"] = ^(msg) { return @"Yes!"; };
// Javascript: 调用 prompt 弹框,使用特定内容格式以利于拦截 alert(window.propose('Will you merry me?'));
WKWebView: WKScriptMessageHandler 方法
然后到了 WKWebView,JSContext 不好使了。不过 WKWebView 提供了另外一个方法,那就是 WKScriptMessageHandler。在创建一个 WKWebView 的时候,可以通过配置将一个 WKScriptMessageHandler 对象指针和 NAME 传进去。这样在加载页面中,通过 window.webkit.messageHandlers.NAME.postMessage 就可以将消息传给原生的 WKScriptMessageHandler 对象。
// OC: 编写 Message 回调,并注册 Message Handler @interface Brige : NSObject- (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { if ([message.name isEqualToString:@"Bridge"]) { // 处理 message } } ... _bridge = [[Brige alloc] init]; [[_webView configuration].userContentController addScriptMessageHandler:_bridge name:@"Bridge"];
// Javascript: 发消息给注入的 Message Handler window.webkit.messageHandlers.Bridge.postMessage("Will you merry me?");
WKScriptMessageHandler 同样也是无法直接返回结果。
WKWebView: 弹出框拦截
与 Android 类似,WKWebView 也提供了弹出框的回调函数,可以通过此类函数实现参数的传递。
// WKUIDelegate - (void)webView:(WKWebView *)webView runJavaScriptAlertPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(void))completionHandler; - (void)webView:(WKWebView *)webView runJavaScriptConfirmPanelWithMessage:(NSString *)message initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(BOOL result))completionHandler; - (void)webView:(WKWebView *)webView runJavaScriptTextInputPanelWithPrompt:(NSString *)prompt defaultText:(NSString *)defaultText initiatedByFrame:(WKFrameInfo *)frame completionHandler:(void (^)(NSString *result))completionHandler;
Local Server
见上文中对 Android Local Server 调用方式的讨论。
4 notify-fetch-run 间接机制
上文中讲到的很多还是较为直接的 JS-NA 相互调用方法,其实还有一些更开脑洞的方法。比如 notify-fetch-run 机制,不需要直接传递参数或者代码,只需要传递一个信号,然后通过可以共同访问的第三方传递真正的参数,进行执行。
4.1 notify
如果仅仅把相互调用简化成一个 0/1 信号,那除了上面讲到的内容,还有太多东西可以做为信号。比如 event,比如通过远程服务器通知之类,下面讲一个比较奇葩的方法:
notify 中的奇葩:online/offline event
HTML5 中有一对标准的 event,叫做 online/offline,可以反应当前浏览器的联网状况。而 WebView 呢,可以通过 webView.setNetworkAvailable() 来控制联网状态。那也就意味着,原生代码只要控制 webView 的联网状态变化,就可以发送 0/1 信号给 JS。JS 收到 0/1 信号后,可以通过下文 JS 调用原生的方式获取原生代码要传入的内容,然后执行这些内容。
这种方式最大的问题在于,需要非常精巧地设计整个状态流转。因为传入的信号信息量非常少,而且正常情况下网络状况的变化也会触发这两个 event。
4.2 fetch
fetch 也可以有很多种,只要是 JS 和 NA 都能访问到的目标,都可以做第三方信息交换。比如本地 socket,远端网站,或者本地文件 file://,或者 cookie,localstorage。
5 安卓 & iOS 统一 API
我们讨论 Android & iOS API 的统一,主要是在 JS 里的统一,因为只有 JS 是跨平台的。统一 API 有两种实现方法:
- 一种是通过封装的统一,就是说 JS 与原生代码的底层通信方式是不同的,但通过一个嵌入 WebView 的 JS 库实现 API 的统一。
- 另一种是无需封装的统一,也就是在底层通信的接口就保持了统一,在两端的 JS 代码上是完全一致的。
5.1 JS 调用原生代码
URL 拦截(Android & iOS)
从上文介绍的方法就可以直接看出,通过 URL 拦截实现 JS 调用原生代码是统一适用于所有平台的方法,而且没有版本限制。所以很多 JSBridge 都使用了这种方法以做到最大的兼容性。
// Android Java: 解析要加载的 URL,如果是自定义 scheme,调用相关的函数 class MyWebViewClient extends WebViewClient { @Override public boolean shouldOverrideUrlLoading(WebView view, String url) { Uri uri = Uri.parse(url); // FIXME 异常处理 if (uri.getScheme().contentEquals("bridge")) { if (uri.getAuthority().contentEquals("propose")) { view.evaluateJavascript(uri.getQueryParameter("callback") + "('Yes!')", null); } } else { view.loadUrl(url); } return true; } } webView.setWebViewClient(new MyWebViewClient());
// iOS OC: 解析要加载的 URL,如果是自定义 scheme,调用相关的函数 - (BOOL)webView:(UIWebView*)webView shouldStartLoadWithRequest:(NSURLRequest*)request navigationType:(UIWebViewNavigationType)navigationType { NSURL * url = [request URL]; if ([[url scheme] isEqualToString:@"bridge"] && [[url host] isEqualToString:@"propose"]) { NSArray *params =[url.query componentsSeparatedByString:@"&"]; for (NSString *paramStr in params) { if ([paramStr hasPrefix:@"callback"]) { NSArray *kv = [paramStr componentsSeparatedByString:@"="]; [webView stringByEvaluatingJavaScriptFromString:[kv[1] stringByAppendingString: @"('Yes!')"]]; } } return NO; } return YES; }
// 统一的 Javascript: 用不可见 iframe 打开一个自定义 URL,参数需要 urlencode bridgeFrame = document.createElement('iframe'); bridgeFrame.style.display = 'none'; bridgeFrame.src = 'bridge://propose?msg=Will%20you%20merry%20me%3F&callback=showResult'; document.documentElement.appendChild(bridgeFrame);
这种方法的问题:
- 对 URL 格式有 UrlEncode 的要求,对于要传递复杂参数的情况不友好。比如我们需要在参数中传递一个正常的 URL,就需要对这个参数进行两次 UrlEncode,才能保证解码不出问题。
- 通过 iframe 打开 URL 的方式不太直观,也缺少调用成功的返回确认,需要在 JS 端再封装一下。
对象植入(Android & iOS UIWebView)
放宽兼容性限制,Android 不再兼容 4.1 及以前版本,iOS 不再兼容 iOS 6 及以前版本。那就可以直接通过 Android 的 addJavascriptInterface 和 iOS 的 JSContext 实现将要调用的方法以对象的方式注入到 JS 上下文中,同时也可以直接获得返回结果。
// Android Java: 定义一个类,提供一个接口,返回一个内容 class NativeApis { @JavascriptInterface public String propose(String msg) { return "Yes!"; } }; webView.addJavascriptInterface(new NativeApis(), "Bridge");
// iOS OC: 定义一个类,提供一个接口,返回一个内容 // *.h #import@protocol BrigeProtocol - (NSString *)propose:(NSString *)msg; @end @interface Bridge : NSObject @end // *.m // 永远返回 Yes @implementation Bridge - (NSString *)propose:(NSString *)msg { return @"Yes!"; } @end ... // 注意生命周期 bridge = [[Bridge alloc] init]; ... JSContext *context = [webView valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"]; context[@"Bridge"] = bridge;
// 统一的Javascript: 执行一个 native 的方法 showResult(window.Bridge.propose("Will you merry me?"));
对象植入(Android & iOS WKWebView)
如果使用 WKWebView,那就意味着进一步放宽了兼容性限制,因为 WKWebView 不支持 iOS 7 及以前版本。上文说到,WKWebView 不支持 JavaScriptCore,但提供了一个 WKScriptMessageHandler 方法。这也意味着我们只能将调用方式尽量往 WKWebView 的方式上统一。
WKWebView 注入的对象,只能使用 postMessage 接口,而且是注入到 window.webkit.messageHandlers 。虽然 Android 的 addJavascriptInterface 不能实现属性的注入,也就是说我们无法在原生代码中在 JS 上下文中添加一个 window.webkit.messageHandlers.NAME 这样一个对象。但我们可以在 WKWebView 中通过 addUserScript 注册一个加载页面时就执行的脚本,将 window.webkit.messageHandlers.NAME 赋给 window.NAME,就实现在注入对象层面的统一。即 Android 和 iOS 里的 Brige 对象都注入到了 window 下。
然后 Android addJavascriptInterface 注入的对象也实现一个与 WKWebView 类似的 postMessage 接口,那么两端就实现了底层接口上的统一。
// Android Java: 定义一个类似于 WKScriptMessageHandler 的类 class NativeApis { private WebView mWebView; public NativeApis(WebView webview) { mWebView = webview; } @JavascriptInterface public void postMessage(String msg) { try { JSONObject json_obj = new JSONObject(msg); final String callback = json_obj.getString("callback"); // JS 是异步线程,转到 UI 线程执行 JS mWebView.post(new Runnable() { @Override public void run() { mWebView.evaluateJavascript( callback + "('Yes!')", null); } }); } catch (JSONException e) { Log.i("Bridge", "postMessage: " + e.getMessage()); } } }; // 初始化 NativeApis 时多一个 webView 句柄 webView.addJavascriptInterface(new NativeApis(webView), "Bridge");
// iOS OC: 定义 WKScriptMessageHandler 处理接口 - (void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { // 解析 JSON,调用 callback 返回数据 NSData *jsonData = [message.body dataUsingEncoding:NSUTF8StringEncoding]; NSDictionary * msgBody = [NSJSONSerialization JSONObjectWithData:jsonData options:kNilOptions error:nil]; NSString *callback = [msgBody objectForKey:@"callback"]; [message.webView evaluateJavaScript: [NSString stringWithFormat:@"%@('Yes!')", callback] completionHandler:^(id _Nullable result, NSError * _Nullable error) { // FIXME 出错处理 }]; } ... [[_webView configuration].userContentController addScriptMessageHandler:self name:@"Bridge"]; // 将 window.webkit.messageHandlers.Bridge 改名成 window.Bridge 与 Android 统一 WKUserScript* userScript = [[WKUserScript alloc]initWithSource:@"if (typeof window.webkit != 'undefined' && typeof window.webkit.messageHandlers.Bridge != 'undefined') { window.Bridge = window.webkit.messageHandlers.Bridge;}" injectionTime:WKUserScriptInjectionTimeAtDocumentStart forMainFrameOnly:YES]; [[_webView configuration].userContentController addUserScript:userScript];
// 统一的Javascript: 给 Native 发送一个消息,通过回调返回结果 message = { func: "propose", options : { msg: "Will you merry me?" }, callback: showResult.name }; window.Bridge.postMessage(JSON.stringify(message));
5.2 原生代码调用 JS
JS 调用原生代码,主要目的是为了增强 JS 的能力。而原生代码调用 JS 大部分情况下主要是为了便捷 JS 的调用,这可以分为三种情况:
- 主动设置上下文。每次加载页面必须执行一些 setup,将一些 JS 环境设置好,不需要每次都从服务器端获取。比如上文中讲到的 addUserScript 添加一个加载页面时的上下文环境。
- 主动发起与 JS 交互。在某些比较少见的场合下,原生代码可能想要主动将一些信息通知给 JS,尤其是一些不在官方 HTML5 支持能力的事件,比如语音的输入、扫码的结果、调用失败等等。
- 最常见的,是被动的回调 JS。也就是 JS 发起了一个调用,由于调用方式的限制无法返回,或者需要较长时间才能拿到结果,这就需要原生代码在执行完调用后通过回调回传给 JS。
主动设置上下文不需要 API 的统一。
主动发起与 JS 的交互场景比较少,可以有两种方法实现:一种是页面加载过程中将回调注册给 NA;另一种是通过 Web Event 的方式由 NA 广播给 JS 上下文。我们更建议通过 Web Event 的方式广播,这样不受页面加载状态之类的限制,交互上更简单。当然,也可以两种方法结合,增加一个 Event 到 NA 的注册,保证有效广播。
被动的回调 JS,实现上比较直观,只要在 JS 调用 NA 的接口中增加一个 callback 参数,NA 在完成之后回调记录下来的接口即可。
6 JS Bridge 设计上的更多考虑
6.1 是否使用第三方 JS Bridge 库
使用第三方 JS Bridge 库,理论上能避免很多烦恼,按照它的 step by step 指引,很容易就能配出来一个可以工作的 JS Bridge 环境。
但第三方库也有一些缺点。前面讲到,第三方库为了易用,往往在 NA 层和 JS 层都会做一套新的 Adapter API 封装,但不好意思的是,它提供的仍然是一套通用 API 封装,往往应用方还得在上面再封装一层业务 API。这也就意味着,每次 JS-NA 的调用,需要走下面的一套流程:
中间的三层是由第三方库实现的。如果不熟悉第三方库的代码,或者说第三方库在这三层做了过重的封装,那调试问题就会非常困难。
我上文讲到无需二次封装的统一 API,就是希望通过选取合适的 Bridge 方法,把 JS Adapter 这一层去掉或者让它尽量地薄。这样整个调用过程能得到充分地简化,更便于问题的追查和整体的设计。
第三方库还有一个问题就是,它往往追求大而全。比如有些第三方库就是想非常完整地支持 Hybrid App 的设计,但很多时候我们往往仅需要有限个接口调用而已。为了实现有限地一些功能,还得去了解第三方库的整体设计,有时候代价也高了些。
6.2 参数约束
由于 Javascript 是弱类型的语言,而 Java 和 OC 都是强类型的,在参数的互相传递时,需要进行严格的检查。虽说 addJavascriptInterface 等方法可以动态地注入无数个对象或者方法,但仍然不建议这样做,因为维护成本太高。就像 URL 拦截一样,搭桥的路有一条就足够了。
JS Bridge 的接口,就像是一个 RPC 协议。这个 RPC 协议需要有一个版本,这样我们知道哪些版本有哪些 API,更利于有效地调用。这个 RPC 协议需要约定哪些固定的字段,这样我们可以用在入口统一校验字段是否完整,字段类型是否可用。
6.3 出错信息
跨平台的接口,很多时候 DEBUG 比较困难,尤其是上文讲到一些方式无法直接返回结果,自然也无法直接返回错误。所以在接口上,要尽量考虑出错时错误信息的回传通道,例如接口需要提供出错的 callback。
那么问题来了,如果 callback 参数也写错了怎么办?总不能让 FE 看 APP 的 log 吧?
所以建议在接口设计上,增加一个全局错误的 Web Event,就像 Linux 系统下的 errno。任何 JS 调用 NA 失败或者回调失败,都通过这个 Event 分发出去,这样前端就很容易知道错在哪里了。
6.4 API 安全性
虽然网页是在 APP 自己的 WebView 中打开的,但因为网页天然具有的超链接性质,也很难保证所有可以点开的页面都是可信的,比如有些时候活动的落地页可能会到第三方页面等。所以对一些影响 APP 运行逻辑的关键 API 接口,需要做站点的白名单控制,避免第三方站点调用此类 API。
7 总结
这篇文章列举了可用于 JS Bridge 的各平台技术实现,建议了几种无需二次封装的 Android & iOS 平台 JS Bridge 统一 API 的可选方案,讨论了设计一个简洁、规范、安全的 JS Bridge API 需要考虑的问题和解决思路。希望对读者能有所助益。