HarmonyOS 应用要在端内预览或编辑 Word、Excel、PPT,常见做法是集成 @wps/wps_sdk,用 WPSApi 拉起本机已安装的 WPS。打开动作本身不分散在多个入口类里:构造 OpenFileRequest,填路径与是否可编辑,再调用 WPSApi.sendRequest。联调里把「点按钮没反应」写成打开失败,多数时候其实是注册未完成、路径不在应用沙箱,或把 Promise 异常当成了 Result.code。本文按接口语义写打开链路:构造参数、沙箱拷贝、enableEdit、结果分支与可复用封装。字段以官方对接文档为准,随 HAR 批次核对后再合入。
一、打开链路在 SDK 调用中的位置
完整顺序固定为:集成 HAR → 启动阶段 registerApp → 回调到 ResultCode.OK → 按凭据约定可选调用 setWpsFileToken → 把待打开文件拷进本应用沙箱 → new OpenFileRequest(context, path) → 设置 enableEdit → sendRequest。打开层只消费已经就绪的 SDK 与可读路径,不负责签发 appKey。
| 层级 | 职责 | 入口 |
|---|---|---|
| 依赖 | HAR 与包名绑定凭据 | @wps/wps_sdk |
| 门禁 | 应用注册 | registerApp |
| 打开 | 沙箱路径、只读或可编辑 | OpenFileRequest |
| 调度 | 拉起 WPS 并返回结果 | WPSApi.sendRequest |
当前请求类型只有 OPEN_FILE。水印、extraOptions、关窗回传都是 OpenFileRequest 上的附加字段,不要在只读打开尚未稳定时一起打开。出现 1013 时暂停改路径与 enableEdit,先对齐 bundleName、HAR 与申请归档。
二、构造参数:context 与 fileUri
签名是 new OpenFileRequest(context: common.UIAbilityContext, fileUri: string)。两个参数都必填。context 必须是当前 UIAbility 的上下文,页面里用错 Ability 或把 undefined 传进去,打开侧常见泛化 ResultCode.ERROR。fileUri 文档写成本地文件路径;实践上应传入本应用沙箱内可读路径。系统文件选择器给出的 URI 往往没有跨进程读权限,WPS 进程打不开,日志却不一定写「权限」二字。
建议在打开前把源文件拷到 context.filesDir 下的固定子目录,再把拷贝后的路径交给构造函数。目录按业务拆分可以,但全仓应共用同一个拷贝函数,避免每个页面各写一套临时文件名规则。
typescript
import { common } from '@kit.AbilityKit';
import fs from '@ohos.file.fs';
export function copyIntoSandbox(
ctx: common.UIAbilityContext,
src: string,
ext: string = 'docx'
): string {
const dir = `${ctx.filesDir}/wps_inbox`;
fs.mkdirSync(dir, true);
const dest = `${dir}/${Date.now()}.${ext}`;
fs.copyFileSync(src, dest);
return dest;
}
拷贝失败应在打开前抛给调用方,不要带着半截路径去 sendRequest。扩展名与真实文件类型保持一致,避免客户端按后缀解析失败。调试包与商店包的 filesDir 相互隔离,不要把调试机上的绝对路径写进正式包配置。
三、enableEdit:未赋值即只读
| 赋值 | 实际模式 |
|---|---|
| 未设置 | 只读(ReadOnly) |
false |
只读 |
显式 true |
可编辑(Normal) |
预览入口与编辑入口应走同一封装,只差一个布尔参数。两套 new OpenFileRequest 很容易在后续叠水印时漂移。enableEdit 与关窗回传独立:能编辑不等于必须回传;只读预览也可以不设 wpsTransferType。联调顺序建议:注册 OK → 沙箱只读打开 → 同一路径 enableEdit = true → 再叠策略字段。
不推荐在每次构造时写 request.wpsToken。需要激活序列号时,在 registerApp 回调 ResultCode.OK 之后调用 WPSApi.setWpsFileToken,后续所有打开请求自动携带。Request 字段与全局设置同时存在时,以全局设置为准,日志会误导排查。
四、sendRequest 的两种失败形态
WPSApi.sendRequest(request) 返回 Promise<Result>。对接文档写明:注册尚未成功时抛异常,而不是返回一个带 code 的 Result。UI 必须同时处理 .then 里的非 OK,以及 .catch 里的异常。把异常文案直接展示成「文档损坏」会误导测试。
Result 常用字段:requestType、code、msg、data。未开启关闭回传时,拉起成功即 code === ResultCode.OK,data 为空,这是正常语义,不要弹「已保存」。开启回传后,用户关窗且回传成功才在 data 里带 fileUri 或 transferFd。
| 现象 | 优先核对 |
|---|---|
| Promise reject | 是否等到注册 OK |
1013 |
appKey / appSecret / bundleName |
ResultCode.ERROR(-2) |
路径是否沙箱、Context 是否有效 |
OK 且 data 为空 |
是否根本没开回传 |
typescript
import { WPSApi, OpenFileRequest, Result, ResultCode } from '@wps/wps_sdk';
import { common } from '@kit.AbilityKit';
export async function openLocalDoc(
ctx: common.UIAbilityContext,
sandboxPath: string,
editable: boolean
): Promise<Result> {
const req = new OpenFileRequest(ctx, sandboxPath);
req.enableEdit = editable;
try {
const result = await WPSApi.sendRequest(req);
console.info('[WPS] open', result.code, result.msg ?? '', !!result.data);
return result;
} catch (e) {
console.error('[WPS] sendRequest threw', e);
throw e;
}
}
日志前缀固定为 [WPS] open 与 [WPS] register,便于把打开失败和注册失败拆开看。Release 构建不要打印完整 appSecret 或序列号。
五、把注册门禁和打开函数拆开
页面点击回调里直接 registerApp 再立刻 sendRequest,时序上几乎必然撞上未注册异常。把注册做成可 await 的一次性准备,打开函数只检查就绪标记。
typescript
let sdkReady = false;
export function waitUntilRegistered(
appKey: string,
appSecret: string,
activationSn?: string
): Promise<void> {
return new Promise((resolve, reject) => {
if (sdkReady) {
resolve();
return;
}
WPSApi.registerApp(appKey, appSecret, {
onCallback: (r: Result): void => {
if (r.code === ResultCode.ERROR_CODE_AUTH_FAILURE) {
reject(new Error(`1013:${r.msg ?? ''}`));
return;
}
if (r.code !== ResultCode.OK) {
reject(new Error(`register:${r.code}`));
return;
}
if (activationSn) {
WPSApi.setWpsFileToken(activationSn);
}
sdkReady = true;
resolve();
},
});
});
}
冷启动在 Ability onCreate 里发起 waitUntilRegistered。打开按钮用 sdkReady 控制 enabled。activationSn 由构建配置注入:需要序列号的产物传真实值,不需要的产物传空,封装内部跳过 setWpsFileToken。不要在每个 Page 的 aboutToAppear 里再调一次注册。
六、联调清单与现象对照
建议写进测试用例而不是口头约定:
- 冷启动日志出现注册
code=0 - 选择器文件经
copyIntoSandbox后再打开 - 只读入口未写
enableEdit = true,工具栏不可改 - 编辑入口显式
true,可保存 - 未开回传时 OK 且
data为空,UI 不提示保存成功 - 未就绪时点击打开,走 catch,不伪造
Result - 换 HAR 后
ohpm install并 clean,重跑只读打开
| 口头描述 | 更可能的原因 |
|---|---|
| 点了没反应 | 按钮未等 sdkReady,异常被吞 |
| 提示失败但文件能在文件管理器打开 | 没拷沙箱 |
| 以为只读也能改 | 漏写 enableEdit 或写成了 true |
| 以为保存成功 | 把拉起 OK 当成回传 OK |
策略字段放到只读与可编辑都稳定之后再叠。extraOptions 仅显式赋值的项生效,不要整表拷默认值。换 flavor 后 bundleName 与凭据必须重新归档;调试包绿、商店包红,优先查包名而不是再改 OpenFileRequest 字段。
七、小结
OpenFileRequest 打开本地文档可以收成四条工程纪律:注册等到 ResultCode.OK;文件先进入本应用沙箱;默认只读,可编辑必须显式赋值;sendRequest 的异常与 Result.code 分开处理。统一版把打开面收敛到一套 API,工程上仍要把路径与时序做对。把 waitUntilRegistered、copyIntoSandbox、openLocalDoc 分成三个符号后,预览页与编辑页只差一个布尔值,后续叠水印或回传也不必再复制一套构造代码。
接入评审时把当前 HAR 文件名、凭据环境、是否注入序列号、沙箱目录约定写成一页对照。测试按表验收,比口头说「都能打开」可靠。合入后全仓搜索 new OpenFileRequest,命中应集中在打开封装文件。外部 URI 权限不足时常见泛化 ERROR;正式包与调试包包名不同时,凭据批次分开存放。字段语义继续以官方对接文档为准,随 SDK 小版本更新注释,而不是把整张参数表贴进业务页。
基于 WPS Open SDK 鸿蒙版对接实践整理,仅供开发者参考。