mcpField
F
firebase-firestore

firebase-firestore

github.com/jezweb数据与数据库1.1k426 次安装3 周前更新
代码仓库

关于 firebase-firestore

面向使用 Firebase Firestore 的开发者,提供从初始化到数据建模的 TypeScript 实践指南。涵盖客户端与 Admin SDK 操作,并着重说明常见错误及其规避方式。

主要功能

  1. 01客户端与 Admin SDK 初始化
  2. 02文档增删改查与实时监听
  3. 03复合查询、分页与集合组查询
  4. 04批量写入与事务
  5. 05安全规则、索引与离线持久化
  6. 06常见问题排查与预防

适用场景

  1. 01为 Web 或移动应用实现 Firestore 数据操作
  2. 02为服务端配置 Admin SDK 访问数据库
  3. 03构建实时查询、分页及跨子集合查询
  4. 04排查索引、权限及事务相关问题

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/jezweb/claude-skills --skill firebase-firestore

安装后在 Claude Code 中输入/firebase-firestore确认已加载。

包含的文件

除 SKILL.md 外,安装时一起下载的参考资料和脚本。

README.md
.claude-plugin/
plugin.json
rules/
firebase-firestore.md
templates/
firebase-admin.ts
firebase-client.ts
firestore.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 错误:

问题 #错误/问题描述如何避免来源
#1permission-denied安全规则阻止操作先在 Firebase Console 模拟器中测试规则常见
#2failed-precondition(索引)缺少复合索引点击错误链接创建索引,或在 firestore.indexes.json 中定义常见
#3无效的查询组合多个不等式筛选条件仅对一个字段使用不等式筛选条件,其他字段使用相等条件文档
#4监听器导致的内存泄漏未取消订阅 onSnapshot始终在清理时调用 unsubscribe(useEffect 的 return)常见
#5离线持久化冲突多个标签页启用了持久化使用 persistentMultipleTabManager() 或处理错误文档
#6事务副作用副作用会运行多次切勿在 runTransaction 内执行副作用文档
#7超出批次限制操作数超过 500拆分为多个批次文档
#8resource-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"
  }
}

官方文档


最后验证时间:2026-01-25 | 技能版本:1.0.0

评论

支持 Markdown