协同编辑
注意事项
协同编辑功能需要 Univer 服务端支持,请确保你已经正确安装并配置了 Univer 服务端。具体请参考:升级到 Pro
协同编辑功能允许多个用户同时编辑同一文档,实时同步更改,适用于团队协作和多人编辑场景。
预设模式
安装
@univerjs/preset-sheets-collaboration 的 UniveSheetsCollaborationPreset 预设在运行时依赖 UniverSheetsDrawingPreset 和 UniverSheetsAdvancedPreset 预设,请先安装 @univerjs/preset-sheets-drawing 和 @univerjs/preset-sheets-advanced。
npm install @univerjs/preset-sheets-drawing @univerjs/preset-sheets-advanced @univerjs/preset-sheets-collaboration使用
import { UniverSheetsAdvancedPreset } from '@univerjs/preset-sheets-advanced'
import UniverPresetSheetsAdvancedZhCN from '@univerjs/preset-sheets-advanced/locales/zh-CN'
import { UniverSheetsCollaborationPreset } from '@univerjs/preset-sheets-collaboration'
import UniverPresetSheetsCollaborationZhCN from '@univerjs/preset-sheets-collaboration/locales/zh-CN'
import { UniverSheetsCorePreset } from '@univerjs/preset-sheets-core'
import UniverPresetSheetsCoreZhCN from '@univerjs/preset-sheets-core/locales/zh-CN'
import { UniverSheetsDrawingPreset } from '@univerjs/preset-sheets-drawing'
import UniverPresetSheetsDrawingZhCN from '@univerjs/preset-sheets-drawing/locales/zh-CN'
import { createUniver, LocaleType, mergeLocales } from '@univerjs/presets'
import '@univerjs/preset-sheets-core/lib/index.css'
import '@univerjs/preset-sheets-drawing/lib/index.css'
import '@univerjs/preset-sheets-advanced/lib/index.css'
import '@univerjs/preset-sheets-collaboration/lib/index.css'
const { univerAPI } = createUniver({
locale: LocaleType.ZH_CN,
locales: {
[LocaleType.ZH_CN]: mergeLocales(
UniverPresetSheetsCoreZhCN,
UniverPresetSheetsDrawingZhCN,
UniverPresetSheetsAdvancedZhCN,
UniverPresetSheetsCollaborationZhCN,
),
},
collaboration: true,
presets: [
UniverSheetsCorePreset(),
UniverSheetsDrawingPreset({
collaboration: true,
}),
UniverSheetsAdvancedPreset({
// 配置高级功能
}),
UniverSheetsCollaborationPreset({
universerEndpoint: 'http://localhost:3010',
}),
],
})如果你拥有 Univer 的商业许可证,请参考在客户端使用许可证进行配置。
预设与配置
interface IUniverSheetsCollaborationPresetConfig {
/**
* 是否上报前端异常日志到 Universer,以便于通过搜索 `Frontend` 关键字查看日志。
* @default false
*/
enableFrontendLog?: boolean
}插件模式
安装
npm install @univerjs-pro/collaboration @univerjs-pro/collaboration-client @univerjs-pro/collaboration-client-ui使用
import { UniverCollaborationPlugin } from '@univerjs-pro/collaboration'
import { UniverCollaborationClientPlugin } from '@univerjs-pro/collaboration-client'
import { BrowserCollaborationSocketService, UniverCollaborationClientUIPlugin } from '@univerjs-pro/collaboration-client-ui'
import CollaborationClientZhCN from '@univerjs-pro/collaboration-client/locale/zh-CN'
import { IAuthzIoService, IMentionIOService, IUndoRedoService, LocaleType, mergeLocales, Univer } from '@univerjs/core'
import '@univerjs-pro/collaboration-client/facade'
import '@univerjs-pro/collaboration-client-ui/facade'
import '@univerjs-pro/collaboration-client/lib/index.css'
const univer = new Univer({
locale: LocaleType.ZH_CN,
locales: {
[LocaleType.ZH_CN]: mergeLocales(
CollaborationClientZhCN,
),
},
})
// 通过将 override 选项设置为 [[IAuthzIoService, null]],可以告诉 Univer 不要注册内置的 IAuthzIoService。
// 通过将 override 选项设置为 [[IUndoRedoService, null]],可以告诉 Univer 不要注册内置的 IUndoRedoService。
// 通过将 override 选项设置为 [[IMentionIOService, null]],可以告诉 Univer 不要注册内置的 IMentionIOService。
// 这样,Univer 将使用 UniverCollaborationPlugin 中提供的服务作为权限、撤销/重做和提及功能的实现。
const univer = new Univer({
override: [
[IAuthzIoService, null],
[IUndoRedoService, null],
[IMentionIOService, null],
],
})
const serverEndpoint = 'http://localhost:3010'
univer.registerPlugin(UniverCollaborationPlugin)
univer.registerPlugin(UniverCollaborationClientPlugin, {
socketService: BrowserCollaborationSocketService,
authzUrl: `${serverEndpoint}/universer-api/authz`,
snapshotServerUrl: `${serverEndpoint}/universer-api/snapshot`,
collabSubmitChangesetUrl: `${serverEndpoint}/universer-api/comb`,
collabWebSocketUrl: 'ws://localhost:3010/universer-api/comb/connect',
loginUrlKey: `${serverEndpoint}/universer-api/oidc/authpage`,
uploadFileServerUrl: `${serverEndpoint}/universer-api/stream/file/upload`,
signUrlServerUrl: `${serverEndpoint}/universer-api/file/{fileID}/sign-url`,
downloadEndpointUrl: `${serverEndpoint}/`,
wsSessionTicketUrl: `${serverEndpoint}/universer-api/user/session-ticket`,
startFormulaLimitUrl: `${serverEndpoint}/universer-api/license/formula/limit/start`,
getFormulaLimitStatusUrl: `${serverEndpoint}/universer-api/license/formula/limit/status`,
releaseFormulaLimitUrl: `${serverEndpoint}/universer-api/license/formula/limit/done`,
sendChangesetTimeout: 200,
})
univer.registerPlugin(UniverCollaborationClientUIPlugin) 如果你拥有 Univer 的商业许可证,请参考在客户端使用许可证进行配置。
插件与配置
interface IUniverCollaborationClientUIConfig {
/**
* 是否上报前端异常日志到 Universer,以便于通过搜索 `Frontend` 关键字查看日志。
* @default false
*/
enableFrontendLog?: boolean
}协同文档数据
协同编辑插件依赖 Univer 服务,协同文档的数据存储在 Univer 服务中。
unitId
每篇协同文档在 Univer 服务中都有一个唯一的 unitId。Univer 协同客户端使用 unitId 可从 Univer 服务获取到对应的协同文档数据进行协同编辑。
type
type 是协同文档的类型,协同文档的类型决定了协同文档的初始数据结构。
创建协同文档
有多种方式在 Univer 服务中创建协同文档:
通过 URL 参数加载协同文档
@univerjs-pro/collaboration-client 插件内部提供了根据 URL 参数 unit 和 type 自动加载对应的数据的功能,这可以简化一些场景下的数据加载逻辑。
如果你想使用该特性,你需要适当地修改一下原有的加载数据逻辑,并将 unit 和 type 参数添加到 URL 中,如下所示:
import { UniverInstanceType } from '@univerjs/core'
// 预设模式下 `UniverInstanceType` 可从 @univerjs/presets 导入
import { UniverInstanceType } from '@univerjs/presets'
// 原有逻辑,只适用于非协同文档
univer.createUnit(UniverInstanceType.UNIVER_SHEET, {})
// 修改后的逻辑,适用于协同文档
const url = new URL(window.location.href)
const unit = url.searchParams.get('unit')
if (unit) {
// 如果 URL 中包含 unit 参数,则自动加载数据
} else {
// 创建一个新的文档
fetch(`/universer-api/snapshot/${UniverInstanceType.UNIVER_SHEET}/unit/-/create`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
},
body: JSON.stringify({
type: UniverInstanceType.UNIVER_SHEET, // instance type
name: 'New Sheet By Univer', // workbook name
creator: 'user', // creator name
}),
}).then((response) => {
if (!response.ok) {
throw new Error('create unit failed')
}
return response.json()
}).then((data) => {
if (!data.unitID) {
throw new Error('create unit failed')
}
url.searchParams.set('unit', data.unitID)
url.searchParams.set('type', String(UniverInstanceType.UNIVER_SHEET))
window.location.href = url.toString()
}).catch((error) => {
console.error(error)
})
} Facade API
引入
插件模式提示
仅插件模式需要手动引入 Facade 包。预设模式已内置对应的 Facade 包,无需额外导入。
import '@univerjs-pro/collaboration-client/facade'
import '@univerjs-pro/collaboration-client-ui/facade'加载协同文档
如果你不想使用 URL 参数来加载协同文档,你也可以通过 Facade API 来加载协同文档。
const collaboration = univerAPI.getCollaboration()
const workbook = await collaboration.loadSheetAsync('your-unit-id')协同在 Node.js 环境下的使用示例
import { FUniver } from '@univerjs/core/facade'
import { createUniverOnNode } from './univer'
async function run(): Promise<void> {
const univer = createUniverOnNode()
const univerAPI = FUniver.newAPI(univer)
await univerAPI.getCollaboration().loadSheetAsync('your-unit-id')
TestSheetsFacadeAPI(univerAPI)
TestSheetsConditionalFormattingAPI(univerAPI)
}
run()
async function TestSheetsFacadeAPI(univerAPI: FUniver) {
const fWorkbook = univerAPI.getActiveWorkbook()
if (!fWorkbook) return
// Set workbook name
fWorkbook.setName('Test Workbook - Node Collaboration Client')
const fWorksheet = fWorkbook.getActiveSheet()
// Set a value in a cell A1
fWorksheet.getRange('A1').setValue(`Hello! message from the Node Collaboration Client! The current time is: ${new Date().toString()}.`)
// Merge C2:E4 range cells
const fRange2 = fWorksheet.getRange('C2:E4')
if (fRange2.isPartOfMerge() || fRange2.isMerged()) {
fRange2.breakApart()
}
fRange2.merge().setValue(1234).setNumberFormat('#,##0.00')
// Create a defined name for cell A1 and use it in cell A6
const definedNameBuilder = univerAPI.newDefinedName()
.setRef(`${fWorksheet.getSheetName()}!$A$1`)
.setName('MyDefinedName')
.build()
fWorkbook.insertDefinedNameBuilder(definedNameBuilder)
fWorksheet.getRange('A6').setValue('=MyDefinedName')
// Insert a new sheet
const newSheet = fWorkbook.insertSheet('New Sheet from Node Client', {
index: 1,
sheet: {
rowCount: 100,
columnCount: 20,
cellData: {
0: {
0: {
v: 1,
},
},
1: {
1: {
v: 2,
},
},
2: {
2: {
v: 3,
},
},
3: {
3: {
v: 4,
},
},
},
},
})
// Insert 3 rows after row 3 (0-based index).
newSheet.insertRowsAfter(2, 3)
const sheets = fWorkbook.getSheets()
if (sheets.length) {
// Rename the first sheet and set its row and column count
sheets[0].setName('Renamed Sheet 1').setRowCount(100).setColumnCount(10)
// Freeze first row and first column in the first sheet
sheets[0].setFrozenRows(1).setFrozenColumns(1)
// Set grid lines color to red in the first sheet
sheets[0].setGridLinesColor('#FF0000')
// Get data range of the first sheet and log its A1 notation
const dataRange = sheets[0].getDataRange()
console.warn(`Renamed Sheet 1 Data Range A1 Notation: ${dataRange.getA1Notation()}`)
// Clear content of the data range after 5 seconds
setTimeout(() => {
dataRange.clearContent()
}, 5000)
}
}
async function TestSheetsConditionalFormattingAPI(univerAPI: FUniver) {
const fWorkbook = univerAPI.getActiveWorkbook()
if (!fWorkbook) return
const newSheet = fWorkbook.insertSheet('Conditional Formatting Sheet', {
sheet: {
cellData: {
0: {
0: { v: 1, t: 2 },
1: { v: 2, t: 2 },
2: { v: 3, t: 2 },
3: { v: 4, t: 2 },
},
1: {
0: { v: 2, t: 2 },
1: { v: 3, t: 2 },
2: { v: 4, t: 2 },
3: { v: 5, t: 2 },
},
2: {
0: { v: 3, t: 2 },
1: { v: 4, t: 2 },
2: { v: 5, t: 2 },
3: { v: 6, t: 2 },
},
3: {
0: { v: 4, t: 2 },
1: { v: 5, t: 2 },
2: { v: 6, t: 2 },
3: { v: 7, t: 2 },
},
},
},
})
// Add conditional formatting rule for range A1:D4: italic font, red background, green font color when cell value > 3
const fRange = newSheet.getRange('A1:D4')
const rule = newSheet.newConditionalFormattingRule()
.whenNumberGreaterThan(3)
.setRanges([fRange.getRange()])
.setItalic(true)
.setBackground('red')
.setFontColor('green')
.build()
newSheet.addConditionalFormattingRule(rule)
}import path from 'node:path'
import { UniverCollaborationPlugin } from '@univerjs-pro/collaboration'
import { UniverCollaborationClientPlugin } from '@univerjs-pro/collaboration-client'
import { NodeCollaborationSocketService, UniverCollaborationClientNodePlugin } from '@univerjs-pro/collaboration-client-node'
import { UniverSheetsChartPlugin } from '@univerjs-pro/sheets-chart'
import { UniverSheetsPivotTablePlugin } from '@univerjs-pro/sheets-pivot'
import { UniverSheetSparklinePlugin } from '@univerjs-pro/sheets-sparkline'
import { IAuthzIoService, IUndoRedoService, LocaleType, Univer } from '@univerjs/core'
import { UniverDataValidationPlugin } from '@univerjs/data-validation'
import { UniverDocsPlugin } from '@univerjs/docs'
import { UniverDocsDrawingPlugin } from '@univerjs/docs-drawing'
import { UniverDrawingPlugin } from '@univerjs/drawing'
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula'
import { UniverNetworkPlugin } from '@univerjs/network'
import { UniverRPCNodeMainPlugin } from '@univerjs/rpc-node'
import { UniverSheetsPlugin } from '@univerjs/sheets'
import { UniverSheetsConditionalFormattingPlugin } from '@univerjs/sheets-conditional-formatting'
import { UniverSheetsDataValidationPlugin } from '@univerjs/sheets-data-validation'
import { UniverSheetsDrawingPlugin } from '@univerjs/sheets-drawing'
import { UniverSheetsFilterPlugin } from '@univerjs/sheets-filter'
import { UniverSheetsFormulaPlugin } from '@univerjs/sheets-formula'
import { UniverSheetsHyperLinkPlugin } from '@univerjs/sheets-hyper-link'
import { UniverSheetsNotePlugin } from '@univerjs/sheets-note'
import { UniverSheetsNumfmtPlugin } from '@univerjs/sheets-numfmt'
import { UniverSheetsSortPlugin } from '@univerjs/sheets-sort'
import { UniverSheetsTablePlugin } from '@univerjs/sheets-table'
import { UniverThreadCommentPlugin } from '@univerjs/thread-comment'
import '@univerjs/engine-formula/facade'
import '@univerjs/sheets/facade'
import '@univerjs/sheets-formula/facade'
import '@univerjs/sheets-numfmt/facade'
import '@univerjs/sheets-conditional-formatting/facade'
import '@univerjs/sheets-data-validation/facade'
import '@univerjs/sheets-filter/facade'
import '@univerjs/sheets-hyper-link/facade'
import '@univerjs/sheets-note/facade'
import '@univerjs/sheets-table/facade'
import '@univerjs/sheets-thread-comment/facade'
import '@univerjs-pro/sheets-pivot/facade'
import '@univerjs-pro/sheets-sparkline/facade'
import '@univerjs-pro/collaboration-client/facade'
export interface ICreateUniverOnNodeOptions {
useComputingWorker?: boolean
}
export function createUniverOnNode(options: ICreateUniverOnNodeOptions = {}): Univer {
const { useComputingWorker = false } = options
const univer = new Univer({
locale: LocaleType.ZH_CN,
locales: {},
override: [
[IAuthzIoService, null],
[IUndoRedoService, null],
],
})
registerBasicPlugins(univer, useComputingWorker)
registerSharedPlugins(univer)
if (useComputingWorker) {
registerRPCPlugin(univer)
}
registerDocPlugins(univer)
registerSheetPlugins(univer)
registerCollaborationPlugins(univer)
return univer
}
function registerBasicPlugins(univer: Univer, useComputingWorker: boolean): void {
univer.registerPlugin(UniverFormulaEnginePlugin, { notExecuteFormula: useComputingWorker })
}
function registerSharedPlugins(univer: Univer): void {
univer.registerPlugin(UniverThreadCommentPlugin)
univer.registerPlugin(UniverDrawingPlugin)
}
function registerDocPlugins(univer: Univer): void {
univer.registerPlugin(UniverDocsPlugin)
univer.registerPlugin(UniverDocsDrawingPlugin)
}
function registerSheetPlugins(univer: Univer): void {
univer.registerPlugin(UniverSheetsPlugin)
univer.registerPlugin(UniverSheetsFormulaPlugin)
univer.registerPlugin(UniverSheetsNumfmtPlugin)
univer.registerPlugin(UniverSheetsConditionalFormattingPlugin)
univer.registerPlugin(UniverDataValidationPlugin)
univer.registerPlugin(UniverSheetsDataValidationPlugin)
univer.registerPlugin(UniverSheetsFilterPlugin)
univer.registerPlugin(UniverSheetsHyperLinkPlugin)
univer.registerPlugin(UniverSheetsNotePlugin)
univer.registerPlugin(UniverSheetsDrawingPlugin)
univer.registerPlugin(UniverSheetsSortPlugin)
univer.registerPlugin(UniverSheetsTablePlugin)
univer.registerPlugin(UniverSheetsChartPlugin)
univer.registerPlugin(UniverSheetsPivotTablePlugin)
univer.registerPlugin(UniverSheetSparklinePlugin)
}
function registerRPCPlugin(univer: Univer): void {
const childPath = path.join(__dirname, '../sdk/worker.js')
univer.registerPlugin(UniverRPCNodeMainPlugin, { workerSrc: childPath })
}
function registerCollaborationPlugins(univer: Univer): void {
univer.registerPlugin(UniverNetworkPlugin)
univer.registerPlugin(UniverCollaborationPlugin)
univer.registerPlugin(UniverCollaborationClientPlugin, {
socketService: NodeCollaborationSocketService,
enableOfflineEditing: false,
enableSingleActiveInstanceLock: false,
snapshotServerUrl: 'https://dev.univer.plus/universer-api/snapshot',
collabSubmitChangesetUrl: 'https://dev.univer.plus/universer-api/comb',
collabWebSocketUrl: 'http://dev.univer.plus/universer-api/comb/connect',
wsSessionTicketUrl: 'http://dev.univer.plus/universer-api/user/session-ticket',
sendChangesetTimeout: 200,
retryConnectingInterval: 1000,
customHeaders: {
// Example: If your Univer server requires authentication via cookies, you can set the 'cookie' header here.
cookie: '_univer=GY3WYUTBIJ2FEVDRI5FFI6SONN2XONDGGJAW2',
// ... more headers
},
})
univer.registerPlugin(UniverCollaborationClientNodePlugin)
}import { LocaleType, Univer } from '@univerjs/core'
import { UniverFormulaEnginePlugin } from '@univerjs/engine-formula'
import { UniverRPCNodeWorkerPlugin } from '@univerjs/rpc-node'
import { UniverSheetsPlugin } from '@univerjs/sheets'
const univer = new Univer({
locale: LocaleType.ZH_CN,
})
univer.registerPlugin(UniverSheetsPlugin, { onlyRegisterFormulaRelatedMutations: true })
univer.registerPlugin(UniverFormulaEnginePlugin)
univer.registerPlugin(UniverRPCNodeWorkerPlugin)拓展阅读
如果你想进一步了解协同编辑的工作原理,可以阅读以下文章:
你觉得这篇文档如何?
