协作白板应用实战

知识库
知识库文档
/tech-stacks/firestore/tutorial/协作白板应用实战.md

文档

Firebase Firestore 从零到实战:协作白板应用

1. 背景与概念

1.1 Firestore 独特之处

Firestore 不是传统"请求-响应"数据库。它的灵魂是实时监听

客户端 A ──写入──▶ Firestore ──推送──▶ 客户端 B、C、D

这使其天然适合:聊天、协作文档、实时仪表盘、游戏状态同步。

1.2 数据模型

collection (集合)
  └── document (文档)
       ├── field: value
       ├── nested_object: { ... }
       └── subcollection (子集合)
            └── document

⚠️ 与 Realtime Database 不同:Firestore 支持丰富的查询(where / orderBy / limit),Realtime Database 是 JSON 树。

2. 分步实战:构建协作白板

场景

多人在线白板:用户可在画布上放置便签(Sticky Note),其他用户实时看到。便签支持拖拽移动和文字编辑。

步骤一:设计数据结构

/boards/{boardId}
  - name: "小组讨论"
  - createdBy: "user123"

/boards/{boardId}/notes/{noteId}
  - text: "会议主题:项目进度"
  - x: 150      (像素坐标)
  - y: 200
  - color: "#FFD700"
  - createdBy: "user123"
  - updatedAt: Timestamp

步骤二:初始化白板

import { getFirestore, collection, doc, setDoc, addDoc, onSnapshot, updateDoc, deleteDoc, serverTimestamp } from 'firebase/firestore';

const db = getFirestore();
const boardId = 'board_001';

// 创建画板
await setDoc(doc(db, 'boards', boardId), {
    name: '小组讨论',
    createdBy: 'user123',
    createdAt: serverTimestamp()
});

步骤三:实时同步便签

// 核心:实时监听便签变更
function subscribeNotes(boardId, callback) {
    const notesRef = collection(db, 'boards', boardId, 'notes');

    return onSnapshot(notesRef, (snapshot) => {
        snapshot.docChanges().forEach(change => {
            const note = { id: change.doc.id, ...change.doc.data() };

            switch (change.type) {
                case 'added':
                    callback('add', note);
                    break;
                case 'modified':
                    callback('update', note);
                    break;
                case 'removed':
                    callback('remove', note);
                    break;
            }
        });
    });
}

// 使用
const unsubscribe = subscribeNotes(boardId, (type, note) => {
    if (type === 'add') {
        renderStickyNote(note);          // 创建便签 DOM
    } else if (type === 'update') {
        updateStickyNote(note);          // 更新位置/文字
    } else if (type === 'remove') {
        removeStickyNote(note.id);       // 删除 DOM
    }
});

// 添加便签(任何用户)
async function addNote(text, x, y) {
    await addDoc(collection(db, 'boards', boardId, 'notes'), {
        text: text,
        x: x,
        y: y,
        color: getRandomColor(),
        createdBy: currentUser.uid,
        updatedAt: serverTimestamp()
    });
}

// 拖拽移动(实时同步坐标)
async function moveNote(noteId, newX, newY) {
    await updateDoc(doc(db, 'boards', boardId, 'notes', noteId), {
        x: newX,
        y: newY,
        updatedAt: serverTimestamp()
    });
}

// 编辑文字
async function editNote(noteId, newText) {
    await updateDoc(doc(db, 'boards', boardId, 'notes', noteId), {
        text: newText,
        updatedAt: serverTimestamp()
    });
}

// 删除便签
async function deleteNote(noteId) {
    await deleteDoc(doc(db, 'boards', boardId, 'notes', noteId));
}

步骤四:安全规则

// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /boards/{boardId} {
      allow read: if request.auth != null;
      allow create: if request.auth != null;

      match /notes/{noteId} {
        allow read, write: if request.auth != null;

        // 验证:便签坐标不能为负
        allow update: if request.resource.data.x >= 0
                      && request.resource.data.y >= 0;
      }
    }
  }
}

步骤五:离线支持

import { enableIndexedDbPersistence } from 'firebase/firestore';

// 启用离线持久化(IndexedDB 缓存)
await enableIndexedDbPersistence(db);

// 离线时本地写入,联网后自动同步
// 无需额外代码!

3. 思考题

  1. 如果 100 个用户同时拖拽同一个便签,Firestore 如何处理冲突?谁"胜出"?
  2. docChanges() 返回的 change.type 有哪几种?modifiedadded 什么时候会被同时触发?
  3. Firestore 计费按文档读写次数。一个用户拖拽便签 100 次会产生多少次写操作?如何优化(如 throttle)?

信息

路径
/tech-stacks/firestore/tutorial/协作白板应用实战.md
更新时间
2026/5/31