原文链接:www.ably.io/blog/airtab… 作者:Srushtika Neelakantam
在本文中,我们将以群聊应用为例,了解如何使用Airtable来存储实时消息。我们将使用Ably的实时基础架构来为聊天应用提供动力,并使用Web Hooks以正确的顺序直接从Ably发布消息Airtable。
查看GitHub上用VueJS编写的群聊应用程序的完整源代码,以及https:///realtime-chat-的应用程序现场演示storage.ably.dev/
什么是Airtable?
Airtable将自己描述为“部分电子表格,部分数据库,完全灵活”,这正是这个词的含义。它以其强大的REST API和非常漂亮的可视化UI(具有自定义字段来管理和表示数据)来迎合组织中的工程和商业部门。它将任务管理器、数据库、CRM、电子表格等各种工具组合成一个产品。
Airtable中的示例表
AirtableREST API
Airtable附带了一个简单的REST API,用于对存储的数据执行基本的CRUD操作。在您查看文档之前,您需要建立一个基础,即一个表/表。这是有充分理由的——他们的整个文档都是动态显示的,包括真实的键、id、列名等,以及基于您的数据的示例响应,这使得您可以非常容易地复制代码并按原样使用。他们用cURL和JavaScript提供这些文档。JS代码片段需要使用AirtableJavaScript Client SDK。下面是聊天应用程序库的文档。
AirtableREST API
如何使用Airtable作为数据库
在本例中,我们将查看两个操作--从Airtable存储和检索数据。我们将使用Web Hooks在每次发布新聊天消息时向AirtableAPI发送创建记录REST请求。然后,我们将使用“列表记录”根据用户请求检索以前存储的消息。下面是数据库的一个子集,因此您可以了解模式,或者简单地说,我们数据库中的列名table/spreadsheet:
使用Airtable作为数据库:列表记录示例
每个新消息都有一个唯一的(随机创建的)msgId。这将是我们的主键。数据由ID列按升序预先排序,ID列是Airtable自动分配给每个新记录的增量数。
与Ably和Web Hooks实时更新
如果您已经使用Ab ly,您可以跳过此部分,如果没有,您可以通过创建帐户开始。Ab ly提供了具有高可扩展性的可靠实时消息传递基础架构。它主要通过WebSocket运行,并提供开箱即用的Pub/Sub消息传递基础架构。它与协议和平台无关,因为您可以将它与WebSocket、MQTT或SSE以及您正在使用的任何语言和平台一起使用。您不必花时间去理解它所解决的硬分布式系统问题,只需简单地用几行代码就可以开始发布和订阅实时数据。
我们将使用Ably的JavaScript实时SDK来支持聊天应用和WebHook集成功能,以将Airtable直接与Ably应用集成。
使用Ab ly集成将实时消息发布到第三方服务
就灵活性而言,Airtable和Ab ly是完美的匹配,因为您可以以适合您自定义用例的方式使用这两个平台。
Ably的Pub/Sub消息传递是使用“渠道”的概念实现的。每个Ably应用程序可以有任意数量的通道,其中每个通道携带一组信息。例如,在物流应用程序中,您将有一个渠道用于车队的位置更新,另一个渠道用于工作更新,以通知交付条件等的任何变化。根据数据的不同,可以通过附加到该通道来设置谁可以在该通道上发布或订阅数据的权限。您可以在官方文档中了解更多关于渠道的信息。
什么是网络钩子?
简单地说,网络钩子是用户定义的HTTP回调(或链接到Web应用程序的小代码片段),当外部网站或服务上发生特定事件时触发。当您在应用程序中构建通知功能和事件驱动响应时,它们尤其有用。您可以在概念性的深入文章中了解更多关于网络钩子的信息。
Web Hooks非常适合我们的用例-将消息作为事件的结果发送到Airtable,即在特定频道上发布的新聊天消息。如果您在登录/注册后转到Ably应用程序仪表板上的反应堆选项卡,您应该能够创建一个“新*反应堆规则”并选择反应堆事件>*网络钩子选项。在反应器规则中,您基本上配置了一个HTTP端点以及相关的标头、格式等。然后选择事件触发器的源。这里有几个选项——“存在”、“消息”和“渠道生命周期”。在这种情况下,我们只需要一个常规的“信息”。
Ably集成规则
您还将看到批处理请求或用Ably元数据封装请求的选项。如果期望以高频率触发请求,可以选择批处理选项。这将阻止您达到Airtable的速率限制,在撰写本文时,该限制为30个请求/秒。我们不会用Ably元数据封装消息,因为Airtable期望请求完全是某种格式。
将这一切整合到一个使用VueJS构建的群聊应用程序中
群聊演示是用VueJS编写的。这里有一个例子来更好地理解所有组件是如何组合在一起的:
应用程序的通信架构
就您在GitHub项目中看到的文件夹结构而言,以下是本文中我们感兴趣的主要文件。
可移动存储
__src
| __ | __ App.vue
|______|__组件
|__________|__信息框
| __________ | __聊天框
| ______________ | __ ChatCard.vue
| ______________ | __ UsernameInput.vue
| ______________ | __ ChatMessage.vue
| ______________ | __ ChatInput.vue
server.js
可运行的存储文件夹保存VueJS应用程序,而根中的server.js文件为VueJS应用程序服务,并向前端应用程序发出身份验证令牌,以通过Ably进行身份验证。(稍后会有更多)
正如您在现场演示中看到的,我们在侧面还有一个信息框,显示了您使用该应用程序时幕后的逐场播放。您可以利用它来理解每个步骤中到底发生了什么,并使用代码片段亲自尝试。此代码位于组件文件夹下的infobox文件夹中。在这篇文章中,我们不会过多地讨论信息框。
让我们看看其余文件中发生了什么。
- server.js
这是一个超级简单的Express服务器,提供Vue应用程序dist文件夹中的index.html页面。完成Vue应用程序的工作后,运行构建命令时会生成dist文件夹。您可以在VueJS文档中了解更多信息。
您会注意到我们还有一个/auth端点。如前所述,这是为了发行令牌,以便Vue应用程序可以使用Ably的实时服务安全地进行身份验证。Ab ly提供了两种身份验证方法-基本身份验证和令牌身份验证。基本身份验证直接使用API密钥,而令牌身份验证需要身份验证令牌或JWT,这使得它成为一种更安全的前端应用程序身份验证方式。您可以在Ably的文档和最佳实践指南中详细了解这些类型和权衡。
Vue JS聊天应用
-
App.vue
-
这是整个应用的主要父组件。因此,这是一个实例化和管理与Ably连接的好地方。
我们在该组件的创建的()生命周期钩子中实例化Ab ly,并在销毁的()生命周期钩子中断开连接:
created() {
this.ablyRealtimeInstance = new Ably.Realtime({
authUrl: "/auth",
});
this.ablyRealtimeInstance.connection.once("connected", () => {
this.myClientId = this.ablyRealtimeInstance.auth.clientId;
this.isAblyConnected = true;
this.chatChannelInstance = this.ablyRealtimeInstance.channels.get(
this.chatChannelId
);
});
},
destroyed() {
this.ablyRealtimeInstance.connection.close();
},
发送到Ab ly的auth Url对象。实时实例提示Ab ly,我们希望通过给定的URL通过令牌身份验证,以便在令牌过期前自动续订令牌。
在连接状态变为连接后,我们将获得稍后订阅的通道的实例。如果您还记得上一步,我们需要使用聊天可导航频道名称来发布和订阅聊天消息,因为这是我们用来触发发送到Airtable数据库的消息的频道。如果您注意到,我们指定的全名是[?rewing=2m]chat-air table.通道名称前面有一些包含在方括号中的元信息。在那里使用的通道参数是倒带,其值设置为2分钟。这允许您在成功建立到Ab ly的连接并附加到通道之前的最后2分钟内获取任何先前发布的消息。您可以从Ably的文档中了解更多关于所有可用通道参数的信息。
\2. ChatCard.vue
这是群聊应用的父组件,所以我们在这里订阅聊天频道的更新:
created() {
this.isReadyToChat = false;
this.chatChannelInstance.subscribe((msg) => {
this.handleNewMessage(msg);
});
},
我们订阅聊天频道,并在每次调用回调时调用新方法来处理新消息。稍后会有更多信息。
此组件有三个子组件:
-
UsernameInput.vue-在加入聊天之前接受用户的姓名
-
ChatInput.vue-接受用户的聊天消息,如果他们想发送一个
-
ChatMessage.vue-显示群聊中的所有聊天消息
父组件也有相当多的常规方法,下面是每个方法的细分:
i)SaveUsername And Join()方法
saveUsernameAndJoin(username) {
this.clientUsername = username;
this.isReadyToChat = true;
this.chatChannelInstance.presence.enter(username);
backgroundEventBus.$emit("updateBackgroundEventStatus", "join-chat");
}
从UsernameInput.vue组件调用此方法并保存用户输入的用户名。Ab ly的存在功能允许您查看任何客户端的实时连接状态。这有助于查看哪些用户当前在线。在这个方法中,我们让这个用户用他们的用户名输入存在集。background Event Bus是一种VueJS状态管理机制,用于向infobox组件发出各种事件。
ii)句柄New Message()方法:
async handleNewMessage(msg) {
let messageContent = msg.data.records[0].fields;
let msgTimestamp = msg.timestamp;
await this.chatMsgsArray.push({
messageContent,
msgTimestamp,
msgType: "live",
});
if (this.$refs.chatMsgsBox) {
let divScrollHeight = this.$refs.chatMsgsBox.scrollHeight;
this.$refs.chatMsgsBox.scrollTop = divScrollHeight;
}
if (messageContent.clientId != this.myClientId && this.isReadyToChat) {
backgroundEventBus.$emit(
"updateBackgroundEventStatus",
"live-msgs-loaded"
);
}
}
从频道订阅继续,在聊天频道上推送的每条新消息都会调用此方法。我们从消息中提取所需的字段,并将其推送到用于在聊天屏幕中显示消息的chat Msgs Array中。这是一个实时的msg(相对于从数据库中检索的msg)。
iii)load Previous Msgs()方法:
loadPreviousMsgs() {
if (this.chatMsgsArray[0]) {
this.getMsgsFromDBWithMsgID();
} else {
this.getLatestMsgsFromDB();
}
}
单击加载以前的消息弹出窗口时调用此方法。它检查聊天数组中是否存在先前的消息。相应地,调用其他方法从数据库中检索消息。
iv)get Msgs From DB With Msg ID方法:
getMsgsFromDBWithMsgID() {
this.latestMsgId = this.chatMsgsArray[0].messageContent.msgId;
this.showLoadMoreBtn = false;
setTimeout(() => {
this.showLoadMoreBtn = true;
}, 500);
this.base = new Airtable({
apiKey: configVars.AIRTABLE_API_KEY,
}).base(configVars.AIRTABLE_BASE_ID);
let vueContext = this;
this.base("Table 1")
.select({
view: "Grid view",
filterByFormula: "SEARCH('" + vueContext.latestMsgId + "',{msgId})",
})
.eachPage(function page(records, fetchNextPage) {
const latestRecordID = records[0].fields.ID;
vueContext.dbAutoNumber = latestRecordID;
if (latestRecordID) {
vueContext.getMsgsFromDBWithAutoID();
} else {
fetchNextPage();
}
});
}
当数组中存在先前的消息时,将调用此方法。请注意,数据库中的所有记录都是按时间顺序预先排序的,并带有自动递增的ID字段。我们使用最早消息的msgId在Airtable数据库中找到该记录的ID,然后发送另一个请求来检索ID小于先前检索到的记录ID的三个记录。这是在接下来添加的get Msgs From Db With Auto ID()方法中完成的:
getMsgsFromDBWithAutoID() {
let vueContext = this;
this.base("Table 1")
.select({
maxRecords: 3,
view: "Grid view",
filterByFormula: "({ID}<" + vueContext.dbAutoNumber + ")",
sort: [{ field: "ID", direction: "desc" }],
})
.eachPage(
function page(records, fetchNextPage) {
records.forEach(async function(record) {
await vueContext.chatMsgsArray.unshift({
messageContent: record.fields,
msgTimestamp: 123,
msgType: "db",
});
backgroundEventBus.$emit(
"updateBackgroundEventStatus",
"db-msgs-loaded"
);
if (vueContext.$refs.chatMsgsBox) {
vueContext.$refs.chatMsgsBox.scrollTop = 0;
}
});
fetchNextPage();
},
function done(err) {
if (err) {
console.error(err);
return;
}
}
);
}
我们将每个检索到的记录添加到chat Msg sArray的前面,这样它们就会出现在UI中聊天列表的顶部,因为消息是按时间顺序排列的。
v)get Latest Msgs From DB()方法:
getLatestMsgsFromDB() {
this.base = new Airtable({
apiKey: configVars.AIRTABLE_API_KEY,
}).base(configVars.AIRTABLE_BASE_ID);
let vueContext = this;
this.base("Table 1")
.select({
maxRecords: 3,
view: "Grid view",
sort: [{ field: "ID", direction: "desc" }],
})
.eachPage(
function page(records, fetchNextPage) {
records.forEach(async function(record) {
await vueContext.chatMsgsArray.unshift({
messageContent: record.fields,
msgTimestamp: 123,
msgType: "db",
});
backgroundEventBus.$emit(
"updateBackgroundEventStatus",
"db-msgs-loaded"
);
if (vueContext.$refs.chatMsgsBox) {
vueContext.$refs.chatMsgsBox.scrollTop = 0;
}
});
fetchNextPage();
},
function done(err) {
if (err) {
console.error(err);
return;
}
}
);
}
如果chat Msgs Array中没有消息,则调用此方法,这意味着没有最早的记录可供参考。我们只需要数据库中可用的最后三条消息。前面的选项可以与此相结合,因为Filter By Formula字段是唯一的微分器,但是它是在两个不同的方法中添加的,以使这两种情况明显清楚。
\3. ChatInput.vue
如前所述,此方法管理输入框以添加新的聊天消息。它有一个在单击发送按钮时调用的方法:
publishMessage() {
if (this.myMessageContent != "") {
const uniqueMsgId =
"id-" +
Math.random()
.toString(36)
.substr(2, 16);
this.msgPayload = [
{
fields: {
clientId: this.myClientId,
msgId: uniqueMsgId,
username: this.clientUsername,
"chat-message": this.myMessageContent,
},
},
];
this.chatChannelInstance.publish("chat-msg", {
records: this.msgPayload,
});
backgroundEventBus.$emit("updateBackgroundEventStatus", "publish-msg");
this.myMessageContent = "";
}
}
在这个方法中,我们计算一个随机的(唯一的)id来分配给消息,并将它与消息副本和其他信息(如Client Id和用户名)一起发布在聊天频道上。默认情况下,由于echo Messages Ab ly客户端选项关闭,同一客户端还会在通道上接收此消息作为订阅更新,从而将该消息添加到数组中并最终显示在UI中。
由于UsernameInput.vueandChatMessage.vue组件几乎不言自明,只需进行少量数据转换和显示,我们将跳过对这些组件的解释。
这样,我们就完成了从发布者到订阅者再到数据库再回到订阅者的完整数据传输循环。这是现场演示的链接,这样你就可以再次查看并将上述信息拼凑在一起: https://realtime-chat-storage.ably.dev/
与Ably和Airtable的群聊应用
探索从Airtable获取数据到Ably的其他方法
你可能会说一切都很好,为什么要探索其他方法?虽然我们可以将消息直接发布到Airtable,并从前端应用程序中再次检索这些消息,但我们在这个项目中有一些差距,阻止它为生产做好准备。
如果出于某种原因,有人在Airtable添加了一条信息呢?在整个更新之前,我们无法在聊天应用程序中显示这些新消息,在处理实时数据时,更新并不有趣,也不可行。虽然Airtable不是实时数据库,即它不会推出任何更改,但我们有解决这个问题的方法。进来,扎皮尔!
使用Zapier和Ably将Airtable转换为实时数据库
Zapier是一个连接两个或多个SaaS平台以共享事件驱动数据的工作流管理应用程序。我们可以在Zapier上连接Airtable和Ably,并在Airtable数据库中添加新记录时,让它向给定的Ably通道发布消息。它会喜欢这样的东西: