刚接 @wps/wps_sdk 时,我把打开写在按钮回调里,注册写在 Ability 另一处,结果冷启动连点全是 reject;修完注册后又踩沙箱路径 ERROR;再修完发现编辑按钮其实一直只读。后来把链路收成「注册断言 → 沙箱拷贝 → 按模式打开 → 可选回传装饰」,联调才稳定。本文按工程视角写痛点与收敛代码,方便对照真机日志。
为什么痛点总在联调才爆
文档里的 API 并不长,麻烦在于语义分层:
- reject 与
result.code不是同一通道:未注册走前者,鉴权失败常见 1013,路径问题常见 ERROR。 - 拉起成功 与 关窗回传 不是同一状态:没配
wpsTransferType时OK && !data合法。 - 只读默认 很安静:忘写
enableEdit = true时没有「大声失败」,只是菜单不对。
所以「一次写满水印 + 功能开关 + 回传」往往把四种失败叠在一起。建议分层:最小打开通了,再叠策略。
注册层:把 1013 与重复调用收掉
import { common } from '@kit.AbilityKit';
import {
WPSApi,
RegisterAppRequest,
Result,
ResultCode,
SdkConstants,
} from '@wps/wps_sdk';
export let wpsReady = false;
export function assertOk(r: Result, stage: string): void {
if (r.code === ResultCode.ERROR_CODE_AUTH_FAILURE) {
throw new Error(`${stage} 1013: ${r.msg ?? ''}`);
}
if (r.code === ResultCode.ERROR) {
throw new Error(`${stage} ERROR: ${r.msg ?? 'path/params'}`);
}
if (r.code !== ResultCode.OK) {
throw new Error(`${stage} code=${r.code}`);
}
}
export async function ensureRegistered(ctx: common.UIAbilityContext): Promise<void> {
if (wpsReady) return;
const r = await WPSApi.sendRequest(
new RegisterAppRequest(ctx, APP_KEY, APP_SECRET)
);
console.info(`[register] code=${r.code} msg=${r.msg ?? ''}`);
assertOk(r, 'register');
if (!SdkConstants.isPersonalSdk() && ACTIVATION_SN) {
WPSApi.setWpsFileToken(ACTIVATION_SN);
}
wpsReady = true;
}
要点:wpsReady 短路避免重复注册;1013 单独抛,方便和路径 ERROR 区分;序列号用 setWpsFileToken 全局注入,不在每次 OpenFileRequest 上塞 wpsToken。凭据走 flavor rawfile 或 CI 注入,Release 不打 secret。换 HAR 后务必 clean,旧 so 残留会让错误码「看起来随机」。
打开层:沙箱 + enableEdit 一个入口
import fs from '@ohos.file.fs';
import { OpenFileRequest, WPSApi } from '@wps/wps_sdk';
export function copyToFilesDir(ctx: common.UIAbilityContext, src: string): string {
const dir = `${ctx.filesDir}/wps_lite`;
fs.mkdirSync(dir, true);
const dest = `${dir}/${Date.now()}_${src.split('/').pop() ?? 'file.docx'}`;
fs.copyFileSync(src, dest);
return dest;
}
export async function openByMode(
ctx: common.UIAbilityContext,
picked: string,
mode: 'preview' | 'edit'
) {
await ensureRegistered(ctx);
if (!wpsReady) return;
const sandbox = copyToFilesDir(ctx, picked);
const req = new OpenFileRequest(ctx, sandbox);
req.enableEdit = mode === 'edit';
const r = await WPSApi.sendRequest(req);
console.info(`[${mode}] code=${r.code} msg=${r.msg ?? ''}`);
assertOk(r, mode);
return r;
}
| mode | enableEdit | 用途 |
|---|---|---|
| preview | false / 不设 | 查看附件 |
| edit | true | 修改附件 |
联调先测 preview 确认注册与路径,再测 edit。拷贝失败与打开失败拆 toast,避免同事一直怀疑 enableEdit。同一单据多次编辑用新时间戳文件名,减少覆盖中间态。
回传层:后置装饰,别和 assertOk 耦死
import { TransferType, WaterMark, OpenFileExtraOptions } from '@wps/wps_sdk';
export function decoratePolicy(req: OpenFileRequest, stamp?: string): void {
req.wpsTransferType = TransferType.URI;
if (stamp) {
const wm = new WaterMark();
wm.Enable = true;
wm.WaterMarkText = stamp;
wm.Angle = -30;
wm.FontColor = '#19000000';
wm.FontSize = 16;
req.wpsWaterMarkParams = wm;
}
const opt = new OpenFileExtraOptions();
opt.enableShare = false;
opt.enableCloud = false;
opt.enablePrint = false;
req.extraOptions = opt;
}
export async function pickOpenAndMaybeTransfer(
ctx: common.UIAbilityContext,
mode: 'preview' | 'edit',
withTransfer: boolean,
stamp?: string
) {
await ensureRegistered(ctx);
const picked = await selectOneDocument();
const sandbox = copyToFilesDir(ctx, picked);
const req = new OpenFileRequest(ctx, sandbox);
req.enableEdit = mode === 'edit';
if (withTransfer) {
decoratePolicy(req, stamp);
}
const r = await WPSApi.sendRequest(req);
assertOk(r, 'pickOpen');
// withTransfer 时:关窗后才可能有 r.data.fileUri;切回应用不等于关窗
return r;
}
上传成功不要写进 assertOk。关窗回传的路径在 WPS 侧沙箱,业务要再 copy 到本应用目录。FD 模式用 transferFd 分块读并校验 transferFileSize。不落地模式下部分开关会被强制关闭,以真机菜单为准。
页面拼装与踩坑对照
// EntryAbility.onCreate
await ensureRegistered(this.context);
async function onOpen(mode: 'preview' | 'edit') {
try {
if (!wpsReady) return;
await openByMode(hostCtx, pickerUri, mode);
} catch (e) {
promptAction.showToast({ message: String(e) });
}
}
| 现象 | 先查 |
|---|---|
| reject | 未注册 / wpsReady |
| 1013 | 包名与凭据 / HAR |
| ERROR | 沙箱路径 |
| 只能看不能改 | enableEdit |
| 以为没保存 | 轻量未开回传时正常 |
清单:冷启动连点、多 flavor bundleName、换 HAR 后 clean、小 docx/xlsx 各测 preview/edit、弱网抽测注册超时。系统或 WPS 客户端升级后再跑一轮最小闭环。可为 assertOk 写 Mock 1013/ERROR/OK 单测,减少真机连点。
工程里我还会再抽一层「打开入口」,避免页面散落字面量路径与重复 try/catch。示意如下:把 DocumentPicker、拷贝、mode、可选 decorate 串起来,业务页只传 preview|edit 与水印文案。这样换 HAR、改 flavor 凭据时,回归范围集中在 wps/ 目录而不是几十个按钮回调。Debug 包可开详细日志看完整 msg;Release 只保留 code 与精简错误映射,避免 secret 与绝对路径进崩溃上报。
export async function pickAndOpen(
ctx: common.UIAbilityContext,
mode: 'preview' | 'edit',
stamp?: string
) {
await ensureRegistered(ctx);
if (!wpsReady) return;
const picked = await selectOneDocument();
const sandbox = copyToFilesDir(ctx, picked);
const req = new OpenFileRequest(ctx, sandbox);
req.enableEdit = mode === 'edit';
if (stamp) {
decoratePolicy(req, stamp);
}
const r = await WPSApi.sendRequest(req);
assertOk(r, 'pickAndOpen');
return r;
}
需要回传时再在 decoratePolicy 里打开 TransferType.URI,并用单独的 copyCallback 落盘;不要把上传成功耦合进 assertOk。同一单据多次编辑请用新时间戳文件名。机型差异主要体现在生命周期动画速度,测试用例应明确「关闭窗口」触发回传。联调检查清单通常写成四条:冷启动后是否只调用一次 registerApp 且无 1013;选择器路径是否已进入 filesDir;预览与编辑两按钮是否共用入口且仅差 enableEdit;日志是否能按 requestType 过滤。四条都绿再开 wpsTransferType。以上叠完后,日常联调成本会明显下降,新人也不容易把注册逻辑又抄回页面。
小结
痛点收敛的本质是语义分层:注册挡住 reject 与 1013,沙箱挡住 ERROR,enableEdit 分清模式,回传配置解释空 data。入口收进 wps/ 模块后,业务页只负责触发与提示,后续加水印或上传也不会把鉴权摊回页面。字段以官方对接文档为准;文档更新后对照 OpenFileRequest / Result 扫一遍即可。把入口与断言收进模块后,业务页只负责触发与提示,后续加上传或水印也不会再把鉴权逻辑摊回页面。
基于 WPS Open SDK 鸿蒙版实践整理。
官方对接文档:365.kdocs.cn/l/clQl5cek2…
QQ 群:628436767