Совместное редактирование
Следуйте руководству по Интеграция совместного редактирования: запустите пример, подключите приложение и проверьте хранение данных и права доступа. Реализация и примеры Web/Server находятся в Быстрый старт совместного редактирования.
Создайте Unit через бизнес-API с аутентификацией, вызывающий Collaboration Service, затем загрузите полученный ID в браузере. Настройте HTTP, Session Ticket и WebSocket по Quick Start, хранение через Database Adapter и отдельные проверки прав на чтение, JOIN и изменение.
Перед использованием этих API проверьте поддержку PDF Unit целевым сервером. Работающий пример Sheets не подтверждает поддержку PDF.
Загрузка PDF для совместной работы
После создания модуля PDF через серверный API или importPdfToUnitIdAsync загрузите его с помощью фасада совместной работы:
Задайте текущего пользователя до загрузки документа. См. раздел Интеграция совместного редактирования.
import { UserManagerService } from '@univerjs/core'import '@univerjs-pro/collaboration-client/facade'univer.__getInjector().get(UserManagerService).setCurrentUser({ userID, name, avatar })const pdf = await univerAPI.getCollaboration().loadPdfAsync(unitId)if (!pdf) { throw new Error(`PDF unit not found: ${unitId}`)}console.log(pdf.getId(), pdf.getName())loadPdfAsync загружает снимок сервера, подготавливает сеанс совместной работы и возвращает FPdf. Если модуль не удаётся связать с PDF, метод возвращает null.
Используйте состояние, повторные отправки и восстановление конфликтов для индикации сохранения и защиты неподтверждённых изменений перед перезагрузкой.
Удаление встроенной загрузки совместных документов по URL
Обратите внимание
Начиная с версии 1.2.0, Univer больше не содержит встроенной логики загрузки совместных документов по URL. Параметры unit и type больше не считываются автоматически для загрузки совместного документа.
Если ваше приложение зависит от этого поведения, его можно восстановить с помощью собственного плагина.
Подключение собственного плагина
Скопируйте четыре файла ниже в проект, сохранив следующую структуру каталогов:
custom-url-load/├── plugin.ts├── controllers/│ └── data-loader.controller.ts└── services/ ├── url.service.ts └── web-url.service.tsПосле инициализации редактора и плагинов совместной работы задайте текущего пользователя, а затем зарегистрируйте CustomUrlLoadPlugin.
import { UserManagerService } from '@univerjs/core'import { CustomUrlLoadPlugin } from './custom-url-load/plugin'univer.__getInjector().get(UserManagerService).setCurrentUser({ userID, name, avatar })univer.registerPlugin(CustomUrlLoadPlugin)После регистрации плагин сам загружает документ, поэтому вызывать loadPdfAsync() для того же документа не требуется. Если приложение уже загружает документы через Facade, можно сохранить подход из предыдущего раздела и обрабатывать URL в соответствии с требованиями приложения.
Полный код плагина
import { UniverCollaborationClientPlugin } from '@univerjs-pro/collaboration-client'import { DependentOn, Inject, Injector, Plugin } from '@univerjs/core'import { CollaborationDataLoaderController } from './controllers/data-loader.controller'import { IURLService } from './services/url.service'import { WebURLService } from './services/web-url.service'@DependentOn(UniverCollaborationClientPlugin)export class CustomUrlLoadPlugin extends Plugin { static override pluginName = 'CUSTOM_URL_LOAD_PLUGIN' constructor( _config: undefined, @Inject(Injector) protected override readonly _injector: Injector ) { super() } override onStarting(): void { this._injector.add([IURLService, { useClass: WebURLService }]) this._injector.add([CollaborationDataLoaderController]) this.disposeWithMe(this._injector.get(IURLService)) this.disposeWithMe(this._injector.get(CollaborationDataLoaderController)) }}import { DataLoaderService } from '@univerjs-pro/collaboration-client'import { ILogService, Inject, RxDisposable } from '@univerjs/core'import { IURLService } from '../services/url.service'/** * Загружает совместный документ по параметрам URL. */export class CollaborationDataLoaderController extends RxDisposable { constructor( @IURLService private readonly _urlService: IURLService, @ILogService private readonly _logService: ILogService, @Inject(DataLoaderService) private readonly _dataLoaderService: DataLoaderService ) { super() const unitId = this._urlService.getParam('unit') const type = this._urlService.getParam('type') if (!unitId || !type) { return } this._dataLoaderService.loadUnit(unitId, Number(type)) .catch((error: unknown) => { this._logService.error('[CollaborationDataLoaderController]', error) }) }}import type { IDisposable } from '@univerjs/core'import type { Observable } from 'rxjs'import { createIdentifier } from '@univerjs/core'export interface IURLService extends IDisposable { getParam: (key: string) => string | undefined setParam: (key: string, value: string, useReplace?: boolean) => void removeParam: (key: string, useReplace?: boolean) => void readonly urlChange$: Observable<void>}export const IURLService = createIdentifier<IURLService>('demo.custom-url-load.url-service')import type { IURLService } from './url.service'import { RxDisposable } from '@univerjs/core'import { fromEvent, map, shareReplay, takeUntil } from 'rxjs'export class WebURLService extends RxDisposable implements IURLService { readonly urlChange$ = fromEvent(window, 'popstate').pipe( map(() => undefined), takeUntil(this.dispose$), shareReplay({ bufferSize: 1, refCount: true }) ) getParam(key: string): string | undefined { return new URL(window.location.href).searchParams.get(key) ?? undefined } setParam(key: string, value: string, useReplace = false): void { const url = new URL(window.location.href) url.searchParams.set(key, value) this._updateUrl(url, useReplace) } removeParam(key: string, useReplace = false): void { const url = new URL(window.location.href) url.searchParams.delete(key) this._updateUrl(url, useReplace) } private _updateUrl(url: URL, useReplace: boolean): void { if (useReplace) { window.history.replaceState(window.history.state, '', url.toString()) } else { window.history.pushState(window.history.state, '', url.toString()) } }}Насколько полезно это руководство?