Firebase Firestore

技术栈
数据库
nosqldocumentrealtimeserverlessgoogle-cloudbaas

概览

Firebase Firestore 是 Google 的全托管 NoSQL 文档数据库,核心卖点是实时监听——数据变更时自动推送到所有在线客户端。深度集成 Firebase 生态(Auth、Cloud Functions、Hosting),无需管理基础设施。支持离线持久化、结构化查询、多区域复制。适合聊天应用、协作工具、移动 App 等需要实时同步的场景。

安装

Firebase Firestore 安装与配置指南

1. 环境准备

要求 说明
Google 账号 注册 https://firebase.google.com/
Node.js 16+(用于 Firebase CLI)
开发框架 Web(JS SDK)、Android、iOS、Flutter、Unity 等
Firebase 项目 在 Firebase Console 创建
计费 Spark 免费计划(1 GB 存储 + 每日 5 万读/2 万写/2 万删)

Firestore 是云端托管服务,无需安装数据库。本地开发用 Firebase Emulator。

2. 配置命令

Firebase CLI 安装

# 安装 CLI
npm install -g firebase-tools

# 登录
firebase login

# 初始化项目
firebase init firestore

Web SDK 安装

# npm
npm install firebase

初始化代码

import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';

const firebaseConfig = {
  apiKey: "YOUR_API_KEY",
  authDomain: "YOUR_PROJECT.firebaseapp.com",
  projectId: "YOUR_PROJECT_ID",
  storageBucket: "YOUR_PROJECT.appspot.com",
  messagingSenderId: "YOUR_SENDER_ID",
  appId: "YOUR_APP_ID"
};

const app = initializeApp(firebaseConfig);
const db = getFirestore(app);

本地 Emulator(开发用)

# 安装 emulator
firebase init emulators

# 启动
firebase emulators:start --only firestore

# 配置 SDK 连接 emulator
import { connectFirestoreEmulator } from 'firebase/firestore';
connectFirestoreEmulator(db, 'localhost', 8080);

Firestore NoSQL 客户端(Python)

pip install google-cloud-firestore

# 认证
export GOOGLE_APPLICATION_CREDENTIALS="/path/to/service-account.json"

3. 常见配置问题

Q1: "PERMISSION_DENIED" 错误

默认安全规则禁止所有读写。修改 firestore.rules

rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {
    match /{document=**} {
      allow read, write: if request.time < timestamp.date(2025, 1, 1);
    }
  }
}

部署:firebase deploy --only firestore:rules

Q2: CLI 无法登录

终端浏览器无法打开时使用:

firebase login --no-localhost

Q3: Emulator 数据持久化

默认退出后数据丢失。持久化:

firebase emulators:start --only firestore --export-on-exit=./emulator_data

Q4: 中文集合/文档 ID

Firestore 支持中文 ID,但推荐使用英文 slug 或自动生成的 ID 以保证兼容性。

示例

Firestore Hello World:实时聊天消息

目标

连接 Firestore,创建聊天消息集合并实现实时监听——新增消息自动出现在客户端。

完整代码

Web 版 (JavaScript)

<!DOCTYPE html>
<html>
<head>
    <title>Firestore 聊天室</title>
</head>
<body>
    <h2>实时聊天室</h2>
    <div id="messages" style="height:300px;overflow-y:auto;border:1px solid #ccc;padding:10px;margin-bottom:10px;"></div>
    <input id="msgInput" placeholder="输入消息..." style="width:300px;">
    <button onclick="sendMessage()">发送</button>

    <script type="module">
        import { initializeApp } from 'https://www.gstatic.com/firebasejs/10.12.0/firebase-app.js';
        import { getFirestore, collection, addDoc, query, orderBy, limit, onSnapshot, serverTimestamp } from 'https://www.gstatic.com/firebasejs/10.12.0/firebase-firestore.js';

        // == 1. 初始化 ==
        const firebaseConfig = {
            apiKey: "YOUR_API_KEY",
            authDomain: "YOUR_PROJECT.firebaseapp.com",
            projectId: "YOUR_PROJECT_ID",
        };
        const app = initializeApp(firebaseConfig);
        const db = getFirestore(app);

        // == 2. 发送消息 ==
        window.sendMessage = async function() {
            const input = document.getElementById('msgInput');
            const text = input.value.trim();
            if (!text) return;

            await addDoc(collection(db, 'messages'), {
                text: text,
                user: '匿名用户',
                createdAt: serverTimestamp()
            });
            input.value = '';
        };

        // == 3. 实时监听 ==
        const messagesRef = collection(db, 'messages');
        const q = query(messagesRef, orderBy('createdAt', 'desc'), limit(50));

        onSnapshot(q, (snapshot) => {
            const container = document.getElementById('messages');
            container.innerHTML = '';
            snapshot.docChanges().forEach(change => {
                if (change.type === 'added') {
                    const msg = change.doc.data();
                    const div = document.createElement('div');
                    div.textContent = `[${msg.user}] ${msg.text}`;
                    container.prepend(div);
                }
            });
        });
    </script>
</body>
</html>

Node.js 版

// npm install firebase-admin
const { initializeApp, cert } = require('firebase-admin/app');
const { getFirestore, Timestamp, FieldValue } = require('firebase-admin/firestore');

initializeApp({ credential: cert('./serviceAccountKey.json') });
const db = getFirestore();

// 写入消息
async function sendMessage(user, text) {
    const docRef = await db.collection('messages').add({
        user: user,
        text: text,
        createdAt: FieldValue.serverTimestamp(),
    });
    console.log(`消息 ID: ${docRef.id}`);
    return docRef;
}

// 实时监听(服务端订阅)
function listenForMessages() {
    const unsubscribe = db.collection('messages')
        .orderBy('createdAt', 'desc')
        .limit(10)
        .onSnapshot(snapshot => {
            snapshot.docChanges().forEach(change => {
                if (change.type === 'added') {
                    const msg = change.doc.data();
                    console.log(`[新消息] ${msg.user}: ${msg.text}`);
                }
            });
        });

    // 取消监听:unsubscribe();
}

// 查询历史
async function getMessages(limit = 20) {
    const snapshot = await db.collection('messages')
        .orderBy('createdAt', 'desc')
        .limit(limit)
        .get();

    const messages = [];
    snapshot.forEach(doc => {
        messages.push({ id: doc.id, ...doc.data() });
    });
    return messages;
}

// 运行
sendMessage('Alice', '大家好!');
getMessages().then(msgs => console.log(msgs));

Python 版

# pip install firebase-admin
import firebase_admin
from firebase_admin import credentials, firestore
from google.cloud.firestore_v1 import SERVER_TIMESTAMP

cred = credentials.Certificate('./serviceAccountKey.json')
firebase_admin.initialize_app(cred)
db = firestore.client()

# 写入
doc_ref = db.collection('messages').document()
doc_ref.set({
    'user': 'Bob',
    'text': 'Hello from Python!',
    'createdAt': SERVER_TIMESTAMP
})
print(f"消息 ID: {doc_ref.id}")

# 查询
docs = db.collection('messages').order_by('createdAt', direction='DESCENDING').limit(10).stream()
for doc in docs:
    print(f"{doc.to_dict()['user']}: {doc.to_dict()['text']}")

# 实时监听(需长时间运行的脚本)
def on_snapshot(col_snapshot, changes, read_time):
    for change in changes:
        if change.type.name == 'ADDED':
            print(f"[实时] {change.document.to_dict()['user']}: {change.document.to_dict()['text']}")

col_ref = db.collection('messages')
watch = col_ref.on_snapshot(on_snapshot)
# watch.unsubscribe()  # 停止监听

预期输出

消息 ID: abc123...
[新消息] Alice: 大家好!
[新消息] Bob: Hello from Python!

// 实时监听:当有新消息时自动打印
[实时] Carol: 我也在!

关键点

  • onSnapshot 是 Firestore 的核心:变更实时推送
  • serverTimestamp() 避免客户端时间不同步
  • docChanges() 区分 add/modify/remove 事件
  • 离线数据由 SDK 自动处理,联网后同步
  • 免费层足够学习使用(每日 5 万读 / 2 万写)

教程

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)?

参考资料

  1. [1] Google LLC. Firebase Firestore 官方文档. 2024.
  2. [2] Shama Hoque. Full-Stack React Projects (Ch 9: Firebase). 2020.
  3. [3] Todd Kerpelman. Cloud Firestore Data Modeling. 2023.