協同編輯
讓使用者開啟同一個 Base,共同編輯資料表、欄位、記錄與檢視。他們可以在不同檢視中工作,同時修改同一份文件。
整合步驟
- 參考協同整合,執行協同範例並接入登入、權限與資料庫儲存。
- 為現有 Bases 編輯器加入 Collaboration Client 設定。瀏覽器設定請參考 協同快速開始,但應建立並儲存 Bases 文件,不要直接複製範例中的 Sheets 資料。
- 透過應用程式後端建立 Base,並將其 ID 傳回瀏覽器。
開啟伺服器上的 Base
先設定目前使用者,再載入文件;詳見協同整合。
TypeScript
import '@univerjs-pro/bases/facade'import { UserManagerService } from '@univerjs/core'import '@univerjs-pro/collaboration-client/facade'univer.__getInjector().get(UserManagerService).setCurrentUser({ userID, name, avatar })const base = await univerAPI.getCollaboration().loadBaseAsync('your-base-id')if (!base) throw new Error('Base could not be loaded')重新開啟或分享文件時,請重用此 ID。不要以 createBase() 建立新的本機空白文件來取代從伺服器載入。
透過同步狀態、重送與衝突復原實作儲存提示,並在重新整理前保護尚未確認的修改。
移除內建 URL 載入協同文件
注意事項
從 1.2.0 版本開始,Univer 移除了內建的 URL 協同文件載入邏輯,不再自動讀取 URL 中的 unit 和 type 參數來載入協同文件。
如果你的應用程式依賴這項行為,可以透過自訂外掛恢復。
接入自訂外掛
將下方四個檔案依照以下目錄結構複製到專案中:
text
custom-url-load/├── plugin.ts├── controllers/│ └── data-loader.controller.ts└── services/ ├── url.service.ts └── web-url.service.ts完成編輯器和協同外掛初始化後,先設定目前使用者,再註冊 CustomUrlLoadPlugin。
TypeScript
import { UserManagerService } from '@univerjs/core'import { CustomUrlLoadPlugin } from './custom-url-load/plugin'univer.__getInjector().get(UserManagerService).setCurrentUser({ userID, name, avatar })univer.registerPlugin(CustomUrlLoadPlugin)註冊此外掛後,由外掛負責載入文件,無需再為同一份文件呼叫 loadBaseAsync()。如果應用程式已透過 Facade 載入文件,可以繼續使用上一節的方式,並依業務需求自行處理 URL。
完整外掛程式碼
TypeScript
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)) }}TypeScript
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) }) }}TypeScript
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')TypeScript
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()) } }}驗證同步與儲存
在兩個獨立的瀏覽器工作階段中開啟相同 ID。在其中一個新增記錄或修改欄位,確認另一個收到變更。檢查重新整理頁面與重啟伺服器後,修改仍然保留。
請在伺服器端檢查檢視者與未授權使用者的存取權。本機唯讀設定只控制編輯器行為。匯入匯出、留言與歷史記錄各自需要對應的後端功能,請參閱匯入匯出、留言與歷史記錄。
你覺得這篇文檔如何?