刚接 @wps/wps_sdk 时,我最容易犯的错不是「没配水印」,而是这三类:页面按钮里直接 new OpenFileRequest,忘记进程级 registerApp;选择器 URI 原样传入,联调全是 ERROR;预览/编辑共用一套请求,把默认只读当成 SDK 异常。后来把链路收成「注册 → 拷贝 → 按模式打开 → 分支断言」,联调能稳定复现。本文按工程视角写最小闭环,代码尽量完整,并补上断言、页面拼装与后置策略扩展,方便对照真机日志接手维护。
接口时序与注册断言
推荐时序:onCreate → ensureRegistered → 按钮 copyToFilesDir → openByMode(preview|edit) → then(assertOk) / catch(reject)。sendRequest 在未注册时 reject ;注册后失败走 result.code(常见 ERROR、鉴权 1013)。日志固定 requestType/code/msg。appKey/appSecret 与 bundleName 绑定,调试包与上架包各申请一套。轻量阶段一般不配 wpsTransferType,此时 OK && !data 表示拉起成功,不是「没保存」。
typescript
import { common } from '@kit.AbilityKit';
import {
WPSApi, RegisterAppRequest, Result, ResultCode,
} from '@wps/wps_sdk';
export let wpsReady = false;
export function logResult(tag: string, r: Result) {
console.info(`[${tag}] type=${r.requestType} code=${r.code} msg=${r.msg ?? ''}`);
}
export function assertOk(r: Result, stage: string) {
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 ?? 'check path/params'}`);
}
if (r.code !== ResultCode.OK) {
throw new Error(`${stage} code=${r.code}`);
}
}
export async function ensureRegistered(ctx: common.UIAbilityContext) {
if (wpsReady) return;
const r = await WPSApi.sendRequest(
new RegisterAppRequest(ctx, APP_KEY, APP_SECRET)
);
logResult('register', r);
assertOk(r, 'register');
if (ACTIVATION_SN) {
WPSApi.setWpsFileToken(ACTIVATION_SN);
}
wpsReady = true;
}
激活序列号只在有申请材料时注入。首页 wpsReady 前禁用打开,避免冷启动连点 reject。凭据走 flavor rawfile 或 CI 注入,Release 不打 secret。可为 assertOk 写 Mock 1013/ERROR/OK 单测,减少手测依赖。
沙箱拷贝与 enableEdit 打开
选择器路径直接 open 常见 ERROR。先拷到 filesDir,再按 mode 设 enableEdit。
| mode | enableEdit | 用途 |
|---|---|---|
preview |
false(或不设) |
查看附件 |
edit |
true |
修改附件 |
typescript
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,
sandboxPath: string,
mode: 'preview' | 'edit'
) {
await ensureRegistered(ctx);
const req = new OpenFileRequest(ctx, sandboxPath);
req.enableEdit = mode === 'edit';
const r = await WPSApi.sendRequest(req);
logResult(mode, r);
assertOk(r, mode);
return r;
}
联调先测 preview 确认路径与注册,再测 edit。不要一上来叠水印和 extraOptions,否则 ERROR 难归因。拷贝失败与打开失败要拆 toast,避免同事一直怀疑 enableEdit。修订、水印、回传可后置挂到同一 req,按钮层保持 openByMode 不变。
页面拼装与踩坑对照
typescript
// EntryAbility.onCreate
await ensureRegistered(this.context);
async function onOpen(mode: 'preview' | 'edit') {
try {
if (!wpsReady) return;
const picked = await selectOneDocument();
const sandbox = copyToFilesDir(hostContext, picked);
await openByMode(hostContext, sandbox, mode);
} catch (e) {
promptAction.showToast({ message: String(e) });
}
}
| 现象 | 先查 |
|---|---|
| reject | 未注册 / wpsReady |
| 1013 | 包名与凭据 |
| ERROR | 未拷沙箱 |
| 只能看不能改 | enableEdit |
| 以为没回调 | 轻量未开回传,或未关窗 |
清单:冷启动连点、多 flavor bundleName、换 HAR 后 clean、小 docx/xlsx 各测 preview/edit、弱网抽测注册超时。系统升级后再跑一轮最小闭环。切回应用 ≠ 关窗;有回传时不要在空 data 上报成功。
后置回传与策略参数
最小闭环通了以后,按需往 OpenFileRequest 加字段,不必改按钮语义:
typescript
import { TransferType, OpenFileExtraOptions, WaterMark } from '@wps/wps_sdk';
function decoratePolicy(req: OpenFileRequest, stamp?: string) {
req.wpsTransferType = TransferType.URI;
if (stamp) {
const wm = new WaterMark();
wm.Enable = true;
wm.WaterMaskText = stamp;
wm.Angle = -30;
wm.FontColor = '#19000000';
wm.FontSize = 16;
req.wpsWaterMarkParams = wm;
}
const opt = new OpenFileExtraOptions();
opt.enableShare = false;
opt.enableExport = false;
req.extraOptions = opt;
}
分层联调:openByMode 稳定 → URI 回传 → 水印 → extraOptions。不落地模式下部分开关会被强制关闭,以真机菜单为准。统一解析可后抽 resolveCallbackUri(r) 读 fileUri,FD 再扩 transferFd。业务增量通常只改水印文案或上传接口,注册模块保持不动;单元测试继续覆盖空 data、ERROR、1013。若接断点续传,只需替换上传层,打开与注册回归面更小。
工程里我还会再抽一层「打开入口」,避免页面散落字面量路径与重复 try/catch。示意如下:把 DocumentPicker、拷贝、mode、可选 decorate 串起来,业务页只传 preview|edit 与水印文案。这样换 HAR、改 flavor 凭据时,回归范围集中在 wps/ 目录而不是几十个按钮回调。Debug 包可开详细日志看完整 msg;Release 只保留 code 与精简错误映射,避免 secret 与绝对路径进崩溃上报。
typescript
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);
await ensureRegistered(ctx);
const req = new OpenFileRequest(ctx, sandbox);
req.enableEdit = mode === 'edit';
if (stamp) {
decoratePolicy(req, stamp);
}
const r = await WPSApi.sendRequest(req);
logResult('pickAndOpen', r);
assertOk(r, 'pickAndOpen');
return r;
}
需要回传时再在 decoratePolicy 里打开 TransferType.URI,并用单独的 copyCallback 落盘;不要把上传成功耦合进 assertOk。同一单据多次编辑请用新时间戳文件名。机型差异主要体现在生命周期动画速度,测试用例应明确「关闭窗口」触发回传。以上叠完后,日常联调成本会明显下降,新人也不容易把注册逻辑又抄回页面。
联调检查清单我通常写成四条,交给同学按序打勾:冷启动后是否只调用一次 registerApp 且无 1013;选择器路径是否已进入 filesDir 且扩展名未丢;预览与编辑两按钮是否共用入口且仅差 enableEdit;日志是否能按 requestType 过滤。四条都绿再开 wpsTransferType。机型覆盖至少一台完整版 WPS、一台预装阉割场景,菜单项与预期不一致时优先查不落地模式与 extraOptions,而不是改注册。文档字段以对接文档为准,HAR 升级后对照 OpenFileRequest 与 Result 各扫一遍即可。
小结
最小闭环靠时序:注册拿到 wpsReady,沙箱保证路径,enableEdit 区分预览与编辑,assertOk 分层吃错误。跑通后再叠策略参数,比一次堆满配置稳得多。字段表以官方对接文档为准;文档更新后对照 OpenFileRequest / Result 扫一遍即可。把入口与断言收进 wps/ 模块后,业务页只负责触发与提示,后续加上传或水印也不会再把鉴权逻辑摊回页面。
基于 WPS Open SDK 鸿蒙版实践整理。
官方对接文档:365.kdocs.cn/l/clQl5cek2...
QQ 群:628436767