文档
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. 思考题
- 如果 100 个用户同时拖拽同一个便签,Firestore 如何处理冲突?谁"胜出"?
docChanges()返回的change.type有哪几种?modified和added什么时候会被同时触发?- Firestore 计费按文档读写次数。一个用户拖拽便签 100 次会产生多少次写操作?如何优化(如 throttle)?