如果我们想在我们的Flutter应用程序中添加认证和远程数据库,Firebase让我们的生活变得简单。
但在某些情况下,我们想在响应一些事件时运行服务器端的代码,而不是在客户端执行复杂的逻辑。
这方面的例子包括。
- 当我们更新购物车中的物品时,计算总价
- 当我们留下评论时,更新平均评价分数
- 在聊天程序中执行文本审核并删除脏话
云函数很适合这些场景,因为它允许我们安全地访问所有的Firebase服务,并给我们提供比客户端实现更大的控制和灵活性。
但是,为Firebase开发云函数与在Flutter中构建客户端应用程序有很大不同。
首先,我们必须用JavaScript或TypeScript(官方支持的语言),而不是Dart来编写它们。
而且,正如我在Firebase云函数的介绍中所说的,云函数是根据事件 触发的。
Firebase的云函数。触发器
这意味着需要用不同的思维方式来处理云函数。
所以在本教程中,我们将学习如何使用它们来建立一个简单的情绪跟踪应用程序。
而且我们还将设置并运行Firebase本地仿真器,以加快开发速度,这样我们就不必在每次做出改变时都部署我们的函数了。
情绪跟踪应用程序预览
这里是我们将建立的应用程序的预览。
使用Firestore和云功能的情绪跟踪器演示
以下是它的工作原理。
- 每当我们点击其中一种情绪(快乐、中性、悲伤),我们就向Firestore添加一个新的文档,其中有相应的值(例如,
{'mood': '😀'})。 - 反过来,这将触发一个云函数,重新计算所有的总数,并将其写入一个单独的Firestore文档。
- 最后,客户端应用程序监听总数并实时更新计数器。
请注意,这个例子是为了说明问题,可以用更简单的方式实现。我这样设置是为了展示云函数是如何与Cloud Firestore结合工作的。
让我们深入了解一下代码。我们先看Flutter的代码,然后看云函数。最后,我们将用Firebase本地仿真器来运行,并完成客户端应用程序。
在本教程的其余部分,我们将专注于Firestore的具体代码。整个项目可以在GitHub上找到,并且已经被配置成可以在Firebase上运行。
Flutter客户端代码
让我们从一个FirebaseMoodRepository 类开始,我们将用它来与Firebase对话。
class FirebaseMoodRepository {
FirebaseMoodRepository(this._firestore);
final FirebaseFirestore _firestore;
@override
Future<void> addMood(String mood) async {
final ref = _firestore.collection('mood');
await ref.add({'mood': mood});
}
}
目前,我们只需要addMood 方法。
当任何一个表情符号按钮被按下时,我们会调用这个方法并传递相应的字符串值('😀','😐','😟' )。
这个方法得到一个对mood 集合的引用,并添加一个带有单个键值对的新文档。
接下来,我们需要设置一个云函数来计算总数。
Firebase云函数设置
在我们可以运行任何代码之前,我们需要创建一个本地的Firebase项目。如果你对这个不熟悉,你可以按照我的指南来做。
注意:当用
firebase init创建项目时,请确保在交互式提示中同时选择Firestore和Functions。
一旦这样做了,我们就会有一个functions/src/index.ts 文件,我们可以用它来编写我们需要的函数。
更新总数的云函数
接下来,让我们用这段代码取代functions/src/index.ts 的全部内容。
import * as admin from 'firebase-admin'
import * as functions from 'firebase-functions'
// 1. don't forget to initialize the admin SDK
admin.initializeApp();
// 2. this Cloud Function will be triggered
// when a document is created inside mood/{moodId}
// and call the updateCounters() method below
exports.updateMoodCounters = functions.firestore
.document('mood/{moodId}').onCreate((_, __) => updateCounters());
// 3. this is where all the business logic goes
async function updateCounters() {
// setup all counters to 0
var positive = 0
var neutral = 0
var negative = 0
// get the collection data located at `mood`
const firestore = admin.firestore()
const collection = await firestore.collection(`mood`).get()
// iterate through all the documents
for (const doc of collection.docs) {
// extract the mood value
// note: this code uses *destructuring*
const { mood } = doc.data()
// update the counters
if (mood === '😀') {
positive++
} else if (mood === '😐') {
neutral++
} else if (mood === '😟') {
negative++
}
}
// log the values (for debugging purposes)
console.log({
'positive': positive,
'neutral': neutral,
'negative': negative,
})
// write the updated counters to `totals/mood`
return await firestore.doc(`totals/mood`).set({
'positive': positive,
'neutral': neutral,
'negative': negative,
})
}
一些注意事项。
- 在顶部,我们调用
admin.initializeApp(),因为我们需要它来使用管理SDK。 - 我们注册一个云函数,当里面有一个文件被创建时,就会被触发。
mood/{moodId} - 在
updateCounters(),我们循环浏览所有的文件,计算出更新的总数,并将其写入totals/mood
这就是了!这就是我们需要更新Firestore中的总数的所有代码。
但我们如何运行它呢?
运行云函数
如果我们想在生产中运行这段代码,我们需要部署我们的新函数。一旦我们确信我们的代码是有效的,这是一个好办法。
但是,每次我们做出改变时都要部署我们的函数是不现实的。更不用说,如果我们想在服务器上运行任何功能,我们需要注册FirebaseBlaze计划(并输入我们的信用卡信息)。
出于测试的目的,用Firebase本地仿真器来运行所有的东西要快得多。
所以我们来设置一下。
在Flutter中使用Firebase本地仿真器
Firebase本地模拟器是Firebase命令行工具的一部分,它允许我们选择我们想在本地模拟的Firebase服务。
我们甚至可以混合搭配,在本地运行云功能,同时用生产数据库保存数据。
关于如何设置的一个很好的概述,请看官方文档。
最重要的是,我们可以运行firebase init emulators ,选择我们要使用的仿真器。
=== Emulators Setup
? Which Firebase emulators do you want to set up? Press Space to select emulators, then Enter to confirm your choices. (Press <space> to select, <a> to toggle all
, <i> to invert selection)
❯◯ Authentication Emulator
◉ Functions Emulator
◉ Firestore Emulator
◯ Database Emulator
◯ Hosting Emulator
◯ Pub/Sub Emulator
◯ Storage Emulator
运行这个命令后,我们会看到所选的仿真器被列在firebase.json 文件中。
{
"emulators": {
"functions": {
"port": 8081
},
"firestore": {
"port": 8080
}
}
}
然后我们可以记下这些端口,并设置我们的Flutter应用来使用这些模拟器。
要做到这一点,我们可以在main.dart 内添加以下代码。
void main() async {
WidgetsFlutterBinding.ensureInitialized();
// standard Firebase setup
await Firebase.initializeApp();
// Use local Firestore emulator
final firestore = FirebaseFirestore.instance;
firestore.settings =
const Settings(persistenceEnabled: false, sslEnabled: false);
firestore.useFirestoreEmulator('localhost', 8080);
runApp(const ProviderScope(child: MyApp()));
}
在这种情况下,我们告诉Firebase使用Firestore模拟器在localhost的端口8080 (而不是使用生产DB)。
启动本地仿真器
还有两个步骤要做
- 从
functions文件夹中运行npm run build,编译我们的云功能 - 运行
firebase emulators:start,启动仿真器。
这应该给我们提供以下输出。
i emulators: Starting emulators: functions, firestore
⚠ functions: The following emulators are not running, calls to these services from the Functions emulator will affect production: auth, database, hosting, pubsub, storage
✔ functions: Using node@14 from host.
i firestore: Firestore Emulator logging to firestore-debug.log
⚠ ui: Emulator UI unable to start on port 4000, starting on 4002 instead.
i ui: Emulator UI logging to ui-debug.log
i functions: Watching "/Users/andrea/work/codewithandrea/github/flutter/mood_tracker_flutter/functions" for Cloud Functions...
✔ functions[us-central1-updateMoodCounters]: firestore function initialized.
┌─────────────────────────────────────────────────────────────┐
│ ✔ All emulators ready! It is now safe to connect your app. │
│ i View Emulator UI at http://localhost:4002 │
└─────────────────────────────────────────────────────────────┘
┌───────────┬────────────────┬─────────────────────────────────┐
│ Emulator │ Host:Port │ View in Emulator UI │
├───────────┼────────────────┼─────────────────────────────────┤
│ Functions │ localhost:8081 │ http://localhost:4002/functions │
├───────────┼────────────────┼─────────────────────────────────┤
│ Firestore │ localhost:8080 │ http://localhost:4002/firestore │
└───────────┴────────────────┴─────────────────────────────────┘
Emulator Hub running at localhost:4400
Other reserved ports: 4500
Issues? Report them at https://github.com/firebase/firebase-tools/issues and attach the *-debug.log files.
注意,functions[us-central1-updateMoodCounters] 函数已经被初始化了,这就是我们想要的。
运行Flutter应用程序
如果我们现在运行应用程序,我们可以点击表情符号按钮,看到我们的云函数调用被打印到控制台。
i functions: Beginning execution of "us-central1-updateMoodCounters"
> { positive: 0, neutral: 1, negative: 0 }
i functions: Finished "us-central1-updateMoodCounters" in ~1s
i functions: Beginning execution of "us-central1-updateMoodCounters"
> { positive: 0, neutral: 2, negative: 0 }
i functions: Finished "us-central1-updateMoodCounters" in ~1s
i functions: Beginning execution of "us-central1-updateMoodCounters"
> { positive: 1, neutral: 2, negative: 0 }
i functions: Finished "us-central1-updateMoodCounters" in ~1s
i functions: Beginning execution of "us-central1-updateMoodCounters"
> { positive: 1, neutral: 3, negative: 0 }
i functions: Finished "us-central1-updateMoodCounters" in ~1s
这证实了一切都按预期进行。
如果我们打开Firebase仿真器用户界面,选择Firestore标签,我们就可以看到所有的数据。
Firestore仿真器界面
完成Flutter应用程序
最后要做的是再写一些Dart代码来获得总数并在用户界面上显示出来。
显示总数
由于我们是优秀的程序员,我们可以定义一个类型安全的模型类来表示心情总数。
class MoodTotals {
MoodTotals({
required this.positive,
required this.neutral,
required this.negative,
});
final int positive;
final int neutral;
final int negative;
// helper method to be used when there is no data
MoodTotals.zero()
: positive = 0,
neutral = 0,
negative = 0;
Map<String, dynamic> toMap() {
return {
'positive': positive,
'neutral': neutral,
'negative': negative,
};
}
factory MoodTotals.fromMap(Map<String, dynamic>? map) {
// this will be null if the totals were never written before
if (map == null) {
// hence, return all zeros
return MoodTotals.zero();
}
// else, parse the values from the map
return MoodTotals(
positive: map['positive'],
neutral: map['neutral'],
negative: map['negative'],
);
}
@override
String toString() =>
'Mood(positive: $positive, neutral: $neutral, negative: $negative)';
}
然后我们可以像这样更新我们的FirebaseMoodRepository 。
class FirebaseMoodRepository {
FirebaseMoodRepository(this._firestore);
final FirebaseFirestore _firestore;
@override
Future<void> addMood(String mood) async {
final ref = _firestore.collection('mood');
await ref.add({'mood': mood});
}
@override
Stream<MoodTotals> moodTotals() {
final ref = _firestore.doc('totals/mood').withConverter(
fromFirestore: (doc, _) => MoodTotals.fromMap(doc.data()),
toFirestore: (MoodTotals mood, options) => mood.toMap());
return ref
.snapshots()
// return snapshot.data() or fallback if the document doesn't exist
.map((snapshot) => snapshot.data() ?? MoodTotals.zero());
}
}
关于moodTotals 方法,有几件事需要注意。
- 我们读取
totals/mood里面的文档数据。这正是我们的云函数所写入的文档。 - 我们使用
withConverter来序列化文档数据。 - 我们映射到
ref.snapshots(),从每个快照中提取数据,如果文档不存在,则使用MoodTotals.zero()作为后备。
这就完成了我们的FirestoreDatabase 实现,我们只需要使用新的Stream 来实时重建我们的用户界面。我不会在这里介绍这些,但你可以在GitHub上找到完整的源代码。
瞧,我们的应用程序现在可以按计划工作了。
使用Firestore和云功能的情绪跟踪器演示
总结
我们的应用程序现在已经完成了,我们学到了一些非常有用的东西。
- 如何编写云函数以响应Firestore的触发器
- 如何设置和运行Firebase本地仿真器
- 如何使用类型安全的模型类和转换器来读写Firestore数据。
这个简单的例子为我们用云函数创建更复杂的应用程序提供了一个良好的起点。而通过使用Firebase本地仿真器,我们也得到了一些生产力的提升。
参考资料
以下是我在准备本教程时发现的一些额外指南。
编码愉快!