iOS Hybrid 的实践
iOS Hybrid 相关大致上有以下几块内容:
- 基础能力支持,H5 和 native 的互相通信能力
- 业务支撑,Bridge 的协议制定
- 性能优化,hybrid 离线能力建设
- 数据统计和性能分析以及监控
一、JavaScript 和 Native 的相互通信
JS 调用 Native
业内比较好的通用做法是上下文注入,安卓 addJavascriptInterface
注入。iOS 使用 WKWebView - scriptMessageHandler
注入,这种方式注入其实只给注入对象起了一个名字nativeObject
,这种对象只有一个函数 postMessage
1 | //准备要传给native的数据,包括指令,数据,回调等 |
客户端接收
1 | -(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message { |
另外还有 1. 请求拦截,2. 弹窗拦截两种方式。
请求拦截 也可以实现 JS call Native,这是唯一一个在 iOS 上可以从 iOS 6 及以前版本支持的方案,也是唯一一个支持 Android WebView,iOS UI/WK WebView 的方案;但是这个方案有丢消息和消息长度的限制。
著名开源框架 WebViewJavascriptBridge 使用的就是这种通信方案。
Native 调用 JS
使用 evaluatingJavaScript
执行JS代码
JS 代码
1 | function calljs(data){ |
户端此时要调用需要在 OBJC 拼接字符串,拼出一个 JS 代码,传递的数据用 JSON
1 | //不展开了,data是一个字典,把字典序列化 |
二、Bridge 的协议制定
Bridge 的参数必须遵守设置规则,否则每个方法都在开发时随意设置,以后就不好统一管理。
比如参数最外层可以有5个:
- method,定位到具体是哪个方法
- data,所有的业务参数
- callbackId,native接收到时给 H5 的响应
- successId,动作成功给 H5 的响应
- failedId,动作失败给 H5 的响应,如网络请求失败
那么一个正常的参数看起来是这样的:
1 | { |
随着业务的迭代,Bridge 方法会越来越多,如果 method
只使用一层结构的话,native 把所有的方法都放到一个类里,这个类就会越来越大,不易于维护;如果每个方法都是一个类的话,类似功能的方法之间则不能统一管理,过于离散。可以加个域的概念,比如按功能来划分,在 method
加入代表着功能的 域:
1 | { |
1 | { |
但是随着业务的快速增长,膨胀的 APP 会划分几个业务模块,某个模块想新增 Bridge 的话,就有可能冲掉之前已有的 Bridge,比如已经存在了 "location.forward"
方法,这个时候,便可以再增加一层,对于 "method"
的约定就是:模块 + 功能 + 具体的方法
,
比如电商模块想要打开某个页面,方法就是 "mall.location.forward"
。当然,对于所有业务模块都需要使用的基础能力,是要放入基础Bridge中而非每个模块都自己实现一套。
三、离线能力建设
到了上面的那一步,基本上可以完成大部分的活动或者需求中的功能了。但是如果想用户体验更好,使用 H5 页得到近似于原生的页面时,就要想办法加快页面的加载速度,在转场动画期间尽可能减少 H5 页面白屏的时间。
比较优雅的方法就是提前把 H5 打包下载到本地,然后使用 NSURLProtocol
拦截请求把本地资源替换线上资源。但是这种方案有个问题,需要使用私有 API,具有一定的风险。使用私有 API 进行拦截,还有另外一个问题,就是 POST 请求会丢失 body,所以尽量只拦截 GET 请求。
到了 iOS 11 就可以使用系统提供的 [setURLSchemeHandler:forURLScheme:]
实现离线。
但 WKURLSchemeHandler
不能处理 Http、Https 等常规 scheme,所以需要自定义 scheme。
基本方案就是,在 WebView loadRequest 前判断本地是否有离线资源,支持离线且有离线资源的时候,修改 Http/Https 为自定义的 scheme,然后在 NSURLProtocol
或者 WKURLSchemeHandler
去实现对本地资源的加载。
四、数据统计和监控建设
doing。。