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
- 忽略索引 - 检查控制台是否有缺失索引错误
- 没有仿真器测试 - 在部署前总是测试规则