ONLYOFFICE集成(Vue3+Nest)

3,461 阅读3分钟
ONLYOFFICE

ONLYOFFICE 文档 是一个开源办公套件,包括文本文档、电子表格、演示文稿和可填写表单的编辑器。 它提供以下功能:创建、编辑和查看文本文档、电子表格、演示文稿和可填写表单。

如何实现前后端数据一致

主体流程:

graph TD
前端 <---> onlyoffice-Docker <---> 回调服务 <--->前端
  1. 因为不希望前端暴露端口映射文件,所以改为上传到服务端返回文件地址给到onlyoffice Docker服务
  2. 前端上传文档文件至onlyoffice Docker服务,onlyoffice Docker服务将文件转换为Office Open XML格式返回给前端进行渲染。
  3. 前端将修改推送至onlyoffice Docker服务,onlyoffice Docker服务将修改后的文件地址给到回调服务。
  4. 可用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项目

  1. npm install --save @onlyoffice/document-editor-vue安装 ONLYOFFICE Docs Vue.js 组件
  2. documentServerUrl地址为onlyoffice docker镜像地址
  3. 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回调服务
  1. 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);
  }
}


根据不同的状态执行不同的回调操作,文档存储服务 必须返回以下响应,否则 文档编辑器 将显示错误消息 image.png

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;
}