在"选择图片---生成拼豆图---进入详情"的链路里,最危险的异常处理并不是直接报错,而是失败后仍然返回一份看起来正常的数据。用户明明选择了自己的照片,页面却展示一张内置示例图;界面没有崩溃,但结果已经失真。
本文以 ImageConvertService 为切入点,把图片选择取消、资源解码失败、像素读取失败和显式示例体验拆成不同语义,再用 ArkTS 类型约束页面只能做正确的恢复动作。

一、先区分"程序继续运行"和"业务结果正确"
图片生成属于强输入关联操作,输出必须能追溯到用户刚刚选择的资源。若解码失败后调用 createFallbackPattern(),虽然可以让页面继续渲染,却会制造三类问题:
- 用户误以为生成算法没有使用自己的图片;
- 保存记录时把示例图当成用户作品;
- 后续排查只能看到"生成成功",原始失败原因已经丢失。
因此,兜底图不能充当异常返回值。真正合理的契约是:成功就返回由本次输入生成的图纸,失败就返回可识别的错误,示例图只能由用户主动进入示例入口。
二、把失败阶段写进领域类型
仅抛出 Error('图片解析失败') 信息太少,页面无法判断应该让用户重选、重试还是提示资源不受支持。可以先定义稳定的错误码和阶段:
ts
export type ConvertErrorCode =
'SOURCE_CREATE_FAILED' |
'PIXEL_MAP_CREATE_FAILED' |
'PIXEL_READ_FAILED' |
'EMPTY_IMAGE' |
'UNSUPPORTED_IMAGE'
export type ConvertStage = 'source' | 'decode' | 'read' | 'quantize'
export interface ConvertFailure {
ok: false
code: ConvertErrorCode
stage: ConvertStage
userMessage: string
retryable: boolean
}
export interface ConvertSuccess {
ok: true
pattern: Pattern
sourceUri: string
}
export type ConvertResult = ConvertSuccess | ConvertFailure
这组类型的关键不在于字段多,而在于 ok 能让 ArkTS 收窄分支。成功分支一定有 pattern,失败分支一定有恢复信息,不再依赖空对象或特殊标题猜测结果。
三、转换服务只负责如实返回结果
服务层应把系统异常翻译成应用能够理解的结果,但不要替页面决定交互,更不要偷偷替换输入。
ts
export class ImageConvertService {
static async convertFromPickedImage(uri: string): Promise<ConvertResult> {
let pixelMap: image.PixelMap | undefined
try {
const source = image.createImageSource(uri)
pixelMap = await source.createPixelMap({
editable: false,
desiredPixelFormat: image.PixelMapFormat.RGBA_8888
})
const info = await pixelMap.getImageInfo()
if (info.size.width <= 0 || info.size.height <= 0) {
return ImageConvertService.failure(
'EMPTY_IMAGE', 'decode', '图片没有可读取的像素', false
)
}
const pattern = await ImageConvertService.buildPattern(pixelMap, info.size)
return { ok: true, pattern: pattern, sourceUri: uri }
} catch (error) {
return ImageConvertService.mapFailure(error)
} finally {
if (pixelMap !== undefined) {
pixelMap.release()
}
}
}
private static failure(
code: ConvertErrorCode,
stage: ConvertStage,
userMessage: string,
retryable: boolean
): ConvertFailure {
return { ok: false, code, stage, userMessage, retryable }
}
}
finally 释放 PixelMap,结果类型表达业务语义,这两个职责互不替代:资源释放解决稳定性,显式失败保证正确性。

四、取消选择不是解码失败
相册选择器返回空数组时,通常意味着用户主动返回。它不应进入转换服务,更不应显示红色错误提示。
ts
export type PickResult =
| { kind: 'picked', uri: string }
| { kind: 'cancelled' }
| { kind: 'failed', message: string }
export async function pickSingleImage(): Promise<PickResult> {
try {
const result = await new photoAccessHelper.PhotoViewPicker().select({
MIMEType: photoAccessHelper.PhotoViewMIMETypes.IMAGE_TYPE,
maxSelectNumber: 1
})
if (result.photoUris.length === 0) {
return { kind: 'cancelled' }
}
return { kind: 'picked', uri: result.photoUris[0] }
} catch (error) {
return { kind: 'failed', message: '暂时无法打开图片选择器' }
}
}
页面收到 cancelled 后只需要回到原状态。这样用户的正常返回不会污染错误统计,也不会打断已经编辑的内容。
五、页面按错误码给出恢复动作
统一显示"解析失败"会让错误码失去价值。页面可以保留简短主提示,同时根据 retryable 和 code 提供不同按钮:
ts
private async generateFrom(uri: string): Promise<void> {
this.createStatus = '正在读取图片...'
const result = await ImageConvertService.convertFromPickedImage(uri)
if (result.ok) {
this.generatedPattern = result.pattern
this.createStatus = `已生成 ${result.pattern.beadCount} 颗拼豆`
return
}
this.generatedPattern = undefined
this.createStatus = result.userMessage
this.canRetryCurrentImage = result.retryable
this.shouldReselectImage = result.code === 'UNSUPPORTED_IMAGE' ||
result.code === 'EMPTY_IMAGE'
}
失败时主动清空 generatedPattern 很重要,否则页面可能继续展示上一次成功的图纸,使"当前图片"和"当前结果"错位。
六、示例图必须拥有独立入口
示例体验当然有价值,但它应当是显式业务能力,例如"先看看示例",而不是异常处理分支。可以把方法命名为 loadDemoPattern(),并在图纸上保留来源:
ts
export type PatternSource = 'asset' | 'user-image' | 'demo'
export interface PatternWithSource extends Pattern {
source: PatternSource
}
private openDemo(): void {
const pattern = DemoPatternFactory.create()
this.generatedPattern = { ...pattern, source: 'demo' }
this.createStatus = '当前展示的是示例图'
}
独立入口带来两个直接收益:界面能够清楚标注"示例",保存记录时也能决定是否允许把它计入用户作品。
七、错误对象不要携带本地 URI
为了定位问题,可以记录阶段、错误码、图像尺寸和系统错误编号,但不要把相册 URI、文件名或完整异常堆栈直接展示给用户。
ts
export interface ConvertTrace {
code: ConvertErrorCode
stage: ConvertStage
width: number
height: number
elapsedMs: number
}
function toTrace(failure: ConvertFailure, elapsedMs: number): ConvertTrace {
return {
code: failure.code,
stage: failure.stage,
width: 0,
height: 0,
elapsedMs: elapsedMs
}
}
这类结构化信息已经足够定位"在哪一步失败",同时避免把用户资源路径带进日志或提示框。
八、错误映射要有默认分支
系统能力抛出的异常形态可能随场景变化,映射层不能假设每个对象都有 code。ArkTS 中可以只读取应用明确声明的错误结构,其余都归为安全的默认错误。
ts
interface PlatformFailure {
code: number
message: string
}
private static mapFailure(error: Error | PlatformFailure): ConvertFailure {
if ('code' in error && error.code === 62980137) {
return ImageConvertService.failure(
'UNSUPPORTED_IMAGE', 'decode', '图片格式暂不支持,请重新选择', false
)
}
return ImageConvertService.failure(
'PIXEL_READ_FAILED', 'read', '图片读取失败,请稍后重试', true
)
}
实际项目应以当前 SDK 的错误码文档为准,应用层错误码则保持稳定,避免系统编号扩散到每个页面。
九、用场景矩阵验证语义而不是只验证有返回值
建议至少覆盖以下路径:
| 场景 | 期望结果 | 页面动作 |
|---|---|---|
| 用户取消选择 | cancelled |
保持当前页面 |
| 图片正常 | ok: true |
展示本次生成图 |
| 空尺寸资源 | EMPTY_IMAGE |
引导重选 |
| 格式不支持 | UNSUPPORTED_IMAGE |
引导重选 |
| 像素读取短暂失败 | PIXEL_READ_FAILED |
允许重试 |
| 用户点击示例 | source: demo |
明确标注示例 |
尤其要加入一条反向断言:任何失败结果都不能含有 pattern。这比验证一句错误文案更能守住业务边界。

十、落地检查清单
- 选择取消、系统失败和转换失败是三种独立结果;
- 转换失败不会返回内置图纸;
- 失败后清理上一次生成结果;
PixelMap在所有路径都能释放;- 错误码能映射到重试或重选动作;
- 示例图由用户主动打开,并带有来源标识;
- 日志不记录用户的完整资源 URI。
执行清单时应保留同一张正常图片作为基线,再准备空资源、受损资源和不支持格式分别触发分支。每次失败后重新选择基线图片,确认页面可以恢复成功,且先前错误不会残留到新的作品状态中。
总结
图片生成链路的核心不是"永远有图可显示",而是"显示的图一定来自用户理解中的那次输入"。把取消、失败、成功和示例体验拆成类型明确的结果后,服务层不会掩盖真实问题,页面也能给出准确的恢复动作。对于涉及用户内容的 Harmony os 应用,这种结果语义比一个万能兜底更可靠。
标签:Harmony os、ArkTS、ImageKit、错误处理、状态建模