不久以前,电子邮件被认为是革命性的。能够向世界另一端的人发送信息,并在几分钟内收到回复,是一种游戏规则的改变。
然而,今天,电子邮件是互联网中行动缓慢的恐龙。现在,用户期望从他们的在线互动中得到即时的反馈,并与人和机器进行即时沟通。如果你的应用程序需要他们不断地刷新网页以看到最新的更新,他们很快就会去寻找其他地方。
但技术是如何跟上这些不断增长的需求的呢?
一种方法是发布/订阅架构模式(简称pub/sub)。这是一种异步通信的方法,它使应用程序能够向一个通道发布消息,而订阅该通道的任何客户端都能接收这些消息。
本教程将告诉你如何使用pub/sub来实现博客中的实时评论。一旦有人向博客文章添加评论,任何阅读该文章的人都会立即看到:不需要刷新页面。请看它的操作
在博客文章上立即发表评论的能力可能是你过去没有错过的,但这是一个简单的、自成一体的例子,它是一个使用实时体验来吸引受众的应用。你可以在这里学到的基础上,实现其他的受众参与功能,添加到你自己的应用程序中。
技术栈
为了实现这一目标,我们将使用以下技术。
Next.js
Next.js是一个React框架,它使构建快速和可扩展的网络应用变得容易。React是一个非常流行和强大的UI库,但需要大量配置。Next.js省去了很多工作,还为开发者提供了是否在客户端、服务器或使用两者混合的方式渲染内容的选项。
如果你在开始本教程之前对Next.js有一点了解,那会很方便,但这并不是严格意义上的必要,因为我们会提供一个构建应用程序的良好起点,并指导你完成其余部分。但Next.js网站有一个很好的迷你课程,可以让你快速上手,他们的文档也很不错。
星球规模(PlanetScale
PlanetScale是一个数据库即服务平台。它允许开发者快速创建高度可扩展的数据库,并与这些数据库一起工作,就像它们是Git仓库中的代码一样。开发人员可以为开发创建分支,并在这些分支按预期工作时推送到生产中。你将使用PlanetScale来存储博客评论。
Prisma
Prisma是一个Node.js和TypeScript ORM。它是一个服务器库,可以帮助开发人员使用熟悉的面向对象的方法编写查询,与数据库一起工作。你将使用它来处理你的PlanetScale数据库。
Ably
Ably是一个功能丰富的pub/sub消息传递平台,使开发者能够实时地创建有吸引力的数字体验。它能保证信息以巨大的规模传递给各种设备,同时保持信息的顺序和完整性。你将用它来发布博客评论到一个频道并订阅该频道。
Vercel
最后,你将使用Vercel来托管你的应用程序,并将其提供给全世界。Vercel的好心人也是给你带来Next.js的人,使你的应用程序的部署变得轻而易举。
现在我们知道了我们要建立什么,以及我们要用什么来建立它,让我们开始吧!
教程步骤
第1步:创建Next.js应用程序
这个应用程序中有很多模板代码,我们已经为你创建了这些代码,这样你就可以专注于加入实时元素了。首先将本教程的Github资源库分叉。
点击仓库主页上的 "Fork "按钮。该Git仓库的分叉副本将被添加到你的个人GitHub或GitLab repo中。就这样了。
git clone https://github.com/ably/ably-next-prisma-planetscale.git
这个仓库包含两个分支。
main- 其中包含本教程的解决方案代码starter-project- 这是本教程的起点
查看starter-project 分支:在本教程的整个过程中,你都将使用这个分支。如果你遇到困难,可以使用main 分支将你的代码与解决方案代码进行比较。
git checkout starter-project
这个启动项目为你提供了开始实现实时通信和数据持久性所需的所有基本组件。它是用MUI构建的,以前被称为Material-UI。
看一下components 文件夹,感受一下哪个组件做什么。
在你的项目根目录下执行以下npm 命令,以安装核心依赖,并启动该项目。
npm install
npm run dev
在你的浏览器中访问http://localhost:3000,你会看到你的博客应用样本,其中有一些假的内容和评论部分。
Next.js会在你开发的过程中继续热加载项目,所以一般来说,在你完成剩余的步骤时,你可以看到项目的雏形,而不需要重启服务器。当你更新某些配置文件时,你将不得不重新启动服务器,但我们会让你知道什么时候是这样的情况。
第二步:安装Ably React Hooks软件包
你将在这个应用程序中实现的实时评论是由Ably提供的。
Next.js是由React驱动的,而React对你在什么地方和什么时候使用Ably等库的代码会有点挑剔。为了让React开发者的生活更简单,Ably开发了Ably React HooksNPM模块。现在就把它安装到你的项目中。
npm install ably @ably-labs/react-hooks
第3步:注册Ably
要使用Ably的服务,你需要注册一个免费账户并获得一个API密钥。
你的新Ably账户带有一个预先创建的名为Default的项目,你的该项目API密钥将显示在你的屏幕上。
如果由于任何原因,你没有看到它,请点击仪表板上的Default项目,然后选择顶部的API密钥标签。点击 "显示"按钮,显示根API密钥(不要与 "只订阅 "API密钥混淆,它只允许你从一个频道读取信息,而不是发布到该频道)。因此,确保你得到正确的一个。
第4步:配置Ably React Hooks
在这一步,你将实例化Ably客户端,并与Ably平台进行认证。
首先,在你项目的根目录下创建一个名为.env 的文件,并创建一个名为ABLY_API_KEY 的配置密钥,它存储了你在上一步中检索到的API密钥。请注意,尽管这个API密钥是一个字母数字字符串,但你不需要用引号环绕它。
ABLY_API_KEY=<paste your api key here>
Next.js的许多优点之一是它支持API路由。这使你能够将API端点构建为Node.js无服务器函数。这对我们来说非常有用,因为我们可以创建一个API端点,对Ably服务进行认证,而不必将我们的API密钥暴露给浏览器。
实现一个名为/createTokenRequest 的API端点,它将为你生成一个令牌,以便在客户端对Ably进行认证。Next.js使在你的应用程序中包含API路由变得非常容易。你需要做的就是在pages/api 目录下的同名文件中创建路由处理程序。
在你的Next.js应用程序中,在你的pages/api 文件夹中创建一个createTokenRequest.js 文件。然后这个路由就可以作为/createTokenRequest 的端点。
在该文件中,创建一个新的Ably客户端,并使用它来生成一个新的令牌。
import Ably from "ably/promises";
export default async function handler(req, res) {
const client = new Ably.Realtime(process.env.ABLY_API_KEY);
const tokenRequestData = await client.auth.createTokenRequest({
clientId: "ably-blog-app",
});
res.status(200).json(tokenRequestData);
}
然后你可以使用这个端点在客户端配置Ably。但是,如果你使用一个相对的,而不是绝对的URL,这将产生一个错误,因为它的一个依赖性的错误。这不会阻止Ably服务的工作,但它很快就会使你的控制台变得混乱。所以我们现在通过为URL创建一个环境变量来解决这个问题。当我们在后面的步骤中部署到Vercel时,这个URL将会改变。
它还需要在浏览器中可用,但默认情况下不是这样的。幸运的是,Next.js可以帮助解决这个问题。任何以NEXT_PUBLIC_ 为前缀的环境变量都可以在客户端访问。所以千万不要把任何敏感的东西放在里面(比如你的API密钥),但这是一个配置你的应用程序的主机URL的好地方。在你的.env 文件中输入以下内容。
NEXT_PUBLIC_HOSTNAME=http://localhost:3000
现在,到你的_app.js 文件中,使用configureAbly ,从@ably-labs/react-hooks 调用你的新认证端点。
import { configureAbly } from "@ably-labs/react-hooks";
import "../styles/globals.css";
configureAbly({
authUrl: `${process.env.NEXT_PUBLIC_HOSTNAME}/api/createTokenRequest`,
});
function MyApp({ Component, pageProps }) {
return <Component {...pageProps} />;
}
export default MyApp;
你的认证已经到位了。不幸的是,如果你的服务器正在运行,你可能会看到一个SyntaxError: Unexpected token 'export' issue in your browser 。原因是@ably-labs/react-hooks package 作为ES6模块导出,而Next.js不会转译从node_modules 导入的函数。
值得庆幸的是,有一个简单的解决办法。首先,安装next-transpile-modules 库。
npm install --save next-transpile-modules
然后,前往根文件夹中的next.config.js 文件,指定应该转译哪个库。
const withTM = require('next-transpile-modules')(['@ably-labs/react-hooks']);
module.exports = withTM({
reactStrictMode: true,
})
你需要重新启动你的服务器,在你的终端点击CTRL+C并重新运行npm run dev 。但是错误应该消失了。
第5步:实现Pub/Sub
现在你已经安装了Ably库,你可以进行实时元素的工作。
Ably的pub/sub是基于通道的概念。发布者向一个指定的通道发送消息,订阅者消费发送到该通道的消息。
Ably React Hooks NPM模块提供了一个名为useChannel 的钩子。这个钩子在组件挂载时订阅一个指定的频道,在组件卸载时取消订阅。
useChannel 钩子需要一个回调函数。每次有新的消息到达通道时,这个函数就会被调用,这样你就可以处理它并进行任何必要的更新。
import { useChannel } from "@ably-labs/react-hooks";
const [channel] = useChannel("your-channel-name", (message) => {
//Process the incoming message
});
让我们来谈谈你将如何在你的应用程序中使用它。
当一个用户在一篇博文上提交了一个新的评论,你的应用程序将发布一个包含这个评论的消息到一个叫做comment-channel 的频道。
任何其他阅读该博文的人都会立即看到这个新评论,因为他们订阅了该频道,因此已经收到并处理了这个新评论。
要做到这一点,请前往你的Comments 组件,并注意有一个名为comments 的状态变量。这将存储你的评论列表。这个变量被传递给CommentsList 组件。
创建一个submitComment() 函数并使用channel.publish() 来发布一个新的消息到频道。在Ably中,一个消息有两个参数:name 和data 。对于数据,你只需要评论者的username 和评论本身的text 。将这个submitComment() 函数传递给你的AddCommentSection 组件。
一旦这个消息被推送,所有订阅这个频道的人,包括创建评论的人,都会收到这个消息,并且它将被添加到你的状态变量的评论列表中,这要感谢useChannel 中的回调。
下面是Comments.js 的代码。
import React, { useState } from "react";
import { useChannel } from "@ably-labs/react-hooks";
import Typography from "@mui/material/Typography";
import CommentsList from "./CommentsList";
import AddCommentSection from "./AddCommentSection";
export default function Comments({ initialComments = [] }) {
const [comments, setComments] = useState(initialComments);
const [channel] = useChannel("comment-channel", (message) => {
// Add new incoming comment to the list of comments
setComments((comments) => {
return [...comments, message.data];
});
});
const submitComment = async (username, comment) => {
try {
const body = { username, comment };
await fetch(`${process.env.NEXT_PUBLIC_HOSTNAME}/api/comment`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
channel.publish({
name: "comment",
data: {
username,
comment,
},
});
} catch (error) {
console.error("An error occurred when creating a comment: ", error);
}
};
return (
<React.Fragment>
<Typography variant="h6" gutterBottom>
Comments ({comments.length})
</Typography>
<CommentsList comments={comments} />
<AddCommentSection submitComment={submitComment} />
</React.Fragment>
);
}
第6步:连接评论表格
在你的AddCommentSection 组件中,首先接收Comment 组件submitComment 函数作为道具。
创建两个新的状态变量:username 和comment 。然后,将组件的JSX标记中对console.log()s的调用替换为对设置用户名和状态变量的函数的调用:setUsername() 和setComment() ,分别。同时,将相应的TextField value 属性设置为{username} 和{comment}
最后,创建一个addComment() 函数,该函数将使用你的新submitComment() 函数来发布消息并重置你的表单。
下面是AddCommentSection.js 的代码。
import React, { useState } from "react";
import Grid from "@mui/material/Grid";
import TextField from "@mui/material/TextField";
import FormControl from "@mui/material/FormControl";
import Button from "@mui/material/Button";
export default function AddCommentSection({ submitComment }) {
const [username, setUsername] = useState("");
const [comment, setComment] = useState("");
const addComment = () => {
//Publish message
submitComment(username, comment);
//Reset form
setUsername("");
setComment("");
};
return (
<FormControl>
<Grid container spacing={2}>
<Grid item xs={12}>
<TextField
required
id="username"
name="username"
label="Username"
variant="outlined"
value={username}
onChange={(event) => setUsername(event.target.value)}
/>
</Grid>
<Grid item xs={12}>
<TextField
required
id="comment"
name="comment"
label="Comment"
variant="outlined"
multiline
rows={4}
value={comment}
onChange={(event) => setComment(event.target.value)}
/>
</Grid>
<Grid item xs={12}>
<Button variant="contained" onClick={addComment}>
Submit
</Button>
</Grid>
</Grid>
</FormControl>
);
}
第7步:试试吧!
如果你的服务器还没有运行,用npm run dev 来启动它。
在两个独立的浏览器标签中打开http://localhost:3000,并尝试从每个标签中提交评论。如果一切工作正常,那么你应该看到新的评论会立即出现在两个标签中。
唯一的问题是,如果你刷新页面,你会失去所有的评论。这是因为你的应用程序缺少拼图的最后一块:数据持久性。这就是你接下来要实现的。
第8步:创建你的PlanetScale数据库
在本教程中,你将使用PlanetScale作为托管数据库,在其中存储博客评论。首先,到planetscale.com/,创建一个免费账户。登录后,点击新建数据库按钮,创建一个数据库。
在出现的对话框中,将数据库命名为ably-realtime-db ,并选择离你最近的地区以尽量减少延迟。
点击创建数据库按钮,你的数据库就可以使用了。
一旦你完成了,回到你的终端,安装PlanetScale CLI。这将使你能够运行许多有用的命令,例如打开一个shell来查询你的数据库。
第9步:安装和配置Prisma ORM
在你的应用程序的根目录下,运行以下命令。
npx prisma init
这个命令做了三件事。
- 在你的
.env文件中添加一个环境变量DATABASE_URL,并添加一个假的URL - 在你的应用程序中创建一个
prisma文件夹 - 在
prisma文件夹内添加一个schema.prisma文件
schema.prisma 文件配置了你的数据库URL和提供者。你也将在未来的步骤中在这里定义你的评论数据模型。
然而,现在,.env 中的DATABASE_URL 是一个假的。所以回到PlanetScale的网页上。在你的页面的右边,你应该看到一个连接按钮。点击它,然后点击生成密码。选择Prisma格式,你应该看到像这样的东西。
datasource db { provider = "mysql" url = "mysql://******:************@******.region.psdb.cloud/database-name?sslaccept=strict" }
复制这个URL并把它添加到你的.env文件中,但要去掉双引号,因为当你在后面的步骤中部署你的应用程序时,这会导致Vercel的问题。
DATABASE_URL=mysql://****:****@***.region.psdb.cloud/ably-realtime-db?sslaccept=strict
在你的schema.prisma 文件中,将提供者改为mysql ,并执行参考完整性,如下所示。由于Prisma和PlanetScale之间的不兼容,这一步是必要的,因为后者不支持外键约束。为了克服这个问题,我们必须设置referentialIntegrity 属性。
generator client {
provider = "prisma-client-js"
previewFeatures = ["referentialIntegrity"]
}
datasource db {
provider = "mysql"
url = env("DATABASE_URL")
referentialIntegrity = "prisma"
}
在同一个文件中,为博客评论定义一个数据模型。这将是一个非常简单的模型,有一个自动生成的ID,一个用户名,和一个评论。
model Comment {
id Int @default(autoincrement()) @id
username String
comment String
}
一旦你的模型就位,将你的模式同步到PlanetScale数据库。首先,通过在终端打开一个新的标签并运行以下命令来连接到你的数据库。
npx prisma db push
如果一切顺利,你应该看到以下信息。
? Your database is now in sync with your schema. Done in 996ms
...
✔ Generated Prisma Client (3.9.1 | library) to ./node_modules/@prisma/client in 122ms
通过使用PlanetScale CLI连接到你的数据库,检查你的模式是否正常工作。指定数据库名称和你正在工作的分支,如下所示。
pscale shell ably-realtime-db starter-project
这将打开一个进入数据库的shell,在那里你可以执行describe Comment; ,看到下面的表定义。
ably-realtime-db/| starter-project |> describe Comment;
+----------+--------------+------+-----+---------+----------------+
| Field | Type | Null | Key | Default | Extra |
+----------+--------------+------+-----+---------+----------------+
| id | int | NO | PRI | NULL | auto_increment |
| username | varchar(191) | NO | | NULL | |
| comment | varchar(191) | NO | | NULL | |
+----------+--------------+------+-----+---------+----------------+
输入exit; ,退出shell。
很好!你的Prisma模式现在已经和PlanetScale同步了。最后一步是将你当前的PlanetScale分支提升为生产分支。要做到这一点,请执行以下步骤。
pscale branch promote ably-realtime-db starter-project
步骤10:安装Prisma客户端软件包
你已经在prisma.schema 中定义了连接细节和数据模型。下一步是让你的应用程序使用这些信息来持久化注释数据。
首先,如果你的服务器仍在运行,请停止它,并安装Prisma客户端包。
npm install @prisma/client
然后,实例化Prisma客户端。为此创建一个名为lib 的文件夹,并在该文件夹内创建一个名为prisma.js 的文件。
import { PrismaClient } from "@prisma/client";
const prisma = new PrismaClient();
export default prisma;
第11步:创建API路由,用于向数据库读写评论
按照你建立/api/createTokenRequest 路由的同样方法,创建另一个叫做/api/comment 的路由,你的应用程序可以对其进行GET和POST请求。GET从数据库中读取现有的评论,POST向数据库中写入一个新的评论。
目前,你唯一需要注意的方法是GET和POST。其他的都应该返回405的HTTP状态。
你的GET端点应该使用一个简单的Prisma查询返回你所有的评论列表。对于POST端点,你的用户名和评论都包含在请求的正文中,你将用它来在你的表中创建一个新的评论。
在/pages/api/comment.js ,输入以下代码。
import prisma from "../../lib/prisma";
export default function handler(req, res) {
switch (req.method) {
case "GET":
return handleGET(req, res);
case "POST":
return handlePOST(req, res);
case "OPTIONS":
return res.status(200).send("ok");
default:
return res.status(405).end();
}
}
// GET /api/comment - retrieves all the comments
async function handleGET(req, res) {
try {
const comments = await prisma.comment.findMany();
res.json(comments);
res.status(200).end();
} catch (err) {
res.status(err).json({});
}
}
// POST /api/comment - creates a new comment
async function handlePOST(req, res) {
try {
const comment = await prisma.comment.create({
data: {
...req.body,
},
});
res.json(comment);
res.status(200).end();
} catch (err) {
res.status(err).json({});
}
}
第12步:从数据库中读取现有的评论
现在你可以向你的/api/comment 端点发出一个GET请求,从数据库中获取所有的评论。你可以用getServerSideProps来做到这一点。这个函数是Next.js的一个非常方便的功能,它可以让你在用户请求页面的时候进行API调用。当用户第一次加载页面时,客户端将进行一个API调用,获取保存的评论,并将其传递给props。
在pages/index.js ,从props中获取评论,并将它们传递给Comments 组件,以填充你的列表。
import Container from "@mui/material/Container";
import Grid from "@mui/material/Grid";
import CssBaseline from "@mui/material/CssBaseline";
import Divider from "@mui/material/Divider";
import Header from "../components/Header";
import Footer from "../components/Footer";
import Content from "../components/Content";
import Comments from "../components/Comments";
const Home = ({ comments = [] }) => {
return (
<div>
<CssBaseline />
<Container maxWidth="lg">
<Header />
<Grid container spacing={5} sx={{ mt: 3 }}>
<Grid
item
xs={12}
sx={{
py: 3,
}}
>
<Content />
<Divider
variant="middle"
sx={{
my: 3,
}}
/>
<Comments initialComments={comments} />
</Grid>
</Grid>
<Footer />
</Container>
</div>
);
};
export const getServerSideProps = async () => {
const res = await fetch(`${process.env.NEXT_PUBLIC_HOSTNAME}/api/comment`);
const comments = await res.json();
return {
props: { comments },
};
};
export default Home;
第13步:向数据库写入新的评论
在你的Comments 组件中,你现在可以从你的submitComment 函数中保存一个新的评论。你所要做的就是发出一个POST请求,在请求体中加入username 和comment 。
然后,当你发布消息时,所有的订阅者都可以得到通知并进行相应的更新。
import React, { useState } from "react";
import { useChannel } from "@ably-labs/react-hooks";
import Typography from "@mui/material/Typography";
import CommentsList from "./CommentsList";
import AddCommentSection from "./AddCommentSection";
export default function Comments({ initialComments = [] }) {
const [comments, setComments] = useState(initialComments);
const [channel] = useChannel("comment-channel", (message) => {
// Add new incoming comment to the list of comments
setComments((comments) => {
return [...comments, message.data];
});
});
const submitComment = async (username, comment) => {
try {
const body = { username, comment };
await fetch(`${process.env.NEXT_PUBLIC_HOSTNAME}/api/comment`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(body),
});
channel.publish({
name: "comment",
data: {
username,
comment,
},
});
} catch (error) {
console.error("An error occurred when creating a comment: ", error);
}
};
return (
<React.Fragment>
<Typography variant="h6" gutterBottom>
Comments ({comments.length})
</Typography>
<CommentsList comments={comments} />
<AddCommentSection submitComment={submitComment} />
</React.Fragment>
);
}
使用npm run dev 再次运行你的服务器,并尝试添加一些评论。
然后,打开另一个终端窗口,执行以下命令。
npx prisma studio
这将在一个新的浏览器标签中打开http://localhost:5555/,在那里你可以看到并操作你的评论表的内容。
这就是了!现在你已经准备好与世界分享你的工作成果了。你将在最后一步做到这一点,将你的应用程序部署到Vercel。
第14步:将你的应用程序部署到Vercel上
在部署你的应用程序之前,你必须做一些整理工作,以处理当你托管任何Web应用程序时不可避免的CORS问题。
在next.config.js ,添加以下HTTP头信息。这些指定了你的请求将支持哪些起源、方法和其他功能。
const withTM = require("next-transpile-modules")(["@ably-labs/react-hooks"]);
module.exports = withTM({
reactStrictMode: true,
async headers() {
return [
{
// match all API routes
source: "/api/:path*",
headers: [
{ key: "Access-Control-Allow-Credentials", value: "true" },
{ key: "Access-Control-Allow-Origin", value: "*" },
{
key: "Access-Control-Allow-Methods",
value: "GET,OPTIONS,PATCH,DELETE,POST,PUT",
},
{
key: "Access-Control-Allow-Headers",
value:
"X-CSRF-Token, X-Requested-With, Accept, Accept-Version, Content-Length, Content-MD5, Content-Type, Date, X-Api-Version",
},
],
},
];
},
});
你还需要支持OPTIONS方法,它被用作API请求中的预检。在你的pages/api/comment.js 文件中,通过返回HTTP状态200来增加对OPTIONS的处理。
export default function handler(req, res) {
switch (req.method) {
case "GET":
return handleGET(req, res);
case "POST":
return handlePOST(req, res);
case "OPTIONS":
return res.status(200).send("ok");
default:
return res.status(405).end();
}
}
一旦你确信你的应用程序工作正常,提交并推送你的starter-project 分支。
git checkout starter-project
git add .
git commit -m "Final code"
git push
如果你还没有,在Vercel创建一个账户。
登录后,点击新建项目,并导入代码所在的Git仓库。复制Vercel为你生成的URL,它应该是https://[项目名称].vercel.app/这样的。
编辑你的.env 文件NEXT_PUBLIC_HOSTNAME 的设置,指向Vercel的URL,而不是http://localhost:3000。提交并推送该更改。
git add .
git commit -m "Configure deployment URL"
git push
你目前仍在starter-project 分支上工作,所以现在把它设为Vercel的生产分支。
要做到这一点,在概览页面中点击你的 Github 项目,选择设置标签,然后从左侧导航菜单中选择Git页面。将生产分支改为starter-project ,然后点击保存。
然后,访问部署选项卡,点击你的部署右边的三个小圆点,选择重新部署。
一旦部署完成,你的应用程序就可以开始运行了。
总结
在本教程中,你学到了如何在Next.js应用程序中使用Ably平台实现实时的pub/sub消息传递。你还学习了如何用PlanetScale创建一个数据库,以及如何用Prisma ORM获取和写入数据。最后,你使用Vercel在实时环境中部署你的应用程序。
实时通信对一些开发者来说可能还是新鲜事,但公众对他们在网上的所有互动的即时反馈的要求越来越高。无论你的应用程序或服务的性质如何,你现在处于一个很好的位置,可以给他们他们想要的东西。
我们要感谢Mark Lewin对本文的重大贡献,他是Ably的高级开发人员教育者。