了解 2023 年 Google I/O 大会上介绍的 Firebase 亮点。了解详情

离线访问数据

Cloud Firestore 支持离线数据持久化。这一功能会为您的应用正在使用的 Cloud Firestore 数据缓存一份副本,以便让您的应用可以在设备离线时访问数据。您可以写入、读取、监听和查询缓存的数据。当设备恢复在线状态时,Cloud Firestore 会将您的应用在本地所做的所有更改都同步到 Cloud Firestore 后端。

您无需更改用于访问 Cloud Firestore 数据的代码,即可使用离线持久化功能。启用离线持久化后,Cloud Firestore 客户端库会自动管理在线和离线数据访问,并在设备恢复在线状态时同步本地数据。

配置离线持久化

在初始化 Cloud Firestore 时,您可以启用或停用离线持久化:

  • 对于 Android 和 Apple 平台,离线持久化默认处于启用状态。如需停用持久化,请将 PersistenceEnabled 选项设置为 false
  • 对于 Web 应用,离线持久化默认处于停用状态。如需启用持久化,请调用 enablePersistence 方法。在不同会话之间切换时,系统不会自动清除 Cloud Firestore 的缓存。因此,如果您的 Web 应用涉及到敏感信息的处理,请务必在启用持久化之前询问用户其设备是否可信。

Web version 9

// Memory cache is the default if no config is specified.
initializeFirestore(app);

// This is the default behavior if no persistence is specified.
initializeFirestore(app, {localCache: memoryLocalCache()});

// Defaults to single-tab persistence if no tab manager is specified.
initializeFirestore(app, {localCache: persistentLocalCache(/*settings*/{})});

// Same as `initializeFirestore(app, {localCache: persistentLocalCache(/*settings*/{})})`,
// but more explicit about tab management.
initializeFirestore(app,
  {localCache:
    persistentLocalCache(/*settings*/{tabManager: persistentSingleTabManager()})
});

// Use multi-tab IndexedDb persistence.
initializeFirestore(app,
  {localCache:
    persistentLocalCache(/*settings*/{tabManager: persistentMultipleTabManager()})
  });
  

Web version 8

firebase.firestore().enablePersistence()
  .catch((err) => {
      if (err.code == 'failed-precondition') {
          // Multiple tabs open, persistence can only be enabled
          // in one tab at a a time.
          // ...
      } else if (err.code == 'unimplemented') {
          // The current browser does not support all of the
          // features required to enable persistence
          // ...
      }
  });
// Subsequent queries will use persistence, if it was enabled successfully
Swift
注意:此产品不适用于 watchOS 和 App Clip 目标。
let settings = FirestoreSettings()
settings.isPersistenceEnabled = true

// Any additional options
// ...

// Enable offline data persistence
let db = Firestore.firestore()
db.settings = settings
Objective-C
注意:此产品不适用于 watchOS 和 App Clip 目标。
FIRFirestoreSettings *settings = [[FIRFirestoreSettings alloc] init];
settings.persistenceEnabled = YES;

// Any additional options
// ...

// Enable offline data persistence
FIRFirestore *db = [FIRFirestore firestore];
db.settings = settings;

Kotlin+KTX

val settings = firestoreSettings {
    isPersistenceEnabled = true
}
db.firestoreSettings = settings

Java

FirebaseFirestoreSettings settings = new FirebaseFirestoreSettings.Builder()
        .setPersistenceEnabled(true)
        .build();
db.setFirestoreSettings(settings);

Dart

// Apple and Android
db.settings = const Settings(persistenceEnabled: true);

// Web
await db
    .enablePersistence(const PersistenceSettings(synchronizeTabs: true));

配置缓存大小

启用持久化后,Cloud Firestore 会缓存从后端接收的每个文档以便在离线状态下进行访问。Cloud Firestore 会为缓存大小设置默认阈值。超出默认值后,Cloud Firestore 会定期尝试清理较旧的未使用文档。您可以配置不同的缓存大小阈值,也可以完全停用清理功能:

Web version 9

import { initializeFirestore, CACHE_SIZE_UNLIMITED } from "firebase/firestore";

const firestoreDb = initializeFirestore(app, {
  cacheSizeBytes: CACHE_SIZE_UNLIMITED
});

Web version 8

firebase.firestore().settings({
    cacheSizeBytes: firebase.firestore.CACHE_SIZE_UNLIMITED
});
Swift
注意:此产品不适用于 watchOS 和 App Clip 目标。
// The default cache size threshold is 100 MB. Configure "cacheSizeBytes"
// for a different threshold (minimum 1 MB) or set to "FirestoreCacheSizeUnlimited"
// to disable clean-up.
let settings = Firestore.firestore().settings
settings.cacheSizeBytes = FirestoreCacheSizeUnlimited
Firestore.firestore().settings = settings
Objective-C
注意:此产品不适用于 watchOS 和 App Clip 目标。
// The default cache size threshold is 100 MB. Configure "cacheSizeBytes"
// for a different threshold (minimum 1 MB) or set to "kFIRFirestoreCacheSizeUnlimited"
// to disable clean-up.
FIRFirestoreSettings *settings = [FIRFirestore firestore].settings;
settings.cacheSizeBytes = kFIRFirestoreCacheSizeUnlimited;
[FIRFirestore firestore].settings = settings;
  

Kotlin+KTX


// The default cache size threshold is 100 MB. Configure "setCacheSizeBytes"
// for a different threshold (minimum 1 MB) or set to "CACHE_SIZE_UNLIMITED"
// to disable clean-up.
val settings = FirebaseFirestoreSettings.Builder()
        .setCacheSizeBytes(FirebaseFirestoreSettings.CACHE_SIZE_UNLIMITED)
        .build()
db.firestoreSettings = settings

Java


// The default cache size threshold is 100 MB. Configure "setCacheSizeBytes"
// for a different threshold (minimum 1 MB) or set to "CACHE_SIZE_UNLIMITED"
// to disable clean-up.
FirebaseFirestoreSettings settings = new FirebaseFirestoreSettings.Builder()
        .setCacheSizeBytes(FirebaseFirestoreSettings.CACHE_SIZE_UNLIMITED)
        .build();
db.setFirestoreSettings(settings);

Dart

db.settings = const Settings(
  persistenceEnabled: true,
  cacheSizeBytes: Settings.CACHE_SIZE_UNLIMITED,
);

监听离线数据

当设备离线时,如果您已启用离线持久化,则当本地缓存的数据发生更改时,您的监听器将收到监听事件。您可以监听文档、集合和查询。

如需检查您收到的数据是来自服务器还是来自缓存,可使用快照事件的 SnapshotMetadata 中的 fromCache 属性。如果 fromCachetrue,则说明数据来自缓存,可能是过时的或不完整的。如果 fromCachefalse,则说明数据是完整的,并且与服务器上的最新更新保持一致。

默认情况下,如果只有 SnapshotMetadata 发生更改,则不会引发任何事件。如果您依赖于 fromCache 值,请在附加监听处理程序时指定 includeMetadataChanges 监听选项。

Web version 9

import { collection, onSnapshot, where, query } from "firebase/firestore";

const q = query(collection(db, "cities"), where("state", "==", "CA"));
onSnapshot(q, { includeMetadataChanges: true }, (snapshot) => {
    snapshot.docChanges().forEach((change) => {
        if (change.type === "added") {
            console.log("New city: ", change.doc.data());
        }

        const source = snapshot.metadata.fromCache ? "local cache" : "server";
        console.log("Data came from " + source);
    });
});

Web version 8

db.collection("cities").where("state", "==", "CA")
  .onSnapshot({ includeMetadataChanges: true }, (snapshot) => {
      snapshot.docChanges().forEach((change) => {
          if (change.type === "added") {
              console.log("New city: ", change.doc.data());
          }

          var source = snapshot.metadata.fromCache ? "local cache" : "server";
          console.log("Data came from " + source);
      });
  });
Swift
注意:此产品不适用于 watchOS 和 App Clip 目标。
// Listen to metadata updates to receive a server snapshot even if
// the data is the same as the cached data.
db.collection("cities").whereField("state", isEqualTo: "CA")
    .addSnapshotListener(includeMetadataChanges: true) { querySnapshot, error in
        guard let snapshot = querySnapshot else {
            print("Error retreiving snapshot: \(error!)")
            return
        }

        for diff in snapshot.documentChanges {
            if diff.type == .added {
                print("New city: \(diff.document.data())")
            }
        }

        let source = snapshot.metadata.isFromCache ? "local cache" : "server"
        print("Metadata: Data fetched from \(source)")
}
Objective-C
注意:此产品不适用于 watchOS 和 App Clip 目标。
// Listen to metadata updates to receive a server snapshot even if
// the data is the same as the cached data.
[[[db collectionWithPath:@"cities"] queryWhereField:@"state" isEqualTo:@"CA"]
    addSnapshotListenerWithIncludeMetadataChanges:YES
    listener:^(FIRQuerySnapshot *snapshot, NSError *error) {
      if (snapshot == nil) {
        NSLog(@"Error retreiving snapshot: %@", error);
        return;
      }
      for (FIRDocumentChange *diff in snapshot.documentChanges) {
        if (diff.type == FIRDocumentChangeTypeAdded) {
          NSLog(@"New city: %@", diff.document.data);
        }
      }

      NSString *source = snapshot.metadata.isFromCache ? @"local cache" : @"server";
      NSLog(@"Metadata: Data fetched from %@", source);
    }];

Kotlin+KTX

db.collection("cities").whereEqualTo("state", "CA")
    .addSnapshotListener(MetadataChanges.INCLUDE) { querySnapshot, e ->
        if (e != null) {
            Log.w(TAG, "Listen error", e)
            return@addSnapshotListener
        }

        for (change in querySnapshot!!.documentChanges) {
            if (change.type == DocumentChange.Type.ADDED) {
                Log.d(TAG, "New city: ${change.document.data}")
            }

            val source = if (querySnapshot.metadata.isFromCache) {
                "local cache"
            } else {
                "server"
            }
            Log.d(TAG, "Data fetched from $source")
        }
    }

Java

db.collection("cities").whereEqualTo("state", "CA")
        .addSnapshotListener(MetadataChanges.INCLUDE, new EventListener<QuerySnapshot>() {
            @Override
            public void onEvent(@Nullable QuerySnapshot querySnapshot,
                                @Nullable FirebaseFirestoreException e) {
                if (e != null) {
                    Log.w(TAG, "Listen error", e);
                    return;
                }

                for (DocumentChange change : querySnapshot.getDocumentChanges()) {
                    if (change.getType() == Type.ADDED) {
                        Log.d(TAG, "New city:" + change.getDocument().getData());
                    }

                    String source = querySnapshot.getMetadata().isFromCache() ?
                            "local cache" : "server";
                    Log.d(TAG, "Data fetched from " + source);
                }

            }
        });

Dart

db
    .collection("cities")
    .where("state", isEqualTo: "CA")
    .snapshots(includeMetadataChanges: true)
    .listen((querySnapshot) {
  for (var change in querySnapshot.docChanges) {
    if (change.type == DocumentChangeType.added) {
      final source =
          (querySnapshot.metadata.isFromCache) ? "local cache" : "server";

      print("Data fetched from $source}");
    }
  }
});

获取离线数据

如果您在设备离线时获取某个文档,Cloud Firestore 会从缓存中返回数据。

查询集合时,如果没有缓存的文档,系统会返回空结果。提取特定文档时,系统会返回错误。

查询离线数据

查询功能可与离线持久化功能结合使用。您可以直接使用 get 方法或通过监听(如前面的部分所述)来检索查询的结果。当设备离线时,您也可以针对已在本地永久保存的数据创建新的查询,但这些查询最开始只会针对缓存的文档运行。

配置离线查询索引

默认情况下,在执行离线查询时,Firestore SDK 会在其本地缓存中扫描集合内包含的所有文档。鉴于此默认行为,如果用户长时间处于离线状态,离线查询的性能可能会受到影响。

您可以通过配置本地查询索引来提高离线查询的性能:

Swift

Apple 平台 SDK 提供了一个 setIndexConfiguration 方法,可读取用来在服务器上配置索引的相同 JSON 结构配置,并采用相同的索引定义格式

// You will normally read this from a file asset or cloud storage.
let indexConfigJson = """
  {
    indexes: [
        ...
    ],
    fieldOverrides: [
        ...
    ]
  }
"""

// Apply the configuration.
Firestore.firestore().setIndexConfiguration(indexConfigJson)
Objective-C

Apple 平台 SDK 提供了一些 setIndexConfiguration 方法,可读取用来在服务器上配置索引的相同 JSON 结构配置,并采用相同的索引定义格式

// You will normally read this from a file asset or cloud storage.
NSString *indexConfigJson = @" {                   "
                             "   indexes: [        "
                             "     ...             "
                             "   ],                "
                             "   fieldOverrides: [ "
                             "     ...             "
                             "   ]                 "
                             " }                   ";

// Apply the configuration.
[[FIRFirestore firestore] setIndexConfigurationFromJSON:indexConfigJson
                                             completion:^(NSError * _Nullable error) {
    // ...
}];

Java

Android SDK 提供了一个 setIndexConfiguration 方法,可读取用来在服务器上配置索引的相同 JSON 结构配置,并采用相同的索引定义格式

// You will normally read this from a file asset or cloud storage.
String indexConfigJson = " {                   "
                       + "   indexes: [        "
                       + "     ...             "
                       + "   ],                "
                       + "   fieldOverrides: [ "
                       + "     ...             "
                       + "   ]                 "
                       + " }                   ";
// Apply the configuration.
FirebaseFirestore.getInstance().setIndexConfiguration(indexConfigJson);

Kotlin+KTX

Android SDK 提供了一个 setIndexConfiguration 方法,可读取用来在服务器上配置索引的相同 JSON 结构配置,并采用相同的索引定义格式

// You will normally read this from a file asset or cloud storage.
val indexConfigJson = """
{
  indexes: [
      ...
  ],
  fieldOverrides: [
      ...
  ]
}
"""

// Apply the configuration.
FirebaseFirestore.getInstance().setIndexConfiguration(indexConfigJson)

Dart

Flutter SDK 提供了一个 setIndexConfigurationFromJSON 方法,可读取用来在服务器上配置索引的相同 JSON 结构配置,并采用相同的索引定义格式

// You will normally read this from a file asset or cloud storage.
var indexConfigJson = """
{
  indexes: [
      ...
  ],
  fieldOverrides: [
      ...
  ]
}
""";

// Apply the configuration.
await FirebaseFirestore.instance.setIndexConfigurationFromJSON(json: indexConfigJson);

或者,您也可以使用 setIndexConfiguration 方法通过基于类的 API 配置索引。

var indexes = [
  Index(
    collectionGroup: "posts",
    queryScope: QueryScope.collection,
    fields: [
      IndexField(fieldPath: "author", arrayConfig: ArrayConfig.contains),
      IndexField(fieldPath: "timestamp", order: Order.descending)
    ],
  ),
];
await FirebaseFirestore.instance.setIndexConfiguration(indexes: indexes);

具体使用何种离线索引配置,取决于您的应用在离线时会频繁访问哪些集合和文档,以及您对离线性能有何要求。虽然您可以导出后端索引配置以在客户端上使用,但应用的离线访问模式与在线访问模式可能有很大区别,因此您的在线索引配置可能不适合在离线时使用。您需要考虑自己希望应用离线访问哪些集合和文档,同时还兼具高性能。对应用的行为进行分析后,在定义索引时,您还需遵循索引指南中提供的原则。

如需提供离线索引配置以便在客户端应用中加载,请执行以下操作:

  • 对这些配置进行编译并通过您的应用进行分发
  • 从 CDN 下载配置
  • Cloud Storage for Firebase 等存储系统提取配置。

停用和启用网络访问权限

您可以使用以下方法为您的 Cloud Firestore 客户端停用网络访问权限。网络访问权限停用后,所有快照监听器和文档请求都将从缓存中检索结果。写入操作会排入队列,直到网络访问权限重新启用。

Web version 9

import { disableNetwork } from "firebase/firestore";

await disableNetwork(db);
console.log("Network disabled!");
// Do offline actions
// ...

Web version 8

firebase.firestore().disableNetwork()
    .then(() => {
        // Do offline actions
        // ...
    });
Swift
注意:此产品不适用于 watchOS 和 App Clip 目标。
Firestore.firestore().disableNetwork { (error) in
    // Do offline things
    // ...
}
Objective-C
注意:此产品不适用于 watchOS 和 App Clip 目标。
[[FIRFirestore firestore] disableNetworkWithCompletion:^(NSError *_Nullable error) {
  // Do offline actions
  // ...
}];

Kotlin+KTX

db.disableNetwork().addOnCompleteListener {
    // Do offline things
    // ...
}

Java

db.disableNetwork()
        .addOnCompleteListener(new OnCompleteListener<Void>() {
            @Override
            public void onComplete(@NonNull Task<Void> task) {
                // Do offline things
                // ...
            }
        });

Dart

db.disableNetwork().then((_) {
  // Do offline things
});

使用以下方法可重新启用网络访问权限:

Web version 9

import { enableNetwork } from "firebase/firestore";

await enableNetwork(db);
// Do online actions
// ...

Web version 8

firebase.firestore().enableNetwork()
    .then(() => {
        // Do online actions
        // ...
    });
Swift
注意:此产品不适用于 watchOS 和 App Clip 目标。
Firestore.firestore().enableNetwork { (error) in
    // Do online things
    // ...
}
Objective-C
注意:此产品不适用于 watchOS 和 App Clip 目标。
[[FIRFirestore firestore] enableNetworkWithCompletion:^(NSError *_Nullable error) {
  // Do online actions
  // ...
}];

Kotlin+KTX

db.enableNetwork().addOnCompleteListener {
    // Do online things
    // ...
}

Java

db.enableNetwork()
        .addOnCompleteListener(new OnCompleteListener<Void>() {
            @Override
            public void onComplete(@NonNull Task<Void> task) {
                // Do online things
                // ...
            }
        });

Dart

db.enableNetwork().then((_) {
  // Back online
});