一次小程序 Canvas 海报真机调试复盘:从空白导出到 iOS 保存失败

这篇记录一个名字海报功能的调试过程。功能很直观:用户选择名字后生成海报,海报包含名字、拼音、字义和诗句,最后保存到相册。

真正的开发过程却不是"调用 Canvas API,得到一张图片"这么简单。

问题一:看不到错误,不代表没有错误

第一版出现过生成失败但页面没有明确反馈的情况。排查后发现,Canvas 节点由弹窗条件渲染,生成函数调用时节点可能还没有完成挂载。

后来先等待页面准备,再把失败转成明确的 UI 状态:生成中、成功、失败和超时分别处理。

js 复制代码
this.generating = true;
try {
  const ctx = uni.createCanvasContext('posterCanvas', this);
  this.drawPoster(ctx);
  ctx.draw(false, () => this.exportPoster());
} catch (_) {
  this.generating = false;
  uni.showToast({ title: '海报生成失败,请重试', icon: 'none' });
}

这里的重点不是 try/catch,而是让页面不会因为一次异步失败永远停留在"生成中"。

问题二:隐藏 Canvas 导出可能挂起

为了不让用户看到绘制过程,曾经尝试把 Canvas 隐藏。结果在部分环境中,隐藏状态影响了导出,页面一直等待。

处理方式是让预览区域保持可见,并给导出增加超时保护。开发者工具中不容易暴露这类差异,所以这一步必须在目标设备上确认。

问题三:绘制回调是导出边界

绘制命令提交后立即导出,是最容易出现空白图的写法。项目后来固定在 draw 回调中开始导出:

js 复制代码
ctx.draw(false, () => {
  const timer = setTimeout(() => {
    this.generating = false;
    uni.showToast({ title: '生成超时,请重试', icon: 'none' });
  }, 8000);

  uni.canvasToTempFilePath({
    canvasId: 'posterCanvas',
    success: ({ tempFilePath }) => {
      clearTimeout(timer);
      this.posterImage = tempFilePath;
      this.generated = true;
      this.generating = false;
    },
    fail: () => {
      clearTimeout(timer);
      this.generating = false;
    }
  }, this);
});

这里的超时只负责让 UI 结束等待,并不会取消底层导出请求。后续如果要进一步增强,还可以为每次生成分配任务编号,忽略旧任务的晚到回调。

问题四:尺寸和坐标必须同时看

Canvas 的展示尺寸、逻辑绘制尺寸和导出尺寸并不是一回事。项目曾经出现过海报内容错位,最后通过统一画布尺寸和绘制坐标解决。

排查时不要只看 CSS 宽高,还要同时确认:

  • Canvas 实际逻辑宽高;
  • 绘制坐标使用的单位;
  • 导出时的 destWidthdestHeight
  • 图片和字体加载完成的时机。

问题五:iOS 真机直接保存临时文件失败

生成成功后,iOS 真机上直接调用相册保存,曾出现 file not exists。最后使用先落盘再保存的路径:

js 复制代码
const tempPath = this.posterImage;
const fs = uni.getFileSystemManager();

fs.saveFile({
  tempFilePath: tempPath,
  success: ({ savedFilePath }) => {
    this.saveFileToAlbum(savedFilePath);
  },
  fail: () => {
    this.saveFileToAlbum(tempPath);
  }
});

保存完成后再清理落盘副本。落盘失败时保留直接保存的兼容尝试,但不能把这个分支写成所有环境的保证。

问题六:失败以后仍然要能完成任务

保存失败可能是相册权限问题,也可能是临时文件或平台接口问题。项目将权限错误导向授权/设置,将其他错误导向预览:

js 复制代码
const message = (error && error.errMsg) || '';
if (/auth deny|authorize|permission/i.test(message)) {
  this.requestAlbumAuth();
} else {
  uni.previewImage({
    urls: [this.posterImage],
    current: this.posterImage
  });
  uni.showToast({ title: '长按图片保存', icon: 'none' });
}

这条预览兜底很重要。自动保存失败时,至少让用户看到已经生成的真实图片。

Git 修复时间线

这个问题不是一次提交解决的,项目记录大致经历了:

text 复制代码
72a9783  处理静默失败与节点挂载
87951c5  改为可见 Canvas 并增加导出超时
9c7112e  调整 Canvas API 兼容路径
3a0acf2  修正尺寸与坐标
20d80ea  绘制回调后立即导出
437f7eb  生成期间禁止关闭弹窗
4b3a990  补齐授权和预览兜底
 aa10533  修复 iOS 真机 file not exists

提交记录能说明排查方向发生过变化,但不能替代真机测试报告。发布前还需要补充实际机型、微信版本、基础库和修复前后截图。

调试时我现在会记录什么

不再只记录"海报失败",而是记录:

  • 当前阶段:绘制、导出、落盘、相册保存;
  • 错误码或错误类型;
  • 开始和结束时间;
  • 当前任务是否已经结束。

同时避免记录姓名、答案、完整云函数请求和临时文件路径。调试日志应当服务于排查,而不是把用户数据带进日志文件。

复盘结论

这次问题让我把一个功能拆成了六个阶段:节点、绘制、导出、文件、权限和兜底。任何一层都可能在开发者工具正常、真机异常的情况下暴露出来。

这份记录来自字笺 SinoName 的开发过程。下一次遇到类似问题,我会先写清运行环境和复现步骤,再开始改代码。

如果你也遇到过相同问题,欢迎补充机型和基础库版本。没有环境信息的"真机不行",通常还不足以定位问题。

相关推荐
MrFlySand_飞沙6 小时前
uniapp开发微信小程序实现接入企业微信客服
微信小程序·小程序·uni-app·企业微信
PedroQue991 天前
Vue Router 4.x风格导航守卫全面升级
前端·uni-app
Haibakeji1 天前
APP小程序定制开发项目频繁延期?前期规避方法与落地避坑指南
小程序·uni-app·软件需求
anyup1 天前
迁移uni-app x,我是如何让 AI 把我一步步搞崩溃的...
前端·uni-app·trae
2501_916007471 天前
申请 iOS 推送证书并配置 APNs 群发推送教程
android·ios·小程序·https·uni-app·iphone·webview
博客zhu虎康1 天前
uni-app云打包显示编译成功,但是一直没有进入打包队列,后面也一直没反应
uni-app
JavaDog程序狗2 天前
【指南】uni-app微信小程序多环境CI-CD完全指南
ci/cd·uni-app·jenkins
PedroQue992 天前
uni-router v2.1.0 升级:导航守卫全面支持返回值模式
前端·uni-app
2501_915909063 天前
iOS test 测试怎么做?功能、性能、兼容、稳定与安全五类测试指南
android·ios·小程序·https·uni-app·iphone·webview