模組邊界與關係

瞭解 Collaboration Client、Transport、Endpoint、Service 與 Database Adapter 的職責、資料流程及組裝方式。

Univer 協同由瀏覽器 Client 與一組服務端模組組成,各模組負責一類職責。應用透過公開介面組裝,不必進入 OT1、協定或持久化的內部實作。

完整模組圖

100%

Collaboration Client

瀏覽器 Collaboration Client 與 Web SDK 一起執行,負責:

  • 請求 Unit 快照及缺少的已確認變更集;
  • 取得一次性 Session Ticket 並開啟 WebSocket;
  • 加入 Unit Room 並交換 Presence;
  • 將本機 mutation 作為變更集提交;
  • 接收 ACK 與廣播,重新連線後補齊缺少的變更。

Client 不判斷使用者是否可信,也不是權威資料來源。服務端仍透過對應 Middleware 執行身分驗證與授權。

Node Transport

@univerjs-pro/collaboration-transport-node 是 Node.js HTTP/WebSocket 入口。它接收宿主 HTTP 伺服器的請求與 WebSocket 升級,執行應用入口 Middleware,並將流量分派至 Endpoint。

Transport 不理解 OT、Unit 資料或 ACL 模型。它提供三個主要組裝方法:

TypeScript
transport.use(httpMiddleware);transport.useUpgrade(upgradeMiddleware);transport.register(endpoint);
  • use() 參與一般 HTTP 請求,可設定 userID、customData、日誌或追蹤;
  • useUpgrade() 參與 WebSocket 握手,可在連線前拒絕;
  • register() 註冊協定 Endpoint,將其生命週期交由 Transport 管理。

Collaboration Endpoint

@univerjs-pro/collaboration-endpoint 實作 Univer Collaboration Client 使用的協定,負責:

能力Endpoint 職責
內容載入向 Client 提供 Unit 快照、區塊與缺少的已確認變更集
變更提交接收變更集、傳回 ACK,並向 Room 廣播已確認變更集
Session 連線簽發一次性 Session Ticket,建立並維護 WebSocket Session
Room 協同處理 JOIN、LEAVE、成員狀態與 Presence
Unit 生命週期將刪除及還原請求對應到 Service 呼叫
多節點透過可選的 pub/sub 介面卡與共用的 Session Ticket 儲存,將 Room、Presence 與廣播投遞到其他節點

Endpoint 不執行 OT,也不直接儲存協同資料。它呼叫 Service 取得權威結果,再轉成 Client 協定。建立文件等產品操作應由應用 API 呼叫 Service。

Collaboration Service

@univerjs-pro/collaboration-service 是不依賴網路的 Sheet、Doc、Slide、Board、Base 協同核心,負責:

  • 讀取 Unit 還原資料與已確認變更集;
  • 從 Unit Data 或快照建立 Unit;
  • 提交變更集、執行 OT、推進連續修訂版本2;
  • 透過 (unitID, sid, reqId) 識別重複提交;
  • 軟刪除、還原或永久刪除 Unit;
  • 執行 Service Middleware,並在確認後發出 Events。

Service 不提供 HTTP、WebSocket、使用者、角色、ACL、層級或檔案管理。Endpoint 使用它實作瀏覽器協定。應用也可從業務 API 或背景任務直接呼叫其公開 API。

核心 API

能力Service API
載入 UnitgetUnitLoadData()、getUnitLoadDataWithBlocks()
讀取增量資料getChangesets()、getSheetBlock()
建立 UnitcreateUnitFromData()、createUnitFromSnapshot()
提交變更submitChangeset()
刪除與還原deleteUnits()、recoverUnits()

Database Adapter

Database Adapter 是 Service 與儲存之間的契約,必須保證:

  • 初始快照及其相依資料原子建立;
  • 修訂版本比較並交換(CAS);
  • 變更集提交冪等;
  • 所有相依資料存在後快照才可見;
  • Unit 生命週期批次操作全部成功或全部不執行。

SDK 提供 Memory 和 SQLite 實作。需要其他資料庫的應用應實作相同契約,詳見資料庫配接器。

四個核心概念

概念意義
Unit以唯一 unitID 與類型識別、可獨立載入和協同的 Univer 內容
Snapshot某修訂版本的持久化 Unit 內容,不是可變的即時物件
Changeset帶基礎修訂版本與冪等識別碼的 mutation 提交集合
Revision已確認 Unit 狀態的連續版本號,初始值為 1

執行中的內容必須透過 Facade API 或 Command 修改。修改快照物件不會更新 Client 或 Service 中的即時 Unit。

載入 Unit

100%

Transport Middleware 建立的 userID/customData 隨目前 HTTP 請求進入 Service Middleware。應用策略決定是否允許讀取;直接 HTTP 讀取不以加入 Room 為前提。

提交編輯

text
Facade / Command  → Mutation  → Client 變更集  → Endpoint 提交  → Service Middleware  → OT + 修訂版本 CAS  → Adapter 原子提交  → 已確認變更集  → ACK / Room 廣播

Database Adapter 中已確認的修訂版本是權威來源。ACK 和廣播以低延遲同步線上用戶端。漏收訊息的用戶端可利用已知修訂版本取得已確認變更集。

最小服務端組裝

TypeScript
import { createServer } from "node:http";import { MemoryDatabaseAdapter } from "@univerjs-pro/collaboration-database-memory";import { UniverCollabEndpoint } from "@univerjs-pro/collaboration-endpoint";import { UniverCollabService } from "@univerjs-pro/collaboration-service";import { createNodeTransport } from "@univerjs-pro/collaboration-transport-node";const database = new MemoryDatabaseAdapter();const service = new UniverCollabService({ dbAdapter: database });const endpoint = new UniverCollabEndpoint(service);const transport = createNodeTransport();transport.use(async (context, next) => {  context.userID = "demo-user";  await next();});transport.register(endpoint);const server = createServer();transport.attach(server);server.listen(3010);

固定使用者與 Memory Adapter 僅示範模組組裝。應用應透過 Middleware 建立真實 Context,並依資料需求選擇 Adapter。

透過應用 API 建立 Unit

應用可定義自己的 HTTP 或 RPC 端點,在驗證身分、輸入及業務規則後呼叫上述 service:

text
業務用戶端  → POST /api/units  → 應用驗證身分與輸入  → service.createUnitFromData()  → Database Adapter

以下 Express 範例假設驗證 Middleware 已設定 response.locals.user,且 request.body.data 已驗證為 Workbook Data:

TypeScript
import { randomUUID } from "node:crypto";import { json } from "express";import { UniverType } from "@univerjs/protocol";app.post("/api/units", json({ limit: "1mb" }), async (request, response, next) => {  try {    const user = response.locals.user as { readonly userID: string };    const unitID = randomUUID();    const result = await service.createUnitFromData(      {        type: UniverType.UNIVER_SHEET,        data: {          ...request.body.data,          id: unitID,          rev: 1,        },      },      {        userID: user.userID,        customData: { traceID: randomUUID() },      },    );    response.status(result.status === "created" ? 201 : 200).json(result);  } catch (error) {    next(error);  }});

POST /api/units 屬於應用,不是 Collaboration Client 協定路由。createUnitFromData() 仍會執行 Service 的 createUnit Middleware。產品記錄、擁有者 ACL、錯誤回應對應與失敗補償仍由應用負責。

資源清理

transport.dispose() 釋放透過 register() 註冊的 Endpoint。應用分別釋放 Service 和注入的 Database Adapter。按組裝的反向順序執行:

TypeScript
await transport.dispose();await service.dispose();await database.dispose();

如果傳入了 pub/sub 介面卡,請在釋放 Transport 之後由應用程式釋放它與其連線,Endpoint 不會代為釋放。

繼續閱讀 Middleware 與 Events,在這些模組邊界擴充行為。

Footnotes

  1. 操作轉換(OT) 會調整基於不同文件修訂號並行產生的操作,使它們能一致地套用。它處理的是編輯操作,而非用最後抵達的快照覆蓋整份文件。 ↩

  2. 修訂號(revision)標示 Unit 已確認編輯歷史中的位置。變更集(changeset)包含一次提交的操作,確認後會推進歷史。修訂號不是 SDK 套件版本,也不是時間戳記。 ↩

你覺得這篇文檔如何?

© 2026 DreamNum Co., Ltd.