跳到正文
4 分钟

模块边界与协作关系

理解 Collaboration Client、Transport、Endpoint、Service 与 Database Adapter 的职责、数据流和组装方式。

Univer Collaboration 由浏览器 Client 和一组服务端模块组成。每个模块只拥有一类职责,应用通过 公开接口组装它们,而不需要进入 OT、协议或持久化内部实现。

完整模块图

Collaboration Client

Browser Collaboration Client 运行在 Univer Runtime SDK 所在的浏览器中,负责:

  • 请求 Unit snapshot 与缺失的 confirmed changeset;
  • 获取一次性 Session Ticket 并建立 WebSocket;
  • JOIN Unit Room 并交换 Presence;
  • 把本地 mutation 组成 changeset 后提交;
  • 接收 ACK、广播,并在连接恢复后补齐缺失修改。

Client 不决定用户是否可信,也不是权威数据源。服务端仍需在对应 Middleware 中执行身份和权限策略。

Node Transport

@univerjs-pro/collaboration-transport-node 是 Node.js HTTP/WebSocket 入口。它接收宿主 HTTP Server 交来的 request 和 WebSocket upgrade,运行应用注册的入口 Middleware,再把流量分派给 Endpoint。

Transport 不理解 OT、Unit 数据或 ACL 模型。它提供三个主要组装接口:

TypeScript
transport.use(httpMiddleware);
transport.useUpgrade(upgradeMiddleware);
transport.register(endpoint);
  • use():参与普通 HTTP 请求,适合建立 userIDcustomData、日志或 trace;
  • useUpgrade():参与 WebSocket handshake,可在连接建立前拒绝;
  • register():注册一个协议 Endpoint,并由 Transport 管理其生命周期。

Collaboration Endpoint

@univerjs-pro/collaboration-endpoint 实现 Univer Collaboration Client 使用的协议,负责以下能力:

能力分类Endpoint 负责的内容
内容加载向 Client 提供 Unit snapshot、block 与缺失的 confirmed changeset
修改提交接收 changeset,返回 ACK,并向 Room 广播 confirmed changeset
Session 连接签发一次性 Session Ticket,建立并维护 WebSocket Session
Room 协同处理 JOIN、LEAVE、成员状态与 Presence
Unit 生命周期把删除与恢复请求转换为对应的 Service 调用

Endpoint 不执行 OT,也不直接保存协同数据。它调用 Service 取得权威结果,并负责把结果映射回 Client 协议。创建文档等产品行为应由应用 API 调用 Service 完成。

Collaboration Service

@univerjs-pro/collaboration-service 是无网络依赖的协同核心,支持 Sheet、Doc、Slide、Board 与 Base。 它负责:

  • 读取 Unit 恢复数据和 confirmed changeset;
  • 从 Unit Data 或 Snapshot 创建 Unit;
  • 提交 changeset,执行 OT,并推进连续 revision;
  • 通过 (unitID, sid, reqId) 识别重复提交;
  • 软删除、恢复或永久删除 Unit;
  • 运行 Service Middleware 并发布确认后的 Event。

Service 不提供 HTTP、WebSocket、用户、角色、ACL、目录或文件管理。Endpoint 使用它实现浏览器协议, 应用也可以直接调用公开 Service API 实现自己的业务 API 或后台任务。

核心 API

能力Service API
加载 UnitgetUnitLoadData()getUnitLoadDataWithBlocks()
读取增量数据getChangesets()getSheetBlock()
创建 UnitcreateUnitFromData()createUnitFromSnapshot()
提交修改submitChangeset()
删除与恢复deleteUnits()recoverUnits()

Database Adapter

Database Adapter 是 Service 与实际存储之间的合同。它必须保证:

  • 初始 snapshot 与依赖数据原子创建;
  • revision compare-and-swap;
  • changeset 提交幂等;
  • snapshot 完整后才对读取可见;
  • Unit 生命周期批次全有或全无。

SDK 提供 Memory 与 SQLite 实现。需要接入其他数据库时,应用应实现相同合同。详细选择见 Database Adapter

四个核心概念

概念含义
Unit一份可独立加载和协同的 Univer 内容,由唯一 unitID 与类型标识
SnapshotUnit 在某个 revision 上的持久化内容状态,不是运行中的可变对象
Changeset一次提交的 mutation 集合,包含 base revision 与幂等身份
RevisionUnit 已确认状态的连续版本号;初始值为 1

运行中的内容必须通过 Facade 或 Command 修改。直接改变 snapshot 对象不会更新 Client 或 Service 中的 live Unit。

加载一个 Unit

Transport Middleware 建立的 userID/customData 会随当前 HTTP 请求传给 Service Middleware。读取是否 允许由应用策略决定,不由客户端是否已经 JOIN Room 决定。

提交一次修改

Text
Facade / Command
  → Mutation
  → Client Changeset
  → Endpoint submit
  → Service Middleware
  → OT + Revision CAS
  → Adapter atomic commit
  → confirmed Changeset
  → ACK / Room broadcast

Database Adapter 保存的 confirmed revision 是权威结果。ACK 与广播帮助在线客户端低延迟同步;客户端 缺少消息时,仍可根据 revision 重新获取 confirmed changeset。

最小服务端组装

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((request, response) => {
  transport.handleRequest(request, response);
});
server.on("upgrade", (request, socket, head) => {
  transport.handleUpgrade(request, socket, head);
});
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。产品记录、owner ACL、错误响应 映射和失败补偿由应用处理。

资源释放

transport.dispose() 会释放通过 register() 注册的 Endpoint。Service 和应用注入的 Database Adapter 需要由应用分别释放。建议按照组装的相反顺序执行:

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

下一步阅读 Middleware 与 Event,了解应用如何在这些模块边界上扩展行为。