转转Hybrid-SDK重构和实践_webview

转转的移动端开发体系主要是基于 Hybrid 方案,但长久以来 Webview 容器和 SDK 管理等存在标准不统一、更新不及时的问题。随着转转/找靓机/采货侠等多环境开发场景越来越多,适配不同场景极大的影响了业务迭代效率。所有我们决定重新规划 SDK 的建设。在介绍方案之前。先了解一下基础知识。

JSBridge 的双向通信原理

JSBridge 是一种 JS 实现的 Bridge,连接着桥两端的 Native 和 H5。它在 APP 内方便地让 Native 调用 JS,JS 调用 Native ,是双向通信的通道。JSBridge 主要提供了 JS 调用 Native 代码的能力,实现原生功能如查看本地相册、打开摄像头、指纹支付等

一、JS 调用 Native

JS 调用 Native 的实现方式较多,目前主流采用是拦截 URL Scheme、MessageHandler。

1、拦截 URL Scheme

Web 端采用创建隐藏的 iframe 进行 Scheme 请求, Android 和 iOS 可以通过拦截 URL Scheme 并解析 Scheme 来是否进行对应的 Native 代码逻辑处理。

Android 端,Webview 提供了 shouldOverrideUrlLoading 方法来进行拦截 H5 发送的 URL Scheme 请求。代码如下:

public boolean shouldOverrideUrlLoading(WebView view, String url){
//读取到url后自行进行分析处理

//如果返回false,则WebView处理链接url,如果返回true,代表WebView根据程序来执行url
return true;
}

iOS 的 WKWebview 可以根据拦截到的 URL Scheme 和对应的参数执行相关的操作。代码如下:

- (void)webView:(WKWebView *)webView decidePolicyForNavigationAction:(WKNavigationAction *)navigationAction decisionHandler:(void (^)(WKNavigationActionPolicy))decisionHandler{
if ([navigationAction.request.URL.absoluteString hasPrefix:@"xxx"]) {
[[UIApplication sharedApplication] openURL:navigationAction.request.URL];
}
decisionHandler(WKNavigationActionPolicyAllow);
}

Web 端通过动态的创建 iframe 和客户端通讯

function iosExecute (action, param) {
param['methodName'] = action
let iframe = env.createIframe()
let paramStr = JSON.stringify(param)
iframe.src = `zznative://zhuanzhuan.hybrid.ios/?infos=${encodeURIComponent(paramStr)}`
document.body.appendChild(iframe)
setTimeout(() => iframe.remove(), 300)
}

2、MessageHandler

基于 Webview 提供的能力,我们可以向 Window 上注入对象或方法。JS 通过这个对象或方法进行调用时,执行对应的逻辑操作,可以直接调用 Native 的方法。使用该方式时,JS 需要等到 Native 执行完对应的逻辑后才能进行回调里面的操作。

Android 的 Webview 提供了 addJavascriptInterface 方法,支持 Android 4.2 及以上系统。

gpcWebView.addJavascriptInterface(new JavaScriptInterface(), 'nativeApiBridge');
public class JavaScriptInterface {
Context mContext;

JavaScriptInterface(Context c) {
mContext = c;
}

public void share(String webMessage){
// Native 逻辑
}
}

iOS 的 UIWebview 提供了 JavaScriptScore 方法,支持 iOS 7.0 及以上系统。WKWebview 提供了 window.webkit.messageHandlers 方法,支持 iOS 8.0 及以上系统。UIWebview 在几年前常用,目前已不常见。以下为创建 WKWebViewConfiguration 和 创建 WKWebView 示例:

WKWebViewConfiguration *configuration = [[WKWebViewConfiguration alloc] init];
WKPreferences *preferences = [WKPreferences new];
preferences.javaScriptCanOpenWindowsAutomatically = YES;
preferences.minimumFontSize = 40.0;
configuration.preferences = preferences;


- (void)viewWillAppear:(BOOL)animated
{
[super viewWillAppear:animated];
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"share"];
[self.webView.configuration.userContentController addScriptMessageHandler:self name:@"pickImage"];
}
- (void)viewWillDisappear:(BOOL)animated
{
[super viewWillDisappear:animated];
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"share"];
[self.webView.configuration.userContentController removeScriptMessageHandlerForName:@"pickImage"];
}

Web 端通过调用客户端注入的全局变量进行通讯

window.zhuanzhuanMApplication.executeCmd(action, oriParam)

二、Native 调用 JS

Native 调用 JS 比较简单,只要 H5 将 JS 方法暴露在 Window 上给 Native 调用即可

Android 中主要有两种方式实现。在 4.4 以前,通过 loadUrl 方法,执行一段 JS 代码来实现。在 4.4 以后,可以使用 evaluateJavascript 方法实现。loadUrl 方法使用起来方便简洁,但是效率低无法获得返回结果且调用的时候会刷新 WebView。evaluateJavascript 方法效率高获取返回值方便,调用时候不刷新 WebView,但是只支持 Android 4.4+。相关代码如下:

webView.loadUrl("javascript:" + javaScriptString);
webView.evaluateJavascript(javaScriptString, new ValueCallback<String>() {
@Override
public void onReceiveValue(String value){
xxx
}
});

iOS 在 WKWebview 中可以通过 evaluateJavaScript:javaScriptString 来实现,支持 iOS 8.0 及以上系统。

[jsContext evaluateJavaScript:@"zhuanzhuanMApplication(ev, data)"]

开源通讯方案介绍

上面讲完了原理。但是自己开发一套完整和好用的还是比较麻烦,现在我们就来介绍一下现在比较好的开源解决方案 DSBridge。

DSBridge 的主要特点:

Android、IOS、Javascript 三端易用,轻量且强大、安全且健壮。


  1. 同时支持同步调用和异步调用
  2. 支持以类的方式集中统一管理 API
  3. 支持 API 命名空间
  4. 支持调试模式
  5. 支持 API 存在性检测
  6. 支持进度回调:一次调用,多次返回
  7. 支持 Javascript 关闭页面事件回调
  8. 支持 Javascript 模态/非模态对话框
  9. 支持腾讯 X5 内核

还有一点可以介绍一下。dsBridge 如果和 Fly.js 一起使用。可以直接使用客户端的通讯能力。正如我们所知,在浏览器中,ajax 请求受同源策略限制,不能跨域请求资源。然而, Fly.js 有一个强大的功能就是支持请求重定向:将 ajax 请求通过任何 Javascript bridge 重定向到端上,并且 Fly.js 官方已经提供的 dsBridge 的 adapter, 可以非常方便的协同 dsBridge 一起使用。由于端上没有同源策略的限制,所以 fly.js 可以请求任何域的资源。另一个典型的使用场景是在混合 APP 中,由于 Fly.js 可以将所有 ajax 请求转发到端上,所以,开发者就可以在端上进行统一的请求管理、证书校验、cookie 管理、访问控制等。

转转面临的问题

介绍完基础原理和优秀的开源方案,现在来看看转转面临的问题。

1、通信协议不统一

转转使用自定义的通讯方案,有 200 个 API,找靓机采用了 dsbrige 通讯方案有 100 个 API

2、URL 参数含义和 UA 不统一

两端都有很多的 URL 参数和 UA 参数,没有一个统一的规范

3、找靓机 Webiview 分新老两个版本

在找靓机合并到转转时,我们做了技术栈统一。把转转的 Webview 迁移到找靓机,但是找靓机业务大量使用的是老的 Webview,所以为了保证老的业务能正常的运行,只能保持两套 Webview 并行使用

4、存在两套文档,不是很好的找到 API

5、SK 文档维护问题

转转的 SDK 文档为了保证准确性和有人维护。把 SDK 设计成一个对象。如果需要添加新的 SDK 需要通知维护人员添加。然后发版还能使用,文档和使用的测试 Demo 由维护人员编写。但是这样就造成 SDK 的体积越来越大。而且当客户端的同学发现一个文档错误,他们不能直接的修改文档,需要把问题反馈,还能修改。综上,导致 SDK 文档维护成本巨大。

基于此,我们和客户端团队成立了标准 Webview 小组,打造转转标准 Webview 容器。

转转解决方案

1、标准化相关: 客户端 Cookie 管理、Url Query 管理 、UA 规范、通讯格式规范

我们整理了所有的 cookie,url, ua, 通讯方案,并制定了规范转转Hybrid-SDK重构和实践_客户端_02

UA 的标准

App 的 UA 分为两个层面:

公共 UA:使用 zz{Key}/{Value} 的形式添加一个键值信息,Key 首字母大写,中间有空格分隔,末尾不包含空格。

自定义 UA:APP 内自定义格式。

公共 UA 与 自定义 UA 不做顺序要求,但公共 UA 中的 zzApp 字段一定是最后一个。

zzVersion/客户端版本号 zzT/客户端终端值 zzDevice/是否支持顶栏穿透_状态栏高度_设备像素比例 zzApp/App标识符

举例:zzVersion/8.18.20 zzT/16 zzDevice/1_44.0_3.0 zzApp/58ZhuanZhuan

Cookie 的标准

Cookie 的诞生背景:由于 HTTP 协议是无状态的,网站为了辨别用户身份向用户本地终端上储存数据,这就是 Cookie。

所以从 Cookie 设计上来讲,它是为了解决服务端在客户端保存数据问题的,这个存储容器的主动权应当在服务端,客户端不应过多干预,即使客户端拥有修改 Cookie 的 api,但除非用户清除缓存时,可以使用 cookie.clear() 方法之外,其余情况不建议对 Cookie 进行修改。

其次,cookie 会被附加在每个可以被携带的 HTTP 请求中。

所以如果客户端想要给 H5 传值,不建议采用通过 Cookie 的方式,因为这些 Cookie 会原封不动的携带给服务端,占用请求头部大小,造成流量浪费。

基于上面的考虑和调研,我们制定了 Cookie 的标准


  • 客户端理论上不再向 WebView 增加新的 Cookie
  • 只有通过了 Cookie 白名单的 Url 才会写入 Cookie
  • Cookie 某一条目不存在、条目的 value 为空或空串时,该条目应视为无效,客户端的无效条目可能为以上三种的任一形式
  • 客户端启动一个 WebView 页面时,在请求首个 Url 之前,会写入 Cookie ,如果该 Cookie 条目已存在则覆盖

Url Query 标准

Url Query 主要为了解决 H5 在执行 JS 方法之前的这段时间里,页面的样式、能力问题。例:加载 H5 页面时,希望页面背景为黑色,如果依赖 Api 实现,则执行 Api 之前页面为非黑色,效果不理想。

H5 加载失败后,容器层也能够进行设置的一些配置项(不建议在失败页面上支持过于丰富的配置项,失败页面仅支持少部分配置项即可)。例:H5 即使加载失败了,页面也可以侧滑关闭。

基于上面的考虑和调研,我们制定了 Url Query 的标准


  • 其余能依赖 Api 解决的问题,不建议放到 Url Query 中。
  • Url Query Name 规范,定义时按照驼峰式,但客户端读取时按照大小写不敏感方式读取。
  • Url Query 匹配规范 遵守 Url 标准规范,如 # 后边的参数不应进行识别。
  • 不使用字符串匹配的方式,如 key = 13 使用 key = 1 会误匹配。

通讯格式规范

之前的通讯协议不太规范,所以我们把之前的 Api 作为 V1,之后新增的 Api 使用 V2。通讯格式分为 V1 版本和 V2 版本,V1 V2 不兼容,当前已经按照 V1 标准开发的 Api 不升级至 V2 版本,V1 V2 标准会长期共存。

协议分为两层:框架层和业务层。普通的业务开发不需要关注框架层 state 部分,state 是 SDK 和客户端沟通的字段,用来展示一些异常,比如客户端没有这个方法等,框架层字段等其他逻辑对于业务层来说是透明的 使用 code 来判断状态,msg 是提示信息,data 是返回的数据。

callback('state', '{code: "", msg: "", data: {}}')

框架层字段(v1 版本使用自然数风格,v2 版本采用 http 状态码风格)

转转Hybrid-SDK重构和实践_webview_03image

业务层通用字段


值(String)

说明

0

业务成功

-1

业务失败

-1001

参数不合法(如缺少必填参数)

2、SDK 重构,实现通讯层,告别频繁的发版

只保留通讯层比较简单,但是如果兼容历史的 300 个 Api 是比较难的问题,我们的解决方案是老的还是保留,所有的新的使用新的 Api 通讯方案,慢慢的把老的不标准的 Api 迁移到新的通讯方案

3、SDK 文档管理后台,保证了文档的及时更新

转转Hybrid-SDK重构和实践_webview_04image

文档使用 VuePress 生成。通过管理后台编辑数据,然后动态的生成 md 文档。执行 VuePress 的命令。通过脚手架命令把文档上传到线上。

转转Hybrid-SDK重构和实践_客户端_05image

基于上面的优化我们搭建转转移动开发平台网站,方便 FE 快速找到相关资料文档转转Hybrid-SDK重构和实践_javascript_06转转Hybrid-SDK重构和实践_javascript_06

展望

做完上面的事情,我们还是有很多的挑战没有做。

1、SDK 进一步标准化

老的 Api 能力不标准。比如分享就有 5 个不同的 Api,我们需要整合成一个完整的 share。把所有的 Api 都标准化是一个工作量巨大的事情,所有只能依靠后面的规范来一点一点做。

2、用户全链路监控完整监控

现在的 SDK 的监控是前端和客户端分开做了。后面准备做全链路监控

3、沉淀 Webview 性能优化方案,降低接入成本,大范围业务覆盖

现在的性能优化手段比较多。但是接入成本还是比较好。需要改动代码。准备在后面降低接入成本。

转转Hybrid-SDK重构和实践_webview_08