Nimbostratus?

Nimbostratus 是一个建立在 Cloud Firestore 之上的响应式数据获取和客户端缓存管理库。

Flutter 的 Cloud Firestore 客户端 API 在获取和流式传输文档方面非常出色。Nimbostratus 扩展了该 API,包含了一些附加功能

  1. 用于通过 Nimbostratus 内存缓存读取、写入和订阅文档更改的 API。
  2. 新的数据获取策略,例如 first-cache 和 cache-and-server,用于实现响应式 UI 的常见数据获取实践。
  3. 通过缓存写入策略支持乐观更新。

用法

读取文档?

import 'package:nimbostratus/nimbostratus.dart';

final snap = await Nimbostratus.instance.getDocument(
  FirebaseFirestore.instance.collection('users').doc('alice'),
  fetchPolicy: GetFetchPolicy.cacheFirst,
);

在此示例中,我们首先请求从缓存读取 Firestore 文档,如果不可用,则回退到服务器。有几种方便的获取策略可供选择,您可以在 此处 查看。

流式传输文档?

文档可以类似地从缓存、服务器或两者的组合流式传输。

final documentStream = Nimbostratus.instance
  .streamDocument(
    FirebaseFirestore.instance.collection('users').doc('user-1'),
    fetchPolicy: StreamFetchPolicy.cacheAndServer,
  ).listen((snap) {
    print(snap.data());
    // { 'id': 'user-1', 'name': 'Anakin Skywalker' }
    // Later when the document changes on the server:
    // { 'id': 'user-1', 'name': 'Darth Vader' }
  });

在本例中,我们正在从缓存和服务器流式传输文档 `users/user-1`。像这样的获取策略可能很有价值,因为数据可以从缓存中急切地返回,以创建流畅的用户体验,同时保持对服务器未来更改的订阅。

流式传输的文档也会在缓存发生更改时进行更新。在下面的示例中,我们可以手动更新内存缓存中的值,从而导致客户端中流式传输该文档的所有位置都更新。

final docRef = FirebaseFirestore.instance.collection('users').doc('user-1');

final documentStream = Nimbostratus.instance
  .streamDocument(
    docRef,
    fetchPolicy: StreamFetchPolicy.cacheAndServer,
  ).listen((snap) {
    print(snap.data());
    // { 'id': 'user-1', 'name': 'Anakin Skywalker' }
    // { 'id': 'user-1', 'name': 'Darth Vader' }
  });

await NimbostratusInstance.updateDocument(
  docRef,
  { 'name': 'Darth Vader' }
  writePolicy: WritePolicy.cacheOnly,
);

执行和响应客户端缓存更改是 `cloud_firestore` 包含的功能集中的一个有意留下的空白,因为它旨在充当相对简单的文档获取层,而不是文档管理层。Nimbostratus 旨在填补这一空白,并提供与具有更广泛客户端 API 的其他数据获取库类似的功能。

查询文档?

查询文档遵循类似的模式。

final stream = Nimbostratus.instance
  .streamDocuments(
    store.collection('users').where('first_name', isEqualTo: 'Ben'),
    fetchPolicy: StreamFetchPolicy.serverFirst,
  ).listen((snap) {
    print(snap.data());
    // [
    //   { 'id': 'user-1', 'first_name': 'Ben', 'last_name': 'Kenobi', 'side': 'light' },
    //   { 'id': 'user-2', 'first_name': 'Ben', 'last_name': 'Solo', 'side': 'light' }
    // ]
  });

使用上面显示的 `serverFirst` 策略,数据将首先从服务器传递到流一次,然后流将监听缓存数据的任何更改。当稍后发生缓存更新时,例如:

await NimbostratusInstance.updateDocument(
   store.collection('users').doc('user-2'),
  { 'side': 'dark' }
  writePolicy: WritePolicy.cacheAndServer,
);

由于流订阅了 `cacheAndServer` 写入策略对 `user-1` 和 `user-2` 文档的任何更改,因此流将立即从缓存接收更新的查询快照。

// [
//   { 'id': 'user-1', 'first_name': 'Ben', 'last_name': 'Kenobi', 'side': 'light' },
//   { 'id': 'user-2', 'first_name': 'Ben', 'last_name': 'Solo', 'side': 'dark' }
// ]

然后,如果服务器响应与初始缓存响应不同,例如在我们上次查询该文档后在服务器上添加了另一个字段,则会稍后发出另一个值。

// [
//   { 'id': 'user-1', 'first_name': 'Ben', 'last_name': 'Kenobi', 'side': 'light' },
//   { 'id': 'user-2', 'first_name': 'Ben', 'last_name': 'Solo', 'side': 'dark', 'relationship_status: 'complicated' }
// ]

乐观更新 ⚡️

上面示例中的 `cacheAndServer` 策略是 **乐观** 写入策略。更新首先被乐观地写入缓存,然后如果服务器响应失败,缓存的更改将回滚到最新值。乐观更新使得有可能向用户呈现即时更新的值,并使应用程序感觉实时和快速,同时确保如果出现问题,体验可以回滚到一致的服务器状态。

批量更新?

Firestore 支持使用 batch API 将多个文档更新批量处理在一起。在使用 batchUpdateDocuments API 进行批量处理时,我们可以利用 Nimbostratus 数据获取和写入功能。

await Nimbostratus.instance.batchUpdateDocuments((batch) async {
  await batch.update(
    store.collection('users').doc('darth_maul'),
    { "alive": false },
    writePolicy: WritePolicy.cacheAndServer,
  );

  await batch.update(
    store.collection('users').doc('qui_gon'),
    { "alive": false },
    writePolicy: WritePolicy.cacheAndServer,
  );

  await batch.commit();
});

在此示例中,我们再次使用 `cacheAndServer` 策略来乐观地应用我们的缓存更新。使用 Firestore 批处理时的区别在于,服务器更新直到 `commit()` 调用成功才最终确定。如果服务器响应失败并且 `commit()` 引发异常,则乐观缓存的更改也将被回滚。

在其他情况下,您仍然希望为非通过 Firestore 进行的远程更新执行缓存中的乐观更新,例如通过 Cloud Function 间接更新 Firestore 中的文档。

await Nimbostratus.instance.batchUpdateDocuments((batch) async {
  await batch.update(
    store.collection('users').doc('darth_maul'),
    { "alive": false },
    writePolicy: WritePolicy.cacheOnly,
  );

  await batch.update(
    store.collection('users').doc('qui_gon'),
    { "alive": false },
    writePolicy: WritePolicy.cacheOnly,
  );

  await FirebaseFunctions.instance.httpsCallable('finish_episode_1').call();
});

在此示例中,我们在调用 Cloud Function 之前乐观地更新缓存中的文档。如果调用失败并引发错误,乐观的缓存更新将自动回滚。如果批量更新函数在没有引发错误的情况下完成,则乐观更新将被提交并永久生效。

注意事项

  1. 在客户端进行的缓存更新是内存中的。Firestore 缓存不支持直接写入,因此 Nimbostratus 缓存层位于内存中的 Firestore 持久化缓存之上。重新启动应用程序不会保留您的缓存更改,您需要进行服务器更改,然后 Firestore 将其保留在其单独的缓存中。

  2. 仅从缓存流式传输文档的查询,例如在使用 `StreamFetchPolicy.cacheOnly` 时,只会响应其当前文档的更改进行更新,而不会响应新添加的文档。例如,如果我们有这样的查询:

final stream = Nimbostratus.instance
  .streamDocuments(
    store.collection('users').where('first_name', isEqualTo: 'Ben'),
    fetchPolicy: StreamFetchPolicy.cacheOnly,
  ).listen((snap) {
    print(snap.data());
    // [
    //   { 'id': 'user-1', 'first_name': 'Ben', 'last_name': 'Kenobi', 'side': 'light' },
    //   { 'id': 'user-2', 'first_name': 'Ben', 'last_name': 'Solo', 'side': 'light' }
    // ]
  });

稍后,如果在缓存中添加了一个名为 `first_name` 为“Ben”的新文档,此查询将不会使用新用户更新,因为它不知道他们应该被包含在内。如果我们将 `user-2` 更新为 `first_name: Han`,则情况正好相反。即使 `user-2` 不再满足查询的过滤器,查询仍然会从流中重新发出 `user-2`。为了流式传输重新评估查询的文档,您需要使用 `StreamFetchPolicy.cacheAndServer` 或 `StreamFetchPolicy.serverOnly` 等服务器策略。

  1. 当前的缓存更新不像服务器那样合并数据。Firestore 的 setupdate API 支持文档字段的一些 高级数据合并选项。当我们像这样对缓存进行更新时:

await NimbostratusInstance.setDocument(
  FirebaseFirestore.instance.collection('users').doc('user-1'),
  { 'name': 'Darth Vader', ...nestedFields }
  options: SetOptions(
    mergeFields: ['name', 'nestedFields1.nestedFields2...']
  )
  writePolicy: WritePolicy.cacheAndServer,
);

第一次缓存更新仅使用地图的简单合并,而不是进行更高级的嵌套字段路径更改,这些更改随后将在服务器响应中反映出来。当服务器响应回来时,缓存也将更新为完全合并的服务器更新。这目前是一个待办事项,如果有人想处理以实现与服务器更新的对等,请随时联系。

GitHub

查看 Github