ONLYOFFICE
ONLYOFFICE 文档 是一个开源办公套件,包括文本文档、电子表格、演示文稿和可填写表单的编辑器。 它提供以下功能:创建、编辑和查看文本文档、电子表格、演示文稿和可填写表单。
如何实现前后端数据一致
主体流程:
graph TD
前端 <---> onlyoffice-Docker <---> 回调服务 <--->前端
- 因为不希望前端暴露端口映射文件,所以改为上传到服务端返回文件地址给到onlyoffice Docker服务
- 前端上传文档文件至onlyoffice Docker服务,onlyoffice Docker服务将文件转换为Office Open XML格式返回给前端进行渲染。
- 前端将修改推送至onlyoffice Docker服务,onlyoffice Docker服务将修改后的文件地址给到回调服务。
- 可用websocket即时通讯文件修改回调服务主动通讯前端进行拉取覆盖本地文件,这里前端采用轮询查询文件状态再由前端进行拉取覆盖。
拉取onlyoffice的docker镜像
这里使用onlyoffice7.1.1版本(7.2版本默认开启jwt验证)
services:
onlyoffice:
image: onlyoffice/documentserver:7.1.1
container_name: onlyoffice7.1.1
ports:
- 8011:443
- 8012:80
volumes:
- ./onlyoffice/logs:/var/log/onlyoffice
- ./onlyoffice/data:/var/www/onlyoffice/Data
- ./onlyoffice/lib:/var/lib/onlyoffice
- ./onlyoffice/db:/var/lib/postgresql
restart: always
environment:
- JWT_ENABLED=false
- ./onlyoffice/logs:/var/log/onlyoffice: 将主机上./onlyoffice/logs目录挂载到容器内的/var/log/onlyoffice,用于存储日志文件。- ./onlyoffice/data:/var/www/onlyoffice/Data: 将主机上./onlyoffice/data目录挂载到容器内的/var/www/onlyoffice/Data,用于存储 OnlyOffice 数据文件。- ./onlyoffice/lib:/var/lib/onlyoffice: 将主机上./onlyoffice/lib目录挂载到容器内的/var/lib/onlyoffice,用于存储 OnlyOffice 应用程序文件。- ./onlyoffice/db:/var/lib/postgresql: 将主机上./onlyoffice/db目录挂载到容器内的/var/lib/postgresql,用于存储 PostgreSQL 数据库文件(OnlyOffice 可能使用 PostgreSQL 进行数据存储)。- JWT_ENABLED=false 关闭jwt验证
Vue3
ONLYOFFICE Docs Vue.js 组件 集成 ONLYOFFICE Docs 到Vue.js项目
npm install --save @onlyoffice/document-editor-vue安装 ONLYOFFICE Docs Vue.js 组件- documentServerUrl地址为onlyoffice docker镜像地址
- config的document中url为文档地址可以选为文件服务器地址,fileType为文件类型,key作为文档唯一标识
<template>
<DocumentEditor
ref="docEditor"
documentServerUrl="xxxx"
:config="config"
:events_onDocumentReady="onDocumentReady"
/>
</template>
<script lang="ts" setup>
import { DocumentEditor } from "@onlyoffice/document-editor-vue";
const docEditor = ref<HTMLElement | null>(null)
const config = ref({
document: {
fileType: "",// 文件类型
title: "",// 文件名称
key: "",// 文档唯一值,同一个key二次渲染则取onlyoffice缓存
url: ""// 文件地址
},
documentType: "",// 文档类型
editorConfig: {
callbackUrl: "",// 回调地址
lang: "zh-CN",// 中文设置
customization: {
forcesave: true,// 强保存
autosave:false// 自动保存
},
},
})
// 文档加载时
const onDocumentReady = () => {
longPolling()
}
// 轮询文件修改状态
async function longPolling() {
const fileName = StorageUtils.getLocalStorage('fileName')
const lastModified = StorageUtils.getLocalStorage('lastModified')
function poll() {
axios.get(`http://xx/check?fileName=${fileName}&lastModified=${lastModified}`, {
signal: controller.value.signal,
timeout: 60000, // 设置长时间的超时时间
}).then(async response => {
const excelFolderPath = await StorageUtils.getLocalDB('tableFolderPath')
if(response.data.state){
await ipcRendererInvoke('file-overwrite', excelFolderPath, fileName)
}
setTimeout(() => {
poll();
}, 3000);
}).catch(()=>{
setTimeout(() => {
poll();
}, 3000);
})
}
poll();
}
onBeforeUnmount(()=>{
// 终止请求
controller.value.abort()
// 销毁office实例
window.DocEditor.instances['docEditor'].destroyEditor()
})
Nest回调服务
- Nest主要服务为回调处理程序官方文档
import {
Body,
Controller,
Get,
Post,
Query,
UploadedFile,
UseInterceptors,
} from '@nestjs/common';
import { AppService } from './app.service';
import { CreateUserDto } from './callback.scheama';
import { FileInterceptor } from '@nestjs/platform-express';
@Controller()
export class AppController {
constructor(private readonly appService: AppService) {}
@Post('callback')
callback(@Body() body: CreateUserDto) {
return this.trackService.callback(body);
}
@Post('file')
@UseInterceptors(FileInterceptor('file'))
upload(@UploadedFile() file: Express.Multer.File) {
return this.trackService.upload(file);
}
@Get('check')
check(@Query() Query) {
return this.trackService.fileCheck(Query);
}
}
根据不同的状态执行不同的回调操作,文档存储服务 必须返回以下响应,否则 文档编辑器 将显示错误消息
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './callback.scheama';
import axios from 'axios';
import { join } from 'path';
import { writeFileSync } from 'fs';
import { RedisClientType } from 'redis';
@Injectable()
export class TrackService {
constructor(
@Inject('REDIS_CLIENT') private readonly redisClient: RedisClientType,
) {}
// 回调函数
async callback(data: CreateUserDto) {
if (data.status == 1) {
} else if (data.status == 6) {
const file = await axios.get(data.url, {
responseType: 'arraybuffer',
});
await this.redisClient.set(
`fileStatus:${data.key}`,
new Date().getTime(),
{
EX: 1800,
});
const savePath = join('D:/Users/admin/Desktop/test_csv/', data.key);
writeFileSync(savePath, file.data);
return '{"error":0}';
} else {
return '{"error":0}';
}
}
// 上传文件并返回临时文件路径
async upload(file: Express.Multer.File) {
const savePath = join('xxxx', file.originalname);
writeFileSync(savePath, file.buffer);
return savePath;
}
// 根据缓存中的时间戳进行比对返回文件状态
fileCheck(Query) {
const time = await this.redisClient.get(
`fileStatus:${Query.fileName}`,
new Date().getTime(),
{
EX: 1800,
},
);
if (time && Number(time) < Query.time) {
return { state: 1, error: 0 };
}
return { state: 0, error: 0 };
}
}
export class bodyDto {
readonly changesurl?: string;
readonly forcesavetype?: number;
readonly history?: history;
readonly filetype?: string;
readonly key?: string;
readonly status?: number;
readonly url?: string;
readonly users?: Array<string>;
readonly userdata?: string;
readonly actions?: Array<actions>;
readonly lastsave?: string;
}
class actions {
readonly type: number;
readonly userid: string;
}
class history {
readonly changes?: string;
readonly serverVersion?: string;
}