共同編集
共同編集の統合に沿って、サンプルの実行、アプリへの組み込み、永続化と権限の検証を進めてください。実装と Web/Server サンプルは 共同編集クイックスタート を参照してください。
認証済みの業務 API から Collaboration Service を呼び出して Unit を作成し、返された ID をブラウザーで読み込みます。共同編集クイックスタート で HTTP、Session Ticket、WebSocket を設定し、Database Adapter で永続化します。読み取り、JOIN、編集はサーバー側の権限で個別に保護してください。
文書を読み込む
文書を読み込む前に現在のユーザーを設定してください。現在のユーザーは共同編集の身元とローカルのオフラインキャッシュの所有者を決めます。設定後、Facade で明示的に読み込みます。
import { UserManagerService } from '@univerjs/core'import '@univerjs-pro/collaboration-client/facade'univer.__getInjector().get(UserManagerService).setCurrentUser({ userID, name, avatar })const collaboration = univerAPI.getCollaboration()const unit = await collaboration.loadSheetAsync('your-unit-id')同期状態、再送、競合の復旧を使い、保存状況の表示と再読み込み前の未確認編集の保護を実装できます。
共同編集ドキュメントの組み込み URL 読み込みの削除
注意
1.2.0 以降、Univer は共同編集ドキュメントの組み込み URL 読み込み処理を削除しました。URL の unit、type、subunit パラメーターを自動的に読み取って、共同編集ドキュメントを読み込み、ワークシートを選択することはなくなりました。
アプリケーションがこの動作に依存している場合は、カスタムプラグインで復元できます。
カスタムプラグインの組み込み
以下の 4 つのファイルを、次のディレクトリ構成でプロジェクトにコピーしてください。
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)登録後はプラグインがドキュメントを読み込むため、同じドキュメントに対して loadSheetAsync() を呼び出す必要はありません。すでに 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 type { ISetWorksheetActivateCommandParams } from '@univerjs/sheets'import type { Subscription } from 'rxjs'import { DataLoaderService } from '@univerjs-pro/collaboration-client'import { ICommandService, ILogService, Inject, IUniverInstanceService, RxDisposable, toDisposable, UniverInstanceType, Workbook,} from '@univerjs/core'import { SetWorksheetActivateCommand } from '@univerjs/sheets'import { takeUntil } from 'rxjs'import { IURLService } from '../services/url.service'/** * URL パラメーターから共同編集ドキュメントを読み込み、同じドキュメント内のワークシート移動を同期する。 */export class CollaborationDataLoaderController extends RxDisposable { private _unitId: string | undefined private _unitType = UniverInstanceType.UNIVER_SHEET private _workbook: Workbook | null = null private _activeSheetSubscription: Subscription | null = null constructor( @IURLService private readonly _urlService: IURLService, @ILogService private readonly _logService: ILogService, @ICommandService private readonly _commandService: ICommandService, @Inject(DataLoaderService) private readonly _dataLoaderService: DataLoaderService, @IUniverInstanceService private readonly _instanceService: IUniverInstanceService ) { super() this._init() } private _init(): void { this._unitId = this._urlService.getParam('unit') const type = this._urlService.getParam('type') if (!this._unitId || !type) { return } this._unitType = Number(type) this.disposeWithMe(toDisposable(() => this._unbindWorkbook())) // 履歴の復元でドキュメント ID を保持したまま Workbook が置き換わることがあるため、再度バインドする。 this._dataLoaderService.unitLoaded$.pipe(takeUntil(this.dispose$)).subscribe((unit) => { if (unit instanceof Workbook && unit.getUnitId() === this._unitId) { this._bindWorkbook(unit).catch((error: unknown) => this._reportError(error)) } }) this._instanceService.unitDisposed$.pipe(takeUntil(this.dispose$)).subscribe((unit) => { if (unit === this._workbook) { this._unbindWorkbook() } }) this._urlService.urlChange$.pipe(takeUntil(this.dispose$)).subscribe(() => { if (this._workbook) { this._activateWorksheetFromUrl(this._workbook).catch((error: unknown) => this._reportError(error)) } }) this._dataLoaderService.loadUnit(this._unitId, this._unitType, this._urlService.getParam('subunit')) .catch((error: unknown) => this._reportError(error)) } private async _bindWorkbook(workbook: Workbook): Promise<void> { this._unbindWorkbook() this._workbook = workbook // スナップショットの再生はモデルを直接更新するため、ワークシートを一度アクティブにしてレンダラーに通知する。 await this._activateWorksheetFromUrl(workbook, true) if (this._workbook !== workbook || this._disposed) { return } this._activeSheetSubscription = workbook.activeSheet$.subscribe((worksheet) => { if (worksheet) { this._updateUrl(worksheet.getSheetId()) } }) } private _unbindWorkbook(): void { this._activeSheetSubscription?.unsubscribe() this._activeSheetSubscription = null this._workbook = null } private _matchesDocument(): boolean { return this._urlService.getParam('unit') === this._unitId && Number(this._urlService.getParam('type')) === this._unitType } private async _activateWorksheetFromUrl(workbook: Workbook, forceActivate = false): Promise<void> { if (this._disposed || this._workbook !== workbook || !this._matchesDocument()) { return } const requestedSheetId = this._urlService.getParam('subunit') const worksheet = (requestedSheetId ? workbook.getSheetBySheetId(requestedSheetId) : null) ?? workbook.getSheetBySheetId(workbook.getUnhiddenWorksheets()[0]) if (!worksheet) { return } const subUnitId = worksheet.getSheetId() if (subUnitId !== requestedSheetId) { this._updateUrl(subUnitId, true) } if (!forceActivate && workbook.getActiveSheet()?.getSheetId() === subUnitId) { return } await this._commandService.executeCommand<ISetWorksheetActivateCommandParams>(SetWorksheetActivateCommand.id, { unitId: workbook.getUnitId(), subUnitId, }) } private _updateUrl(subUnitId: string, replace = false): void { if (this._matchesDocument() && this._urlService.getParam('subunit') !== subUnitId) { this._urlService.setParam('subunit', subUnitId, replace) } } private _reportError(error: unknown): void { 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()) } }}このガイドはいかがですか?