数据捆绑包
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
// ...
}
fs_bundle_load.js
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
// ...
}
test.solution-bundles.js
Swift
注意:此产品不适用于 watchOS 和 App Clip 目标。
// 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)
}
}
SolutionBundles.swift
Objective-C
注意:此产品不适用于 watchOS 和 App Clip 目标。
// 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
}];
}
FIRSolutionsBundleViewController.m
Kotlin
@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
// ...
}
}
SolutionBundles.kt
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
// ...
}
});
}
SolutionBundles.java
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,
),
);
firestore.dart
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> &){
// ...
});
});
});
snippets.cpp
请注意,如果从不到 30 分钟前构建的捆绑包中加载命名的查询,那么等到您利用查询从后端(而不是缓存)读取数据后,您只需为更新文档以匹配后端存储的数据所需的数据库读取付费;也就是说,您只需为增量付费。
后续步骤
- 请参阅适用于客户端(Apple、Android、Web)和服务器端 (Node.js) 的数据捆绑包 API 参考文档。
- 请参阅使用 Cloud Functions 和 Firebase Hosting 解决方案构建和分发捆绑包(如果您还没有读过的话)。
