Web 与 App 数据交互原理和实现

背景

点击图片查看大图已经成了用户浏览页面时的一种习惯,原生 App 往往都实现了这些图片处理功能(点击查看、缩放、保存、滑动浏览等)。对于 Web 页面,为了更好的体验,一般开发者都会自己实现或是使用三方的图片处理框架。但如果一个 Web 页面能在一些特定的原生 App 中打开,那完全可以让 App 去代理处理这些图片。毕竟原生 App 的体验会更好,而且同一个 App 内点击原生图片和 Web 里面的图片,体验应该是一致的。

所以需求很简单,就是Web 页能直接调用原生的图片显示功能嘛!

交互原理

这个需求背后要解决的问题,实际上是要通过 Web 与原生的交互,让 Web 把图片资源交给原生应用去处理。

iOS7 之后苹果推出 JSCore,通过获取 Web 上下文环境,实现了 App 可以直接调用 JS 方法,或者将 block 赋值给 JS 方法来实现 Web 调用 App 并传值。所以在不考虑安卓的情况下,可以通过 JSCore 实现 Web 与 App 交互。

JSCore

App 端

// 获取JS上下文
 JSContext *jsContext;
-(void)WebViewDidFinishLoad:(UIWebView *)webView {
  jsContext = [_webview valueForKeyPath:@"documentView.webView.mainFrame.javaScriptContext"];
}

// App 调 Web insertText 事件
JSValue *funcValue = jsContext[@"insertText"];
[funcValue callWithArguments:@[@"hello web!"]];

// Web 调 App,App注册事件名为callNative
jsContext[@"callNative"] = ^(NSString*str){
  NSLog(str);
};

Web 端

// 接收 App 调用
function insertText(text) {
  ...
}

// 调用 App
btnNode.onclick = () => {
  callNative('hello app!');
}

这种方式使用简单,但大多数情况需要兼容 iOS 与安卓,所以需要找到更合适的方式。

传统方式

苹果推出 JSCore 以前,App 调用 Web 只能通过 WebView 执行 JSString 来实现。而 Web 没法直接调用 App,只能触发特定链接,让 App 在 WebView 代理方法中捕获到这特定链接,从而执行相应操作,间接实现 Web 调 App。这种传统方式也适用于安卓端的的实现。

App 调用 Web

[_webView stringByEvaluatingJavaScriptFromString:jsString];

这里的 JSString 是一个 JS 方法的调用,这个方法能给 Web 传值的前提是在 Web 端定义一个全局方法如:

function handlerMessageFromApp(data) {
  ...
}

那 App 端需要去拼接出这个 JSString 然后再调用 Webview 的 stringByEvaluatingJavaScriptFromString 方法:

NSString* jsString = [NSString stringWithFormat:@"handlerMessageFromApp('%@');", messageJSON];

[_webView stringByEvaluatingJavaScriptFromString:jsString];

App 调用 Web 就是利用这种 Webview 去执行一个 JS 方法的方式去实现。JS 方法可以直接返回值给 App,但通常情况下 Web 端收到 App 的消息会去进行一些异步操作,这样在 handlerMessageFromApp 中直接返回就不太合适了。此时需要有另外一个流程去保证 Web 端把消息传递给 App,也就是下面会说的 Web 调 App。

Web 调用 App

Web 调 App 则需要双方事先沟通定好协议。比如如果要点击 Web 页面链接跳转到 App 主页,那可以将协议的名称定为 xr://home,HTML 内容为 <a href="xr://home">查看主页>></a>,点击链接就会发生 url 的改变,同时原生 WebView 的代理方法也会被触发。通过在代理方法中监听 url 的变化实现约定的协议。

// 在 webview 代理方法中处理
- (BOOL)webView:(UIWebView *)webView shouldStartLoadWithRequest:(NSURLRequest *)request navigationType:(UIWebViewNavigationType)navigationType {
    NSURL *url = [request URL];
    if  (url == "xr://home") {...}
}

如果基于这种方式让 Web 给 App 传值可以有两种方式:
1. 直接在协议后面拼接参数如 xr://message?name=carson,这种方式适合于简单的值传递,但对于复杂结构的数据传递这种方式不合适。
2. Web 端 与 App 端定好消息协议如:xr://message,Web 端要发送消息给 App 端时触发该协议,同时将要传递的值放在页面上,App 端监听到该协议变化时从 Web 页面上取值。这种方式适合传递复杂数据。

现实开发中的传值往往都是复杂结构的,所以我们选择第2种方式去完成 Web 端调用 App 端。

function getMessageFromWeb() {
  return messageObject;
}

基于前面的经验在 Web 端实现方法 getMessageFromWeb,这个方法负责返回要传递给 App 的值。

id messages = [_webView stringByEvaluatingJavaScriptFromString:@"getMessageFromWeb()"];

当特定的消息协议被触发时 App 通过 Webview 执行带有返回值的 JS 方法拿到数据,这就间接实现 Web 调用 App 并传值的原理。

但这种方式 Web 端每次调用时都需要触发特定协议,同时将全局变量 messageObject 赋值。我们往往希望有一个抽象层来做这些事情,每当调用的时候作为调用方最好是能一个方法传递消息名称与消息内容就行了,接收方也只需要按消息名称接收消息内容。Web 端与 App 端都应该具备这样一个抽象层,这样具体的两端交互就简化成了一端只管调用另一端只管接收。有了这样一个抽象层再来看 Web 端调 App 端就好比:

  1. W 委托 B 发消息给 A 说有包糖要给他,此时 W 已经把糖交给了 B。
  2. B 发出通知给 A。
  3. A 收到通知后跑过来从 B 这里拿走了糖。

这样一个单向的 W 把消息发送给 A 就完成了,原理还是基于前面 Web 调用 App 的原理。因为有 B 的存在此时 W 要做的事情少了很多。

完整的调用

上面的过程只是最简单的情况,是一个单向的,正常情况下 W 给 A 送了包糖或许还想知道 A 什么时候吃完,吃完了 W 可以再去买。

  1. W 委托 B 发消息给 A 说有包糖要给他,此时 W 已经把糖交给了 B。
  2. B 发出通知给 A。
  3. A 收到通知后跑过来从 B 这里拿走了糖。
  4. A 吃完糖后再通知到 B 说他吃完了。
  5. B 最后通知到 W,W 再去买糖。

步骤4与步骤5代表 App 回调 Web。W 最后再买糖相当于回调函数的实现,这是在 W 送糖的时候就已经决定的事情。 因为整个一去一回的过程并非同步,所以这个地方就需要处理好这样一个映射:消息 —> 回调处理函数。也就是说每条发送的消息对应上各自的回调处理函数,这就需要 B 去维护这样一个映射关系。B 给要发送的消息加上 callbackId,同时以 { callbackId: callbackHandler } 的方式将回调处理函数存储起来。A 收到了有 callbackId 的消息再响应时又回继续带上这个 callbackId,最后 B 按照 callbackId 找到回调处理函数去执行。这样才是一个完整的带有回调处理的 Web 调用 App 并传值。

所以关键任务是去实现 B 这样一个抽象层,B 的任务很重,它是一个 Web 端与 App 端交互的桥梁,它要具备发送消息提示、存储消息内容、存储消息回调、 接收消息、执行消息回调等功能。

实现这样一个抽象层并不算太难,但重复造轮子的事就不做了,前面描述的过程也正是参照第三方库 WebViewJavascriptBridge 的实现,B 也正是这个框架的核心 bridge,其整个实现基于观察者模式,在最基本的原理上加上了一些封装,抽象出一层 bridge 负责两端的交互,最终暴露给开发者的只有简单的 API(初始化、注册、调用)。

正如 bridge 这个名字一样,它起着桥梁作用,实现了两端的数据交互。这两个 bridge 基本实现了相同的功能,唯一的区别在于 Web 端这边的 bridge 没法直接发送消息内容,只能告诉 App 端有消息。

应用

回到最开始的需求,还是在 WebViewJavascriptBridge 的基础上去实现让 App 去处理 Web 里的图片。做过图片浏览的应该都知道,实现图片浏览需要提供所有图片的数组 array 以及当前点击图片的 index。所以给 img 标签绑定点击事件,再获取所有图片的数组,最后利用 JSBridge 传给 App 即可。至于第三方库的接入可以去查看官方文档。

App 端注册 handler,用于处理接收图片:

[self.bridge registerHandler:@"previewImage" handler:^(id data, WVJBResponseCallback responseCallback) {
    // 处理图片 data
}]

相应的 Web 端要去 callHandler:

// setup 去触发 App 将 bridge 注入 window
setupWebViewJavascriptBridge(function(bridge) { 
  bridge.callHandler('previewImage', { urls: [ /* ... */ ], index: 0 });
}

就这么简单的一端管接收另一端管调用就完成了 Web 将图片传给原生。
熟悉微信网页开发的应该知道微信也提供了相关功能,微信中的网页可以利用微信的 JSSDK 使用原生提供的一整套图片处理功能:相册、相机、裁剪、上传、下载、图片浏览...


App 与 Web 交互的场景还有很多,比如 Web 页自定义分享、控制 App 页面跳转,随着小程序的出现 Web 端与 App 的交互也是更加有了深度。总之原理不难,掌握了原理才能做出更好更多的事情。

1+

我们正在招聘Java工程师,欢迎有兴趣的同学投递简历到 rd-hr@xingren.com

发表评论

电子邮件地址不会被公开。 必填项已用*标注