关于 firebase-firestore
面向使用 Firebase Firestore 的开发者,提供从初始化到数据建模的 TypeScript 实践指南。涵盖客户端与 Admin SDK 操作,并着重说明常见错误及其规避方式。
主要功能
- 01客户端与 Admin SDK 初始化
- 02文档增删改查与实时监听
- 03复合查询、分页与集合组查询
- 04批量写入与事务
- 05安全规则、索引与离线持久化
- 06常见问题排查与预防
适用场景
- 01为 Web 或移动应用实现 Firestore 数据操作
- 02为服务端配置 Admin SDK 访问数据库
- 03构建实时查询、分页及跨子集合查询
- 04排查索引、权限及事务相关问题
安装
复制下面的命令,在终端中运行。
$
npx skills add https:/ / github.com/ jezweb/ claude-skills --skill firebase-firestore安装后在 Claude Code 中输入/firebase-firestore确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
README.md.claude-plugin/plugin.jsonrules/firebase-firestore.mdtemplates/firebase-admin.tsfirebase-client.tsfirestore.indexes.json详细介绍
Firebase Firestore 数据库
状态:已可用于生产环境 最后更新:2026-01-25 依赖项:无(独立技能) 最新版本:[email protected]、[email protected]
快速入门(5 分钟)
1. 安装 Firebase SDK
# Client SDK (web/mobile)
npm install firebase
# Admin SDK (server/backend)
npm install firebase-admin
2. 初始化 Firebase(客户端)
// src/lib/firebase.ts
import { initializeApp } from 'firebase/app';
import { getFirestore } from 'firebase/firestore';
const firebaseConfig = {
apiKey: process.env.FIREBASE_API_KEY,
authDomain: process.env.FIREBASE_AUTH_DOMAIN,
projectId: process.env.FIREBASE_PROJECT_ID,
storageBucket: process.env.FIREBASE_STORAGE_BUCKET,
messagingSenderId: process.env.FIREBASE_MESSAGING_SENDER_ID,
appId: process.env.FIREBASE_APP_ID,
};
const app = initializeApp(firebaseConfig);
export const db = getFirestore(app);
3. 初始化 Firebase Admin(服务端)
// src/lib/firebase-admin.ts
import { initializeApp, cert, getApps } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';
// Initialize only once
if (!getApps().length) {
initializeApp({
credential: cert({
projectId: process.env.FIREBASE_PROJECT_ID,
clientEmail: process.env.FIREBASE_CLIENT_EMAIL,
// Replace escaped newlines in private key
privateKey: process.env.FIREBASE_PRIVATE_KEY?.replace(/\\n/g, '\n'),
}),
});
}
export const adminDb = getFirestore();
重要:
- 切勿在客户端代码中暴露
FIREBASE_PRIVATE_KEY - 服务端操作使用 Admin SDK(可绕过安全规则)
- 经过身份验证的用户操作使用客户端 SDK
核心操作
文档 CRUD(客户端 SDK - Modular v9+)
import {
collection,
doc,
addDoc,
getDoc,
getDocs,
setDoc,
updateDoc,
deleteDoc,
query,
where,
orderBy,
limit,
serverTimestamp,
Timestamp,
} from 'firebase/firestore';
import { db } from './firebase';
// CREATE - Auto-generated ID
const docRef = await addDoc(collection(db, 'users'), {
name: 'John Doe',
email: '[email protected]',
createdAt: serverTimestamp(),
});
console.log('Created document with ID:', docRef.id);
// CREATE - Specific ID
await setDoc(doc(db, 'users', 'user-123'), {
name: 'Jane Doe',
email: '[email protected]',
createdAt: serverTimestamp(),
});
// READ - Single document
const docSnap = await getDoc(doc(db, 'users', 'user-123'));
if (docSnap.exists()) {
console.log('Document data:', docSnap.data());
} else {
console.log('No such document!');
}
// READ - Collection with query
const q = query(
collection(db, 'users'),
where('email', '==', '[email protected]'),
orderBy('createdAt', 'desc'),
limit(10)
);
const querySnapshot = await getDocs(q);
querySnapshot.forEach((doc) => {
console.log(doc.id, ' => ', doc.data());
});
// UPDATE - Merge fields (doesn't overwrite entire document)
await updateDoc(doc(db, 'users', 'user-123'), {
name: 'Jane Smith',
updatedAt: serverTimestamp(),
});
// UPDATE - Set with merge (creates if doesn't exist)
await setDoc(doc(db, 'users', 'user-123'), {
lastLogin: serverTimestamp(),
}, { merge: true });
// DELETE
await deleteDoc(doc(db, 'users', 'user-123'));
文档 CRUD(Admin SDK)
import { adminDb } from './firebase-admin';
import { FieldValue, Timestamp } from 'firebase-admin/firestore';
// CREATE
const docRef = await adminDb.collection('users').add({
name: 'John Doe',
createdAt: FieldValue.serverTimestamp(),
});
// CREATE with specific ID
await adminDb.collection('users').doc('user-123').set({
name: 'Jane Doe',
createdAt: FieldValue.serverTimestamp(),
});
// READ
const doc = await adminDb.collection('users').doc('user-123').get();
if (doc.exists) {
console.log('Document data:', doc.data());
}
// READ with query
const snapshot = await adminDb
.collection('users')
.where('email', '==', '[email protected]')
.orderBy('createdAt', 'desc')
.limit(10)
.get();
snapshot.forEach((doc) => {
console.log(doc.id, '=>', doc.data());
});
// UPDATE
await adminDb.collection('users').doc('user-123').update({
name: 'Jane Smith',
updatedAt: FieldValue.serverTimestamp(),
});
// DELETE
await adminDb.collection('users').doc('user-123').delete();
实时监听器(客户端 SDK)
import { onSnapshot, query, where, collection, doc } from 'firebase/firestore';
import { db } from './firebase';
// Listen to single document
const unsubscribe = onSnapshot(doc(db, 'users', 'user-123'), (doc) => {
if (doc.exists()) {
console.log('Current data:', doc.data());
}
});
// Listen to collection with query
const q = query(
collection(db, 'messages'),
where('roomId', '==', 'room-123'),
orderBy('createdAt', 'desc'),
limit(50)
);
const unsubscribeMessages = onSnapshot(q, (querySnapshot) => {
const messages: Message[] = [];
querySnapshot.forEach((doc) => {
messages.push({ id: doc.id, ...doc.data() } as Message);
});
// Update UI with messages
setMessages(messages);
});
// Handle errors
const unsubscribeWithError = onSnapshot(
doc(db, 'users', 'user-123'),
(doc) => {
// Handle updates
},
(error) => {
console.error('Listener error:', error);
// Handle permission denied, etc.
}
);
// IMPORTANT: Unsubscribe when done (React useEffect cleanup)
useEffect(() => {
const unsubscribe = onSnapshot(/* ... */);
return () => unsubscribe();
}, []);
重要:
- 始终取消订阅监听器,以防止内存泄漏
- 监听器会计入并发连接数限制
- 每个监听器都会建立一个 WebSocket 连接
查询模式
复合查询
import { query, where, orderBy, limit, startAfter, collection } from 'firebase/firestore';
// Multiple where clauses (requires composite index)
const q = query(
collection(db, 'products'),
where('category', '==', 'electronics'),
where('price', '<=', 1000),
orderBy('price', 'asc')
);
// Range query (only one field can have inequality)
const rangeQuery = query(
collection(db, 'events'),
where('date', '>=', new Date('2025-01-01')),
where('date', '<=', new Date('2025-12-31')),
orderBy('date', 'asc')
);
// Array contains
const arrayQuery = query(
collection(db, 'posts'),
where('tags', 'array-contains', 'firebase')
);
// Array contains any (max 30 values)
const arrayAnyQuery = query(
collection(db, 'posts'),
where('tags', 'array-contains-any', ['firebase', 'google', 'cloud'])
);
// In query (max 30 values)
const inQuery = query(
collection(db, 'users'),
where('status', 'in', ['active', 'pending'])
);
// Not in query (max 10 values)
const notInQuery = query(
collection(db, 'users'),
where('status', 'not-in', ['banned', 'deleted'])
);
使用游标进行分页
import { query, orderBy, limit, startAfter, getDocs, collection, DocumentSnapshot } from 'firebase/firestore';
let lastVisible: DocumentSnapshot | null = null;
async function getNextPage() {
let q = query(
collection(db, 'posts'),
orderBy('createdAt', 'desc'),
limit(10)
);
if (lastVisible) {
q = query(q, startAfter(lastVisible));
}
const snapshot = await getDocs(q);
// Save last document for next page
lastVisible = snapshot.docs[snapshot.docs.length - 1] || null;
return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() }));
}
集合组查询
import { collectionGroup, query, where, getDocs } from 'firebase/firestore';
// Query across all subcollections named 'comments'
// Structure: posts/{postId}/comments/{commentId}
const q = query(
collectionGroup(db, 'comments'),
where('authorId', '==', 'user-123')
);
const snapshot = await getDocs(q);
// Returns all comments by user-123 across all posts
重要: 集合组查询需要索引。请在 Firebase Console 中创建,或通过 firestore.indexes.json 部署。
批量操作与事务
批量写入(最多 500 个操作)
import { writeBatch, doc, collection, serverTimestamp } from 'firebase/firestore';
import { db } from './firebase';
const batch = writeBatch(db);
// Add multiple documents
const usersRef = collection(db, 'users');
batch.set(doc(usersRef), { name: 'User 1', createdAt: serverTimestamp() });
batch.set(doc(usersRef), { name: 'User 2', createdAt: serverTimestamp() });
// Update existing document
batch.update(doc(db, 'counters', 'users'), { total: 100 });
// Delete document
batch.delete(doc(db, 'temp', 'old-doc'));
// Commit all operations atomically
await batch.commit();
事务(先读后写)
import { runTransaction, doc, increment } from 'firebase/firestore';
import { db } from './firebase';
// Transfer credits between users
async function transferCredits(fromId: string, toId: string, amount: number) {
await runTransaction(db, async (transaction) => {
const fromRef = doc(db, 'users', fromId);
const toRef = doc(db, 'users', toId);
const fromDoc = await transaction.get(fromRef);
const toDoc = await transaction.get(toRef);
if (!fromDoc.exists() || !toDoc.exists()) {
throw new Error('User not found');
}
const fromCredits = fromDoc.data().credits;
if (fromCredits < amount) {
throw new Error('Insufficient credits');
}
transaction.update(fromRef, { credits: fromCredits - amount });
transaction.update(toRef, { credits: increment(amount) });
});
}
重要:
- 事务可能失败,并会自动重试(最多 5 次)
- 不要在事务中执行副作用操作(可能会运行多次)
- 事务中的所有读取都必须在写入之前完成
安全规则
基本规则结构
// firestore.rules
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
// Helper functions
function isAuthenticated() {
return request.auth != null;
}
function isOwner(userId) {
return request.auth.uid == userId;
}
function isValidUser() {
return request.resource.data.keys().hasAll(['name', 'email'])
&& request.resource.data.name is string
&& request.resource.data.email is string;
}
// Users collection
match /users/{userId} {
allow read: if isAuthenticated();
allow create: if isAuthenticated() && isOwner(userId) && isValidUser();
allow update: if isOwner(userId);
allow delete: if isOwner(userId);
}
// Posts collection with subcollections
match /posts/{postId} {
allow read: if resource.data.published == true || isOwner(resource.data.authorId);
allow create: if isAuthenticated() && request.resource.data.authorId == request.auth.uid;
allow update, delete: if isOwner(resource.data.authorId);
// Comments subcollection
match /comments/{commentId} {
allow read: if true;
allow create: if isAuthenticated();
allow update, delete: if isOwner(resource.data.authorId);
}
}
// Admin-only collection
match /admin/{document=**} {
allow read, write: if request.auth.token.admin == true;
}
}
}
部署规则
# Deploy rules
firebase deploy --only firestore:rules
# Deploy rules and indexes
firebase deploy --only firestore
索引
复合索引(firestore.indexes.json)
{
"indexes": [
{
"collectionGroup": "products",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "category", "order": "ASCENDING" },
{ "fieldPath": "price", "order": "ASCENDING" }
]
},
{
"collectionGroup": "comments",
"queryScope": "COLLECTION_GROUP",
"fields": [
{ "fieldPath": "authorId", "order": "ASCENDING" },
{ "fieldPath": "createdAt", "order": "DESCENDING" }
]
}
],
"fieldOverrides": []
}
# Deploy indexes
firebase deploy --only firestore:indexes
重要:
- Firestore 会自动创建单字段索引
- 必须手动创建复合索引,或通过错误消息中的链接创建
- 集合组查询始终需要索引
离线持久化
启用离线支持(Web)
import { initializeFirestore, persistentLocalCache, persistentMultipleTabManager } from 'firebase/firestore';
import { app } from './firebase';
// Enable multi-tab offline persistence
const db = initializeFirestore(app, {
localCache: persistentLocalCache({
tabManager: persistentMultipleTabManager()
})
});
// OR: Enable single-tab persistence (simpler)
import { enableIndexedDbPersistence, getFirestore } from 'firebase/firestore';
const db = getFirestore(app);
enableIndexedDbPersistence(db).catch((err) => {
if (err.code === 'failed-precondition') {
// Multiple tabs open, persistence can only be enabled in one tab
console.warn('Persistence failed: multiple tabs open');
} else if (err.code === 'unimplemented') {
// Browser doesn't support persistence
console.warn('Persistence not supported');
}
});
处理离线状态
import { onSnapshot, doc, SnapshotMetadata } from 'firebase/firestore';
onSnapshot(doc(db, 'users', 'user-123'), (doc) => {
const source = doc.metadata.fromCache ? 'local cache' : 'server';
console.log(`Data came from ${source}`);
if (doc.metadata.hasPendingWrites) {
console.log('Local changes pending sync');
}
});
数据建模最佳实践
通过反规范化提升读取性能
// Instead of joining users and posts...
// Store author info directly in post document
// posts/{postId}
{
title: 'My Post',
content: '...',
authorId: 'user-123',
// Denormalized author data for fast reads
author: {
name: 'John Doe',
avatarUrl: 'https://...'
},
createdAt: Timestamp
}
子集合与根集合
// Subcollections: Good for parent-child relationships
// posts/{postId}/comments/{commentId}
// - Easy to query all comments for a post
// - Deleting post doesn't auto-delete comments (use Cloud Functions)
// Root collections: Good for cross-cutting queries
// comments (with postId field)
// - Easy to query all comments by a user across posts
// - Requires manual data consistency
计数器模式(高写入场景)
// Direct increment (low traffic)
await updateDoc(doc(db, 'posts', postId), {
viewCount: increment(1)
});
// Distributed counter (high traffic - 1000+ writes/sec)
// Use Cloud Functions to aggregate shard counts
// counters/{counterId}/shards/{shardId}
错误处理
常见错误及解决方案
| 错误 | 原因 | 解决方案 |
|---|---|---|
permission-denied | 安全规则阻止访问 | 检查规则,确保用户已通过身份验证 |
not-found | 文档不存在 | 访问数据前使用 exists() 检查 |
already-exists | 已存在相同 ID 的文档 | 使用带合并选项的 setDoc,或生成新 ID |
resource-exhausted | 超出配额 | 升级方案或优化查询 |
failed-precondition | 查询缺少索引 | 创建复合索引(错误消息中有链接) |
unavailable | 服务暂时不可用 | 实现带退避的重试机制 |
invalid-argument | 查询组合无效 | 检查查询限制(见下文) |
deadline-exceeded | 操作超时 | 减少数据量或进行分页 |
查询限制
// INVALID: Multiple inequality filters on different fields
query(collection(db, 'posts'),
where('date', '>', startDate),
where('likes', '>', 100) // ERROR: Can't use inequality on second field
);
// VALID: Use range on one field, equality on others
query(collection(db, 'posts'),
where('category', '==', 'tech'),
where('date', '>', startDate)
);
// INVALID: orderBy field different from inequality field
query(collection(db, 'posts'),
where('date', '>', startDate),
orderBy('likes') // ERROR: Must orderBy('date') first
);
// VALID: orderBy inequality field first
query(collection(db, 'posts'),
where('date', '>', startDate),
orderBy('date'),
orderBy('likes')
);
已知问题预防
此技能可预防 10 个已记录的 Firestore 错误:
| 问题 # | 错误/问题 | 描述 | 如何避免 | 来源 |
|---|---|---|---|---|
| #1 | permission-denied | 安全规则阻止操作 | 先在 Firebase Console 模拟器中测试规则 | 常见 |
| #2 | failed-precondition(索引) | 缺少复合索引 | 点击错误链接创建索引,或在 firestore.indexes.json 中定义 | 常见 |
| #3 | 无效的查询组合 | 多个不等式筛选条件 | 仅对一个字段使用不等式筛选条件,其他字段使用相等条件 | 文档 |
| #4 | 监听器导致的内存泄漏 | 未取消订阅 onSnapshot | 始终在清理时调用 unsubscribe(useEffect 的 return) | 常见 |
| #5 | 离线持久化冲突 | 多个标签页启用了持久化 | 使用 persistentMultipleTabManager() 或处理错误 | 文档 |
| #6 | 事务副作用 | 副作用会运行多次 | 切勿在 runTransaction 内执行副作用 | 文档 |
| #7 | 超出批次限制 | 操作数超过 500 | 拆分为多个批次 | 文档 |
| #8 | resource-exhausted | 达到配额限制 | 实施分页、减少读取次数并使用缓存 | 常见 |
| #9 | 私钥换行符问题 | 环境变量中的 \\n 未转换 | 对私钥使用 .replace(/\\n/g, '\n') | 常见 |
| #10 | 集合组查询失败 | 缺少集合组索引 | 创建 queryScope: COLLECTION_GROUP 的索引 | 文档 |
Firebase CLI 命令
# Initialize Firestore
firebase init firestore
# Start emulators
firebase emulators:start --only firestore
# Deploy rules and indexes
firebase deploy --only firestore
# Export data (for backup)
gcloud firestore export gs://your-bucket/backups/$(date +%Y%m%d)
# Import data
gcloud firestore import gs://your-bucket/backups/20250125
软件包版本(已于 2026-01-25 验证)
{
"dependencies": {
"firebase": "^12.8.0"
},
"devDependencies": {
"firebase-admin": "^13.6.0"
}
}
官方文档
- Firestore 概览:https://firebase.google.com/docs/firestore
- 快速入门:https://firebase.google.com/docs/firestore/quickstart
- 数据模型:https://firebase.google.com/docs/firestore/data-model
- 安全规则:https://firebase.google.com/docs/firestore/security/get-started
- 查询数据:https://firebase.google.com/docs/firestore/query-data/queries
- 管理数据:https://firebase.google.com/docs/firestore/manage-data/add-data
- 离线数据:https://firebase.google.com/docs/firestore/manage-data/enable-offline
最后验证时间:2026-01-25 | 技能版本:1.0.0
评论
支持 Markdown