Nimbostratus?
Nimbostratus 是一个建立在 Cloud Firestore 之上的响应式数据获取和客户端缓存管理库。
Flutter 的 Cloud Firestore 客户端 API 在获取和流式传输文档方面非常出色。Nimbostratus 扩展了该 API,包含了一些附加功能
- 用于通过 Nimbostratus 内存缓存读取、写入和订阅文档更改的 API。
- 新的数据获取策略,例如 first-cache 和 cache-and-server,用于实现响应式 UI 的常见数据获取实践。
- 通过缓存写入策略支持乐观更新。
用法
读取文档?
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 之前乐观地更新缓存中的文档。如果调用失败并引发错误,乐观的缓存更新将自动回滚。如果批量更新函数在没有引发错误的情况下完成,则乐观更新将被提交并永久生效。
注意事项
-
在客户端进行的缓存更新是内存中的。Firestore 缓存不支持直接写入,因此 Nimbostratus 缓存层位于内存中的 Firestore 持久化缓存之上。重新启动应用程序不会保留您的缓存更改,您需要进行服务器更改,然后 Firestore 将其保留在其单独的缓存中。
-
仅从缓存流式传输文档的查询,例如在使用 `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` 等服务器策略。
await NimbostratusInstance.setDocument(
FirebaseFirestore.instance.collection('users').doc('user-1'),
{ 'name': 'Darth Vader', ...nestedFields }
options: SetOptions(
mergeFields: ['name', 'nestedFields1.nestedFields2...']
)
writePolicy: WritePolicy.cacheAndServer,
);
第一次缓存更新仅使用地图的简单合并,而不是进行更高级的嵌套字段路径更改,这些更改随后将在服务器响应中反映出来。当服务器响应回来时,缓存也将更新为完全合并的服务器更新。这目前是一个待办事项,如果有人想处理以实现与服务器更新的对等,请随时联系。