Univer Workspace 是仓库 apps/workspace 中的一个 private package。React Web 应用、产品 API、
Univer Collaboration Endpoint 和后台任务由同一个 Node 进程部署,共享静态资源目录和
SQLite 数据目录。
| 关注点 | 选择 |
|---|---|
| 前端 | React、TypeScript、Vite |
| UI | Base UI、Tailwind CSS、应用 UI Primitive |
| 路由 | TanStack Router 文件路由 |
| 服务端状态 | TanStack Query |
| Web 本地状态 | React 本地状态 |
| HTTP | Express 5 |
| 产品实时失效 | 认证 WebSocket、TanStack Query invalidation |
| 产品数据库 | Node node:sqlite、显式 SQL schema |
| API 客户端 | openapi-typescript、openapi-fetch |
| API 文档 | OpenAPI 3.1、Redocly CLI、Scalar |
| 单元与集成测试 | Vitest |
OpenAPI 是独立的 HTTP 契约,用于文档、Web API 类型生成和契约检查,不参与 Express 运行时路由。每个服务端业务模块显式注册自己的 Router。
apps/workspace/
├── web/
│ ├── index.html
│ └── src/
│ ├── app/
│ │ ├── entry.tsx
│ │ ├── providers.tsx
│ │ └── styles/
│ ├── routes/
│ │ └── TanStack Router 文件路由
│ ├── features/
│ │ ├── auth/
│ │ ├── nodes/
│ │ ├── editor/
│ │ ├── resources/
│ │ ├── permissions/
│ │ ├── spaces/
│ │ ├── trash/
│ │ ├── views/
│ │ └── worktrees/
│ └── shared/
│ └── api/
├── server/
│ └── src/
│ ├── main.ts
│ ├── app.ts
│ ├── config.ts
│ ├── db/
│ │ ├── database.ts
│ │ ├── initialize.ts
│ │ ├── legacy-v0/
│ │ ├── migrations/
│ │ └── schema.sql
│ ├── middleware/
│ │ └── errors.ts
│ ├── modules/
│ │ ├── access/
│ │ ├── identity/
│ │ ├── spaces/
│ │ ├── nodes/
│ │ ├── resources/
│ │ ├── blobs/
│ │ ├── exchange/
│ │ ├── univer-assets/
│ │ ├── operations/
│ │ ├── trash/
│ │ ├── permissions/
│ │ ├── worktrees/
│ │ └── views/
│ ├── integrations/
│ │ ├── blob/
│ │ └── univer/
│ └── jobs/
│ └── operation-recovery.ts
├── contracts/
│ └── http/
│ ├── openapi.yaml
│ ├── paths/
│ └── schemas/
├── generated/
│ └── http/
│ ├── openapi.bundled.yaml
│ └── schema.d.ts
├── test/
│ └── integration/
├── docs/
│ ├── adr/
│ ├── architecture.md
│ ├── application-design.md
│ └── data-model.md
├── Dockerfile
├── package.json
├── redocly.yaml
├── tsconfig.web.json
├── tsconfig.server.json
└── vite.config.ts
web 和 server 是同一 package 的两个编译入口:
- Vite 把 Web 应用构建到
dist/public。 - TypeScript 把服务端构建到
dist/server。 - Node 进程挂载产品 API、Collaboration Endpoint、API 文档和 Web 静态资源。
- Docker 镜像只包含 production dependencies、
dist和数据库 schema。
routes 对应 URL 和页面组合,features 按用户能力组织,shared 保存无业务归属的
基础代码。依赖方向为:
shared → features → routes → app
低层目录不能导入高层目录。Feature 通过 index.ts 暴露公开内容,其他 Feature 不导入
其内部文件。
应用 UI Primitive 提供工作台表单、菜单、弹窗和反馈组件。shared/ui 保存统一行为与
视觉封装。Univer 编辑器相关的创建、销毁、协同 Scope 和只读状态全部封装在
features/editor。
TanStack Query 管理 Session、Node、Resource、Recent、Trash、Permission、Worktree 和 Operation 等服务端状态。Dialog、表单输入和当前选中项使用 React 本地状态,不引入额外全局状态库。
Worktree 页面使用一条用户级 /api/worktree-events WebSocket 接收粗粒度缓存失效信号。
连接通过现有的一次性 Collaboration Session Ticket 认证;首次连接和收到变更信号时使
worktrees 及可能被 Worktree 合入改变的 Node、Recent、Owned、Shared Query 失效并重新读取
权威产品 API,不把 WebSocket payload 当作产品数据。
main.ts 读取配置、创建应用并监听端口;app.ts 创建 Express 实例并挂载中间件、业务
Router、Collaboration Endpoint、API 文档和静态资源。
业务代码按 Module 组织。例如:
modules/nodes/
├── nodes.router.ts
├── nodes.service.ts
├── nodes.repository.ts
├── nodes.types.ts
└── index.ts
这些文件按需要创建,不要求每个 Module 拥有相同文件:
- Router 只负责 HTTP 输入输出和调用 Module。
- Service 只在存在业务流程、权限组合或 transaction 时创建。
- Repository 封装本 Module 的 SQL 和 row mapping。
- Policy 保存可独立表达和测试的授权规则。
index.ts是 Module 的公开入口。
不建立全局 controllers、services、repositories 和 models 目录。数据库 row、
Express Request/Response 和 Univer SDK class 不进入业务 Module 的公开 Interface。
Univer 集中在 integrations/univer,向业务 Module 提供产品语义的 Interface,不对 SDK
方法做一一对应的空壳封装。外部 OAuth Provider 位于 Identity Module,并通过
GitHubOAuthProvider / DiscordOAuthProvider Interface 在测试中替换。Identity Router
为部署注册的 OAuth client 提供通用 authorize/token 交接:authorize 复用
workspace_session,未登录时回到现有登录流程;token 只兑换一次性、短期、绑定 PKCE
和已注册 redirect URI 的 code。Workspace Session 仍是唯一的身份权威来源,现有登录、
Cookie、OAuth callback 和产品 API 保持原有行为;外部 client 只通过通用 OAuth 协议
接入,代码不感知其业务身份。
跨产品数据库和 Collaboration Service 的写入由 operations Module 持久化和恢复,不用
一次 SQLite transaction 假装覆盖两个系统。
Worktree Service 在 Collaboration 与产品写入均完成后调用专用 Change Feed。Change Feed 不是通用应用 Event Bus;它只向该 Worktree 变更前后可发现的已连接用户发送不含 Worktree 身份或内容的失效信号。实时发送失败不改变已经完成的产品写入,客户端重连后通过首帧统一 失效查询,从产品 API 恢复当前状态。
产品数据库使用 Node node:sqlite。db/schema.sql 定义完整 V6 结构,initialize.ts
负责在业务 Module 初始化前识别数据库状态:空数据库应用 V6;V6 校验指纹;V5/V4/V3/V2/V1/V0
先生成一致性备份,再调用隔离的一次性迁移器。
- 应用数据目录为
.data/。 - 容器内数据目录为
/app/univer-workspace/.data。 - 默认数据库文件为
.data/univer-workspace.sqlite。 - 部署和普通重启均保留数据库;V6 不重复备份或迁移。
- 普通进程重启不清理数据库。
- 每个测试使用独立的临时数据库文件或内存数据库。
产品数据库保存产品元数据、unit_id、Tree Blob 与 Univer Asset 元数据,不保存对象字节、
snapshot、changeset 或 revision。Tree Blob 和内嵌 Asset 共用注入的 BlobStore;当前实现是
本地目录,未来可替换为 S3BlobStore 或迁移包装器。Univer Collaboration Database Adapter
独立管理协作数据。
Office Exchange Module 使用已发布的 @univerjs-pro/exchange-node 将 Office 字节与 Univer
数据互转。/universer-api/exchange/**、Exchange File Upload 和签名下载遵循 Universer
协议形状,不进入产品 OpenAPI。导入为 Unit 时通过 Resource Module 创建 Personal Space
根目录下的正式 Node/Resource/Unit;导出时通过 Collaboration Service 固定当前 Trunk head,
读取包含 Sheet blocks 的恢复材料,再由 UnitSnapshotMaterializer 补全 snapshot 后交给 Exchange
Node。转换源文件、JSON snapshot
和导出文件是 BlobStore 中的临时对象,任务元数据只存在当前进程并在两小时后过期;它们
不写入产品数据库,也不改变 Collaboration Database Adapter 的所有权边界。
contracts/http/openapi.yaml + paths + schemas
├── Redocly lint
├── bundle → generated/http/openapi.bundled.yaml
├── openapi-typescript → generated/http/schema.d.ts
├── openapi-fetch → React typed client
└── Scalar → interactive API documentation
源契约与生成物分目录,生成物不手工修改。Express 路由和输入校验显式编写,不由 OpenAPI 文件动态驱动。
- 跨 Module、数据库 schema、权限和 Collaboration Endpoint 测试放在
test/integration。 - CI 依次执行 OpenAPI 生成检查、typecheck、集成测试、生产构建和生产入口导入检查。
- 不把单一 Workspace 应用继续拆成更多内部 packages。
- 不做 SSR。
- 不引入 Redux、Zustand 或 MobX。
- 不使用完整 FSD 七层。
- 不建立 BaseRepository、BaseService、依赖注入容器或 Event Bus。
- 不生成服务端业务代码。
- 目录随真实代码创建,不预先建立空层。