背景
哈啰出海项目目前是基于Google地图提供的服务进行地图相关能力的场景应用。
在H5侧Google动态地图能力是一项收费服务,对于初期我们进行海外业务拓展探索中,这部分费用占据了出海营收的一部分。为了减少Google地图的费用支出,前期我们也进行一系列的产品侧、研发侧的优化,目前整体 Dynamic maps 单均消费大约下降了50%,但这远远还是不够。
随着业务的不断迭代上线,Native地图相较于Web地图的天然性能优势日趋明显,首屏体验也随之差距越来越大。
目标价值
为了改善现状,我们希望通过在业务层使用Webview承载H5,展示地图部分则使用Native地图,而非采用Web地图。
- 解决Google地图在Web侧服务的收费问题,达到降本的目的。
- 提升用户体验,追平与竞品地图性能差距。
- 保持H5的开发灵活性,无需维护两套核心主流程业务代码,方便维护的统一性。
设计方案
基于出海现有的需求,我们采用Native地图+H5页面的设计原则,既希望能够正常处理H5页面的事件,又要满足Native地图的相关事件派发。
调研初期,我们曾设想过通过Native地图+多个webview容器承载页面散落的元素。
这种设计方案虽然解决了布局问题,但是却存在不少不足:
- 多个Webview之间内存空间不共享,信息同步问题难度较大
- 一个业务页面存在多个Webview组件对系统性能是一个高度浪费
- 核心主流程中涉及多个地图页,相关改造成本偏高,不确定性高,拓展性差
因此,该方案也满足不了出海场景的需求。
在探索中,我们借鉴了业内的思路,并进行了可行性评估。最终从出海场景出发,实践出一套基于React的融合方案。
框架设计
融合方案设计
页面架构图层
融合技术架构图
数据通讯流程图
预期效果
- 当我们点击 Webview 内的H5元素时,点击事件派发到 Webview 容器处理
- 当我们操作地图区域时,操作事件派发到 Native 层地图组件处理
- 通过JSBridge实现 Native 地图与 Webview 层的信息通讯
核心思路
- 思路为页面模块为 Native 地图层+ Webview 层,Webview 层置于 Native 地图层之上并保持透明
- 通过 Web 提供热区数据模块分布,Native 区分热区进行 Native地图层或 Webview 层逻辑分发
- Native 地图层和 Webview层之间的数据、事件通信通过JSBridge进行相互通信
- Native 地图层主要负责提供通用型地图渲染类,供 Web 调用
- Webview 层负责地图渲染数据渲染层处理,不负责地图 UI 绘制渲染
看到这里,有的人可能就会产生疑问,Native如何根据热区进行区分?热区模块如何定义?
热区数据与热区坐标
其实在摸索出来之前,我们并不知道Native还有此等“神力”可以再一个页面里对事件进行有效派发,故当我们确认了这个能力能够得到有效支持时,我们约束好相关Webview层热区的定义规则,对热区数据进行统一维护与定义。
我们以左上角作为坐标原点[0,0]进行热区的坐标定义,做好事件分发策略。如果手势消息的发生产生在热区之内,则消息派发到Webview 层,否则派发到 Native层。至于热区像素坐标格式,则采用基于Webview组件左上角为原点[left, top, width, height]。毕竟前人栽树后人乘凉,没必要整太多花活,解决有效问题最关键。
动态更新
由于不同热区模块存在动态变更的场景,所以我们还需要考虑热区的动态更新。
import Bridge from '@/Bridge'
const getHotData = (data: number[]) => {
//...
return [0,0,100,100]
}
useEffect(() => {
if (cardRef.current) {
// 获取当前卡片dom元素的相关信息
const rect = cardRef.current.getBoundingClientRect();
// 转化成传输的格式坐标规则
const coordinates = getHotData(rect)
// 通过 Bridge 传递 Native
Bridge.setHotZoneInfo(coordinates)
}
}, [
cardRef,
]);
以上核心代码可以将指定的元素纳入动态热区的监听当中,但还有一部分业务侧的交互是比较复杂且不好收口的,比如不同组件之间的弹窗、Popup、Confirm之类的一系列全屏元素,这部分的动态更新策略是值得探讨的。
下面我们将介绍一下基于这个方案所做的全屏元素动态更新热区的策略。
全屏元素监听动态更新
这里我们采用的是MutationObserver接口,它提供了监视对 DOM 树所做更改的能力,该功能是 DOM3 Events 规范的一部分。DOM 的任何变动,比如节点的增减、属性的变动、文本内容的变动,这个API 都可以得到通知。
特点:
- 异步执行,并不会立马执行,等待所有DOM改变结束后触发
- 变动记录封装成数组处理,而不是一条条处理DOM变动
- 可观察DOM节点所有变动 ,也可观察某一类变动
因为其异步执行的天然优势,少了 DOM 的频繁变动,大大有利于性能。
在出海主流程的场景里,我们只希望观测弹窗、Popup等类似的铺满整个屏幕的元素,所以这里我们只需要关注宽高与客户端宽高匹配的元素。通过Dom元素的getBoundingClientRect方法拿到对应元素的相关信息。
const width = document.documentElement.clientWidth || document.body.clientWidth;
const height = document.documentElement.clientHeight || document.body.clientHeight;
const clientRect = Dom.getBoundingClientRect()
const isFull = clientRect.width === width && clientRect.height === height
const config: MutationObserverInit = {
attributes: true, // 观察目标属性改变
attributeOldValue: true, // 记录改变前的目标属性 方便对比
childList: true, // 表示观察目标子节点变化 添加/删除
subtree: true, // 目标及目标后代改变都监听
attributeFilter: ['id', 'class', 'style'], //设置需要被监听的属性
};
变动执行回调函数
let elementPath = [] // 用于维护当前铺满整个屏幕的元素,用于多蒙层之类的场景判断
const callback = (mutationsList: MutationRecord[], observer: MutationObserver) => {
try {
for (const mutation of mutationsList) {
const { target, type, attributeName, addedNodes } = mutation;
if (type === 'childList') {
let tag = false;
if (addedNodes.length) {
// ...
} else {
// ...
}
} else if (type === 'attributes') {
if (attributeName === 'class') {
//...
} else {
// attributes变化
}
}
}
} catch (e) {
//异常场景 兜底走H5地图
}
}
整个全屏判断就都在回调函数里进行判断 ,如果当前有铺满屏幕的 H5 元素 则通过 Bridge 告知 Native 整个手势派发由Webview接管,否则按原有的热区模块进行逻辑分发。
每次的Callback触发中,我们主要依旧三个核心思路进行元素的查找定位:
- 根据变动节点,向上查找,直到BODY结束
- 根据变动节点,向下查找,把节点父容器进行元素遍历
- 根据全屏元素路径,定向查找,把原有记录全屏的节点进行重新校验
测试工具
关于热区
我们基于vConsole编写了一个简易的插件进行相关Bridge的调用以及热区的测试。通过Canvas将热区绘制在屏幕中进行相关数据的呈现,快速诊断和排查热区异常情况。
关于地图Bridge
目前我们主要在进行相关地图通用能力接口的编写,通过对出入参的数据收集以及整个交互过程配合控制台的输出进行边界问题排查,后续将通过单测的方式对其进行自动化测试。
这一实践,有效的将Webview和Native组件融合起来,通过该融合技术为出海业务有效提升开发迭代效率,同时保障了用户地图的体验。除了首次发版依赖App发版、审核、下载等繁琐流程,拥有了需求随时可发、随时能发的能力。
(本文作者:黄鸿达)
本文系哈啰技术团队出品,未经许可,不得进行商业性转载或者使用。非商业目的转载或使用本文内容,敬请注明“内容转载自哈啰技术团队”。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。