firebase
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
ChineseFirebase Skill
Firebase 技能指南
Load with: base.md + security.md
Firebase/Firestore patterns for web and mobile applications with real-time data, offline support, and security rules.
加载依赖:base.md + security.md
适用于Web和移动应用的Firebase/Firestore实践模式,涵盖实时数据、离线支持和安全规则。
Core Principle
核心原则
Denormalize with purpose, secure with rules, scale horizontally.
Firestore is a document database - embrace denormalization for read efficiency. Security rules are your server-side validation. Design for your access patterns.
有目的地进行数据反规范化,通过规则保障安全,实现水平扩展。
Firestore是文档型数据库——为了读取效率应采用数据反规范化。安全规则充当你的服务端验证机制,需根据数据访问模式进行设计。
Firebase Stack
Firebase 技术栈
| Service | Purpose |
|---|---|
| Firestore | NoSQL document database with real-time sync |
| Authentication | User auth, OAuth, anonymous sessions |
| Storage | File uploads with security rules |
| Functions | Serverless backend (Node.js) |
| Hosting | Static site + CDN |
| Extensions | Pre-built solutions (Stripe, Algolia, etc.) |
| 服务 | 用途 |
|---|---|
| Firestore | 支持实时同步的NoSQL文档型数据库 |
| Authentication | 用户身份验证、OAuth授权、匿名会话 |
| Storage | 带安全规则的文件上传存储 |
| Functions | 无服务器后端(Node.js) |
| Hosting | 静态站点托管 + CDN |
| Extensions | 预构建解决方案(如Stripe、Algolia等) |
Project Setup
项目搭建
Install Firebase CLI
安装Firebase CLI
bash
undefinedbash
undefinedInstall globally
全局安装
npm install -g firebase-tools
npm install -g firebase-tools
Login
登录
firebase login
firebase login
Initialize in project
在项目中初始化
firebase init
undefinedfirebase init
undefinedInitialize with Emulators
使用模拟器初始化
bash
firebase init emulatorsbash
firebase init emulatorsStart local development
启动本地开发环境
firebase emulators:start
undefinedfirebase emulators:start
undefinedProject Structure
项目结构
project/
├── firebase.json # Firebase config
├── firestore.rules # Security rules
├── firestore.indexes.json # Composite indexes
├── storage.rules # Storage security rules
└── functions/ # Cloud Functions
├── src/
├── package.json
└── tsconfig.jsonproject/
├── firebase.json # Firebase 配置文件
├── firestore.rules # Firestore 安全规则
├── firestore.indexes.json # 复合索引
├── storage.rules # Storage 安全规则
└── functions/ # 云函数
├── src/
├── package.json
└── tsconfig.jsonFirestore Data Modeling
Firestore 数据建模
Document Structure
文档结构
typescript
// Good: Flat documents with all needed data
interface Post {
id: string;
title: string;
content: string;
authorId: string;
authorName: string; // Denormalized for display
authorAvatar: string; // Denormalized
tags: string[];
likeCount: number; // Aggregated counter
createdAt: Timestamp;
updatedAt: Timestamp;
}
// Collection: posts/{postId}typescript
// 推荐:包含所有所需数据的扁平化文档
interface Post {
id: string;
title: string;
content: string;
authorId: string;
authorName: string; // 反规范化用于直接展示
authorAvatar: string; // 反规范化
tags: string[];
likeCount: number; // 聚合计数器
createdAt: Timestamp;
updatedAt: Timestamp;
}
// 集合路径:posts/{postId}When to Use Subcollections
何时使用子集合
typescript
// Use subcollections for:
// 1. Unbounded lists (comments, messages)
// 2. Data with different access patterns
// 3. Data that grows independently
// posts/{postId}/comments/{commentId}
interface Comment {
id: string;
text: string;
authorId: string;
authorName: string;
createdAt: Timestamp;
}typescript
// 以下场景适合使用子集合:
// 1. 无界列表(如评论、消息)
// 2. 具有不同访问模式的数据
// 3. 独立增长的数据
// 路径:posts/{postId}/comments/{commentId}
interface Comment {
id: string;
text: string;
authorId: string;
authorName: string;
createdAt: Timestamp;
}Data Model Patterns
数据模型模式
typescript
// Pattern 1: Embedded data (bounded, always needed)
interface User {
id: string;
email: string;
profile: {
displayName: string;
bio: string;
avatar: string;
};
settings: {
notifications: boolean;
theme: 'light' | 'dark';
};
}
// Pattern 2: Reference with denormalization
interface Order {
id: string;
userId: string;
userEmail: string; // Denormalized for display
items: OrderItem[]; // Embedded (bounded)
total: number;
status: 'pending' | 'paid' | 'shipped';
}
// Pattern 3: Aggregation documents
// Keep counters in parent document
interface Channel {
id: string;
name: string;
memberCount: number; // Updated via Cloud Function
messageCount: number;
}typescript
// 模式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+)
TypeScript SDK(模块化v9+)
Initialize Firebase
初始化Firebase
typescript
// 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
};
// Initialize only once
const app = getApps().length === 0 ? initializeApp(firebaseConfig) : getApps()[0];
export const db = getFirestore(app);
export const auth = getAuth(app);
export const storage = getStorage(app);
// Connect to emulators in development
if (process.env.NODE_ENV === 'development') {
connectFirestoreEmulator(db, 'localhost', 8080);
connectAuthEmulator(auth, 'http://localhost:9099');
connectStorageEmulator(storage, 'localhost', 9199);
}typescript
// 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 Operations
CRUD 操作
typescript
import {
collection,
doc,
getDoc,
getDocs,
addDoc,
setDoc,
updateDoc,
deleteDoc,
query,
where,
orderBy,
limit,
startAfter,
serverTimestamp,
Timestamp
} from 'firebase/firestore';
import { db } from './firebase';
// Create
async function createPost(data: Omit<Post, 'id' | 'createdAt' | 'updatedAt'>) {
const docRef = await addDoc(collection(db, 'posts'), {
...data,
createdAt: serverTimestamp(),
updatedAt: serverTimestamp()
});
return docRef.id;
}
// Read single document
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;
}
// Query with filters
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));
}
// Pagination
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));
}
// Update
async function updatePost(postId: string, data: Partial<Post>) {
await updateDoc(doc(db, 'posts', postId), {
...data,
updatedAt: serverTimestamp()
});
}
// Delete
async function deletePost(postId: string) {
await deleteDoc(doc(db, 'posts', postId));
}typescript
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));
}Real-time Listeners
实时监听器
typescript
import { onSnapshot, QuerySnapshot, DocumentSnapshot } from 'firebase/firestore';
// Listen to single document
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
);
}
// Listen to collection with query
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 hook example
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 };
}typescript
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 Hook 示例
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 };
}Offline Persistence (Web)
离线持久化(Web端)
typescript
import { enableIndexedDbPersistence, enableMultiTabIndexedDbPersistence } from 'firebase/firestore';
// Enable offline persistence (call once at startup)
async function enableOffline() {
try {
// Single tab
await enableIndexedDbPersistence(db);
// OR multi-tab (recommended)
await enableMultiTabIndexedDbPersistence(db);
} catch (err: any) {
if (err.code === 'failed-precondition') {
// Multiple tabs open, only works in one
console.warn('Persistence only available in one tab');
} else if (err.code === 'unimplemented') {
// Browser doesn't support
console.warn('Persistence not supported');
}
}
}
// Check if data is from cache
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');
}
});typescript
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('持久化仅支持单个标签页');
} else if (err.code === 'unimplemented') {
// 浏览器不支持
console.warn('当前浏览器不支持持久化');
}
}
}
// 检查数据是否来自缓存
onSnapshot(docRef, (snapshot) => {
const source = snapshot.metadata.fromCache ? '缓存' : '服务器';
console.log(`数据来源:${source}`);
if (snapshot.metadata.hasPendingWrites) {
console.log('本地修改待同步至服务器');
}
});Security Rules
安全规则
Basic Rules Structure
基础规则结构
javascript
// 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 isAdmin() {
return request.auth.token.admin == true;
}
// Posts collection
match /posts/{postId} {
// Anyone can read published posts
allow read: if resource.data.status == 'published';
// Only authenticated users can create
allow create: if isAuthenticated()
&& request.resource.data.authorId == request.auth.uid
&& request.resource.data.keys().hasAll(['title', 'content', 'authorId']);
// Only author can update
allow update: if isOwner(resource.data.authorId)
&& request.resource.data.authorId == resource.data.authorId; // Can't change author
// Only author or admin can delete
allow delete: if isOwner(resource.data.authorId) || isAdmin();
// Comments subcollection
match /comments/{commentId} {
allow read: if true;
allow create: if isAuthenticated();
allow update, delete: if isOwner(resource.data.authorId);
}
}
// User profiles
match /users/{userId} {
allow read: if true;
allow create: if isAuthenticated() && isOwner(userId);
allow update: if isOwner(userId);
allow delete: if false; // Never allow delete
}
// Private user data
match /users/{userId}/private/{document=**} {
allow read, write: if isOwner(userId);
}
}
}javascript
// 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;
}
// 帖子集合
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();
// 评论子集合
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);
}
}
}Data Validation in Rules
规则中的数据验证
javascript
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();
}javascript
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();
}Test Rules Locally
本地测试规则
bash
undefinedbash
undefinedInstall emulators
安装模拟器
firebase emulators:start
firebase emulators:start
Run rules tests
运行规则测试
npm test
```typescript
// 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('unauthenticated users cannot write', async () => {
const unauthedDb = testEnv.unauthenticatedContext().firestore();
await assertFails(
setDoc(doc(unauthedDb, 'posts/test'), { title: 'Test' })
);
});
test('users can only update own posts', async () => {
const aliceDb = testEnv.authenticatedContext('alice').firestore();
const bobDb = testEnv.authenticatedContext('bob').firestore();
// Create as Alice
await assertSucceeds(
setDoc(doc(aliceDb, 'posts/test'), { title: 'Test', authorId: 'alice' })
);
// Bob cannot update
await assertFails(
updateDoc(doc(bobDb, 'posts/test'), { title: 'Hacked' })
);
});
});npm test
```typescript
// 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' })
);
});
});Authentication
身份验证
Email/Password Auth
邮箱/密码认证
typescript
import {
createUserWithEmailAndPassword,
signInWithEmailAndPassword,
signOut,
onAuthStateChanged,
User
} from 'firebase/auth';
import { auth } from './firebase';
// Sign up
async function signUp(email: string, password: string) {
const credential = await createUserWithEmailAndPassword(auth, email, password);
return credential.user;
}
// Sign in
async function signIn(email: string, password: string) {
const credential = await signInWithEmailAndPassword(auth, email, password);
return credential.user;
}
// Sign out
async function logout() {
await signOut(auth);
}
// Auth state listener
function onAuthChange(callback: (user: User | null) => void) {
return onAuthStateChanged(auth, callback);
}typescript
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 Providers
OAuth 第三方登录
typescript
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) {
// Handle errors
throw error;
}
}typescript
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;
}
}Cloud Functions
云函数
Basic HTTP Function
基础HTTP函数
typescript
// 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 endpoint
export const helloWorld = onRequest((request, response) => {
response.json({ message: 'Hello from Firebase!' });
});
// Firestore trigger
export const onPostCreated = onDocumentCreated('posts/{postId}', async (event) => {
const snapshot = event.data;
if (!snapshot) return;
const post = snapshot.data();
// Update author's post count
await db.doc(`users/${post.authorId}`).update({
postCount: FieldValue.increment(1)
});
});typescript
// 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)
});
});Callable Functions
可调用函数
typescript
// Backend
import { onCall, HttpsError } from 'firebase-functions/v2/https';
export const createPost = onCall(async (request) => {
// Auth check
if (!request.auth) {
throw new HttpsError('unauthenticated', 'Must be logged in');
}
const { title, content } = request.data;
// Validation
if (!title || title.length < 3) {
throw new HttpsError('invalid-argument', 'Title must be at least 3 characters');
}
// Create post
const postRef = await db.collection('posts').add({
title,
content,
authorId: request.auth.uid,
createdAt: FieldValue.serverTimestamp()
});
return { postId: postRef.id };
});
// Frontend
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 };
}typescript
// 后端实现
import { onCall, HttpsError } from 'firebase-functions/v2/https';
export const createPost = onCall(async (request) => {
// 认证检查
if (!request.auth) {
throw new HttpsError('unauthenticated', '必须登录后操作');
}
const { title, content } = request.data;
// 参数验证
if (!title || title.length < 3) {
throw new HttpsError('invalid-argument', '标题长度至少3个字符');
}
// 创建帖子
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 };
}Batch Operations & Transactions
批量操作与事务
Batch Writes
批量写入
typescript
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(); // Atomic
}typescript
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(); // 原子操作
}Transactions
事务
typescript
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) });
});
}typescript
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('帖子不存在');
const likeSnap = await transaction.get(likeRef);
if (likeSnap.exists()) throw new Error('已点赞过该帖子');
transaction.set(likeRef, { createdAt: serverTimestamp() });
transaction.update(postRef, { likeCount: increment(1) });
});
}Indexes
索引
Composite Indexes
复合索引
json
// 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" }
]
}
]
}bash
undefinedjson
// 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" }
]
}
]
}bash
undefinedDeploy indexes
部署索引
firebase deploy --only firestore:indexes
---firebase deploy --only firestore:indexes
---CLI Quick Reference
CLI 快速参考
bash
undefinedbash
undefinedProject setup
项目设置
firebase login # Authenticate
firebase init # Initialize project
firebase projects:list # List projects
firebase login # 登录认证
firebase init # 初始化项目
firebase projects:list # 列出所有项目
Emulators
模拟器操作
firebase emulators:start # Start all emulators
firebase emulators:start --only firestore,auth # Specific emulators
firebase emulators:start # 启动所有模拟器
firebase emulators:start --only firestore,auth # 启动指定模拟器
Deploy
部署命令
firebase deploy # Deploy everything
firebase deploy --only firestore # Deploy rules + indexes
firebase deploy --only functions # Deploy functions
firebase deploy --only hosting # Deploy hosting
firebase deploy # 部署所有资源
firebase deploy --only firestore # 部署Firestore规则和索引
firebase deploy --only functions # 部署云函数
firebase deploy --only hosting # 部署静态站点
Functions
云函数操作
cd functions && npm run build # Build TypeScript
firebase functions:log # View logs
---cd functions && npm run build # 构建TypeScript代码
firebase functions:log # 查看云函数日志
---Anti-Patterns
反模式
- No security rules - Always write rules, never use test mode in production
- Deep nesting - Keep documents flat, max 2-3 levels
- Large documents - Max 1MB, split if larger
- Unbounded arrays - Use subcollections for lists that grow
- No offline handling - Enable persistence for mobile/PWA
- Reading all fields - Use field masks or Firestore Lite
- Ignoring indexes - Check console for missing index errors
- No emulator testing - Always test rules before deploy
- 未配置安全规则 - 务必编写安全规则,生产环境绝不能使用测试模式
- 深度嵌套 - 保持文档扁平化,最多2-3级
- 超大文档 - 文档大小上限为1MB,超过则拆分
- 无界数组 - 对于会持续增长的列表,使用子集合
- 未处理离线场景 - 为移动应用或PWA启用离线持久化
- 读取所有字段 - 使用字段掩码或Firestore Lite
- 忽略索引 - 检查控制台是否有缺失索引的错误提示
- 未使用模拟器测试 - 部署前务必测试规则