OnlyOffice 编辑器实现记录
在一个项目中引入了 OnlyOffice 作为 office 编辑器,这里记录一下实现过程和一些配置上的坑。
1. 总体架构
我们使用自部署的 OnlyOffice Server,使用 docker-compose.yml 启动 server。
总体的文档编辑流程如下:
2. 后端配置
OnlyOffice 配置生成
首先先为 OnlyOffice 分别生成用于访问和回调的 JWT Token(如果不使用 JWT Token 访问的话会报错说不安全),使用 Token 构造对应的 URL:
const officeToken = jwt.sign(
{
id: userId,
email: userEmail,
fileId,
purpose: "office-content",
},
config.jwtSecret,
{ expiresIn: "15m" },
);
// 通过参数传递 token
const officeContentUrl = `${config.officeCallbackUrl}/api/files/${fileId}/office-content?token=${officeToken}`;
// 生成回调 token(较长有效期,编辑期间需持续有效)
const callbackToken = jwt.sign(
{
id: userId,
email: userEmail,
fileId,
purpose: "office-callback",
},
config.jwtSecret,
{ expiresIn: "24h" },
);
const callbackUrl = `${config.officeCallbackUrl}/api/files/${fileId}/office-callback?token=${callbackToken}`;
然后按照文档生成配置,并随 Token 一起返回:
const documentConfig = {
document: {
fileType: this.getFileType(file.name),
key: `${fileId}_${file.updatedAt.getTime()}`, // 只在更新时变化的 key
title: file.name,
url: officeContentUrl,
permissions: {
comment: true,
copy: true,
download: true,
edit: true,
fillForms: true,
modifyContentControl: true,
modifyFilter: true,
print: true,
review: true,
},
},
documentType: this.getDocumentType(file.name),
editorConfig: {
callbackUrl,
user: {
id: userId,
name: userEmail,
},
customization: {
autosave: true,
forcesave: true,
},
},
};
const response: any = {
url: officeContentUrl,
config: documentConfig,
token: onlyofficeToken;
};
return response;
使用回调响应 Save 等需求
OnlyOffice 的回调所需参数如下(没找到类型定义就直接写了):
callbackBody: {
status: number;
url?: string;
key?: string;
users?: string[];
actions?: Array<{ type: number; userid: string }>;
forcesavetype?: number;
userdata?: string;
};
由于 OnlyOffice 是放在 Docker 中的,简单处理一下确保可以访问:
if ((status === 2 || status === 6) && url) {
try {
// 从 OnlyOffice Document Server 下载修改后的文档
// OnlyOffice 返回的 URL 可能包含 localhost,需要在 Docker 环境中转换为容器名
let downloadUrl = url;
// 在 Docker 环境中,将 localhost 替换为 OnlyOffice 容器名
// 处理各种可能的格式:localhost, localhost:80, localhost:8080, 127.0.0.1
if (downloadUrl.includes("://localhost:8080")) {
// 宿主机8080映射到容器内部80
downloadUrl = downloadUrl.replace(
"://localhost:8080",
"://gdrive-onlyoffice",
);
} else if (downloadUrl.includes("://localhost:80")) {
downloadUrl = downloadUrl.replace(
"://localhost:80",
"://gdrive-onlyoffice",
);
} else if (downloadUrl.includes("://localhost")) {
downloadUrl = downloadUrl.replace(
"://localhost",
"://gdrive-onlyoffice",
);
} else if (downloadUrl.includes("://127.0.0.1")) {
downloadUrl = downloadUrl.replace(
"://127.0.0.1",
"://gdrive-onlyoffice",
);
}
}
}
然后直接 fetch 就行了:
const response = await fetch(downloadUrl);
OnlyOffice 流式预览 API
需要专门为 OnlyOffice 提供流式预览 API、让 Docker 中的 OnlyOffice Server 能够访问文件内容。做 Token 校验后使用普通文件 Preview 即可:
async serveOfficeContent(req: Request, res: Response, next: NextFunction) {
const token = req.query.token as string;
if (!token) {
throw new AppError(StatusCodes.UNAUTHORIZED, "Token is required");
}
const payload = this.fileService.verifyOfficeContentToken(token);
if (
payload.purpose !== "office-content" ||
payload.fileId !== req.params.fileId
) {
throw new AppError(StatusCodes.FORBIDDEN, "Token mismatch");
}
const result = await this.fileService.getPreviewStream({
userId: payload.id,
fileId: payload.fileId,
});
res.setHeader("Content-Type", result.mimeType);
res.setHeader("Content-Length", result.size);
res.setHeader(
"Content-Disposition",
`inline; filename="${encodeURIComponent(result.fileName)}"`,
);
res.setHeader("Cache-Control", "private, max-age=900");
result.stream.pipe(res);
result.stream.on("error", (error) => {
console.error("Office content stream error:", error);
if (!res.headersSent) {
throw new AppError(
StatusCodes.INTERNAL_SERVER_ERROR,
"Failed to stream file",
);
}
});
}
2. 前端使用 OnlyOffice 组件
配置加载
调用 backend 获取 config 即可:
const [result, setResult] = useState<OnlyOfficeResult | null>(null);
try {
const officeData = await fileService.getOfficeContentUrl(fileId);
setResult({
url: officeData.url,
token: officeData.token,
config: officeData.config,
});
} catch (err) {
const message =
err instanceof Error
? err.message
: "Failed to load OnlyOffice configuration";
setResult(null);
}
OnlyOffice 编辑器组件
我们定义同前面一样的 OnlyOffice 类型(偷懒也可以直接写 any),然后把这些东西注入到 OnlyOffice API 脚本中。核心部分如下:
const editorRef = useRef<HTMLDivElement>(null);
const docEditorRef = useRef<DocsAPIDocEditor | null>(null);
docEditorRef.current = new window.DocsAPI.DocEditor(
editorRef.current.id,
config,
);
这里
DocsAPIDocEditor就是一个简单的 Editor 接口,也可以传 any。
然后返回这个组件:
return (
<div className="w-full h-full">
<div
id={`onlyoffice-editor-${fileId}`}
ref={editorRef}
className="w-full h-full"
/>
</div>
);