Article
第一章:Firestore 基础概念
1.1 什么是 Firestore
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| Firestore | Google Firebase 提供的 NoSQL 文档数据库,支持实时同步、离线访问和强一致性 | 不是传统关系型数据库,不支持 JOIN 或 SQL 查询 |
| 云托管 vs 本地模拟器 | 可在 Firebase 云端运行,也可通过 Firebase Emulator Suite 在本地开发调试 | 本地模拟器数据不会同步到生产环境 |
| 多平台支持 | 支持 Web、iOS、Android、Node.js、Python、Go、Java 等 SDK | 各平台 API 风格略有差异,但核心模型一致 |
| 自动扩展 | 无需手动分片或扩容,Firestore 自动处理大规模读写 | 高频写入同一文档仍可能受限(每秒约 1 次写入限制) |
1.2 文档、集合与子集合
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| 文档(Document) | Firestore 中的基本数据单元,以 JSON 格式存储,有唯一 ID(如 user_123) | 文档大小上限为 1 MiB |
| 集合(Collection) | 文档的容器,只能包含文档,不能直接嵌套其他集合 | 集合名必须是字符串,且区分大小写 |
| 子集合(Subcollection) | 文档内部可包含子集合,形成层级结构(如 users/user_123/orders) | 子集合的存在不依赖父文档是否实际存在(“幽灵文档”) |
| 路径(Path) | 唯一标识文档或集合的层级路径,如 /users/alice/messages/msg1 | 路径层级必须为奇数(文档)或偶数(集合) |
1.3 数据模型与结构特点
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| 无模式(Schema-less) | 每个文档可拥有不同字段,无需预定义结构 | 仍建议在应用层保持一致性以利于查询和维护 |
| 嵌套对象支持 | 文档字段可包含嵌套的 Map(对象)结构 | 嵌套深度不限,但整体文档不能超过 1 MiB |
| 数组类型 | 支持数组字段,可用于存储列表(如 tags: [“news”, “tech”]) | 数组内元素不可单独更新,需整体替换或使用 arrayUnion/arrayRemove |
| 时间戳(Timestamp) | 内置时间类型,比 JavaScript Date 更精确,支持纳秒级 | 在客户端使用 serverTimestamp() 可确保服务端生成时间 |
| 引用类型(Reference) | 可存储指向其他文档的引用(如 author: /users/alice) | 查询时不能跨文档联查,仅作指针用途 |
1.4 实时监听机制简介
| 概念名称 | 说明 | 注意事项 |
|---|---|---|
| 快照监听(Snapshot Listener) | 客户端可注册监听器,当文档或集合变更时自动触发回调 | 监听会持续消耗读取配额,需及时取消(调用 unsubscribe) |
| 单次读取 vs 实时监听 | get() 为单次读取,onSnapshot() 为实时监听 | 实时监听首次触发即返回当前快照 |
| 本地缓存同步 | 即使离线,监听器仍能基于本地缓存触发回调 | 离线修改会在恢复网络后自动同步到服务器 |
| 元数据变化(hasPendingWrites / fromCache) | 快照包含元数据,可判断数据来源(本地缓存 or 服务端)及是否有待提交写入 | 用于 UI 显示”同步中”状态等场景 |
第二章:环境准备与项目初始化
2.1 创建 Firebase 项目
| 步骤名称 | 操作细节 | 注意事项 |
|---|---|---|
| 登录 Firebase 控制台 | 访问 https://console.firebase.google.com/,使用 Google 账号登录 | 需启用 Google 账号的两步验证(推荐) |
| 创建新项目 | 点击”添加项目” → 输入项目名称(如 my-firestore-app)→ 按向导完成创建 | 项目 ID 全局唯一,创建后不可修改 |
| 启用 Firestore | 在控制台左侧菜单选择”Firestore 数据库” → 点击”创建数据库” | 首次创建时需选择初始模式(测试模式或锁定模式) |
| 选择位置 | 选择 Firestore 数据存储位置(如 nam5 / us-central) | 位置一旦选定无法更改,影响延迟和合规性 |
| 获取项目配置 | 在”项目设置”中可查看 Web 应用配置(apiKey, projectId 等) | 配置信息用于客户端 SDK 初始化 |
2.2 安装 Firebase CLI
| 步骤名称 | 操作细节 | 注意事项 |
|---|---|---|
| 安装 Node.js | 从 https://nodejs.org 下载并安装 LTS 版本(≥ v18) | Firebase CLI 依赖 Node.js 环境 |
| 全局安装 Firebase CLI | 执行命令:npm install -g firebase-tools | 需确保 npm 全局路径可执行 |
| 验证安装 | 执行命令:firebase --version | 应返回版本号(如 13.x.x) |
| 登录 CLI 账号 | 执行命令:firebase login | 会打开浏览器授权,需使用与 Firebase 项目相同的 Google 账号 |
| 切换项目(可选) | 执行命令:firebase use --add 添加项目,firebase use 切换 | 多项目开发时常用 |
2.3 初始化本地 Firestore 项目
| 步骤名称 | 操作细节 | 注意事项 |
|---|---|---|
| 创建项目目录 | mkdir my-firestore-app && cd my-firestore-app | 建议使用独立目录管理 |
| 初始化 Firebase 项目 | 执行命令:firebase init | 交互式命令行工具 |
| 选择功能 | 使用空格键选中 “Firestore” 和 “Emulators”(建议同时选) | 可多选,按回车确认 |
| 关联 Firebase 项目 | 从列表中选择已创建的项目(如 my-firestore-app) | 必须提前在控制台创建 |
| 生成配置文件 | 自动生成 firebase.json、firestore.rules、firestore.indexes.json 等文件 | 不要手动删除这些文件 |
| 本地目录结构示例 | ├── firebase.json ├── firestore.rules ├── firestore.indexes.json └── public/(若选 Hosting) | 最小可运行项目只需前三者 |
2.4 配置安全规则与本地模拟器
| 步骤名称 | 操作细节 | 注意事项 |
|---|---|---|
| 编辑安全规则文件 | 修改 firestore.rules | 初始建议设为锁定模式(deny all),逐步开放权限 |
| 启动本地模拟器 | 执行命令:firebase emulators:start --only firestore | 默认监听 localhost:8080 |
| 连接应用到模拟器 | 在客户端初始化时添加:connectFirestoreEmulator(db, 'localhost', 8080); | 仅在开发环境调用,生产环境应移除 |
| 查看模拟器 UI | 启动后访问 http://localhost:4000 | 可查看数据、日志、规则测试等 |
| 部署规则到生产环境 | 执行命令:firebase deploy --only firestore:rules | 需具备项目部署权限 |
| 模拟器数据不持久化 | 默认关闭模拟器后数据丢失 | 可通过 —export-on-exit 保存,—import 导入 |
安全规则示例:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /{document=**} {
allow read, write: if false;
}
}
}
第三章:Firestore 命令行操作(Firebase CLI)
3.1 启动与管理本地模拟器
| 操作名称 | 命令语法 | 用途说明 | 注意事项 |
|---|---|---|---|
| 启动 Firestore 模拟器 | firebase emulators:start --only firestore | 仅启动 Firestore 模拟器 | 默认端口 8080,可通过 firebase.json 配置 |
| 启动全部模拟器 | firebase emulators:start | 同时启动 Firestore、Auth、Functions 等 | 需在 firebase init 时已选择对应服务 |
| 指定端口启动 | firebase emulators:start --only firestore --port 9090 | 自定义 Firestore 模拟器端口 | 避免端口冲突 |
| 后台运行并导出数据 | firebase emulators:start --only firestore --export-on-exit ./data | 关闭时自动导出数据到 ./data 目录 | 导出格式为 JSON 和元数据文件 |
| 导入已有数据启动 | firebase emulators:start --only firestore --import ./data | 启动时加载之前导出的数据 | 路径需包含 emulator-export.json |
| 停止模拟器 | Ctrl + C(终端中) | 手动终止模拟器进程 | 强制 kill 可能导致数据未保存 |
3.2 导入与导出数据
| 操作名称 | 命令语法 | 用途说明 | 注意事项 |
|---|---|---|---|
| 导出生产环境数据 | gcloud firestore export gs://[BUCKET]/backup --async | 将 Firestore 生产数据导出到 GCS 存储桶 | 需安装 Google Cloud SDK 并授权 |
| 从 GCS 导入生产数据 | gcloud firestore import gs://[BUCKET]/backup | 从 GCS 备份恢复数据到生产环境 | 会覆盖现有同名文档 |
| 导出模拟器数据 | 使用 --export-on-exit 参数(见 3.1) | 保存本地模拟器当前状态 | 仅适用于模拟器,非生产环境 |
| 导入到模拟器 | 使用 --import 参数(见 3.1) | 加载历史数据用于本地测试 | 必须与导出格式一致 |
| 查看导出结构 | (手动查看导出目录) | 理解数据组织方式 | 目录包含 .json 文件和 firestore_export/ 子目录,不建议手动编辑导出文件 |
注: Firebase CLI 本身不直接支持生产环境数据导入/导出,需依赖
gcloud命令(属于 Google Cloud SDK)。
3.3 部署安全规则与索引配置
| 操作名称 | 命令语法 | 用途说明 | 注意事项 |
|---|---|---|---|
| 部署安全规则 | firebase deploy --only firestore:rules | 将本地 firestore.rules 推送到生产环境 | 规则语法错误会导致部署失败 |
| 部署索引配置 | firebase deploy --only firestore:indexes | 将 firestore.indexes.json 推送至云端 | 复合查询需提前定义索引 |
| 同时部署规则和索引 | firebase deploy --only firestore | 一次性部署规则 + 索引 | 推荐做法 |
| 预览部署变更 | firebase deploy --only firestore --dry-run | 查看将要部署的内容(不实际执行) | 用于 CI/CD 安全检查 |
| 本地规则测试 | 在 Emulator Suite UI 中测试规则 | 无需部署即可验证规则逻辑 | 访问 http://localhost:4000 → Rules 标签页,支持模拟不同用户身份 |
3.4 查看与调试日志
| 操作名称 | 命令语法 / 操作方式 | 用途说明 | 注意事项 |
|---|---|---|---|
| 查看模拟器日志 | 启动模拟器后终端输出 | 实时显示读写、规则拒绝、函数调用等事件 | 日志级别不可配置 |
| 访问 Emulator UI | 浏览器打开 http://localhost:4000 | 图形化查看数据、日志、网络请求、规则测试 | 仅本地有效 |
| 启用详细日志 | 设置环境变量 DEBUG=firebase* | 输出更详细的内部调试信息 | 日志量大,仅开发时使用 |
| 查看规则拒绝原因 | 在日志或 Emulator UI 中查找 “PERMISSION_DENIED” | 定位安全规则拦截的请求 | 结合规则文件逐行排查 |
| 清除日志缓存 | 重启模拟器 | 清空历史日志记录 | 无单独清日志命令 |
第四章:数据操作(CRUD)
4.1 添加文档
| 方法名称 | 语法 | 用途说明 | 注意事项 |
|---|---|---|---|
| addDoc | addDoc(collectionRef, data) | 向集合添加新文档,自动生成唯一 ID | 文档 ID 由 Firestore 自动生成(20 字符随机字符串) |
| setDoc(带自定义 ID) | setDoc(docRef, data) | 使用指定 ID 创建文档 | 若 ID 已存在,默认覆盖整个文档;可用 { merge: true } 避免覆盖 |
| setDoc with merge | setDoc(docRef, data, { merge: true }) | 创建或部分更新文档(保留未提及字段) | 适用于”创建若不存在,否则更新部分字段”场景 |
代码示例:
// addDoc - 自动生成 ID
import { collection, addDoc } from 'firebase/firestore';
const docRef = await addDoc(collection(db, 'users'), { name: 'Alice', age: 30 });
console.log('Added doc ID:', docRef.id);
// setDoc - 自定义 ID
import { doc, setDoc } from 'firebase/firestore';
await setDoc(doc(db, 'users', 'user_123'), { name: 'Bob' });
// setDoc with merge
await setDoc(doc(db, 'users', 'user_123'), { email: 'bob@example.com' }, { merge: true });
4.2 读取文档与集合
| 方法名称 | 语法 | 用途说明 | 注意事项 |
|---|---|---|---|
| getDoc | getDoc(docRef) | 获取单个文档快照 | 即使文档不存在,也不会抛错,需用 exists() 判断 |
| getDocs | getDocs(queryOrCollectionRef) | 获取集合或查询结果的所有文档 | 返回 QuerySnapshot,包含多个 DocumentSnapshot |
| onSnapshot(文档) | onSnapshot(docRef, callback) | 实时监听单个文档变更 | 返回取消函数 unsub(),用于停止监听 |
| onSnapshot(集合) | onSnapshot(collectionRef, callback) | 实时监听整个集合变更 | 可通过 docChanges() 区分 added/modified/removed |
代码示例:
// getDoc - 单文档读取
import { doc, getDoc } from 'firebase/firestore';
const docSnap = await getDoc(doc(db, 'users', 'user_123'));
if (docSnap.exists()) console.log(docSnap.data());
// getDocs - 集合读取
import { collection, getDocs } from 'firebase/firestore';
const querySnapshot = await getDocs(collection(db, 'users'));
querySnapshot.forEach(doc => console.log(doc.id, doc.data()));
// onSnapshot - 文档监听
const unsub = onSnapshot(doc(db, 'users', 'user_123'), (doc) => {
console.log('Current data:', doc.data());
});
// onSnapshot - 集合监听
const unsub = onSnapshot(collection(db, 'users'), (snapshot) => {
snapshot.docChanges().forEach(change => {
console.log(change.type, change.doc.data());
});
});
4.3 更新文档
| 方法名称 | 语法 | 用途说明 | 注意事项 |
|---|---|---|---|
| updateDoc | updateDoc(docRef, updateData) | 更新文档的部分字段 | 仅更新指定字段,其他字段保留;文档必须存在,否则报错 |
| arrayUnion | updateDoc(docRef, { field: arrayUnion(...items) }) | 向数组字段追加不重复元素 | 自动去重,顺序不保证 |
| arrayRemove | updateDoc(docRef, { field: arrayRemove(...items) }) | 从数组字段移除指定元素 | 仅移除匹配项,不影响其他元素 |
| increment | updateDoc(docRef, { field: increment(delta) }) | 对数值字段原子增减 | 支持正负数,服务端原子操作,避免竞态条件 |
代码示例:
// updateDoc - 部分字段更新
import { doc, updateDoc } from 'firebase/firestore';
await updateDoc(doc(db, 'users', 'user_123'), { age: 31 });
// arrayUnion - 数组追加
await updateDoc(doc(db, 'users', 'user_123'), { tags: arrayUnion('premium', 'beta') });
// arrayRemove - 数组移除
await updateDoc(doc(db, 'users', 'user_123'), { tags: arrayRemove('beta') });
// increment - 原子增减
await updateDoc(doc(db, 'users', 'user_123'), { loginCount: increment(1) });
4.4 删除文档与集合
| 方法名称 | 语法 | 用途说明 | 注意事项 |
|---|---|---|---|
| deleteDoc | deleteDoc(docRef) | 删除单个文档 | 删除后文档不可恢复;子集合不受影响(仍存在) |
| 批量删除集合文档 | 使用 getDocs + WriteBatch | 删除集合内所有文档(无内置方法) | Firestore 不支持”删除整个集合”操作,需逐个删除;注意读取配额和批量大小限制(≤500 操作/批) |
| deleteField | updateDoc(docRef, { field: deleteField() }) | 删除文档中的特定字段 | 仅删除字段,保留文档其他内容 |
代码示例:
// deleteDoc - 删除单个文档
import { doc, deleteDoc } from 'firebase/firestore';
await deleteDoc(doc(db, 'users', 'user_123'));
// 批量删除集合文档
const batch = writeBatch(db);
const snapshot = await getDocs(collection(db, 'temp_logs'));
snapshot.docs.forEach(doc => batch.delete(doc.ref));
await batch.commit();
// deleteField - 删除字段
await updateDoc(doc(db, 'users', 'user_123'), { oldEmail: deleteField() });
⚠️ 重要提示: Firestore 没有提供直接删除整个集合的 API。删除集合需先读取所有文档,再逐个删除(或使用 Cloud Functions + 客户端逻辑)。生产环境中应谨慎操作,避免触发大量读写费用。
第五章:查询与过滤
5.1 基本查询(where、orderBy、limit)
| 方法名称 | 语法 | 用途说明 | 注意事项 |
|---|---|---|---|
| query | query(collectionRef, ...constraints) | 构建带条件的查询 | 所有约束必须通过 query() 组合 |
| where | where(fieldPath, opStr, value) | 添加字段过滤条件 | 支持 ==, !=, <, <=, >, >=, in, not-in, array-contains 等 |
| orderBy | orderBy(fieldPath, directionStr?) | 指定排序字段和方向 | 默认升序(asc);若使用 != 或 not-in,必须对同一字段 orderBy |
| limit | limit(limitSize) | 限制返回文档数量 | 常用于分页或性能优化 |
| getDocs + query | await getDocs(query(...)) | 执行查询并获取结果 | 返回 QuerySnapshot,非实时 |
代码示例:
import { collection, query, where, orderBy, limit } from 'firebase/firestore';
const q = query(
collection(db, 'users'),
where('age', '>', 18),
orderBy('name'),
limit(10)
);
const snapshot = await getDocs(q);
snapshot.forEach(doc => console.log(doc.data()));
5.2 复合查询与索引要求
| 概念/操作名称 | 说明 | 注意事项 |
|---|---|---|
| 单字段索引 | Firestore 自动为每个字段创建单字段索引(支持基本 where + orderBy) | 无需手动配置 |
| 复合索引 | 当查询包含多个字段过滤或混合过滤+排序时,需手动定义复合索引 | 否则抛出错误并提供索引创建链接 |
| 索引配置文件 | 定义在 firestore.indexes.json 中 | 可通过 firebase deploy --only firestore:indexes 部署 |
| 复合查询限制 | 不支持对不同字段同时使用范围比较(如 where('a', '>', 1).where('b', '>', 2)) | 必须有一个字段用 ==、in 或 array-contains |
| 错误提示与自动建议 | 查询失败时,错误信息包含”Click here to create index”链接 | 点击可自动生成索引配置并跳转控制台 |
| 示例复合查询 | where('category', '==', 'tech').where('score', '>=', 80).orderBy('score', 'desc') | 需要复合索引:[category (asc), score (desc)] |
💡 索引最佳实践: 尽量用
==条件缩小范围,再对单一字段做范围或排序,可避免复合索引。
5.3 分页查询(startAt / startAfter)
| 方法名称 | 语法 | 用途说明 | 注意事项 |
|---|---|---|---|
| startAt | startAt(snapshotOrFieldValues...) | 从指定值或文档快照开始(含该文档) | 常用于”下一页” |
| startAfter | startAfter(snapshotOrFieldValues...) | 从指定值或文档快照之后开始(不含) | 推荐用于分页,避免重复 |
| 基于字段值分页 | startAfter('2026-01-01') | 直接传入字段值(需与 orderBy 一致) | 字段类型必须匹配 |
| 获取上一页 | 需反向排序 + startAt + limitToLast(不推荐) | Firestore 不原生支持高效”上一页” | 更佳方案:前端缓存前一页最后文档,或使用双向游标逻辑;避免使用 limitToLast,性能差 |
代码示例:
// 首页
const first = query(collection(db, 'posts'), orderBy('date'), limit(10));
const firstSnap = await getDocs(first);
// 下一页
const lastVisible = firstSnap.docs[firstSnap.docs.length - 1];
const next = query(
collection(db, 'posts'),
orderBy('date'),
startAfter(lastVisible),
limit(10)
);
// 基于字段值分页
query(
collection(db, 'logs'),
orderBy('timestamp'),
startAfter(new Date('2026-01-01')),
limit(50)
);
5.4 数组与嵌套字段查询
| 方法名称 / 操作 | 语法 / 说明 | 用途说明 | 注意事项 |
|---|---|---|---|
| array-contains | where('tags', 'array-contains', 'news') | 查询数组字段是否包含某元素 | 一次只能查一个值;不支持多个值(用 array-contains-any) |
| array-contains-any | where('tags', 'array-contains-any', ['news', 'tech']) | 查询数组是否包含任一指定元素 | 最多支持 10 个值 |
| in | where('status', 'in', ['pending', 'approved']) | 字段值是否在给定列表中 | 最多支持 10 个值 |
| not-in | where('status', 'not-in', ['deleted', 'spam']) | 字段值不在给定列表中 | 最多 10 个值;不能与其他 != 共用 |
| 嵌套字段查询 | 使用点号访问嵌套字段 | 查询 Map 类型内部字段 | 嵌套路径用字符串表示,如 'user.profile.name' |
| 嵌套字段索引 | 复合查询涉及嵌套字段时需手动创建索引 | 在 firestore.indexes.json 中定义 | 控制台错误提示会引导创建 |
代码示例:
// array-contains
where('categories', 'array-contains', 'sports')
// array-contains-any
where('roles', 'array-contains-any', ['admin', 'moderator'])
// in / not-in
where('country', 'in', ['US', 'CA', 'MX'])
where('score', 'not-in', [0, -1])
// 嵌套字段查询
where('address.city', '==', 'Beijing')
orderBy('profile.createdAt')
嵌套字段索引示例:
{
"collectionGroup": "users",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "profile.level", "order": "ASCENDING" }
]
}
第六章:实时监听与离线支持
6.1 监听单个文档变更
| 方法名称 | 语法 | 用途说明 | 注意事项 |
|---|---|---|---|
| onSnapshot | onSnapshot(docRef, onNext, onError?) | 实时监听单个文档的变更 | 返回取消函数 unsub(),必须在组件卸载时调用以避免内存泄漏 |
| 带元数据监听 | onSnapshot(docRef, { includeMetadataChanges: true }, callback) | 包含元数据变化(如 fromCache) | 默认不触发纯元数据变更;开启后可能多次回调相同数据 |
| 监听不存在文档 | onSnapshot(docRef, callback) | 即使文档不存在也会触发一次回调 | 可用于检测文档是否存在 |
代码示例:
import { doc, onSnapshot } from 'firebase/firestore';
// 基本监听
const unsub = onSnapshot(
doc(db, 'users', 'alice'),
(docSnap) => {
console.log('Data:', docSnap.data());
},
(error) => {
console.error('Listen failed:', error);
}
);
// 带元数据监听
onSnapshot(
doc(db, 'config', 'app'),
{ includeMetadataChanges: true },
(snap) => {
if (snap.metadata.hasPendingWrites) console.log('Local change');
}
);
// 监听不存在文档
onSnapshot(doc(db, 'users', 'nonexistent'), (snap) => {
if (!snap.exists()) console.log('Doc not found');
});
6.2 监听集合变更
| 方法名称 | 语法 | 用途说明 | 注意事项 |
|---|---|---|---|
| onSnapshot(集合) | onSnapshot(collectionRef, onNext, onError?) | 监听整个集合的增删改 | docChanges() 返回变更列表,类型为 ‘added’ / ‘modified’ / ‘removed’ |
| 带查询监听 | onSnapshot(query(...), callback) | 监听带过滤/排序的查询结果 | 查询必须有对应索引,否则监听失败 |
| 性能优化建议 | 使用 docChanges() 而非遍历所有 docs | 避免重复渲染未变更项 | 尤其在大型集合中可显著提升性能 |
代码示例:
// 集合监听(使用 docChanges 推荐方式)
const unsub = onSnapshot(collection(db, 'messages'), (snapshot) => {
// 推荐:仅处理变更
snapshot.docChanges().forEach(change => {
console.log(change.type, change.doc.id, change.doc.data());
});
// 不推荐:遍历全部
// snapshot.docs.forEach(d => render(d));
});
// 带查询监听
const q = query(
collection(db, 'posts'),
where('published', '==', true),
orderBy('date', 'desc')
);
onSnapshot(q, (snap) => { /* handle */ });
6.3 离线数据持久化配置
| 操作名称 | 语法 / 配置方式 | 用途说明 | 注意事项 |
|---|---|---|---|
| 启用本地持久化(Web) | enableIndexedDbPersistence(db) 或 enableMultiTabIndexedDbPersistence(db) | 在浏览器中启用 IndexedDB 缓存 | Web 默认不启用持久化;需显式调用 |
| 多标签页共享缓存 | enableMultiTabIndexedDbPersistence(db) | 允许多个浏览器标签页共享同一缓存 | 若已存在单 tab 实例,则失败 |
| 移动端(iOS/Android)默认行为 | 无需配置 | 默认启用磁盘持久化 | Web 是唯一需手动启用的平台 |
| 禁用离线写入 | db.settings({ experimentalForceLongPolling: true })(不推荐) | 强制仅在线操作 | 一般不建议禁用离线能力,会破坏用户体验 |
代码示例:
import { enableIndexedDbPersistence } from 'firebase/firestore';
enableIndexedDbPersistence(db).catch(err => {
if (err.code === 'failed-precondition') {
console.warn('Multiple tabs open – persistence disabled');
}
});
⚠️ 注意: Web 端持久化依赖 IndexedDB,受浏览器隐私模式或存储配额限制。若用户清除站点数据,缓存将丢失。
6.4 快照元数据与状态判断
| 元数据属性 | 说明 | 使用场景 | 注意事项 |
|---|---|---|---|
| snap.exists() | 判断文档是否存在 | 避免读取 null 数据 | 所有 DocumentSnapshot 必须先检查 |
| snap.metadata.fromCache | 布尔值,表示数据来自本地缓存(true)还是服务端(false) | 显示”离线中”提示 | 即使联网,首次读取也可能来自缓存 |
| snap.metadata.hasPendingWrites | 布尔值,表示本地有未同步到服务端的写入 | 显示”同步中”状态 | 仅当启用持久化时有效 |
| QuerySnapshot.metadata | 集合快照也包含 metadata,但信息有限 | 一般不用于集合级状态判断 | 不推荐依赖集合快照的 metadata |
| 错误处理 | onError 回调接收 FirebaseError | 捕获权限拒绝、网络失败等 | 监听失败后不会自动重试,需手动重建 |
代码示例:
// 状态判断
if (docSnap.exists()) {
const data = docSnap.data();
}
// 离线判断
if (snap.metadata.fromCache) showOfflineIndicator();
// 同步中判断
if (snap.metadata.hasPendingWrites) showSyncingSpinner();
// 错误处理
onSnapshot(ref, onNext, (err) => {
if (err.code === 'permission-denied') alert('Access denied');
});
第七章:安全规则(Security Rules)
7.1 规则语法基础
| 概念/语法元素 | 说明 | 注意事项 |
|---|---|---|
| rules_version | 声明规则语言版本(必须为 ‘2’) | 版本 1 已弃用,新项目必须使用版本 2 |
| service cloud.firestore | 定义服务类型 | Firestore 规则必须包裹在此块内 |
| match 语句 | 定义资源路径匹配规则 | 路径必须从 /databases/{database}/documents 开始 |
| 通配符 {doc} | 匹配任意文档 ID | 通配符变量可在条件中使用(如 userId) |
| allow 语句 | 授权操作(read, write, create, update, delete) | 可组合多个操作;write = create + update + delete |
| return / function | 支持自定义函数简化逻辑 | 函数必须在 match 块内或全局定义 |
| 默认拒绝 | 未明确允许的操作一律拒绝 | 安全第一原则:最小权限开放 |
规则示例:
rules_version = '2';
service cloud.firestore {
match /databases/{database}/documents {
match /users/{userId} {
allow read: if request.auth != null;
allow write: if request.auth.uid == userId;
}
}
}
7.2 身份验证与用户权限控制
| 概念/表达式 | 说明 | 注意事项 |
|---|---|---|
| request.auth | 当前用户认证信息(未登录时为 null) | 必须集成 Firebase Authentication |
| request.auth.uid | 当前用户唯一 ID | 常用于”仅本人可写”场景 |
| request.auth.token | JWT 中的自定义声明(如 email, admin) | 自定义声明需通过 Admin SDK 设置 |
| resource | 当前文档的当前数据(仅 update/delete 时可用) | create 操作中 resource 为 null |
| request.resource | 客户端请求中的新数据 | 用于验证写入内容 |
| 多层级权限(子集合) | 在子集合规则中引用父文档 ID | 利用路径通配符实现层级权限 |
代码示例:
// 基础认证检查
allow read: if request.auth != null;
// 仅本人可写
allow write: if request.auth.uid == userId;
// 验证邮箱
allow read: if request.auth.token.email_verified == true;
// 仅 owner 可更新
allow update: if resource.data.owner == request.auth.uid;
// 创建时验证 owner
allow create: if request.resource.data.owner == request.auth.uid;
// 子集合层级权限
match /users/{userId}/posts/{postId} {
allow write: if userId == request.auth.uid;
}
7.3 数据验证与字段约束
| 验证方法 / 表达式 | 说明 | 注意事项 |
|---|---|---|
| 字段存在性检查 | 使用 in 操作符 | 防止缺失关键字段 |
| 类型检查 | 使用 is 关键字 | 支持 int, float, string, bool, timestamp, map, list |
| 字符串长度 | 使用 size() | 适用于 string 和 list |
| 数值范围 | 直接比较 | 支持链式比较(v2 语法) |
| 数组元素类型 | 遍历或结合函数 | 无法直接验证所有元素类型,需假设或限制长度 |
| 嵌套字段验证 | 通过点号访问 | 若字段不存在会报错,需先检查存在性 |
| 时间戳验证 | 比较 timestamp 类型 | request.time 是服务端时间,防客户端伪造 |
代码示例:
// 字段存在性
allow create: if 'email' in request.resource.data;
// 类型检查
allow create: if request.resource.data.age is int;
// 字符串长度
allow create: if request.resource.data.name.size() <= 50;
// 数值范围(v2 链式比较)
allow create: if request.resource.data.score >= 0 && <= 100;
// 嵌套字段验证
allow create: if request.resource.data.profile.bio.size() > 0;
// 时间戳验证
allow create: if request.resource.data.createdAt == request.time;
// 数组元素验证函数
function isValidTags(tags) {
return tags is list && tags.size() <= 5 && tags[0] is string;
}
7.4 测试安全规则(使用模拟器)
| 操作名称 | 操作细节 | 注意事项 |
|---|---|---|
| 启动 Emulator Suite | 运行命令启动包含 Firestore 和 Auth 的模拟器 | 需已初始化项目 |
| 访问 Rules 测试 UI | 浏览器打开 http://localhost:4000 → 切换到 “Rules” 标签页 | 可实时修改规则并测试 |
| 模拟认证用户 | 在测试 UI 中设置 UID、Token 声明等 | 模拟不同角色权限 |
| 执行读/写测试 | 选择操作类型(get, set, update, delete),输入文档路径和数据 | 点击”Run”查看是否允许 |
| 查看评估日志 | 测试后显示规则逐行评估过程 | 用于调试复杂规则 |
| 集成到单元测试 | 使用 @firebase/rules-unit-testing 库 | 需在测试中连接模拟器 |
启动命令:
firebase emulators:start --only firestore,auth
单元测试示例:
import { assertFails, assertSucceeds } from '@firebase/rules-unit-testing';
await assertSucceeds(getDoc(doc(db, 'users/alice')));
💡 提示: 所有规则变更必须先在模拟器中测试,再部署到生产环境,避免因规则错误导致应用功能中断。
第八章:性能优化与最佳实践
8.1 索引策略与自动索引
| 概念/操作名称 | 说明 | 注意事项 |
|---|---|---|
| 单字段索引(自动) | Firestore 自动为每个字段创建升序/降序索引,支持基本查询 | 可在 Firebase 控制台禁用某字段的自动索引(节省存储) |
| 复合索引(手动) | 多字段查询或混合排序需手动定义 | 查询失败时错误信息会提供一键生成链接 |
| 覆盖索引(Covering Index) | 将 orderBy 和 where 字段组合,避免回表 | 提升查询效率,减少读取文档数 |
| 索引限制 | 每个项目最多 200 个复合索引 | 设计数据模型时应尽量复用索引 |
| 查看索引使用情况 | 在 Firebase 控制台 → Firestore → “索引” 标签页 | 定期清理未使用索引可降低成本 |
复合索引示例:
查询:where('status','==','active').orderBy('createdAt') → 索引 [status, createdAt]
firestore.indexes.json 示例:
{
"collectionGroup": "posts",
"queryScope": "COLLECTION",
"fields": [
{ "fieldPath": "category", "order": "ASCENDING" },
{ "fieldPath": "score", "order": "DESCENDING" }
]
}
8.2 避免大集合全量读取
| 优化策略 | 说明 | 注意事项 |
|---|---|---|
| 使用分页(limit + startAfter) | 避免一次性加载数千文档 | 结合 5.3 分页查询实现滚动加载 |
| 添加过滤条件缩小范围 | 利用 where 减少返回文档数量 | 利用用户 ID 或状态字段做分区 |
| 集合分片(Sharding) | 将大集合拆分为多个子集合(如 logs_2026_01, logs_2026_02) | 按时间、租户或哈希值分片;增加查询复杂度 |
| 使用聚合文档缓存计数 | 避免 count() 全量扫描(Firestore 不支持原生 count) | 维护一个 stats 文档,通过事务更新 totalPosts 字段 |
| 限制监听范围 | 实时监听时务必加 limit 和 where | 否则可能监听数万文档,耗尽配额 |
代码示例:
// 分页查询
const q = query(collection(db, 'logs'), orderBy('ts'), limit(50));
// 过滤条件缩小范围
where('userId', '==', currentUid).where('archived', '==', false)
// 限制监听范围
onSnapshot(
query(collection(db, 'chat'), where('roomId', '==', 'r1'), limit(100))
);
8.3 批处理与事务操作
| 方法名称 | 语法 | 用途说明 | 注意事项 |
|---|---|---|---|
| writeBatch | batch.set(...); batch.commit(); | 原子性执行多写入(≤500 操作) | 所有操作要么全成功,要么全失败;不支持读取 |
| runTransaction | runTransaction(db, async (transaction) => { ... }) | 带条件的原子读-改-写(支持读取) | 事务内读取的是最新快照;重试机制自动处理冲突 |
| 批量删除 | 结合 getDocs + writeBatch(见 4.4) | 安全删除大量文档 | 单次批处理 ≤500 操作;避免在循环中 commit |
| 避免嵌套事务 | 不要在事务中启动新事务 | Firestore 不支持嵌套事务 | |
| 事务超时 | 默认 27 秒 | 长时间运行逻辑应拆解到 Cloud Functions |
代码示例:
// writeBatch - 原子批量写入
const batch = writeBatch(db);
batch.update(doc(db, 'users/alice'), { posts: increment(1) });
batch.set(doc(db, 'activity/log1'), { action: 'post' });
await batch.commit();
// runTransaction - 带读写的原子操作(如库存扣减)
await runTransaction(db, async (t) => {
const doc = await t.get(docRef);
if (doc.data().stock > 0) {
t.update(docRef, { stock: increment(-1) });
}
});
8.4 成本控制与用量监控
| 优化措施 | 说明 | 注意事项 |
|---|---|---|
| 监控读写删除次数 | Firestore 按操作次数计费(非数据量) | 在 Firebase 控制台 → “用量” 查看每日操作数;实时监听每次变更计为 1 次读取 |
| 避免重复监听 | 同一查询多次 onSnapshot 会重复计费 | 使用全局状态管理(如 React Context)共享监听器 |
| 合理设置监听生命周期 | 在组件卸载时调用 unsub() | 防止内存泄漏和无效计费 |
| 使用缓存减少读取 | 启用持久化后,相同查询可能命中本地缓存(fromCache=true) | 缓存不减少配额,但提升用户体验 |
| 避免高频写入同一文档 | 单文档写入限约 1 次/秒 | 计数器场景改用分布式计数(sharded counter) |
| 设置预算告警 | 在 Google Cloud Console 中配置支出限额和邮件告警 | 路径:Billing → Budgets & alerts |
| 模拟器测试成本敏感逻辑 | 在本地验证查询效率和操作次数 | 开发阶段即可优化成本结构 |
代码示例:
// 正确的监听生命周期管理
useEffect(() => {
const unsub = onSnapshot(/* ... */);
return () => unsub(); // 组件卸载时取消
}, []);
第九章:与其他 Firebase 服务集成
9.1 与 Authentication 集成
| 集成要点 | 说明 | 注意事项 |
|---|---|---|
| 获取当前用户 UID | 在客户端通过 Auth SDK 获取已认证用户 ID | UID 是 Firestore 权限控制的核心依据 |
| 在安全规则中使用 request.auth.uid | 控制文档级访问权限 | 最常见的”仅本人访问”模式 |
| 自动创建用户文档 | 用户首次登录时在 Firestore 创建 profile | 使用 { merge: true } 避免覆盖 |
| 自定义声明(Custom Claims) | 通过 Admin SDK 设置 admin、role 等高级权限 | 声明同步有延迟(约 1 小时),可强制刷新 token |
| 匿名用户转正式用户 | 支持游客体验后升级为注册用户 | 升级后 UID 不变,原有 Firestore 数据保留 |
代码示例:
// 获取当前用户
import { getAuth, onAuthStateChanged } from 'firebase/auth';
onAuthStateChanged(getAuth(), (user) => {
if (user) console.log('UID:', user.uid);
});
// 自动创建用户文档
onAuthStateChanged(auth, async (user) => {
if (user) {
const userRef = doc(db, 'users', user.uid);
await setDoc(userRef, { email: user.email }, { merge: true });
}
});
// 匿名用户升级
const credential = EmailAuthProvider.credential(email, password);
await linkWithCredential(auth.currentUser, credential);
安全规则示例:
// 仅本人访问
match /users/{userId} {
allow read, write: if request.auth.uid == userId;
}
// 管理员才可删除
allow delete: if request.auth.token.admin == true;
Admin SDK 设置自定义声明:
// 后端(Cloud Functions)
admin.auth().setCustomUserClaims(uid, { admin: true });
9.2 与 Cloud Functions 联动
| 集成方式 | 说明 | 注意事项 |
|---|---|---|
| 触发器:onCreate | 监听文档创建事件 | 路径支持通配符 {uid};自动重试失败函数 |
| 触发器:onUpdate / onDelete | 监听更新或删除 | 可访问 before/after 快照对比差异 |
| 在函数中写入 Firestore | 使用 Admin SDK 绕过安全规则 | Admin SDK 拥有完全权限,需谨慎操作 |
| 调用 HTTP 函数更新数据 | 客户端通过 HTTPS 调用函数间接写入 | 适用于复杂业务逻辑或跨服务协调 |
| 避免无限循环 | 函数内写入可能再次触发监听 | 必须设计防重机制 |
代码示例:
// onCreate 触发器
import { onDocumentCreated } from 'firebase-functions/v2/firestore';
export const logUserSignup = onDocumentCreated('users/{uid}', (event) => {
console.log('New user:', event.params.uid);
});
// onUpdate 触发器
onDocumentUpdated('orders/{id}', (event) => { /* 处理状态变更 */ });
// Admin SDK 写入
import { getFirestore } from 'firebase-admin/firestore';
const db = getFirestore();
await db.doc('stats/daily').update({ signups: increment(1) });
// 客户端调用 HTTP 函数
fetch('https://.../createOrder', { method: 'POST', body: JSON.stringify(data) });
// 避免无限循环
if (!change.after.data()?.processed) {
// 执行逻辑
await setDoc(docRef, { processed: true }, { merge: true });
}
9.3 与 Storage 协同使用
| 协同场景 | 说明 | 注意事项 |
|---|---|---|
| 存储文件并记录引用 | 上传文件到 Storage,将下载 URL 或引用路径存入 Firestore | 推荐存储 Storage 引用路径(如 gs://bucket/…),而非公开 URL |
| 在安全规则中联动验证 | Storage 规则可引用 Firestore 文档状态 | 需启用 Firestore 规则中的 firestore.get() 权限 |
| 自动生成缩略图并更新元数据 | 通过 Cloud Functions 监听 Storage 上传,处理图像后写入 Firestore | 典型 Serverless 图像处理流程 |
| 删除文件时清理 Firestore 引用 | 避免遗留无效引用 | 建议使用事务或函数保证一致性 |
| 公开 vs 私有访问控制 | 根据业务决定是否生成公开 URL | 敏感文件应通过 Storage 规则 + Auth 控制访问 |
代码示例:
// 上传文件并存储引用
const storageRef = ref(storage, `avatars/${uid}.jpg`);
await uploadBytes(storageRef, file);
await updateDoc(userDoc, { avatar: storageRef.toString() });
// 获取临时公开链接(默认 7 天)
const url = await getDownloadURL(storageRef);
Storage 规则联动 Firestore 示例:
match /avatars/{userId}/{fileName} {
allow write: if resource.metadata.userId == request.auth.uid &&
firestore.get(/databases/(default)/documents/users/$(userId)).data.status == 'active';
}
删除流程建议:
1. 从 Firestore 移除 avatar 字段
2. 调用 deleteObject(storageRef)
第十章:生产部署与运维
10.1 从模拟器迁移到生产环境
| 步骤名称 | 操作细节 | 注意事项 |
|---|---|---|
| 停用本地模拟器连接 | 移除或注释客户端代码中的 useEmulator() 调用 | 否则客户端会尝试连接本地地址,导致生产环境失败 |
| 部署安全规则 | 使用 CLI 将本地规则推送到生产环境 | 确保规则已通过模拟器充分测试 |
| 部署索引配置 | 同步复合索引定义 | 缺失索引会导致查询失败 |
| 导入初始数据(可选) | 若有种子数据,使用 gcloud 或脚本写入 | 避免在客户端首次启动时批量写入(触发高费用) |
| 验证生产连接 | 在真实设备或浏览器中测试读写操作 | 检查 Firebase 项目 ID 是否正确(非 emulated) |
| 环境隔离策略 | 使用不同 Firebase 项目区分 dev/staging/prod | 防止测试数据污染生产库 |
命令示例:
# 部署规则
firebase deploy --only firestore:rules
# 部署索引
firebase deploy --only firestore:indexes
# 导入初始数据
gcloud firestore import gs://my-bucket/initial-data
# 切换环境
firebase use prod-project-id
客户端代码调整:
// 开发时(上线前需移除)
// connectFirestoreEmulator(db, 'localhost', 8080);
10.2 版本控制与 CI/CD 集成
| 操作名称 | 操作细节 | 注意事项 |
|---|---|---|
| 规则与索引纳入 Git | 将 firestore.rules 和 firestore.indexes.json 加入版本控制 | 这是基础设施即代码(IaC)的核心 |
| 使用 .firebaserc 切换环境 | 定义多个项目别名 | 支持团队统一环境管理 |
| CI/CD 中部署规则 | 在 GitHub Actions / GitLab CI 中执行部署 | 需提前生成 CI 专用部署令牌(firebase login:ci) |
| 自动化测试集成 | 在 CI 流程中运行规则单元测试 | 使用 @firebase/rules-unit-testing 库 |
| 部署前校验(dry-run) | 预览变更而不实际部署 | 用于 PR 检查或人工审核前验证 |
| 回滚机制 | 手动恢复旧版规则文件并重新部署 | Firestore 不提供自动版本回滚,需依赖 Git 历史 |
配置文件示例:
// .firebaserc
{
"projects": {
"dev": "myapp-dev",
"prod": "myapp-prod"
}
}
CI/CD 示例(GitHub Actions):
- name: Deploy Firestore Rules
run: firebase deploy --only firestore --token ${{ secrets.FIREBASE_TOKEN }}
常用命令:
# 纳入 Git
git add firestore.rules firestore.indexes.json
# 部署前预览
firebase deploy --only firestore --dry-run
# 回滚
git checkout v1.2 -- firestore.rules && firebase deploy --only firestore:rules
10.3 监控、告警与错误排查
| 监控/操作项 | 工具 / 路径 | 注意事项 |
|---|---|---|
| 查看实时用量 | Firebase Console → Project Overview → “Usage” tab | 免费层级有配额限制,超限将拒绝请求 |
| 设置预算与支出告警 | Google Cloud Console → Billing → Budgets & alerts | 告警阈值建议设为 50%/90% |
| 分析慢查询 | Firestore 控制台 → “Monitoring” → “Slow queries”(需启用) | 需提前在项目设置中开启监控 |
| 错误日志收集 | 在 onSnapshot onError 或 try/catch 中上报错误到 Crashlytics 或 Sentry | 关键错误应包含用户 ID、文档路径、错误码 |
| 常见错误码排查 | 错误信息通常包含修复建议链接 | — |
| 使用 Cloud Logging | Google Cloud Console → Logging | 需启用 Firestore 审计日志(默认不开启) |
| 性能基准测试 | 编写 Node.js 脚本批量执行典型查询 | 建议在 staging 环境进行 |
常见错误码及排查:
PERMISSION_DENIED:检查安全规则和 Auth 状态UNAVAILABLE:网络或服务中断FAILED_PRECONDITION:索引缺失
Cloud Logging 查询示例:
resource.type="firestore_document"