这篇记录一个名字海报功能的调试过程。功能很直观:用户选择名字后生成海报,海报包含名字、拼音、字义和诗句,最后保存到相册。
真正的开发过程却不是"调用 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 实际逻辑宽高;
- 绘制坐标使用的单位;
- 导出时的
destWidth、destHeight; - 图片和字体加载完成的时机。
问题五: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 的开发过程。下一次遇到类似问题,我会先写清运行环境和复现步骤,再开始改代码。
如果你也遇到过相同问题,欢迎补充机型和基础库版本。没有环境信息的"真机不行",通常还不足以定位问题。