uni-app 跨端 Canvas 图表:交互、层级与生命周期
本文目录
在 uni-app 页面里嵌入交互式 Canvas 图表,看起来像“把组件放进页面”,实际要同时处理渲染层级、触摸事件、画布尺寸、页面缓存和异步数据。H5、App 和各类小程序的渲染实现并不相同;只在 H5 看起来正常,不能证明跨端交付完成。
这份指南把过往跨端图表页面中的通用工程问题重新整理成一套可复用方案。文中的代码为重新编写的示意代码,不依赖特定图表库,也不是原项目代码;实际接入时要以目标 uni-app 版本、运行平台和图表库 API 为准。这里只做静态经验总结,没有声称在原项目或生产环境完成构建验证。
先拆清楚职责
建议把页面分为四个边界:
| 边界 | 负责内容 | 不应负责的内容 |
|---|---|---|
| 页面状态 | 当前图表模式、筛选项、加载/错误状态、页面是否活跃 | 直接操作多个画布实例的内部字段 |
| 图表适配器 | create、update、resize、pause、dispose 等统一操作 | 页面导航、登录态或业务授权 |
| 平台适配层 | H5 容器尺寸、App/小程序画布 ID、原生组件层级差异 | 重复维护一套业务状态 |
| 数据服务 | 参数校验、取消请求、错误映射、缓存策略 | 直接读写页面组件实例 |
页面只保留一个“当前模式”事实来源。切换模式时先更新状态,再由适配器更新已有实例或创建目标实例。若同时保留多个画布实例,要明确它们的内存预算、隐藏时是否暂停以及离开页面时如何销毁;低端设备上,复用单实例或按需创建可能更合适。
Canvas 上的控件为什么会被遮住
某些 App/小程序平台会把 canvas、video、map 等组件放在原生渲染层,普通 view 即使设置更大的 z-index 也不一定能覆盖它。uni-app 提供 cover-view 处理部分层级覆盖场景,但支持范围、事件和嵌套限制因平台而异;H5 没有同一类原生组件层级问题,部分小程序还支持原生组件同层渲染。
因此,覆盖控件不能只靠一份 cover-view 模板。先查目标平台的组件矩阵,再选实际可用的实现:
- 同层渲染可用时,优先用普通布局组件,并验证点击和滚动是否冲突。
- 需要覆盖原生画布且平台支持时,再用
cover-view,把交互尽量收敛到平台支持的事件。 - 若目标平台不支持覆盖,改成画布外的工具栏、原生层方案或图表自身绘制的控件,不要继续叠加
z-index碰运气。
不同端的控件位置、可访问性语义和点击热区也要分别验收。图表上的手势区与页面滚动区应有明确分工,避免一次滑动同时拖动画布和滚动整页。
初始化、唯一标识与尺寸
小程序画布通常需要明确的画布标识;同一页面不要重复使用相同 canvas-id。页面节点尚未就绪时就初始化图表,也可能拿不到真实容器尺寸,因此初始化要放在目标框架对应的页面/组件就绪阶段,并保证重复调用幂等。
尺寸变化不能只改 CSS:图表库还需要知道新的绘制区域并重新布局或绘制。H5 可以观察容器尺寸变化;App 和小程序则使用目标端支持的页面尺寸回调或布局查询方式。不要把某个端的 DOM API 当成所有端通用能力。
let chart = null;
let resizeTimer = null;
function scheduleResize(size) {
if (resizeTimer !== null) clearTimeout(resizeTimer);
resizeTimer = setTimeout(() => {
resizeTimer = null;
if (!chart || size.width <= 0 || size.height <= 0) return;
chart.resize({ width: size.width, height: size.height });
}, 80);
}
function disposeChart() {
if (resizeTimer !== null) clearTimeout(resizeTimer);
resizeTimer = null;
chart?.dispose?.();
chart = null;
}
这里的 resize / dispose 是适配器接口示例,不是 uni-app 内置 API。Canvas 的 CSS 尺寸、绘制缓冲区尺寸和图表库的逻辑尺寸可能是三回事;不同 Canvas 类型和平台也有差别。尺寸更新后调用图表库的正式 resize/redraw 方法,并在真机上检查文字模糊、裁切、坐标偏移和大画布内存占用。
页面显示与资源释放
uni-app 页面可能留在页面栈中,离开后再回来会再次触发 onShow。onShow 不能假设只运行一次:订阅和定时器要幂等启动,不能每次显示都创建新图表或叠加新 interval。onHide 适合暂停可恢复的工作;onUnload 才是释放页面级实例、监听器和未完成任务的重要边界。
let pageActive = false;
let refreshTimer = null;
let chart = null;
function startRefresh() {
if (!pageActive || !chart || refreshTimer !== null) return;
refreshTimer = setInterval(() => {
if (pageActive) void refreshChartData();
}, 5000);
}
function stopRefresh() {
if (refreshTimer !== null) clearInterval(refreshTimer);
refreshTimer = null;
}
onReady(async () => {
chart = await createChartAdapter();
startRefresh();
});
onShow(() => {
pageActive = true;
chart?.resume?.();
startRefresh();
});
onHide(() => {
pageActive = false;
stopRefresh();
chart?.pause?.();
});
onUnload(() => {
pageActive = false;
stopRefresh();
chart?.dispose?.();
chart = null;
});
实际项目要替换成对应 Vue/组合式 API 的页面生命周期导入方式,并确认各端的调用顺序。setInterval 只适合有明确刷新需求的场景;若页面隐藏时无需工作,就暂停它。旧代码若在每次模式切换时重新赋值定时器句柄,之前的 interval 可能失去引用而无法清理。静态代码片段只能说明存在这种风险,不能据此断言完整应用已经发生资源泄漏。
同样要沿生命周期清理图表事件回调、resize 监听、消息订阅、网络请求和延迟任务。请求支持取消时使用 AbortController 或平台等价能力;不支持取消的请求可用递增请求序号丢弃过期结果,避免用户快速切换条件后旧响应覆盖新状态。
触摸与滚动的边界
图表需要的 touchstart、touchmove、touchend 应由图表适配器统一转发,不能同时在页面、画布和覆盖层重复处理。横向缩放/拖动与纵向页面滚动要根据交互设计决定谁优先;H5 的 CSS 手势控制与小程序事件属性不是一套接口,不应假设写一个 touch-action 就能解决所有端。
验收时至少检查:
- 单指点击、拖动和松开后,图表状态正确且不会误触相邻按钮。
- 页面纵向滚动仍可用;图表手势结束后没有卡住页面滚动。
- 切换图表类型或筛选项后,旧实例不会继续接收事件。
- 快速连续切换不会重复初始化、重复请求或显示过期结果。
- 横竖屏切换、窗口缩放和容器显隐后,尺寸与触摸坐标仍一致。
分享链接不要携带身份凭证
分享 URL 不应暴露原始用户 ID、会话 Cookie、JWT 或长期有效的访问凭证。URL 容易进入浏览器历史、代理日志、分析平台和截图;RFC 6750 也明确不推荐把 bearer token 放在 URI 查询参数中。
若产品确实需要分享受限内容,应由服务端生成高熵、不可预测、范围受限且有过期时间的分享码;服务端验证其权限、有效期和撤销状态,再决定能否返回内容。分享码与登录凭证分离,不要把用户 ID 当作授权依据,也不要在日志中记录完整分享码。公开内容仍应由服务端明确标记为公开,不能依赖前端隐藏按钮。
跨端验收矩阵
| 检查项 | H5 | App | 小程序 |
|---|---|---|---|
| 首次加载 | 容器测量、字体/脚本加载失败、空数据 | 真机画布初始化、低端设备内存 | 目标基础库、画布 ID 与组件限制 |
| 控件层级 | 普通 DOM 层叠与键盘操作 | 原生层覆盖和返回/前后台 | cover-view/同层渲染能力与事件矩阵 |
| 尺寸变化 | 浏览器缩放、容器变化、横竖屏 | 页面 resize、刘海/安全区 | 页面尺寸变化、不同机型布局 |
| 手势 | 页面滚动与图表拖动 | 系统边缘手势和触摸回调 | 触摸事件、纵向滚动和覆盖组件 |
| 生命周期 | 路由离开与重新进入 | 前后台切换、页面栈恢复 | onShow 重入、onHide 暂停、onUnload 销毁 |
| 异常恢复 | 请求失败、刷新和重新初始化 | 网络切换、低内存恢复 | 请求失败、页面返回和组件重建 |
测试要覆盖目标平台的真实运行时,而不仅是开发者工具。对每个支持的平台记录版本、设备、画布类型、图表库版本和已知限制;升级 uni-app、基础库或图表库时重跑核心交互矩阵。
官方资料
- uni-app 页面生命周期:核对
onShow、onHide、onUnload和尺寸变化的当前说明。 - uni-app cover-view:查看支持的平台、事件和原生组件覆盖边界。
- uni-app 原生组件说明:理解原生渲染层与普通组件的层级差异。
- uni-app Canvas:核对 Canvas 类型、标识和平台差异。
- RFC 6750,Bearer Token 使用规范:避免通过 URL 查询参数传递 bearer token。
Live Notes
留言提交后会先进入审核,审核通过后对其他读者公开。