在知识管理工具的竞争格局中,Notion与Miro分别占据了文档编辑与可视化白板的领先地位。AFFiNE作为新一代开源替代品,通过TypeScript+Rust的混合架构和创新的块级设计,实现了文档与白板的无缝融合,为知识管理领域带来了全新的工程范式。534个GitHub星标和活跃的开源社区发展,充分证明了这一技术路线的市场认可度。
混合架构的战略选择:TypeScript与Rust的协同设计
AFFiNE选择TypeScript+Rust的技术组合,并非简单的技术堆叠,而是基于性能优化、跨平台兼容性和长期维护性的战略考量。TypeScript提供了优秀的开发体验和丰富的生态,而Rust则承担了底层性能关键的数据处理任务。
前后端语言边界的设计哲学
export interface BlockNode {
id: string;
type: string;
props: Record<string, any>;
children: BlockNode[];
}
export class BlockManager {
private blocks: Map<string, BlockNode> = new Map();
private listeners: Set<BlockChangeListener> = new Set();
async createBlock(type: string, props: any): Promise<string> {
const id = generateId();
const block: BlockNode = {
id,
type,
props: this.sanitizeProps(props),
children: []
};
this.blocks.set(id, block);
this.notifyChange('create', block);
await this.syncToRust(id, block);
return id;
}
private async syncToRust(id: string, block: BlockNode): Promise<void> {
const serializedData = JSON.stringify(block);
const result = await nativeModule.processBlockData(id, serializedData);
if (!result.success) {
throw new Error(`Rust同步失败: ${result.error}`);
}
}
}
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use tokio::sync::mpsc;
#[derive(Debug, Serialize, Deserialize)]
pub struct BlockData {
pub id: String,
pub r#type: String,
pub props: HashMap<String, serde_json::Value>,
pub children: Vec<BlockData>,
}
pub struct RustProcessor {
storage: HashMap<String, BlockData>,
sync_sender: mpsc::UnboundedSender<SyncMessage>,
}
impl RustProcessor {
pub fn new() -> Self {
Self {
storage: HashMap::new(),
sync_sender: mpsc::unbounded_channel().0,
}
}
pub fn process_block_data(&mut self, id: String, data: String) -> Result<String, String> {
let block: BlockData = serde_json::from_str(&data)
.map_err(|e| format!("反序列化失败: {}", e))?;
self.build_block_index(&block);
self.storage.insert(id.clone(), block);
let _ = self.sync_sender.send(SyncMessage::BlockUpdated(id));
Ok(id)
}
fn build_block_index(&self, block: &BlockData) {
self.index_block_content(block);
if !block.children.is_empty() {
for child in &block.children {
self.index_block_content(child);
}
}
}
}
这种混合架构实现了职责分离:TypeScript处理复杂的用户交互逻辑,而Rust专注于高性能的数据处理和存储。这种设计既保持了开发效率,又确保了系统的整体性能。
块级架构的创新实践:从Notion理念到工程实现
AFFiNE的块级架构是"万物皆模块"设计理念的深度工程化实践。每个内容单元都被抽象为独立的Block,支持动态组合和跨视图迁移。
Block系统的类型系统设计
export abstract class BaseBlock<TProps extends BaseProps> {
abstract readonly type: string;
protected props: TProps;
constructor(props: TProps) {
this.props = props;
}
abstract render(): React.ReactNode;
abstract toJSON(): BlockNode;
abstract fromJSON(data: BlockNode): BaseBlock<TProps>;
blockClone(): this {
return new (this.constructor as new (props: TProps) => this)({...this.props});
}
blockMerge(other: this): boolean {
return this.canMergeWith(other);
}
protected canMergeWith(other: this): boolean {
return this.type === other.type && this.props.mergeCompatible;
}
}
export class TextBlock extends BaseBlock<TextProps> {
readonly type = 'text';
render() {
return (
<div
contentEditable
suppressContentEditableWarning
onInput={this.handleTextChange}
className="text-block"
data-block-id={this.props.id}
>
{this.props.content}
</div>
);
}
toJSON(): BlockNode {
return {
id: this.props.id,
type: this.type,
props: this.props,
children: []
};
}
static fromJSON(data: BlockNode): TextBlock {
return new TextBlock(data.props as TextProps);
}
private handleTextChange = (event: React.FormEvent<HTMLDivElement>) => {
const content = event.currentTarget.textContent || '';
this.props.content = content;
this.onBlockChange();
};
private onBlockChange() {
document.dispatchEvent(new CustomEvent('block:update', {
detail: { id: this.props.id, block: this }
}));
}
}
export class DatabaseBlock extends BaseBlock<DatabaseProps> {
readonly type = 'database';
render() {
return (
<DatabaseView
data={this.props.data}
viewType={this.props.currentView}
onViewChange={this.handleViewChange}
onDataChange={this.handleDataChange}
/>
);
}
private handleViewChange = (newView: ViewType) => {
this.props.currentView = newView;
this.onBlockChange();
};
private handleDataChange = (newData: any[]) => {
this.props.data = newData;
this.onBlockChange();
this.syncDatabaseChanges();
};
private async syncDatabaseChanges(): Promise<void> {
const queryResult = await nativeModule.queryDatabase(
this.props.id,
JSON.stringify(this.props.data)
);
if (queryResult.optimized) {
this.applyQueryOptimization(queryResult.suggestions);
}
}
}
跨视图Block渲染引擎
export class BlockRenderer {
private viewMode: 'page' | 'edgeless' = 'page';
private layoutEngine: LayoutEngine;
private reactEngine: ReactEngine;
constructor() {
this.layoutEngine = new LayoutEngine();
this.reactEngine = new ReactEngine();
}
renderBlock(block: BaseBlock<any>): React.ReactNode {
if (this.viewMode === 'page') {
return this.renderForPageView(block);
} else {
return this.renderForEdgelessView(block);
}
}
private renderForPageView(block: BaseBlock<any>): React.ReactNode {
const pageLayout = this.layoutEngine.calculatePageLayout(block);
return (
<div
className="page-view-item"
style={{
position: 'relative',
top: pageLayout.y,
left: pageLayout.x,
width: pageLayout.width,
height: pageLayout.height
}}
>
<BlockContainer>
{block.render()}
</BlockContainer>
</div>
);
}
private renderForEdgelessView(block: BaseBlock<any>): React.ReactNode {
const edgelessLayout = this.layoutEngine.calculateEdgelessLayout(block);
return (
<div
className="edgeless-view-item"
style={{
position: 'absolute',
transform: `translate(${edgelessLayout.x}px, ${edgelessLayout.y}px)`,
transformOrigin: '0 0'
}}
draggable
onDragStart={this.handleDragStart}
onDragEnd={this.handleDragEnd}
>
<DraggableContainer>
<ResizableContainer
width={edgelessLayout.width}
height={edgelessLayout.height}
onResize={this.handleResize}
>
<BlockContainer>
{this.renderForEdgeless(block)}
</BlockContainer>
</ResizableContainer>
</DraggableContainer>
</div>
);
}
private renderForEdgeless(block: BaseBlock<any>): React.ReactNode {
if (block instanceof TextBlock) {
return <TextBlockEdgeless {...block.props} />;
} else if (block instanceof DatabaseBlock) {
return <DatabaseBlockMini {...block.props} />;
} else if (block instanceof ImageBlock) {
return <ImageBlockEdgeless {...block.props} />;
}
return block.render();
}
}
实时协作的CRDT实现:Yjs深度集成
AFFiNE的实时协作能力基于Conflict-free Replicated Data Type (CRDT),使用Yjs作为核心实现。这种设计确保了分布式环境下的数据一致性。
CRDT数据结构设计
import * as Y from 'yjs';
import { Awareness } from 'y-protocols/awareness';
export class CollaborativeDoc {
private ydoc: Y.Doc;
private awareness: Awareness;
private blocks: Y.Map<Y.Map<any>>;
private metadata: Y.Map<any>;
constructor() {
this.ydoc = new Y.Doc();
this.awareness = new Awareness(this.ydoc);
this.blocks = this.ydoc.getMap('blocks');
this.metadata = this.ydoc.getMap('metadata');
this.setupEventListeners();
}
private setupEventListeners(): void {
this.blocks.observe(event => {
event.changes.keys.forEach((change, key) => {
if (change.action === 'add') {
this.onBlockAdded(key, this.blocks.get(key)!);
} else if (change.action === 'delete') {
this.onBlockDeleted(key);
} else if (change.action === 'update') {
this.onBlockUpdated(key, this.blocks.get(key)!);
}
});
});
}
addBlock(blockId: string, blockData: any): void {
this.ydoc.transact(() => {
const blockMap = new Y.Map();
Object.entries(blockData).forEach(([key, value]) => {
blockMap.set(key, this.serializeValue(value));
});
this.blocks.set(blockId, blockMap);
});
}
updateBlock(blockId: string, updates: Record<string, any>): void {
this.ydoc.transact(() => {
const blockMap = this.blocks.get(blockId);
if (blockMap) {
Object.entries(updates).forEach(([key, value]) => {
blockMap.set(key, this.serializeValue(value));
});
}
});
}
private serializeValue(value: any): Y.Any {
if (value === null || value === undefined) {
return value;
}
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
return value;
}
if (Array.isArray(value)) {
const yarr = new Y.Array();
value.forEach(item => {
yarr.push([this.serializeValue(item)]);
});
return yarr;
}
if (typeof value === 'object') {
const ymap = new Y.Map();
Object.entries(value).forEach(([key, val]) => {
ymap.set(key, this.serializeValue(val));
});
return ymap;
}
return value;
}
private deserializeValue(yvalue: Y.Any): any {
if (yvalue === null || yvalue === undefined) {
return yvalue;
}
if (yvalue instanceof Y.Map) {
const obj: Record<string, any> = {};
yvalue.forEach((value, key) => {
obj[key] = this.deserializeValue(value);
});
return obj;
}
if (yvalue instanceof Y.Array) {
return yvalue.toArray().map(item => this.deserializeValue(item));
}
return yvalue;
}
}
冲突解决与状态同步
export class ConflictResolver {
static resolveByTimestamp<T>(local: T, remote: T, localTimestamp: number, remoteTimestamp: number): T {
return remoteTimestamp > localTimestamp ? remote : local;
}
static resolveByOperationType(
localOp: Operation,
remoteOp: Operation,
context: MergeContext
): MergeResult {
const mergeRules: Record<string, OperationMergeRule> = {
'create+create': this.resolveCreateConflict,
'update+update': this.resolveUpdateConflict,
'delete+update': this.resolveDeleteUpdateConflict,
'move+resize': this.resolveMoveResizeConflict
};
const ruleKey = `${localOp.type}+${remoteOp.type}`;
const rule = mergeRules[ruleKey];
if (rule) {
return rule(localOp, remoteOp, context);
}
return {
merged: this.resolveByTimestamp(localOp.data, remoteOp.data, localOp.timestamp, remoteOp.timestamp),
source: 'timestamp'
};
}
private static resolveCreateConflict(
localOp: Operation,
remoteOp: Operation,
context: MergeContext
): MergeResult {
if (localOp.targetId !== remoteOp.targetId) {
return {
merged: [localOp.data, remoteOp.data],
source: 'id-different'
};
}
const adjustedLocal = this.adjustBlockPosition(localOp.data, context.positionConflict);
const adjustedRemote = this.adjustBlockPosition(remoteOp.data, context.positionConflict);
return {
merged: [adjustedLocal, adjustedRemote],
source: 'position-adjusted'
};
}
private static resolveUpdateConflict(
localOp: Operation,
remoteOp: Operation,
context: MergeContext
): MergeResult {
const merged = { ...localOp.data };
Object.keys(remoteOp.data).forEach(field => {
if (context.fieldTimestamps[field] < remoteOp.timestamp) {
merged[field] = remoteOp.data[field];
}
});
return {
merged,
source: 'field-merge'
};
}
private static adjustBlockPosition(block: any, conflict: PositionConflict): any {
if (conflict.type === 'overlap') {
return {
...block,
position: {
x: block.position.x + conflict.offsetX,
y: block.position.y + conflict.offsetY
}
};
}
return block;
}
}
本地优先架构的数据同步设计
AFFiNE采用"本地优先"的架构理念,确保用户数据始终首先存储在本地,同时支持跨设备同步。这种设计平衡了隐私保护与用户体验。
本地存储与同步策略
export class LocalFirstStorage {
private db: LocalDB;
private syncManager: SyncManager;
private conflictResolver: ConflictResolver;
constructor() {
this.db = new IndexedDBStorage('affine-local');
this.syncManager = new SyncManager();
this.conflictResolver = new ConflictResolver();
}
async saveToLocal<T>(key: string, data: T): Promise<void> {
try {
await this.db.put(key, {
data,
timestamp: Date.now(),
version: this.getCurrentVersion()
});
document.dispatchEvent(new CustomEvent('local:update', {
detail: { key, data, timestamp: Date.now() }
}));
this.syncManager.queueSync(key, data);
} catch (error) {
console.error('本地保存失败:', error);
throw new Error('数据保存失败');
}
}
async resolveSyncConflict(localData: any, remoteData: any): Promise<any> {
const localVersion = localData.version || 0;
const remoteVersion = remoteData.version || 0;
if (localVersion === remoteVersion) {
return this.conflictResolver.resolveByOperationType(
localData.operations,
remoteData.operations,
this.createMergeContext(localData, remoteData)
);
}
return this.conflictResolver.resolveByTimestamp(
localData.data,
remoteData.data,
localData.timestamp,
remoteData.timestamp
);
}
async queueOfflineOperation(operation: OfflineOperation): Promise<void> {
const queue = await this.db.get('offline-queue') || [];
queue.push({
...operation,
id: generateId(),
queuedAt: Date.now()
});
await this.db.put('offline-queue', queue);
this.syncManager.attemptSync();
}
}
export class SyncManager {
private syncQueue: Map<string, SyncTask> = new Map();
private syncInterval: NodeJS.Timeout;
private isOnline: boolean = navigator.onLine;
constructor() {
this.setupNetworkListeners();
this.syncInterval = setInterval(() => {
this.processSyncQueue();
}, 5000);
}
private setupNetworkListeners(): void {
window.addEventListener('online', () => {
this.isOnline = true;
this.processSyncQueue();
});
window.addEventListener('offline', () => {
this.isOnline = false;
});
}
async queueSync(key: string, data: any): Promise<void> {
const task: SyncTask = {
key,
data,
attempts: 0,
priority: this.calculatePriority(data),
createdAt: Date.now()
};
this.syncQueue.set(key, task);
if (task.priority === 'high' && this.isOnline) {
this.processTask(task);
}
}
private async processSyncQueue(): Promise<void> {
if (!this.isOnline || this.syncQueue.size === 0) {
return;
}
const tasks = Array.from(this.syncQueue.values())
.sort((a, b) => b.priority - a.priority)
.slice(0, 5);
for (const task of tasks) {
await this.processTask(task);
}
}
private async processTask(task: SyncTask): Promise<void> {
try {
task.attempts++;
const result = await this.performSync(task);
if (result.success) {
this.syncQueue.delete(task.key);
document.dispatchEvent(new CustomEvent('sync:success', {
detail: { key: task.key, result }
}));
} else {
if (task.attempts < this.maxAttempts && this.shouldRetry(task, result)) {
task.nextRetryAt = Date.now() + this.calculateRetryDelay(task);
} else {
this.syncQueue.delete(task.key);
document.dispatchEvent(new CustomEvent('sync:failure', {
detail: { key: task.key, error: result.error }
}));
}
}
} catch (error) {
console.error('同步任务处理失败:', error);
}
}
}
AI集成的架构设计
AFFiNE通过Canvas AI概念,将AI能力深度集成到文档编辑和白板创作中,实现了智能化的内容生成和优化。
AI工作流引擎
export class AIWorkflowEngine {
private aiProviders: Map<string, AIProvider> = new Map();
private workflowQueue: PriorityQueue<AIWorkflow> = new Queue();
private contextManager: ContextManager;
constructor() {
this.contextManager = new ContextManager();
this.initializeAIProviders();
}
private initializeAIProviders(): void {
this.aiProviders.set('openai', new OpenAIProvider());
this.aiProviders.set('claude', new ClaudeProvider());
this.aiProviders.set('local', new LocalAIProvider());
}
async executeWorkflow(workflow: AIWorkflow): Promise<AIResult> {
const context = await this.contextManager.buildContext(workflow.context);
const provider = this.selectOptimalProvider(workflow, context);
const result = await provider.execute(workflow.prompt, context);
return this.processAIResult(result, workflow);
}
async generateContextualContent(
blockType: string,
currentContent: string,
workspace: WorkspaceContext
): Promise<string> {
const contextualPrompt = this.buildContextualPrompt(blockType, currentContent, workspace);
const workflow: AIWorkflow = {
type: 'generate',
prompt: contextualPrompt,
context: {
blockType,
currentContent,
workspace,
recentBlocks: await this.getRecentBlocks(workspace.currentPage),
userPreferences: workspace.user.preferences
},
options: {
maxTokens: 500,
temperature: 0.7,
blockType: blockType
}
};
return this.executeWorkflow(workflow);
}
async optimizeEdgelessLayout(blocks: BlockNode[]): Promise<LayoutOptimization[]> {
const optimizationWorkflow: AIWorkflow = {
type: 'optimize-layout',
prompt: this.buildLayoutOptimizationPrompt(blocks),
context: {
blocks,
canvasSize: { width: 1920, height: 1080 },
viewportConstraints: { minMargin: 20, preferredSpacing: 40 },
userInteractionHistory: await this.getUserLayoutPreferences()
},
options: {
algorithm: 'genetic',
iterations: 100,
populationSize: 50
}
};
const result = await this.executeWorkflow(optimizationWorkflow);
return this.parseLayoutOptimization(result.content);
}
private buildContextualPrompt(blockType: string, content: string, workspace: WorkspaceContext): string {
const templates = {
text: `作为写作助手,请优化以下文本内容:\n\n${content}\n\n基于当前工作区的上下文:\n${JSON.stringify(workspace.recentActivity, null, 2)}`,
database: `作为数据分析助手,基于以下数据库上下文,生成相关的洞察和建议:\n\n数据内容:\n${content}\n\n工作区信息:\n${JSON.stringify(workspace.pageContext, null, 2)}`,
mindmap: `作为思维导图专家,基于以下内容创建思维导图的结构:\n\n核心内容:\n${content}\n\n用户角色:\n${workspace.user.role}\n项目类型:\n${workspace.pageContext.projectType}`
};
return templates[blockType] || templates.text;
}
}
性能优化与工程实践
虚拟化渲染
export class VirtualizedRenderer {
private containerRef: React.RefObject<HTMLDivElement>;
private viewportHeight: number;
private itemHeight: number;
private overscan: number = 5;
renderVisibleBlocks(blocks: BlockNode[], scrollTop: number): React.ReactNode {
const startIndex = Math.floor(scrollTop / this.itemHeight);
const endIndex = Math.min(
startIndex + Math.ceil(this.viewportHeight / this.itemHeight) + this.overscan,
blocks.length
);
const visibleBlocks = blocks.slice(startIndex, endIndex);
return (
<div
style={{
height: blocks.length * this.itemHeight,
position: 'relative'
}}
>
{visibleBlocks.map((block, index) => (
<div
key={block.id}
style={{
position: 'absolute',
top: (startIndex + index) * this.itemHeight,
left: 0,
right: 0,
height: this.itemHeight
}}
>
{this.renderBlock(block)}
</div>
))}
</div>
);
}
}
内存优化策略
export class OptimizedBlockManager {
private blockCache: LRUCache<string, BlockNode> = new LRUCache(1000);
private lazyLoader: LazyBlockLoader;
async loadBlock(id: string): Promise<BlockNode | null> {
const cached = this.blockCache.get(id);
if (cached) {
return cached;
}
const block = await this.lazyLoader.loadBlock(id);
if (block) {
this.blockCache.set(id, block);
}
return block;
}
private handleMemoryPressure(): void {
if (performance.memory) {
const memoryInfo = performance.memory;
const usedPercentage = memoryInfo.usedJSHeapSize / memoryInfo.jsHeapSizeLimit;
if (usedPercentage > 0.8) {
this.blockCache.clear();
this.triggerGarbageCollection();
}
}
}
}
结语
AFFiNE通过TypeScript+Rust的混合架构,实现了文档编辑与可视化白板的无缝融合,为知识管理工具开辟了新的技术路径。其块级架构设计不仅继承了Notion"万物皆模块"的理念,更在工程实现上实现了跨视图的动态渲染和协同编辑。
项目的成功证明了开源技术在复杂应用场景下的可行性。从Rust后端的高性能数据处理,到TypeScript前端的复杂交互逻辑,再到CRDT实现的实时协作能力,每一个技术决策都体现了工程实践的深度思考。
随着AI技术的深度集成和本地优先理念的推广,AFFiNE不仅是一个工具产品,更是下一代知识管理架构的技术实验场。它为开发者提供了宝贵的混合架构实践经验,也为知识管理领域的技术演进指明了方向。
参考资料: