iOS Hybrid 相关大致上有以下几块内容:

  1. 基础能力支持,H5 和 native 的互相通信能力
  2. 业务支撑,Bridge 的协议制定
  3. 性能优化,hybrid 离线能力建设
  4. 数据统计和性能分析以及监控

一、JavaScript 和 Native 的相互通信

JS 调用 Native

业内比较好的通用做法是上下文注入,安卓 addJavascriptInterface 注入。iOS 使用 WKWebView - scriptMessageHandler 注入,这种方式注入其实只给注入对象起了一个名字nativeObject,这种对象只有一个函数 postMessage

1
2
3
4
5
6
7
8
//准备要传给native的数据,包括指令,数据,回调等
var data = {
method:'location',
data:'http://baidu.com',
callback:'1',
};
//传递给客户端
window.webkit.messageHandlers.nativeObject.postMessage(data);

客户端接收

1
2
3
4
5
6
7
-(void)userContentController:(WKUserContentController *)userContentController didReceiveScriptMessage:(WKScriptMessage *)message {
NSDictionary *msgBody = message.body;
NSString *method = msgBody[@"method"];
NSString *data = msgBody[@"data"];
// perform method with data
// ...
}

另外还有 1. 请求拦截,2. 弹窗拦截两种方式。

请求拦截 也可以实现 JS call Native,这是唯一一个在 iOS 上可以从 iOS 6 及以前版本支持的方案,也是唯一一个支持 Android WebView,iOS UI/WK WebView 的方案;但是这个方案有丢消息和消息长度的限制。

著名开源框架 WebViewJavascriptBridge 使用的就是这种通信方案。

Native 调用 JS

使用 evaluatingJavaScript 执行JS代码

JS 代码

1
2
3
4
5
function calljs(data){
console.log(JSON.parse(data))
//1 识别客户端传来的数据
//2 对数据进行分析,从而调用或执行其他逻辑
}

户端此时要调用需要在 OBJC 拼接字符串,拼出一个 JS 代码,传递的数据用 JSON

1
2
3
4
5
//不展开了,data是一个字典,把字典序列化
NSString *paramsString = [self _serializeMessageData:data];
NSString* javascriptCommand = [NSString stringWithFormat:@"calljs('%@');", paramsString];
//要求必须在主线程执行JS
[self.webView evaluateJavaScript:javascriptCommand completionHandler:nil];

二、Bridge 的协议制定

Bridge 的参数必须遵守设置规则,否则每个方法都在开发时随意设置,以后就不好统一管理。

比如参数最外层可以有5个:

  1. method,定位到具体是哪个方法
  2. data,所有的业务参数
  3. callbackId,native接收到时给 H5 的响应
  4. successId,动作成功给 H5 的响应
  5. failedId,动作失败给 H5 的响应,如网络请求失败

那么一个正常的参数看起来是这样的:

1
2
3
4
5
6
7
8
9
{
"method":"location",
"data":{
"url":"http://baidu.com"
},
"callbackId":1,
"successId":2,
"failedId":3
}

随着业务的迭代,Bridge 方法会越来越多,如果 method 只使用一层结构的话,native 把所有的方法都放到一个类里,这个类就会越来越大,不易于维护;如果每个方法都是一个类的话,类似功能的方法之间则不能统一管理,过于离散。可以加个域的概念,比如按功能来划分,在 method 加入代表着功能的

1
2
3
4
5
6
7
8
9
{
"method":"location.forward",
"data":{
"url":"http://baidu.com"
},
"callbackId":1,
"successId":2,
"failedId":3
}
1
2
3
4
5
6
7
{
"method":"location.close",
"data":{},
"callbackId":1,
"successId":2,
"failedId":3
}

但是随着业务的快速增长,膨胀的 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。。

参考:从零收拾一个hybrid框架