做 HarmonyOS 文档二开时,页面里直接写 if (result.code === 0) 很快会失控:注册失败、打开参数错误、关窗回传失败都挤在同一分支,Toast 文案也难以维护。更稳妥的做法是在 SDK 边界加一层 Facade / Adapter,把 @wps/wps_sdk 的 Result 与抛异常,转成业务侧可 switch 的领域错误。本文给出一套可复用的归一化思路与 TypeScript 示例,事实对齐官方对接实践,代码按工程封装习惯重写。
为什么需要归一化
WPSApi.sendRequest 在应用已注册的前提下,多数失败仍会 resolve Result,只有「尚未注册成功」等场景会 throw。若 UI 层只写 try/catch,会漏掉带 code 的失败;若只看 code,又会漏掉 throw。Facade 的职责就是:对外只暴露「成功数据」或「领域错误枚举」,把 requestType / code / msg / data 的细节留在适配层。
调用链仍建议保持清晰:registerApp 到 OK →(按形态)可选 setWpsFileToken → 选择器文件进沙箱 → OpenFileRequest → sendRequest → 解析关闭回传。Facade 不改变时序,只改变错误出口形状。
领域错误模型
下面用一组字符串联合类型表示领域错误,便于埋点与 UI 映射:
| 领域码 | 来源特征 | UI / 运维建议 |
|---|---|---|
AUTH_FAILURE | code === 1013 / ERROR_CODE_AUTH_FAILURE | 引导检查凭据与包名,阻断后续打开 |
NOT_REGISTERED | sendRequest throw 或本地门闩未就绪 | 提示稍后重试,触发重新注册 |
PARAM_OR_OPEN | ResultCode.ERROR(-2) | 查路径、参数、是否默认只读被误判 |
TRANSFER_BIZ | 关闭回传阶段正整数 code | 按回传失败处理,保留原始 code |
UNKNOWN | 其它非 OK | 上报 code+msg,避免静默 |
成功时把 Result.data(如有)交给业务;失败时绝不把原始 Result 直接丢进页面组件。
Facade 实现草图
import { common } from '@kit.AbilityKit';
import {
WPSApi,
OpenFileRequest,
Result,
ResultCode,
ResultData,
SdkConstants,
} from '@wps/wps_sdk';
type WpsDomainError =
| { kind: 'AUTH_FAILURE'; msg?: string }
| { kind: 'NOT_REGISTERED'; msg?: string }
| { kind: 'PARAM_OR_OPEN'; code: number; msg?: string }
| { kind: 'TRANSFER_BIZ'; code: number; msg?: string }
| { kind: 'UNKNOWN'; code?: number; msg?: string };
type WpsOutcome =
| { ok: true; data?: ResultData }
| { ok: false; error: WpsDomainError };
let ready = false;
export function mapResult(stage: 'register' | 'open' | 'transfer', r: Result): WpsOutcome {
if (r.code === ResultCode.OK) {
return { ok: true, data: r.data };
}
if (r.code === ResultCode.ERROR_CODE_AUTH_FAILURE) {
return { ok: false, error: { kind: 'AUTH_FAILURE', msg: r.msg } };
}
if (r.code === ResultCode.ERROR) {
return { ok: false, error: { kind: 'PARAM_OR_OPEN', code: -2, msg: r.msg } };
}
if (stage === 'transfer' && typeof r.code === 'number' && r.code > 0) {
return { ok: false, error: { kind: 'TRANSFER_BIZ', code: r.code, msg: r.msg } };
}
return { ok: false, error: { kind: 'UNKNOWN', code: r.code, msg: r.msg } };
}
export function registerFacade(activationSn?: string): void {
WPSApi.registerApp(APP_KEY, APP_SECRET, {
onCallback: (result: Result) => {
const out = mapResult('register', result);
if (!out.ok) {
ready = false;
console.error('register domain', out.error);
return;
}
// 非个人形态 HAR 时,可在 OK 后注入激活序列号
if (activationSn && !SdkConstants.isPersonalSdk()) {
WPSApi.setWpsFileToken(activationSn);
}
ready = true;
}
});
}
说明:mapResult 按 stage 解释正整数,避免把回传业务码误判成打开参数错误。注册回调里单独处理 AUTH_FAILURE。SdkConstants.isPersonalSdk() 用于决定是否注入序列号,页面层无需感知 HAR 形态细节。
打开与回传走同一出口
export async function openDocFacade(
context: common.UIAbilityContext,
sandboxPath: string,
opts: { editable?: boolean; transfer?: boolean } = {}
): Promise<WpsOutcome> {
if (!ready) {
return { ok: false, error: { kind: 'NOT_REGISTERED', msg: 'gate closed' } };
}
const request = new OpenFileRequest(context, sandboxPath);
request.enableEdit = opts.editable === true; // 默认只读
if (opts.transfer) {
request.enableTransferFile = true;
}
try {
const result = await WPSApi.sendRequest(request);
const stage = opts.transfer ? 'transfer' : 'open';
const out = mapResult(stage, result);
if (!out.ok) {
console.error('open/transfer domain', out.error, result.requestType);
}
return out;
} catch (e) {
return {
ok: false,
error: { kind: 'NOT_REGISTERED', msg: (e as Error).message },
};
}
}
说明:未就绪时直接返回领域错误,避免无意义 throw。enableEdit 显式默认只读,与对接语义一致。开启回传时用 stage='transfer' 解释正整数;未开启时按 open 阶段映射 -2 等。
页面侧只消费 WpsOutcome:
async function onOpenClicked(ctx: common.UIAbilityContext, path: string) {
const out = await openDocFacade(ctx, path, { editable: true, transfer: true });
if (out.ok) {
// 回传成功时再读 out.data?.fileUri / transferFd,并拷贝到沙箱
return;
}
switch (out.error.kind) {
case 'AUTH_FAILURE':
// 引导检查 appKey/appSecret 与 bundleName
break;
case 'NOT_REGISTERED':
registerFacade();
break;
case 'PARAM_OR_OPEN':
// 查沙箱路径、参数组合
break;
case 'TRANSFER_BIZ':
// 上报业务 code,提示回传失败
break;
default:
break;
}
}
与原始错误码表的对应关系
对接侧常量不变,Facade 只做翻译:
SDK code | 常量 | Facade kind |
|---|---|---|
0 | OK | 成功分支 |
-1 | NONE | 通常视为未就绪 / UNKNOWN |
-2 | ERROR | PARAM_OR_OPEN |
1013 | ERROR_CODE_AUTH_FAILURE | AUTH_FAILURE |
| 其它正整数(回传) | — | TRANSFER_BIZ |
| throw | — | NOT_REGISTERED |
Result.msg 建议原样进入日志,UI 文案用领域码映射中文,避免把底层英文/内部文案直接甩给用户。Result.data 仅在 ok: true 时下发,防止失败态误读空 fileUri。
工程实践要点
- 注册必须先于打开;Facade 用
ready门闩双保险。 - 选择器 URI 先拷贝进
filesDir再打开,减少-2。 - 默认只读;可编辑场景显式
enableEdit = true。 - 监控按
kind聚合,再下钻原始code。 - Release 不打印 secret;
msg注意脱敏。
再补几条容易在评审里被问到的边界:
ResultCode.NONE(-1):表示默认/未赋值,不要当成功,也不要直接映射成 PARAM_OR_OPEN。若回调尚未触发就读到 -1,说明时序仍停在注册前,应显示加载态而不是错误 Toast。
成功但无文件:未开启关闭回传时,code === OK 且 data 为空是预期行为。Facade 的 ok: true 仍成立,页面若强依赖 fileUri,应在调用参数里要求 transfer: true,并在成功分支校验 data。
单元测试怎么切:为 mapResult 写表驱动测试即可覆盖 0 / -2 / 1013 / 正整数 / 缺省 code。openDocFacade 对 ready=false 与 catch 两条 NOT_REGISTERED 路径各测一次。不要在单测里真拉起 WPS;把 WPSApi.sendRequest 换成可注入的函数类型即可。
埋点维度:kind 作告警主键,stage 作下钻维度,原始 code 作标签。这样回传业务码上涨时,不会误触发「凭据大面积失败」的值班规则。UI 文案按 kind 配置中文,避免把底层 msg 原文甩给终端用户。
与页面状态机配合:建议页面只有 Idle / Registering / Ready / Opening / Transferring / Failed(kind) 有限状态。Facade 返回后只做状态迁移,不在按钮回调里再写一套 if (code === …)。冷启动在 Ability 阶段调用 registerFacade,首页打开按钮绑定 Ready 门闩,可减少竞态。
小结
在鸿蒙 WPS 二开里,错误处理的质量往往决定联调效率。把 Result 与 throw 收进 Facade,页面只面对有限领域错误,1013、-2、回传正整数就不会再挤在同一个 else。上述适配层可直接嵌进现有 TypeScript 工程:先落地 mapResult 与门闩,再逐步把散落的 result.code === 0 替换掉。当你需要扩展水印、修订或不落地等能力时,继续把失败收口到同一套 WpsDomainError,比在每个页面复制错误码表更稳。
更多参数见官方对接文档:365.kdocs.cn/l/clQl5cek2…
技术交流 QQ 群:628436767