Cloud Firestore 数据捆绑包是您根据 Cloud Firestore 文档和查询快照而构建,并发布在 CDN、托管服务或其他解决方案上的静态数据文件。数据捆绑包中包含您要向客户端应用交付的文档,以及关于生成这些文档的查询的元数据。您可以使用客户端 SDK 通过网络或从本地存储空间下载软件包,之后再将软件包数据加载到 Cloud Firestore 本地缓存。捆绑包加载后,客户端应用可以从本地缓存或后端查询文档。
借助数据捆绑包,您的应用可以更快地加载常见查询的结果,因为启动时文档就已经就绪,无需调用 Cloud Firestore 后端。如果结果是从本地缓存加载的,还可降低访问成本。您无需为查询相同的 100 个初始文档的 100 万个应用实例付费,而只需为将这 100 个文档打包所需的查询付费。
Cloud Firestore 数据捆绑包可与其他 Firebase 后端产品搭配使用。请查看我们提供的集成解决方案,在这种解决方案中,软件包由 Cloud Functions 构建并通过 Firebase Hosting 向用户分发。
将捆绑包与您的应用一起使用涉及以下三个步骤:
- 使用 Admin SDK 构建捆绑包
- 从本地存储空间或从 CDN 分发捆绑包
- 在客户端中加载捆绑包
什么是数据捆绑包?
数据软件包是一个由您构建的静态二进制文件,可用于封装一个或多个文档和/或查询快照,您也可以从中提取“命名的查询”。服务器端 SDK 让您可以构建捆绑包,而客户端 SDK 提供了将捆绑包加载到本地缓存的方法,我们将在下文展开讨论。
“命名的查询”是捆绑包特别强大的一个功能。命名的查询是您可以从捆绑包中提取的 Query
对象,提取后您可以立即从缓存或后端查询数据,就像您平常在应用内与 Cloud Firestore 通讯时所做的那样。
在服务器上构建数据捆绑包
利用 Node.js 或 Java Admin SDK,您可以完全控制要在捆绑包中包含哪些内容以及分发它们的方式。
Node.js
var bundleId = "latest-stories"; var bundle = firestore.bundle(bundleId); var docSnapshot = await firestore.doc('stories/stories').get(); var querySnapshot = await firestore.collection('stories').get(); // Build the bundle // Note how querySnapshot is named "latest-stories-query" var bundleBuffer = bundle.add(docSnapshot); // Add a document .add('latest-stories-query', querySnapshot) // Add a named query. .build()
Java
Firestore db = FirestoreClient.getFirestore(app); // Query the 50 latest stories QuerySnapshot latestStories = db.collection("stories") .orderBy("timestamp", Direction.DESCENDING) .limit(50) .get() .get(); // Build the bundle from the query results FirestoreBundle bundle = db.bundleBuilder("latest-stories") .add("latest-stories-query", latestStories) .build();
Python
from google.cloud import firestore from google.cloud.firestore_bundle import FirestoreBundle db = firestore.Client() bundle = FirestoreBundle("latest-stories") doc_snapshot = db.collection("stories").document("news-item").get() query = db.collection("stories")._query() # Build the bundle # Note how `query` is named "latest-stories-query" bundle_buffer: str = bundle.add_document(doc_snapshot).add_named_query( "latest-stories-query", query, ).build()
分发数据捆绑包
您可以通过从 CDN 分发或从 Cloud Storage 等位置下载的方式,将捆绑包分发到客户端应用。
假设在上一部分中创建的捆绑包已保存到名为 bundle.txt
的文件中,并发布到了服务器上。您可以像分发任何其他资产一样通过网络分发此捆绑包文件,如下方的一个简单 Node.js Express 应用的示例所示。
const fs = require('fs');
const server = require('http').createServer();
server.on('request', (req, res) => {
const src = fs.createReadStream('./bundle.txt');
src.pipe(res);
});
server.listen(8000);
在客户端中加载数据捆绑包
您可以从远程服务器提取 Firestore 捆绑包以进行加载,提取方法包括:发出 HTTP 请求、调用 Storage API 或使用任何其他方法来提取网络中的二进制文件。
提取后,您的应用会使用 Cloud Firestore 客户端 SDK 调用 loadBundle
方法,该方法会返回一个任务跟踪对象,让您可以像监控 Promise 的状态一样监控上述过程的完成情况。捆绑包加载任务成功完成后,便可在本地缓存中获取捆绑包内容了。
Web
import { loadBundle, namedQuery, getDocsFromCache } from "firebase/firestore"; async function fetchFromBundle() { // Fetch the bundle from Firebase Hosting, if the CDN cache is hit the 'X-Cache' // response header will be set to 'HIT' const resp = await fetch('/createBundle'); // Load the bundle contents into the Firestore SDK await loadBundle(db, resp.body); // Query the results from the cache const query = await namedQuery(db, 'latest-stories-query'); const storiesSnap = await getDocsFromCache(query); // Use the results // ... }
Web
// If you are using module bundlers. import firebase from "firebase/app"; import "firebase/firestore"; import "firebase/firestore/bundle"; // This line enables bundle loading as a side effect. // ... async function fetchFromBundle() { // Fetch the bundle from Firebase Hosting, if the CDN cache is hit the 'X-Cache' // response header will be set to 'HIT' const resp = await fetch('/createBundle'); // Load the bundle contents into the Firestore SDK await db.loadBundle(resp.body); // Query the results from the cache // Note: omitting "source: cache" will query the Firestore backend. const query = await db.namedQuery('latest-stories-query'); const storiesSnap = await query.get({ source: 'cache' }); // Use the results // ... }
Swift
// Utility function for errors when loading bundles. func bundleLoadError(reason: String) -> NSError { return NSError(domain: "FIRSampleErrorDomain", code: 0, userInfo: [NSLocalizedFailureReasonErrorKey: reason]) } func fetchRemoteBundle(for firestore: Firestore, from url: URL) async throws -> LoadBundleTaskProgress { guard let inputStream = InputStream(url: url) else { let error = self.bundleLoadError(reason: "Unable to create stream from the given url: \(url)") throw error } return try await firestore.loadBundle(inputStream) } // Fetches a specific named query from the provided bundle. func loadQuery(named queryName: String, fromRemoteBundle bundleURL: URL, with store: Firestore) async throws -> Query { let _ = try await fetchRemoteBundle(for: store, from: bundleURL) if let query = await store.getQuery(named: queryName) { return query } else { throw bundleLoadError(reason: "Could not find query named \(queryName)") } } // Load a query and fetch its results from a bundle. func runStoriesQuery() async { let queryName = "latest-stories-query" let firestore = Firestore.firestore() let remoteBundle = URL(string: "https://example.com/createBundle")! do { let query = try await loadQuery(named: queryName, fromRemoteBundle: remoteBundle, with: firestore) let snapshot = try await query.getDocuments() print(snapshot) // handle query results } catch { print(error) } }
Objective-C
// Utility function for errors when loading bundles. - (NSError *)bundleLoadErrorWithReason:(NSString *)reason { return [NSError errorWithDomain:@"FIRSampleErrorDomain" code:0 userInfo:@{NSLocalizedFailureReasonErrorKey: reason}]; } // Loads a remote bundle from the provided url. - (void)fetchRemoteBundleForFirestore:(FIRFirestore *)firestore fromURL:(NSURL *)url completion:(void (^)(FIRLoadBundleTaskProgress *_Nullable, NSError *_Nullable))completion { NSInputStream *inputStream = [NSInputStream inputStreamWithURL:url]; if (inputStream == nil) { // Unable to create input stream. NSError *error = [self bundleLoadErrorWithReason: [NSString stringWithFormat:@"Unable to create stream from the given url: %@", url]]; completion(nil, error); return; } [firestore loadBundleStream:inputStream completion:^(FIRLoadBundleTaskProgress * _Nullable progress, NSError * _Nullable error) { if (progress == nil) { completion(nil, error); return; } if (progress.state == FIRLoadBundleTaskStateSuccess) { completion(progress, nil); } else { NSError *concreteError = [self bundleLoadErrorWithReason: [NSString stringWithFormat: @"Expected bundle load to be completed, but got %ld instead", (long)progress.state]]; completion(nil, concreteError); } completion(nil, nil); }]; } // Loads a bundled query. - (void)loadQueryNamed:(NSString *)queryName fromRemoteBundleURL:(NSURL *)url withFirestore:(FIRFirestore *)firestore completion:(void (^)(FIRQuery *_Nullable, NSError *_Nullable))completion { [self fetchRemoteBundleForFirestore:firestore fromURL:url completion:^(FIRLoadBundleTaskProgress *progress, NSError *error) { if (error != nil) { completion(nil, error); return; } [firestore getQueryNamed:queryName completion:^(FIRQuery *query) { if (query == nil) { NSString *errorReason = [NSString stringWithFormat:@"Could not find query named %@", queryName]; NSError *error = [self bundleLoadErrorWithReason:errorReason]; completion(nil, error); return; } completion(query, nil); }]; }]; } - (void)runStoriesQuery { NSString *queryName = @"latest-stories-query"; FIRFirestore *firestore = [FIRFirestore firestore]; NSURL *bundleURL = [NSURL URLWithString:@"https://example.com/createBundle"]; [self loadQueryNamed:queryName fromRemoteBundleURL:bundleURL withFirestore:firestore completion:^(FIRQuery *query, NSError *error) { // Handle query results }]; }
Kotlin+KTX
@Throws(IOException::class) fun getBundleStream(urlString: String?): InputStream { val url = URL(urlString) val connection = url.openConnection() as HttpURLConnection return connection.inputStream } @Throws(IOException::class) fun fetchFromBundle() { val bundleStream = getBundleStream("https://example.com/createBundle") val loadTask = db.loadBundle(bundleStream) // Chain the following tasks // 1) Load the bundle // 2) Get the named query from the local cache // 3) Execute a get() on the named query loadTask.continueWithTask<Query> { task -> // Close the stream bundleStream.close() // Calling .result propagates errors val progress = task.getResult(Exception::class.java) // Get the named query from the bundle cache db.getNamedQuery("latest-stories-query") }.continueWithTask { task -> val query = task.getResult(Exception::class.java)!! // get() the query results from the cache query.get(Source.CACHE) }.addOnCompleteListener { task -> if (!task.isSuccessful) { Log.w(TAG, "Bundle loading failed", task.exception) return@addOnCompleteListener } // Get the QuerySnapshot from the bundle val storiesSnap = task.result // Use the results // ... } }
Java
public InputStream getBundleStream(String urlString) throws IOException { URL url = new URL(urlString); HttpURLConnection connection = (HttpURLConnection) url.openConnection(); return connection.getInputStream(); } public void fetchBundleFrom() throws IOException { final InputStream bundleStream = getBundleStream("https://example.com/createBundle"); LoadBundleTask loadTask = db.loadBundle(bundleStream); // Chain the following tasks // 1) Load the bundle // 2) Get the named query from the local cache // 3) Execute a get() on the named query loadTask.continueWithTask(new Continuation<LoadBundleTaskProgress, Task<Query>>() { @Override public Task<Query> then(@NonNull Task<LoadBundleTaskProgress> task) throws Exception { // Close the stream bundleStream.close(); // Calling getResult() propagates errors LoadBundleTaskProgress progress = task.getResult(Exception.class); // Get the named query from the bundle cache return db.getNamedQuery("latest-stories-query"); } }).continueWithTask(new Continuation<Query, Task<QuerySnapshot>>() { @Override public Task<QuerySnapshot> then(@NonNull Task<Query> task) throws Exception { Query query = task.getResult(Exception.class); // get() the query results from the cache return query.get(Source.CACHE); } }).addOnCompleteListener(new OnCompleteListener<QuerySnapshot>() { @Override public void onComplete(@NonNull Task<QuerySnapshot> task) { if (!task.isSuccessful()) { Log.w(TAG, "Bundle loading failed", task.getException()); return; } // Get the QuerySnapshot from the bundle QuerySnapshot storiesSnap = task.getResult(); // Use the results // ... } }); }
Dart
// Get a bundle from a server final url = Uri.https('example.com', '/create-bundle'); final response = await http.get(url); String body = response.body; final buffer = Uint8List.fromList(body.codeUnits); // Load a bundle from a buffer LoadBundleTask task = FirebaseFirestore.instance.loadBundle(buffer); await task.stream.toList(); // Use the cached named query final results = await FirebaseFirestore.instance.namedQueryGet( "latest-stories-query", options: const GetOptions( source: Source.cache, ), );
C++
db->LoadBundle("bundle_name", [](const LoadBundleTaskProgress& progress) { switch(progress.state()) { case LoadBundleTaskProgress::State::kError: { // The bundle load has errored. Handle the error in the returned future. return; } case LoadBundleTaskProgress::State::kInProgress: { std::cout << "Bytes loaded from bundle: " << progress.bytes_loaded() << std::endl; break; } case LoadBundleTaskProgress::State::kSuccess: { std::cout << "Bundle load succeeeded" << std::endl; break; } } }).OnCompletion([db](const Future<LoadBundleTaskProgress>& future) { if (future.error() != Error::kErrorOk) { // Handle error... return; } const std::string& query_name = "latest_stories_query"; db->NamedQuery(query_name).OnCompletion([](const Future<Query>& query_future){ if (query_future.error() != Error::kErrorOk) { // Handle error... return; } const Query* query = query_future.result(); query->Get().OnCompletion([](const Future<QuerySnapshot> &){ // ... }); }); });
请注意,如果从不到 30 分钟前构建的捆绑包中加载命名的查询,那么等到您利用查询从后端(而不是缓存)读取数据后,您只需为更新文档以匹配后端存储的数据所需的数据库读取付费;也就是说,您只需为增量付费。
后续步骤
请参阅适用于客户端(Apple、Android、Web)和服务器端 (Node.js) 的数据捆绑包 API 参考文档。
请参阅使用 Cloud Functions 和 Firebase Hosting 解决方案构建和分发捆绑包(如果您还没有读过的话)。