背景
在 WPS 加载项里做文件上传时,最常见的做法是让用户通过 <input type="file"> 或上传组件重新选择本地文件。但在办公插件场景里,用户真正想上传的往往就是当前正在编辑的文档。
如果还要求用户手动选择一次文件,体验上会有几个问题:
- 用户需要知道当前文档实际保存在哪里。
- 未保存或路径复杂的文件容易找不到。
- 插件已经运行在 WPS 内,却没有利用"当前文档"这个上下文。
- Mac WPS 环境下,普通浏览器文件选择和 WPS 文件系统路径还可能出现兼容问题。
因此,这次实现的核心目标是:不让用户选择文件,而是直接读取 WPS 当前活动文档,将其转成可上传的 Blob,再通过 FormData 提交给后端。
核心流程
整体流程可以拆成几步:
text
获取当前活动文档
记录原始文档路径和保存状态
生成临时文件路径
将当前文档另存到临时路径
读取临时文件的二进制内容
转换成 Blob
通过 FormData 上传
恢复当前文档到原始路径
清理临时文件
其中最重要的是两点:
- 上传的是当前 WPS 文档,而不是用户重新选择的文件。
- 临时保存和读取不能破坏用户继续编辑当前文档的体验。
获取当前活动文档
WPS 加载项中可以通过 wps.WpsApplication().ActiveDocument 获取当前活动文档。
为了兼容不同运行环境,通常会封装一个获取应用对象的方法:
js
getWpsApplication(){
if (window.Application) return window.Application
if (typeof wps === 'undefined') return null
if (wps.Application) return wps.Application
if (typeof wps.WpsApplication === 'function') return wps.WpsApplication()
return null
}
然后再取当前文档:
js
getActiveDocument(){
const app = this.getWpsApplication()
const activeDocument = app && app.ActiveDocument ? app.ActiveDocument : null
if (!activeDocument) {
throw new Error('当前没有打开任何文档')
}
return activeDocument
}
这一步要明确处理"没有打开文档"的情况,否则后续保存、读取都会变成不清晰的运行时错误。
为什么不能直接用当前文件路径
如果文档已经保存过,理论上可以读取 activeDocument.FullName 对应的本地文件。但这并不总是可靠:
- 用户当前编辑内容可能还没有保存到磁盘。
- 直接读取原文件可能拿到旧内容。
- 新建文档可能还没有
FullName。 - Mac WPS 的路径读写行为和普通浏览器环境不完全一致。
因此,更稳的方式是先把当前文档落到一个临时路径,再读取这个临时文件。
临时保存的取舍
理想情况下,应该调用"保存副本"类 API,这样不会改变当前文档路径。但实际运行环境里并不一定存在 SaveCopyAs 方法。
因此最终采用的策略是:
text
记录原始 FullName
SaveAs2(tempPath)
读取 tempPath
SaveAs2(originalFullName)
也就是说,先用 SaveAs2 把当前文档保存到临时路径,读取完成后再立刻 SaveAs2 回原始路径。
这一步的关键是必须记录原始路径:
js
getOriginalDocumentFullName(activeDocument){
const fullName = activeDocument.FullName || ''
if (!fullName || fullName === activeDocument.Name) {
throw new Error('请先保存当前文档后再上传')
}
return fullName
}
对于未保存过的新文档,没有可恢复的原始路径。此时不应该继续上传,而应该提示用户先保存文档。
读取二进制内容
在 WPS 文件系统里,不能简单假设存在 ReadFile API。实际可用的是:
js
wps.FileSystem.readAsBinaryString(filePath)
读取后得到的是二进制字符串,需要手动转成字节数组,再构造 Blob:
js
createBlobFromWpsFileContent(fileContent){
if (typeof fileContent === 'string') {
const bytes = new Uint8Array(fileContent.length)
for (let index = 0; index < fileContent.length; index++) {
bytes[index] = fileContent.charCodeAt(index) & 0xff
}
return new Blob([bytes], { type: 'application/octet-stream' })
}
return new Blob([fileContent], { type: 'application/octet-stream' })
}
这里不能把二进制字符串直接当普通文本处理,否则文件内容会损坏。
避免上传 5 字节假文件
实现过程中遇到过一个很典型的问题:上传出来的文件大小只有 5 字节,类型是 application/octet-stream。
后来发现,这是因为文件读取失败时返回了 false,但代码仍然把它包装成了 Blob:
js
new Blob([false], { type: 'application/octet-stream' }).size
// 5
也就是说,上传的不是文档内容,而是字符串 "false"。
因此读取文件后必须做失败值校验:
js
readWpsFileAsBinaryString(filePath){
const fileSystem = typeof wps !== 'undefined' ? wps.FileSystem : null
if (!fileSystem || typeof fileSystem.readAsBinaryString !== 'function') {
throw new Error('当前环境不支持读取 WPS 临时文件')
}
const fileContent = fileSystem.readAsBinaryString(filePath)
if (
fileContent === false ||
fileContent === undefined ||
fileContent === null ||
fileContent === '' ||
fileContent === 'false'
) {
throw new Error('读取 WPS 临时文件失败')
}
return fileContent
}
这个判断看起来很朴素,但非常关键。它可以避免错误文件继续进入后端流程,也能让问题更早暴露在前端。
通过 FormData 上传
拿到 Blob 后,就可以用 FormData 上传:
js
const formData = new FormData()
formData.append('file', fileBlob, fileName)
await this.$axios.post('/knowledge/uploadFile', formData)
这里建议一定传第三个参数 fileName。
如果只写:
js
formData.append('file', fileBlob)
后端拿到的文件名可能会变成默认的 blob,影响文件名展示、后缀识别或后续处理。
恢复用户编辑状态
因为使用了 SaveAs2(tempPath),当前活动文档会临时切到临时文件路径。如果不恢复,用户上传完成后继续编辑的可能就不是原文件了。
因此上传流程必须在 finally 中恢复:
js
restoreOriginalDocumentPath(activeDocument, originalFullName, originalSaved){
activeDocument.SaveAs2(originalFullName)
if (typeof originalSaved === 'boolean') {
activeDocument.Saved = originalSaved
}
}
这里恢复了两件事:
- 文档路径恢复到原始
FullName。 - 文档保存状态尽量恢复到上传前的状态。
这能减少上传动作对用户编辑体验的干扰。
临时文件清理
上传完成后,需要尝试删除临时文件:
js
removeTempFile(tempPath){
if (!tempPath || typeof wps === 'undefined' || !wps.FileSystem) return
try {
if (wps.FileSystem.Exists(tempPath)) {
wps.FileSystem.Remove(tempPath)
}
} catch (e) {
// 临时文件清理失败不影响上传结果
}
}
清理失败不应该影响主流程。上传已经完成时,临时文件删除失败最多是环境层面的残留问题,不应该反向提示用户"上传失败"。
关键经验
第一,不要假设 WPS 加载项环境等同于浏览器环境。当前文档、文件系统、路径读取都要走 WPS 提供的能力。
第二,Mac WPS 的本地路径和文件读取行为更容易暴露兼容问题。读取结果必须校验,不能直接相信 API 返回值。
第三,SaveAs2 会改变当前活动文档路径。只要用它做临时保存,就必须设计恢复逻辑。
第四,未保存文档需要单独处理。没有原始路径时,无法可靠恢复用户继续编辑的文件。
第五,上传 Blob 时要带文件名。否则后端可能无法正确识别真实文件名和扩展名。
小结
"上传当前文档"看起来是一个很小的功能,但在 WPS 加载项里涉及多个边界:活动文档、临时保存、本地路径、二进制读取、Blob 转换、上传文件名、状态恢复和临时文件清理。
最终实现的重点不是上传接口本身,而是如何在不打断用户编辑体验的前提下,把当前 WPS 文档可靠地转换成一个可上传文件。
这类能力非常适合沉淀成插件基础能力:只要封装好"当前文档转 Blob/File"的过程,后续无论是上传到知识库、发起审批、同步到网盘,还是提交到业务系统,都可以复用同一套底层逻辑。