Firebase技能 firebase

Firebase 是一个提供实时数据库、身份验证、存储和云函数等服务的全面平台,适用于构建和扩展 web 和移动应用。

Serverless 0 次安装 0 次浏览 更新于 3/5/2026

name: firebase description: Firebase Firestore, Auth, Storage, real-time listeners, security rules

Firebase 技能

Load with: base.md + security.md

Firebase/Firestore 模式用于 web 和移动应用,支持实时数据、离线支持和安全规则。

Sources: Firebase Docs | Firestore Best Practices | Security Rules


核心原则

有目的地去规范化,用规则来保障安全,水平扩展。

Firestore 是一个文档数据库 - 为了读取效率,拥抱去规范化。安全规则是你的服务器端验证。根据你的访问模式来设计。


Firebase 技术栈

服务 目的
Firestore 带有实时同步的 NoSQL 文档数据库
认证 用户认证,OAuth,匿名会话
存储 带有安全规则的文件上传
函数 无服务器后端(Node.js)
托管 静态网站 + CDN
扩展 预构建的解决方案(Stripe, Algolia 等)

项目设置

安装 Firebase CLI

# 全局安装
npm install -g firebase-tools

# 登录
firebase login

# 在项目中初始化
firebase init

使用仿真器初始化

firebase init emulators

# 开始本地开发
firebase emulators:start

项目结构

project/
├── firebase.json           # Firebase 配置
├── firestore.rules         # 安全规则
├── firestore.indexes.json  # 复合索引
└── storage.rules           # 存储安全规则
└── functions/              # 云函数
    ├── src/
    ├── package.json
    └── tsconfig.json

Firestore 数据建模

文档结构

// 好的:扁平化文档,包含所有需要的数据
interface Post {
  id: string;
  title: string;
  content: string;
  authorId: string;
  authorName: string;      // 为显示去规范化
  authorAvatar: string;    // 去规范化
  tags: string[];
  likeCount: number;       // 聚合计数器
  createdAt: Timestamp;
  updatedAt: Timestamp;
}

// 集合:posts/{postId}

何时使用子集合

// 使用子集合的情况:
// 1. 无界列表(评论,消息)
// 2. 具有不同访问模式的数据
// 3. 独立增长的数据

// posts/{postId}/comments/{commentId}
interface Comment {
  id: string;
  text: string;
  authorId: string;
  authorName: string;
  createdAt: Timestamp;
}

数据模型模式

// 模式 1:嵌入式数据(有界,总是需要的)
interface User {
  id: string;
  email: string;
  profile: {
    displayName: string;
    bio: string;
    avatar: string;
  };
  settings: {
    notifications: boolean;
    theme: 'light' | 'dark';
  };
}

// 模式 2:引用与去规范化
interface Order {
  id: string;
  userId: string;
  userEmail: string;  // 为显示去规范化
  items: OrderItem[]; // 嵌入式(有界)
  total: number;
  status: 'pending' | 'paid' | 'shipped';
}

// 模式 3:聚合文档
// 在父文档中保持计数器
interface Channel {
  id: string;
  name: string;
  memberCount: number;  // 通过云函数更新
  messageCount: number;
}

TypeScript SDK (Modular v9+)

初始化 Firebase

// lib/firebase.ts
import { initializeApp, getApps } from 'firebase/app';
import { getFirestore, connectFirestoreEmulator } from 'firebase/firestore';
import { getAuth, connectAuthEmulator } from 'firebase/auth';
import { getStorage, connectStorageEmulator } from 'firebase/storage';

const firebaseConfig = {
  apiKey: process.env.NEXT_PUBLIC_FIREBASE_API_KEY,
  authDomain: process.env.NEXT_PUBLIC_FIREBASE_AUTH_DOMAIN,
  projectId: process.env.NEXT_PUBLIC_FIREBASE_PROJECT_ID,
  storageBucket: process.env.NEXT_PUBLIC_FIREBASE_STORAGE_BUCKET,
  messagingSenderId: process.env.NEXT_PUBLIC_FIREBASE_MESSAGING_SENDER_ID,
  appId: process.env.NEXT_PUBLIC_FIREBASE_APP_ID
};

// 只初始化一次
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];

export const db = getFirestore(app);
export const auth = getAuth(app);
export const storage = getStorage(app);

// 在开发中连接到仿真器
if (process.env.NODE_ENV === 'development') {
  connectFirestoreEmulator(db, 'localhost', 8080);
  connectAuthEmulator(auth, 'http://localhost:9099');
  connectStorageEmulator(storage, 'localhost', 9199);
}

CRUD 操作

import {
  collection,
  doc,
  getDoc,
  getDocs,
  addDoc,
  setDoc,
  updateDoc,
  deleteDoc,
  query,
  where,
  orderBy,
  limit,
  startAfter,
  serverTimestamp,
  Timestamp
} from 'firebase/firestore';
import { db } from './firebase';

// 创建
async function createPost(data: Omit<Post, 'id' | 'createdAt' | 'updatedAt'>) {
  const docRef = await addDoc(collection(db, 'posts'), {
    ...data,
    createdAt: serverTimestamp(),
    updatedAt: serverTimestamp()
  });
  return docRef.id;
}

// 读取单个文档
async function getPost(postId: string): Promise<Post | null> {
  const docSnap = await getDoc(doc(db, 'posts', postId));
  if (!docSnap.exists()) return null;
  return { id: docSnap.id, ...docSnap.data() } as Post;
}

// 使用过滤器查询
async function getPostsByAuthor(authorId: string, pageSize = 10) {
  const q = query(
    collection(db, 'posts'),
    where('authorId', '==', authorId),
    orderBy('createdAt', 'desc'),
    limit(pageSize)
  );
  const snapshot = await getDocs(q);
  return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() } as Post));
}

// 分页
async function getNextPage(lastDoc: Post, pageSize = 10) {
  const q = query(
    collection(db, 'posts'),
    orderBy('createdAt', 'desc'),
    startAfter(lastDoc.createdAt),
    limit(pageSize)
  );
  const snapshot = await getDocs(q);
  return snapshot.docs.map(doc => ({ id: doc.id, ...doc.data() } as Post));
}

// 更新
async function updatePost(postId: string, data: Partial<Post>) {
  await updateDoc(doc(db, 'posts', postId), {
    ...data,
    updatedAt: serverTimestamp()
  });
}

// 删除
async function deletePost(postId: string) {
  await deleteDoc(doc(db, 'posts', postId));
}

实时监听器

import { onSnapshot, QuerySnapshot, DocumentSnapshot } from 'firebase/firestore';

// 监听单个文档
function subscribeToPost(
  postId: string,
  onData: (post: Post | null) => void,
  onError: (error: Error) => void
) {
  return onSnapshot(
    doc(db, 'posts', postId),
    (snapshot: DocumentSnapshot) => {
      if (!snapshot.exists()) {
        onData(null);
        return;
      }
      onData({ id: snapshot.id, ...snapshot.data() } as Post);
    },
    onError
  );
}

// 监听查询集合
function subscribeToPosts(
  authorId: string,
  onData: (posts: Post[]) => void,
  onError: (error: Error) => void
) {
  const q = query(
    collection(db, 'posts'),
    where('authorId', '==', authorId),
    orderBy('createdAt', 'desc')
  );

  return onSnapshot(
    q,
    (snapshot: QuerySnapshot) => {
      const posts = snapshot.docs.map(doc => ({
        id: doc.id,
        ...doc.data()
      } as Post));
      onData(posts);
    },
    onError
  );
}

// React 钩子示例
function usePost(postId: string) {
  const [post, setPost] = useState<Post | null>(null);
  const [loading, setLoading] = useState(true);
  const [error, setError] = useState<Error | null>(null);

  useEffect(() => {
    const unsubscribe = subscribeToPost(
      postId,
      (data) => {
        setPost(data);
        setLoading(false);
      },
      (err) => {
        setError(err);
        setLoading(false);
      }
    );
    return unsubscribe;
  }, [postId]);

  return { post, loading, error };
}

离线持久性(Web)

import { enableIndexedDbPersistence, enableMultiTabIndexedDbPersistence } from 'firebase/firestore';

// 启用离线持久性(在启动时调用一次)
async function enableOffline() {
  try {
    // 单标签
    await enableIndexedDbPersistence(db);

    // 或多标签(推荐)
    await enableMultiTabIndexedDbPersistence(db);
  } catch (err: any) {
    if (err.code === 'failed-precondition') {
      // 多个标签打开,只在其中一个工作
      console.warn('Persistence only available in one tab');
    } else if (err.code === 'unimplemented') {
      // 浏览器不支持
      console.warn('Persistence not supported');
    }
  }
}

// 检查数据是否来自缓存
onSnapshot(docRef, (snapshot) => {
  const source = snapshot.metadata.fromCache ? 'cache' : 'server';
  console.log(`Data from ${source}`);

  if (snapshot.metadata.hasPendingWrites) {
    console.log('Local changes pending sync');
  }
});

安全规则

基本规则结构

// firestore.rules
rules_version = '2';
service cloud.firestore {
  match /databases/{database}/documents {

      // 辅助函数
      function isAuthenticated() {
        return request.auth != null;
      }

      function isOwner(userId) {
        return request.auth.uid == userId;
      }

      function isAdmin() {
        return request.auth.token.admin == true;
      }

      // Posts 集合
      match /posts/{postId} {
        // 任何人都可以读取已发布的帖子
        allow read: if resource.data.status == 'published';

        // 只有经过身份验证的用户可以创建
        allow create: if isAuthenticated()
          && request.resource.data.authorId == request.auth.uid
          && request.resource.data.keys().hasAll(['title', 'content', 'authorId']);

        // 只有作者可以更新
        allow update: if isOwner(resource.data.authorId)
          && request.resource.data.authorId == resource.data.authorId; // 不能改变作者

        // 只有作者或管理员可以删除
        allow delete: if isOwner(resource.data.authorId) || isAdmin();

        // Comments 子集合
        match /comments/{commentId} {
          allow read: if true;
          allow create: if isAuthenticated();
          allow update, delete: if isOwner(resource.data.authorId);
        }
      }

      // 用户配置文件
      match /users/{userId} {
        allow read: if true;
        allow create: if isAuthenticated() && isOwner(userId);
        allow update: if isOwner(userId);
        allow delete: if false; // 永远不允许删除
      }

      // 私有用户数据
      match /users/{userId}/private/{document=**} {
        allow read, write: if isOwner(userId);
      }
    }
  }
}

数据验证在规则中

match /posts/{postId} {
  function isValidPost() {
    let data = request.resource.data;
    return data.title is string
      && data.title.size() >= 3
      && data.title.size() <= 100
      && data.content is string
      && data.content.size() <= 50000
      && data.tags is list
      && data.tags.size() <= 5;
  }

  allow create: if isAuthenticated() && isValidPost();
  allow update: if isOwner(resource.data.authorId) && isValidPost();
}

本地测试规则

# 安装仿真器
firebase emulators:start

# 运行规则测试
npm test
// tests/firestore.rules.test.ts
import { assertFails, assertSucceeds, initializeTestEnvironment } from '@firebase/rules-unit-testing';

describe('Firestore Rules', () => {
  let testEnv: RulesTestEnvironment;

  beforeAll(async () => {
    testEnv = await initializeTestEnvironment({
      projectId: 'test-project',
      firestore: { rules: fs.readFileSync('firestore.rules', 'utf8') }
    });
  });

  test('未经身份验证的用户不能写入', async () => {
    const unauthedDb = testEnv.unauthenticatedContext().firestore();
    await assertFails(
      setDoc(doc(unauthedDb, 'posts/test'), { title: 'Test' })
    );
  });

  test('用户只能更新自己的帖子', async () => {
    const aliceDb = testEnv.authenticatedContext('alice').firestore();
    const bobDb = testEnv.authenticatedContext('bob').firestore();

    // 作为 Alice 创建
    await assertSucceeds(
      setDoc(doc(aliceDb, 'posts/test'), { title: 'Test', authorId: 'alice' })
    );

    // Bob 不能更新
    await assertFails(
      updateDoc(doc(bobDb, 'posts/test'), { title: 'Hacked' })
    );
  });
});

认证

电子邮件/密码认证

import {
  createUserWithEmailAndPassword,
  signInWithEmailAndPassword,
  signOut,
  onAuthStateChanged,
  User
} from 'firebase/auth';
import { auth } from './firebase';

// 注册
async function signUp(email: string, password: string) {
  const credential = await createUserWithEmailAndPassword(auth, email, password);
  return credential.user;
}

// 登录
async function signIn(email: string, password: string) {
  const credential = await signInWithEmailAndPassword(auth, email, password);
  return credential.user;
}

// 注销
async function logout() {
  await signOut(auth);
}

// 认证状态监听器
function onAuthChange(callback: (user: User | null) => void) {
  return onAuthStateChanged(auth, callback);
}

OAuth 提供商

import {
  GoogleAuthProvider,
  signInWithPopup,
  signInWithRedirect
} from 'firebase/auth';

const googleProvider = new GoogleAuthProvider();

async function signInWithGoogle() {
  try {
    const result = await signInWithPopup(auth, googleProvider);
    return result.user;
  } catch (error) {
    // 处理错误
    throw error;
  }
}

云函数

基本 HTTP 函数

// functions/src/index.ts
import { onRequest } from 'firebase-functions/v2/https';
import { onDocumentCreated } from 'firebase-functions/v2/firestore';
import { initializeApp } from 'firebase-admin/app';
import { getFirestore } from 'firebase-admin/firestore';

initializeApp();
const db = getFirestore();

// HTTP 端点
export const helloWorld = onRequest((request, response) => {
  response.json({ message: 'Hello from Firebase!' });
});

// Firestore 触发器
export const onPostCreated = onDocumentCreated('posts/{postId}', async (event) => {
  const snapshot = event.data;
  if (!snapshot) return;

  const post = snapshot.data;

  // 更新作者的帖子计数
  await db.doc(`users/${post.authorId}`).update({
    postCount: FieldValue.increment(1)
  });
});

可调用函数

// 后端
import { onCall, HttpsError } from 'firebase-functions/v2/https';

export const createPost = onCall(async (request) => {
  // 认证检查
  if (!request.auth) {
    throw new HttpsError('unauthenticated', 'Must be logged in');
  }

  const { title, content } = request.data;

  // 验证
  if (!title || title.length < 3) {
    throw new HttpsError('invalid-argument', 'Title must be at least 3 characters');
  }

  // 创建帖子
  const postRef = await db.collection('posts').add({
    title,
    content,
    authorId: request.auth.uid,
    createdAt: FieldValue.serverTimestamp()
  });

  return { postId: postRef.id };
});

// 前端
import { getFunctions, httpsCallable } from 'firebase/functions';

const functions = getFunctions();
const createPostFn = httpsCallable(functions, 'createPost');

async function createPost(title: string, content: string) {
  const result = await createPostFn({ title, content });
  return result.data as { postId: string };
}

批量操作和事务

批量写入

import { writeBatch, doc } from 'firebase/firestore';

async function batchUpdate(updates: { id: string; data: any }[]) {
  const batch = writeBatch(db);

  updates.forEach(({ id, data }) => {
    batch.update(doc(db, 'posts', id), data);
  });

  await batch.commit(); // 原子性
}

事务

import { runTransaction, doc, increment } from 'firebase/firestore';

async function likePost(postId: string, userId: string) {
  await runTransaction(db, async (transaction) => {
    const postRef = doc(db, 'posts', postId);
    const likeRef = doc(db, 'posts', postId, 'likes', userId);

    const postSnap = await transaction.get(postRef);
    if (!postSnap.exists()) throw new Error('Post not found');

    const likeSnap = await transaction.get(likeRef);
    if (likeSnap.exists()) throw new Error('Already liked');

    transaction.set(likeRef, { createdAt: serverTimestamp() });
    transaction.update(postRef, { likeCount: increment(1) });
  });
}

索引

复合索引

// firestore.indexes.json
{
  "indexes": [
    {
      "collectionGroup": "posts",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "authorId", "order": "ASCENDING" },
        { "fieldPath": "createdAt", "order": "DESCENDING" }
      ]
    },
    {
      "collectionGroup": "posts",
      "queryScope": "COLLECTION",
      "fields": [
        { "fieldPath": "tags", "arrayConfig": "CONTAINS" },
        { "fieldPath": "createdAt", "order": "DESCENDING" }
      ]
    }
  ]
}
# 部署索引
firebase deploy --only firestore:indexes

CLI 快速参考

# 项目设置
firebase login                       # 认证
firebase init                        # 初始化项目
firebase projects:list               # 列出项目

# 仿真器
firebase emulators:start             # 启动所有仿真器
firebase emulators:start --only firestore,auth  # 特定仿真器

# 部署
firebase deploy                      # 部署一切
firebase deploy --only firestore     # 部署规则 + 索引
firebase deploy --only functions     # 部署函数
firebase deploy --only hosting       # 部署托管

# 函数
cd functions && npm run build        # 构建 TypeScript
firebase functions:log               # 查看日志

反模式

  • 没有安全规则 - 总是编写规则,永远不要在生产中使用测试模式
  • 深层嵌套 - 保持文档扁平,最多 2-3 层
  • 大型文档 - 最大 1MB,如果更大则分割
  • 无界数组 - 对于增长的列表使用子集合
  • 没有离线处理 - 为移动/PWA 启用持久性
  • 读取所有字段 - 使用字段掩码或 Firestore Lite
  • 忽略索引 - 检查控制台是否有缺失索引错误
  • 没有仿真器测试 - 在部署前总是测试规则