1. 为什么我要认真聊聊 AFFiNE 这个项目
第一次听说 AFFiNE 是在一个开源社区的闲聊帖里,有人甩了一句“Notion 的下一代开源替代品”,底下跟了几十条讨论。我当时的第一反应是:又一个蹭 Notion 热度的项目罢了。毕竟这几年打着“Notion 替代”旗号的产品没有一百也有八十,大部分要么是套壳编辑器,要么功能残缺得厉害,用两天就弃了。
但 AFFiNE 不太一样。我花了一个周末把它从源码编译跑起来,又用了大概两周时间把日常的笔记、文档、简单的项目看板都迁移过去试了一遍,结论是:这东西确实值得认真对待。它不是简单的“开源版 Notion”,而是在文档和数据库的融合方式上走出了一条自己的路——把文档、白板、表格三种形态揉在同一个页面空间里,这个设计思路本身就很有意思。
这篇文章适合几类人看:一是对 Notion 这类一体化工作空间有需求、但又在意数据主权和长期可用性的朋友;二是想研究现代协同文档编辑器架构的开发者;三是单纯想找一个能自己部署、不依赖第三方服务的知识管理工具的人。我会从整体设计思路、核心技术点、实际部署和使用的完整流程、以及踩过的坑这几个维度展开,尽量把我知道的都倒出来。
需要提前说明的是,AFFiNE 目前仍在快速迭代中,我写的内容基于我实际使用的版本,后续版本可能有变化。另外,我不会把它吹成完美无缺的东西,该说的问题我也会直说。
2. 整体设计思路:文档、白板、表格为什么要揉在一起
2.1 传统笔记工具的“形态割裂”问题
用过 Notion 的人应该都有体会:写文档是一个页面,做看板是另一个页面,画流程图又得跳到白板工具里去。虽然 Notion 后来加了白板功能,但本质上还是几个独立模块拼在一起,数据之间没有真正的融合。你在文档里写了一段需求描述,想把它直接拖到白板上做头脑风暴,或者变成表格里的一行任务,操作起来是割裂的。
我自己的痛点特别明显。做项目规划的时候,我习惯先在白板上画架构草图,然后在文档里写详细说明,最后在表格里排任务和时间线。这三个东西在传统工具里是三份独立的数据,改了一处,另外两处不会自动同步。时间一长,白板上的图和文档里的描述就对不上了,维护成本很高。
AFFiNE 的设计思路就是冲着这个问题去的。它的核心概念叫“Edgeless”和“Page”两种模式的无缝切换。同一个内容块,你可以在文档视图里把它当成一段文字,切到白板视图里它就变成一个可以自由拖拽的卡片。底层数据是同一份,只是渲染方式不同。这个设计听起来简单,但实现起来涉及不少架构上的取舍。
2.2 底层数据模型的选择逻辑
AFFiNE 底层用的是 CRDT(Conflict-free Replicated Data Type,无冲突复制数据类型)来做协同编辑的数据同步。具体来说,它早期用的是 Yjs 这套库,后来自己做了不少封装和优化。为什么选 CRDT 而不是 OT(Operational Transformation)?这是个很关键的架构决策。
OT 的思路是:所有操作经过服务器排序后再分发,保证一致性。优点是成熟、很多协同编辑器都在用;缺点是强依赖中心服务器,离线场景处理起来很麻烦。CRDT 的思路是:每个操作本身携带足够的信息,任何顺序应用都能收敛到同一个状态。优点是天然支持离线编辑和点对点同步,缺点是数据体积会膨胀,对垃圾回收要求高。
AFFiNE 选择 CRDT,我认为核心原因是它想支持“本地优先”(Local-first)的使用方式。你可以完全离线在本地编辑,等联网后再同步,不会出现冲突丢失。这对那些经常在没网环境下工作、或者对数据隐私特别在意的人来说,是很实在的价值。当然代价就是同步的数据包会大一些,初次加载和长期使用后的存储优化需要额外处理。
2.3 块级编辑器的实现取舍
AFFiNE 的编辑器是基于块(Block)的,这点和 Notion 一样。但它的块模型比 Notion 更“扁平”一些。Notion 的块是严格树形嵌套的,一个块只能有一个父块。AFFiNE 在某些场景下允许块有多个引用关系,这就为白板模式下的自由布局提供了基础。
具体来说,AFFiNE 用了一个叫“BlockSuite”的编辑器框架,这是他们自己开源的。BlockSuite 把文档模型、编辑器视图、协同层做了比较清晰的分层。文档模型层定义块的结构和属性,编辑器视图层负责渲染和交互,协同层处理 CRDT 同步。这种分层的好处是,同一份文档模型可以对应多种视图——文档视图、白板视图、表格视图,都是不同的视图层实现,共享同一个模型层。
这个架构的优点是扩展性好,想加新的视图类型相对容易。缺点是初期开发复杂度高,各层之间的边界需要仔细设计,不然容易出现视图和模型不同步的 bug。我在使用过程中确实遇到过几次切换视图后内容显示异常的情况,刷新一下就好了,说明这块还在打磨中。
3. 核心技术点拆解:几个值得关注的实现细节
3.1 BlockSuite 编辑器框架的分层设计
BlockSuite 是 AFFiNE 团队开源出来的编辑器框架,可以单独使用。它的分层大概是这样的:
- Model 层:定义块的数据结构,包括块的类型、属性、子块关系等。这一层不关心怎么渲染,只关心数据长什么样。
- View 层:负责把 Model 渲染成用户能看到的界面。文档视图、白板视图、表格视图都是不同的 View 实现。
- Command 层:定义各种操作命令,比如插入块、删除块、修改属性、移动块等。所有对文档的修改都通过命令来执行,方便做撤销重做和协同同步。
- Sync 层:基于 CRDT 做多端同步,处理冲突合并。
这个分层的好处是职责清晰。比如我想加一个“思维导图视图”,只需要写一个新的 View 层实现,Model 和 Command 层基本不用动。坏处是抽象层次多了之后,调试起来比较绕。有一次我遇到一个块在白板视图里位置不对的问题,追了半天发现是 View 层的一个坐标转换计算有误,跟 Model 层的数据没关系。
3.2 CRDT 同步机制的实际表现
CRDT 在实际使用中的表现,我分几个场景来说:
单机离线编辑:完全没问题,所有操作都在本地完成,响应速度很快。AFFiNE 会把操作记录存在本地的 IndexedDB 里,等联网后再同步。
多端同时编辑:我试过两台电脑同时编辑同一个文档,一台改文字,一台拖白板上的卡片,同步后两边内容都正确合并了,没有出现丢失。但同步有延迟,大概几秒到十几秒不等,取决于网络状况和操作复杂度。
长时间使用后的性能:CRDT 的一个已知问题是操作历史会不断累积,导致文档加载变慢。AFFiNE 有做快照和压缩机制,但我用了一个月左右、文档里积累了几百个块之后,确实感觉打开速度比刚开始慢了一些。后来我手动整理了一下,把一些不再需要的编辑历史清理掉,速度就恢复了。所以建议定期做一下文档整理,别让一个文档无限膨胀。
3.3 本地优先与云端同步的平衡
AFFiNE 支持两种使用模式:纯本地模式和云端同步模式。纯本地模式就是所有数据存在浏览器或本地客户端里,不经过任何服务器。云端同步模式则需要自己部署一个 AFFiNE 的服务端,或者用他们提供的托管服务。
我两种都试过。纯本地模式适合个人使用,数据完全在自己手里,但多设备同步就没办法了。云端同步模式需要自己部署服务端,我用 Docker 在一台小服务器上跑了一个,配置不算复杂,但要注意数据备份和版本升级的问题。
这里有个细节值得说:AFFiNE 的云端同步不是简单的“把本地数据上传到服务器”,而是服务器也维护一份 CRDT 文档,各端和服务器做双向同步。这意味着服务器本身也是一个“客户端”,只是它不渲染界面。这种设计的好处是同步逻辑统一,坏处是服务器需要一定的计算资源来处理 CRDT 合并。
4. 从零开始部署 AFFiNE 的完整实操流程
4.1 环境准备与依赖检查
我选择用 Docker 部署服务端,这是最省事的方式。先确认服务器上装了 Docker 和 Docker Compose。我用的是 Ubuntu 22.04,Docker 版本 24.x,Compose 版本 v2.x。这些基础环境如果还没装,网上教程很多,这里不展开。
需要准备的资源:
- 一台能跑 Docker 的服务器,配置不用太高,1 核 2G 内存起步就够个人用
- 一个域名(可选,但建议有,方便 HTTPS 访问)
- 如果要用 HTTPS,还需要准备证书,可以用 Let's Encrypt 免费申请
注意:AFFiNE 的服务端对内存有一定要求,如果文档多、协同频繁,建议至少 2G 内存。我一开始用 1G 内存的机器跑,文档多了之后偶尔会卡顿,加到 2G 后就顺畅了。
4.2 Docker 部署服务端的详细步骤
AFFiNE 官方提供了 Docker 镜像,部署命令大致如下。我把它拆成几个步骤来说明:
第一步,创建数据目录和配置文件:
mkdir -p /opt/affine/data cd /opt/affine第二步,写一个 docker-compose.yml 文件:
version: '3' services: affine: image: ghcr.io/toeverything/affine-graphql:stable container_name: affine restart: unless-stopped ports: - "3010:3010" volumes: - ./data:/root/.affine/storage environment: - AFFINE_SERVER_HOST=你的域名或IP - AFFINE_SERVER_PORT=3010 - AFFINE_SERVER_HTTPS=false第三步,启动服务:
docker compose up -d第四步,检查日志确认启动成功:
docker compose logs -f看到类似“Server started”的日志就说明起来了。然后浏览器访问http://你的服务器IP:3010就能看到界面。
提示:如果要用 HTTPS,建议在前面加一个 Nginx 反向代理,把 443 端口的请求转发到 3010。Nginx 配置里记得加 WebSocket 支持,因为 AFFiNE 的协同同步走的是 WebSocket。
4.3 客户端配置与首次使用
服务端跑起来后,客户端可以用浏览器直接访问,也可以下载桌面客户端。桌面客户端的好处是支持本地文件系统集成,可以把附件存在本地而不是浏览器缓存里。
首次使用时,需要注册一个账号。如果是自己部署的服务端,第一个注册的账号通常就是管理员。注册完成后,可以创建一个工作区(Workspace),然后就可以开始建文档了。
我建议第一次使用时先做几件事:
- 在设置里把语言改成中文(如果界面默认是英文)
- 检查一下存储路径,确认数据存在你期望的位置
- 建一个测试文档,试试文档、白板、表格三种视图的切换
- 如果有多台设备,试试同步是否正常
4.4 数据备份与迁移的注意事项
自己部署服务端,数据备份是必须的。AFFiNE 的数据主要存在两个地方:一个是 PostgreSQL 数据库(存元数据和用户信息),一个是文件存储目录(存文档内容和附件)。
备份策略我建议:
- 数据库每天定时 dump 一次,保留最近 7 天的备份
- 文件存储目录用 rsync 定期同步到另一台机器或对象存储
- 升级版本前一定要先备份,因为数据库结构可能变化
迁移的话,把数据库 dump 和文件目录一起搬到新服务器,按同样的方式部署,然后把数据恢复进去就行。注意版本要一致,跨大版本迁移可能会有兼容问题。
5. 实际使用中遇到的坑与排查记录
5.1 同步冲突与数据恢复
我遇到过一次比较严重的问题:两台设备同时编辑同一个文档,其中一台设备在离线状态下改了很多内容,另一台设备也在线改了不少,等离线设备联网同步后,发现部分内容出现了重复块。
排查下来,原因是离线设备在同步前做了本地快照压缩,导致部分操作历史丢失,同步时无法正确合并。解决办法是:尽量不要在长时间离线后直接同步大量修改,可以先导出本地内容,同步后再手动合并。
AFFiNE 有导出功能,支持导出为 Markdown、PDF 等格式。我现在的习惯是,如果要在离线状态下做大量修改,先导出一份备份,心里踏实。
5.2 性能问题的排查思路
前面提到过,文档用久了会变慢。我总结了一个排查流程:
| 现象 | 可能原因 | 排查方法 | 解决方式 |
|---|---|---|---|
| 打开文档慢 | 操作历史过多 | 看文档块数量和编辑历史长度 | 整理文档,删除无用块 |
| 同步延迟高 | 网络问题或服务器负载高 | 检查服务器 CPU 和内存使用率 | 升级服务器配置或优化网络 |
| 白板卡顿 | 块数量过多或浏览器性能不足 | 看白板上的块数量和浏览器内存占用 | 拆分白板,减少单页块数量 |
| 搜索慢 | 索引未更新或数据量大 | 检查搜索索引状态 | 重建索引或限制搜索范围 |
我实测下来,单个文档的块数量控制在 500 以内,体验最好。超过 1000 块之后,编辑和同步都会明显变慢。所以建议把大文档拆成多个小文档,用链接关联起来。
5.3 常见问题速查
问题:Docker 容器启动后无法访问检查端口是否被占用,防火墙是否放行,以及
AFFINE_SERVER_HOST是否配置正确。问题:注册账号后无法登录检查数据库连接是否正常,看服务端日志有没有报错。有时候是数据库迁移没完成导致的。
问题:附件上传失败检查存储目录的权限,确保 Docker 容器有写入权限。另外注意文件大小限制,默认可能有限制,可以在配置里调整。
问题:多设备同步后内容不一致先刷新页面,如果还不一致,检查各设备的网络连接。实在不行,导出内容后重新导入。
问题:升级后数据丢失升级前一定要备份。如果已经丢了,看备份文件是否完整,按备份恢复流程操作。
提示:AFFiNE 的社区比较活跃,遇到问题可以去他们的 GitHub Discussions 搜一下,很多坑别人已经踩过了。提问的时候附上服务端日志和复现步骤,得到回复的概率会高很多。
6. 我对 AFFiNE 的一些个人判断
用了这段时间,我对 AFFiNE 的整体评价是:方向对,完成度在快速提升,但还没到可以无脑推荐给所有人的程度。
如果你是对数据主权有要求、愿意花点时间折腾部署、并且能接受一定不稳定性的用户,AFFiNE 值得一试。它的文档和白板融合设计确实解决了我的一部分痛点,本地优先的架构也让我对数据安全更放心。
但如果你想要一个开箱即用、稳定省心的工具,目前可能还是商业产品更合适。AFFiNE 的迭代速度很快,这意味着功能在不断完善,但也意味着偶尔会遇到 bug 或者 breaking change。
我自己的做法是:把 AFFiNE 作为主力工具之一,但不是唯一工具。重要的、长期保存的内容,我会同时保留一份 Markdown 导出。这样即使工具本身出问题,内容也不会丢。这个习惯我觉得对任何工具都适用,不只是 AFFiNE。
最后分享一个小技巧:AFFiNE 的模板功能挺实用的,可以把自己常用的文档结构存成模板,新建的时候直接套用,省去重复排版的时间。我建了几个常用模板,比如“项目周报”“会议记录”“读书笔记”,用起来效率高不少。