在这篇文章中,我们将向你展示如何使用Next.js和Vercel来构建一个实时链接共享应用。用户可以分享他们认为别人可能感兴趣的文章的URL,任何查看该页面的人都会看到共享的文章立即出现,同时还有预览图片和文章摘要。
在这篇文章中,你将学会如何。
- 创建一个Next.js应用程序并将其部署到Vercel上
- 使用认证和发布的最佳实践
- 使用Ably添加实时功能
- 管理用户的存在
- 使用OpenGraph显示网站预览
- 将你的应用程序部署到Vercel
你可以在Github上查看解决方案的代码,或者玩一玩现场的例子。
在我们开始之前,让我们介绍一下你将要使用的一些技术。
Vercel和Next.js
我们是Vercel的忠实粉丝;他们为部署网站提供了可以说是最方便的云平台之一。Vercel也是Next.js框架的幕后推手,它使开发和部署基于React的高度优化的全栈应用变得容易。
Vercel提供的最有用的功能之一是其无服务器功能,它使你能够按需执行代码,而不必担心建立和管理自己的服务器基础设施。在Next.js的背景下,这只是在你的*/pages/api目录下创建一个HTTP处理模块,然后你可以在/api/module*向其发出请求。
无服务器函数对于用户认证、数据库查询和表单处理等常见任务来说非常棒。然而,由于它们在一定时间后会超时,因此不能支持WebSocket连接。这使得实现实时功能变得很困难。
这就是Ably的用武之地!Ably可以在客户端工作,将该客户端连接到一个命名的通道。然后,客户端可以向该通道发布消息,并订阅接收来自该通道的消息。这种模式通常被称为pub/sub,它是一种高度弹性和可扩展的方式来支持应用程序中的实时消息传递。
让我们开始吧。
第1步 - 创建Next.js应用程序
第一步是创建一个新的Next.js应用程序。确保你已经安装了Node.js,然后在终端执行以下命令。
npx create-next-app
按照提示命名你的项目,然后换到它为你创建的同名目录中。
这个过程为你的Next.js应用程序创建了模板代码,并包括一个你可以尝试的单页应用程序样本。通过执行来运行它。
npm run dev
然后在你的浏览器中访问**https://localhost:3000**。
第2步 - 架构
为了完成本教程,你要对生成的项目结构做一个小改动。这就是完成后的应用程序结构的样子。
.
└── ably-next-vercel-news
├── CODE_OF_CONDUCT.md
├── CONTRIBUTING.md
├── LICENSE
├── README.md
├── components
│ ├── ArticlePreview.js
│ ├── Articles.js
│ └── Participants.js
├── lib
│ └── history.js
├── next.config.js
├── package-lock.json
├── package.json
├── pages
│ ├── _app.js
│ ├── api
│ │ ├── createTokenRequest.js
│ │ └── publish.js
│ └── index.js
├── public
│ ├── ably-logo.svg
│ ├── favicon.ico
│ └── vercel.svg
├── styles
│ ├── Home.module.css
│ └── globals.css
└── yarn.lock
7 directories, 21 files
为了实现这一点,首先创建组件文件夹。你将在这里创建React组件,为你的应用程序提供核心功能。组件是可重复使用的代码位,返回React可以渲染的信息。
然后,创建lib文件夹。这就是我们将放置我们的实用功能的地方。在这个例子中,这将是一个使用Ably的历史API检索历史消息的函数,你将在后面的步骤中创建。
第3步 - 建立基本的用户界面
用以下CSS文件替换样式目录的内容,你可以在Github上的解决方案代码库中找到这些文件。
然后,将以下内容粘贴到你的公共目录中的一个名为ably-logo.svg的文件*。*
<svg width="108" height="32" viewBox="0 0 108 32" xmlns="http://www.w3.org/2000/svg">
<path d="M62.922 24.9786V4.08813H66.6933V11.6512C67.9709 10.435 69.6164 9.76044 71.3538 9.76044C75.4318 9.76044 79.0498 12.8674 79.0498 17.5484C79.0498 22.2293 75.4318 25.3465 71.3538 25.3465C69.5244 25.3465 67.7971 24.6209 66.5094 23.3024V24.9786H62.922ZM75.2785 17.5484C75.2785 14.932 73.4183 13.1025 70.9859 13.1025C68.6148 13.1025 66.7853 14.84 66.6933 17.3644V17.5484C66.6933 20.1648 68.5534 21.9942 70.9859 21.9942C73.4183 21.9942 75.2785 20.1648 75.2785 17.5484ZM80.7975 24.9786V4.08813H84.5688V24.9786H80.7975ZM89.8425 30.3954L92.0399 25.1523L86.0712 10.1284H90.1491L93.9511 20.6247L97.8144 10.1284H101.954L93.8591 30.4056H89.8425V30.3954ZM56.9329 10.1284V12.0191C55.6247 10.5883 53.7952 9.77066 51.9147 9.77066C47.8367 9.77066 44.2187 12.8777 44.2187 17.5586C44.2187 22.2497 47.8367 25.3465 51.9147 25.3465C53.8668 25.3465 55.7166 24.4982 57.0555 22.9754V24.9888H60.3465V10.1284H56.9329ZM56.5649 17.5484C56.5649 20.1341 54.7048 21.9942 52.2724 21.9942C49.8399 21.9942 47.9798 20.1341 47.9798 17.5484C47.9798 14.9626 49.8399 13.1025 52.2724 13.1025C54.6435 13.1025 56.473 14.8706 56.5649 17.3644V17.5484Z" fill="currentColor"></path>
<path d="M19.2858 0L3.14788 29.5369L0 27.3293L14.932 0H19.2858ZM19.5107 0L35.6487 29.5369L38.7965 27.3293L23.8646 0H19.5107Z" fill="url(#paint0_linear)"></path>
<path d="M35.4238 29.7107L19.3983 17.16L3.37271 29.7107L6.64323 32L19.3983 22.0147L32.1533 32L35.4238 29.7107Z" fill="url(#paint1_linear)"></path>
<defs>
<linearGradient id="paint0_linear" x1="5.47361" y1="37.4219" x2="32.4603" y2="7.45023" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF5416"></stop>
<stop offset="0.2535" stop-color="#FF5115"></stop>
<stop offset="0.461" stop-color="#FF4712"></stop>
<stop offset="0.6523" stop-color="#FF350E"></stop>
<stop offset="0.8327" stop-color="#FF1E08"></stop>
<stop offset="1" stop-color="#FF0000"></stop>
</linearGradient>
<linearGradient id="paint1_linear" x1="10.7084" y1="39.3593" x2="26.6583" y2="21.6452" gradientUnits="userSpaceOnUse">
<stop stop-color="#FF5416"></stop>
<stop offset="0.2535" stop-color="#FF5115"></stop>
<stop offset="0.461" stop-color="#FF4712"></stop>
<stop offset="0.6523" stop-color="#FF350E"></stop>
<stop offset="0.8327" stop-color="#FF1E08"></stop>
<stop offset="1" stop-color="#FF0000"></stop>
</linearGradient>
</defs>
</svg>
用以下代码替换pages/index.js的内容。
import Head from "next/head";
import Image from "next/image";
import ablyLogo from "../public/ably-logo.svg";
import styles from "../styles/Home.module.css";
export default function Home() {
return (
<div className={styles.container}>
<Head>
<title>Ably Realtime News Demo</title>
<meta
name="description"
content="Generated by create next app"
/>
<link
rel="icon"
href="https://static.ably.dev/motif-red.svg?realtime-news"
type="image/svg+xml"
/>
</Head>
<main className={styles.main}>
<Image
alt="ably logo"
src={ablyLogo}
width="160px"
height="100%"
></Image>
<h1>Realtime News</h1>
<h2>Share your favorite news articles</h2>
<h3>Participants</h3>
</main>
</div>
);
}
如果你使用npm run dev运行这个,它只是显示以下页面,完全没有任何作用。
这是因为你需要创建与Ably平台互动并显示结果的组件。
继续阅读,我们将指导你完成这个过程!
第四步:创建你的Ably API密钥
在这个应用程序中,你将与两个Ably包一起工作。
- @ably-labs/react-hooks。一个轻量级的Ably SDK,使其能够在React应用程序中轻松使用Ably。你将在客户端使用它来订阅一个频道的消息,并显示哪些用户在那里。
- ably:Ably REST SDK,你将在服务器上使用它来验证客户并向频道发布消息。
这种关注点的分离是最佳实践。只要有可能,你应该在服务器上验证客户,以避免泄露你的API密钥。而从服务器上发布,可以让你执行任何需要的预处理和验证。
在Ably平台上,服务器和客户端都需要不同的功能,所以你将使用单独的API密钥进行认证。
首先,生成服务器API密钥。一旦你创建了Ably账户,你可以通过执行以下步骤生成API密钥。
- 访问你的 应用仪表板,点击 "创建新应用"。
- 给新应用程序起个名字--Next-news-demo就可以了。
- 一旦应用程序被创建,复制私人API密钥。这个密钥是你的 "根 "密钥,它在Ably平台上提供广泛的权限。记下它,这是你的服务器与Ably服务进行验证的方式。
通过点击页面顶部面包屑跟踪中的应用程序名称,返回到应用程序设置页面。
点击API密钥标签,你会看到两个API密钥:根密钥和仅订阅密钥。仅限订阅的那个将是客户认证的密钥。
你可以看到仅有的订阅键,不出所料,只有订阅功能。但你的客户端也需要 存在感的能力,这样它就可以看到通道上有哪些其他客户,所以你需要添加这个能力。
点击 "设置 "按钮,进行以下更改,然后点击 "保存"。
第5步:配置你的.env文件
Next.js可以从*.env*文件中读取,而不需要你安装额外的依赖项。将你的服务器和客户端API密钥添加到这个文件中,如下图所示。
# Server key must have Publish and History permissions - use app's Root key
ABLY_SERVER_API_KEY=UGCYxQ.211oIA:VgSya3hCMoQObVKkmuTYRo8se…
# Client key should have Subscribe and Presence permissions only
ABLY_CLIENT_API_KEY=UGCYxQ.uhSg9Q:A6Ftr7F0HTg1hedNA2n1iks6…
在开发过程中,默认的URL是localhost:3000。当你在以后的步骤中把你的应用程序部署到Vercel时,这将会改变,所以你将从一开始就使它可配置。
创建一个新的环境变量,叫做NEXT_PUBLIC_HOSTNAME。名称中的NEXT_PUBLIC_部分很重要:它使环境变量对浏览器可见。所有其他环境变量只在服务器上可见。
# This will change after deployment
NEXT_PUBLIC_HOSTNAME=http://localhost:3000
第6步:用Ably进行认证
在这个应用程序中,客户将使用API端点与服务器进行认证,以检索一个令牌。这意味着你永远不会把你的API密钥暴露给客户端,也不会有它被破坏的风险。你的服务器将使用API密钥进行认证,使用Ably REST SDK。
为了使你能识别作为频道信息发布者的单个用户,你将使用一个方便的NPM模块,即unique-names-generator,给每个用户分配一个随机的名字。
现在就安装这些模块。
npm install ably unique-names-generator
现在你需要创建一个API端点,来执行认证步骤。还记得我们之前谈到的Vercel无服务器函数吗?这就是一个完美的用例。
在*/pages/api目录下创建一个名为createTokenRequest.js的页面。这段代码将作为端点/api/createTokenRequest*的HTTP处理器。当客户想要对Aply平台进行认证时,他们将向这个端点发出HTTP请求。用下面的代码填充该文件。
import Ably from "ably/promises";
import { uniqueNamesGenerator, adjectives, colors, animals } from "unique-names-generator";
export default async function handler(req, res) {
const client = new Ably.Realtime(process.env.ABLY_CLIENT_API_KEY);
const randomName = uniqueNamesGenerator({
dictionaries: [adjectives, animals, colors],
length: 2,
});
const tokenRequestData = await client.auth.createTokenRequest({
clientId: randomName,
});
res.status(200).json(tokenRequestData);
}
这个处理程序为Ably平台上的用户分配一个随机的clientId,使用客户端的API密钥进行认证,并返回一个令牌,客户端将使用该令牌来执行订阅和存在操作。
要在客户端与Ably合作,你需要安装Ably实验室提供的Ably的React Hooks包。(注意,一定要使用2.0.4或以上的版本,否则你会遇到代码转译错误)。
npm install @ably-labs/react-hooks
然后你可以使用React Hooks的configureAbly函数来验证你的新*/api/createTokenRequest端点。在index.js中输入以下代码,在最后的import语句下面和Home*组件的定义之前。
...
import { configureAbly } from "@ably-labs/react-hooks";
configureAbly({
authUrl: `${process.env.NEXT_PUBLIC_HOSTNAME}/api/createTokenRequest`,
});
...
执行npm run dev并访问 http://localhost:3000.你还看不到它,但你现在已经连接到了Aply!
第7步:建立参与者的名单
接下来是Ably的第一项功能:连接到特定消息渠道的用户列表,你将使用Ably的Presence功能来检索。
在组件目录中创建一个新的组件,名为Attains.js,并输入以下代码。
import React from "react";
import { usePresence, assertConfiguration } from "@ably-labs/react-hooks";
import styles from "../styles/Home.module.css";
export default function Participants(props) {
const ably = assertConfiguration();
const [presenceData] = usePresence("headlines");
const presenceList = presenceData.map((member, index) => {
const isItMe = member.clientId === ably.auth.clientId ? "(me)" : "";
return (
<li key={index} className={styles.participant}>
<span className={styles.name}>{member.clientId}</span>
<span className={styles.me}>{isItMe}</span>
</li>
);
});
return <ul>{presenceList}</ul>;
};
assertConfiguration是一个实用函数,用于检查组件是否可以访问你在index.js中创建的Ably实例。如果不能,它会抛出一个错误。
参与者组件采用了Ably的React Hooks中的usePresence()钩子来检索在特定频道上的成员列表。在这种情况下,我们将使用用户将发布新闻文章URL的频道的名称头条。
它处理成员的列表,并检查clientId是否与认证时分配给这个特定实例的clientId相匹配。如果是,那么它就在列表中的clientId上加上"(我)",然后显示该列表。
你现在必须在index.js页面中导入并使用这个组件。
import Head from "next/head";
import Image from "next/image";
import ablyLogo from "../public/ably-logo.svg";
import styles from "../styles/Home.module.css";
import Participants from "../components/Participants";
import { configureAbly } from "@ably-labs/react-hooks";
configureAbly({
authUrl: `${process.env.NEXT_PUBLIC_HOSTNAME}/api/createTokenRequest`,
});
export default function Home() {
return (
…
<h1>Realtime News</h1>
<h2>Share your favorite news articles</h2>
<h3>Participants</h3>
<Participants />
</main>
</div>
);
}
你的页面应该更新,你将在参与者列表中看到当前用户的名字。
注意:如果你没有看到这个页面,那么你的服务器可能没有运行。执行npm run dev并刷新页面。
打开一个新的标签,注意到一个新的用户出现在列表中,并且两个列表都有相应的更新。
第8步:发布文章
通过选择从服务器上发布,你可以验证和预处理客户发送到频道的内容。例如,你可能想实现一个脏话过滤器,这在服务器端是很容易做到的。
在本教程中,你将获取用户提交的URL,并将其提交给服务器。服务器将首先检查该URL是否有效。如果是,它将查找与该URL相关的OpenGraph数据,并检索任何可用的标题、描述和图像信息,以用于预览。
首先,安装以下NPM包。
npm install open-graph-scraper url-exists-nodejs
然后,创建另一个API端点,就像你为认证所做的那样。客户将向*/api/publish端点发出POST请求,以发布一个URL。请求主体将包含一个文本*字段,其中包含他们想要提交给你的服务的URL。
在*/pages/api目录下创建一个publish.js*文件,并用以下代码填充它。
import Ably from "ably/promises";
import urlExists from "url-exists-nodejs";
import ogs from "open-graph-scraper";
const ably = new Ably.Rest(process.env.ABLY_SERVER_API_KEY);
const channel = ably.channels.get("headlines");
export default async function handler(req, res) {
if (req.method !== "POST") {
res.status(405).json({});
return;
}
const validUrl = await urlExists(req.body.text);
if (!validUrl) {
res.status(400).json({ url: req.body.text, message: "not a valid URL"});
return;
}
const { result } = await ogs({ url: req.body.text });
if (!result.success) {
res.status(400).json({ message: "not a valid URL" });
return;
}
channel.publish("new-headline", {
author: req.body.author,
site: result?.ogSiteName || "unknown",
title: result?.ogTitle || "unknown",
desc: result?.ogDescription || "unknown",
image: result.ogImage?.url || "",
url: req.body.text
});
res.status(200).json({});
}
这段代码。
- 使用REST SDK实例化一个ably的实例,并使用你之前创建的服务器API密钥进行验证
- 使用ably.channels.get存储对头条频道的引用
- 检查传入的请求体是否包含一个URL
- 验证该URL是否存在
- 查阅与URL相关的OpenGraph数据
- 将用户提交的URL和获取的OpenGraph数据发布到头条频道,主题为 "new-headline"
在你的服务器运行时,使用Curl(如果你喜欢使用GUI,也可以使用Postman)对你的新端点进行测试请求。
curl -X POST http://localhost:3000/api/publish -H 'Content-Type: application/json' -d '{"text":"https://bbc.co.uk"}'
如果一切正常,你应该在终端看到一个空的JSON响应。
{}%
要验证你的消息是否正确发布,请访问Ably Dashboard中的应用程序设置,并选择开发者控制台选项卡。在下图所示的地方输入通道名称*(标题*),然后点击输入存在。再次执行Curl(或Postman)请求。
如果你在页面左侧的频道活动中看到你的信息,那么发布就成功了。现在你需要添加一种从客户端发布的方式,同时列出已经发布的文章。你将首先处理发布方面的事情。
第9步:实现一个用于发布的用户界面
你将为此创建一个名为Articles的新组件。在组件目录下一个名为Articles.js的文件中,输入以下代码。
import React, { useState } from "react";
import { useChannel } from "@ably-labs/react-hooks";
import styles from "../styles/Home.module.css";
export default function Articles() {
let inputBox = null;
const [headlineText, setHeadlineText] = useState("");
const [_, ably] = useChannel("headlines", (headline) => {
// update headlines
});
const headlineTextIsEmpty = headlineText.trim().length === 0;
const handleFormSubmission = async (event) => {
const nonEnterKeyPress = event.charCode && event.charCode !== 13;
if (nonEnterKeyPress || headlineTextIsEmpty) {
return;
}
event.preventDefault();
await fetch("/api/publish", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ text: headlineText, author: ably.auth.clientId }),
});
setHeadlineText("");
inputBox?.focus();
};
return (
<div>
<form onSubmit={handleFormSubmission} className={styles.form}>
<input
type="text"
ref={(element) => {
inputBox = element;
}}
value={headlineText}
placeholder="News article url"
onChange={(event) => setHeadlineText(event.target.value)}
onKeyPress={handleFormSubmission}
className={styles.input}
/>
<button
type="submit"
className={styles.submit}
disabled={headlineTextIsEmpty}
>
Submit
</button>
</form>
</div>
);
}
这个组件显示一个表单,供用户提交新的头条新闻。你可以在返回语句中看到表单的标记。
这段代码。
- 创建一个React使用状态钩,以存储用户提交的头条新闻URL。
- 用useChannel连接到Ably中的头条频道。最终,你将用它来向用户显示头条新闻预览列表。现在,你只是用它来访问你在index.js中配置的Ably实例。你的应用程序需要访问这个实例,以确定想要发布头条的用户的clientId ,这样它就可以将头条与正确的用户联系起来。
- 当用户输入一个URL,将其提交到你在步骤8中创建的*/api/publish端点时,调用handleFormSubmission*。
在index.js 中导入文章 组件*,* 在导入参与者 组件之后*。*
import Articles from "../components/Articles";
然后,把它添加到页面标记中,在参与者组件的下面。
...
<main className={styles.main}>
<Image
alt="ably logo"
src={ablyLogo}
width="160px"
height="100%"
></Image>
<h1>Realtime News</h1>
<h2>Share your favorite news articles</h2>
<h3>Participants</h3>
<Participants />
</main>
...
通过确保你的开发服务器仍在运行(如果没有,则使用npm run dev重新启动它)并访问http://localhost:3000。
你应该看到下面的表格。
输入一篇新闻文章的URL,然后点击提交。
然后,回到你的Ably仪表板中的开发控制台,找到该消息。
你的应用程序的发布方面现在正在工作。你现在需要列出已经发布的文章。
第10步。检索最新的消息
当用户第一次访问该页面时,你不希望他们只看到一个空的页面。你还想让他们看到在他们订阅该频道之前发表在该频道的最新信息。
为了做到这一点,我们要使用Ably的历史API,还有Next.js提供的一个非常漂亮的功能,叫做增量静态再生(ISR)。ISR是Next.js提供的方法之一,使你能够从 "实时 "数据中创建静态页面。这对网站的快速加载时间和搜索引擎优化非常有利。
创建lib/history.js 文件,并在其中加入以下代码。
import Ably from "ably";
const rest = new Ably.Rest.Promise({
key: process.env.ABLY_SERVER_API_KEY,
});
const channel = rest.channels.get("headlines");
export async function getHistoricalMessages() {
const resultPage = await channel.history({ limit: 5 });
/*
See issue: https://github.com/vercel/next.js/issues/11993.
The JSON must be re-parsed or certain Message object items
cannot be serialized by props later.
*/
const historicalMessages = JSON.parse(JSON.stringify(resultPage.items));
return historicalMessages;
}
getHistoricalMessages 函数简单地调用了History API,以检索头条频道上的五条最新信息。要把这些消息添加到页面上,请按如下方式修改index.js 。
首先,导入getHistoricalMessages 函数。
import { getHistoricalMessages } from "../lib/history";
然后,给主页组件添加一个props参数。
export default function Home(props) {
return (
<div className={styles.container}>
...
在文章组件中添加一个历史属性,该属性引用props.history。
<Articles history={props.history} />
最后,在index.js的底部添加一个getStaticProps 函数。这是一个特殊的Next.js函数,在构建时预先渲染页面。它通过调用你之前创建的getHistoricalMessages函数来填充props ,并将其传递给Home组件。在我们的例子中,我们将revalidate属性设置为10,以便每十秒钟递增地重新生成页面。
export async function getStaticProps() {
const historicalMessages = await getHistoricalMessages();
return {
props: {
history: historicalMessages,
},
//enable ISR
revalidate: 10,
};
}
这很好!这意味着我们的网站访问者几乎可以即时看到最近的信息,搜索引擎也可以抓取这些页面以提高SEO排名。然而,当我们把它与Ably一起使用时,它确实带来了一个潜在的问题。也就是说,如果我们在检索历史记录时,有新的消息到达频道,会发生什么?这可能会导致竞赛条件和消息出现的顺序不一致。
为了避免这个问题,我们将使用Ably的频道回退功能,它是内置在Ably的React Hooks中的。这是一个方便的方法,可以检索到在当前客户端附加到该频道之前两分钟内已经发布到该频道的消息。这没有给我们历史API的灵活性,但仍然允许我们在用户与页面互动时向他们展示一些历史。
当你的应用程序调用文章 组件中的Ably React HooksuseChannel 钩时,你就会知道用户已经连接到Ably。你要做的是设置一个标志,当更新时,清除由调用历史API(从服务器)填充的现有状态,并从那时起用使用频道回放(在客户端)检索的历史文章来取代它。
修改components/Article.js 文件中的代码如下。
/*
clearHistoryState:
- When true, historical messages are retrieved and rendered statically from the History API.
- When false, historical messages are retrieved using channel rewind to prevent a race condition
where new messages are arriving on the channel while history is still being retrieved.
*/
let clearHistoryState = true;
export default function Articles(props) {
let inputBox = null;
const [headlineText, setHeadlineText] = useState("");
const [headlines, updateHeadlines] = useState(props.history);
const [_, ably] = useChannel("[?rewind=5]headlines", (headline) => {
if (clearHistoryState) {
resetHeadlines();
clearHistoryState = false;
}
updateHeadlines((prev) => [headline, ...prev]);
});
const resetHeadlines = () => {
updateHeadlines([]);
};
每次有新消息出现在头条频道时,useChannel函数就会启动。你捕捉这些头条新闻,并使用一个新的React状态钩来更新组件的状态。你还使用频道倒退来显示频道中的最后五条消息。使用rewind就像在频道名称前加上[?rewind=N]一样简单,其中N是要检索的最大数量的消息。
但现在还没有人可以看到历史信息!你将在下一章中解决这个问题。你将在下一步中解决这个问题。
第11步:生成文章预览
你现在需要显示现有的URL及其在发布步骤中收集的所有相关的OpenGraph数据。
为此创建一个新的组件,叫做ArticlePreview**(/components/articlePreview.js),其代码如下。
import React from "react";
import styles from "../styles/Home.module.css";
import Image from "next/image";
export default function ArticlePreview({ index, headline }) {
return (
<article key={index} className={styles.card}>
<Image
className={styles.pic}
src={`https://res.cloudinary.com/mark-ably/image/fetch/w_200,h_200,c_fill/${headline.data.image}`}
alt={headline.data.title}
width={200}
height={200}
objectFit="cover"
quality={80}
></Image>
<div className={styles.info}>
<h1 className={styles.title}>
<a href={headline.data.url} className={styles.link}>
{headline.data.title}
</a>
</h1>
<div className={styles.details}>
{headline.data.site} - shared {headline.data.timestamp} by {headline.data.author}
</div>
<p>{headline.data.desc}</p>
</div>
</article>
);
};
该组件接受一个由任意索引和标题组成的props对象。索引只是用来为文章提供一个唯一的键,这有助于React跟踪数组中哪些元素发生了变化、被添加或被删除。
注意,我们正在使用OpenGraph为你提交发布的每篇文章返回的预览图片URL的代理。这是因为我们想用Next.js的 Image标签来显示图片,它有一些有用的优化功能。
但是,使用Image的一个限制是,图片本身必须是静态导入的,或通过白名单的外部URL访问。由于你事先不知道我们的用户将向你的应用程序提交什么URL,你需要代理这些URL。本教程使用Cloudinary,它可以让你通过CDN动态地提供图片,还可以使用路径参数来实时转换图片。
要将Cloudinary列入白名单,你必须在next.config.js中添加cloudinary.com作为认可的图像主机。
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
swcMinify: true,
images: {
domains: ["res.cloudinary.com"],
},
}
module.exports = nextConfig
**注意:**在对next.config.js 进行任何修改后,你必须重新启动服务器。
现在你可以添加ArticlePreview组件作为Articles组件的一个子组件。
在*/components/Articles.js文件的底部,添加一个名为processMessage*的函数,检查消息的有效载荷并格式化数据,以便于显示。
function processMessage(headline, currentClientId) {
headline.data.timestamp = "timestamp" in headline ?
formatDate(headline.timestamp) : "earlier";
headline.data.url = headline?.data?.url || "http://example.com";
headline.data.image = headline?.data?.image || "http://placekitten.com/g/200/300";
return headline;
}
你会看到,这段代码正在格式化时间戳中的日期。在processMessage下面立即创建formatDate 函数来处理格式化问题。
function formatDate(date) {
const regex = /(?:[^T]+)T([0-9:]+)/gm;
const dateToFormat = new Date(date).toISOString();
const match = regex.exec(dateToFormat);
return match[1];
}
将ArticlePreview组件作为Articles组件的子组件导入。你在*/components/articles.js*中的导入列表现在应该是这样的。
import React, { useState } from "react";
import { useChannel } from "@ably-labs/react-hooks";
import ArticlePreview from "./ArticlePreview";
import styles from "../styles/Home.module.css";
然后,就在handleFormSubmission函数的上方,为你的React headlines use state hook中的每个标题调用processMessage函数,传入标题和发布者的客户端ID。
const processedHeadlines = headlines.map((headline) =>
processMessage(headline, ably.auth.clientId)
);
在processHeadlines函数的正下方,创建一个名为articles的新变量,为每个被处理的头条新闻填充ArticlePreview组件。
const articles = processedHeadlines.map((headline, index) => (
<ArticlePreview key={index} headline={headline} />
));
最后,将articles变量(在一个*
return (
<div>
<form onSubmit={handleFormSubmission} className={styles.form}>
<input
type="text"
ref={(element) => {
inputBox = element;
}}
value={headlineText}
placeholder="News article url"
onChange={(event) => setHeadlineText(event.target.value)}
onKeyPress={handleFormSubmission}
className={styles.input}
/>
<button
type="submit"
className={styles.submit}
disabled={headlineTextIsEmpty}
>
Submit
</button>
</form>
// add the articles variable here
<div>{articles}</div>
</div>
);
第12步:测试你的创作
确保你的服务器正在运行,然后在**https://localhost:3000**,打开两个或多个标签。
在网上找一篇文章,并提交其URL。如果有关网站有OpenGraph数据与之相关,你会看到文章的预览图同时出现在所有打开的应用标签中。如果没有预览图,你会看到一只可爱的小猫代替。
第13步:将你的应用程序部署到Vercel上
在你向世界发布你的新应用程序之前,你需要为它在Vercel上的新家做准备。
首先,你必须预先解决任何CORS问题。在next.config.js中,添加以下HTTP头信息。这些指定了你的请求将支持哪些来源、方法和其他功能。
/** @type {import('next').NextConfig} */
const nextConfig = {
reactStrictMode: true,
images: {
domains: ["static.ably.dev", "res.cloudinary.com"],
},
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",
},
],
},
];
},
};
module.exports = nextConfig;
Vercel会从GitHub(也包括GitLab和BitBucket,但本教程假设你使用GitHub)部署你的应用程序。
为了将你的新聊天应用程序部署到Vercel,你需要。
- 创建一个GitHub账户(如果你还没有的话)
- 将你的应用程序推送到一个新的GitHub仓库
- 创建一个Vercel账户
- 创建一个新的Vercel项目,从GitHub仓库导入你的应用程序。(这将需要你授权Vercel使用你的GitHub账户)。
在你推送代码到GitHub之前,将你的*.env文件添加到.gitignore*,方法是在你的项目根目录的终端执行以下代码。Vercel会管理自己的环境变量,你不希望你的API密钥在GitHub上可见。
echo ".env" >> .gitignore
然后,提交并推送你的本地仓库到GitHub。(说明)
访问你的Vercel仪表板,将你的仓库导入Vercel。
在显示的对话框中,点击部署按钮。
Vercel将需要一两分钟来部署你的代码。一旦完成,点击你的网站的预览,在浏览器中打开它。
注意,你的应用程序还不能工作,因为你还没有正确配置它,要这样做,你需要Vercel分配给你的URL。从地址栏复制URL(应该以*.vercel.app*结尾),然后关闭该标签。回到显示你的网站预览的选项卡,点击进入仪表板。
进入仪表盘页面顶部的设置标签,从左边的菜单中选择环境变量。
添加以下环境变量。
- NEXT_PUBLIC_HOSTNAME=你的Vercel URL**(重要!**不要包括任何尾部的后缀。不要包括任何尾部的反斜杠(/) )
- ABLY_SERVER_API_KEY=从你的*.env文件中获取*。
- ABLY_CLIENT_API_KEY=来自你的*.env*文件。
点击页面顶部的部署标签,重新部署你的应用程序。
当部署完成后,点击访问按钮,在一个或多个标签中重新打开你的应用程序,并开始发布头条新闻!
总结
在本教程中,你学到了如何在Next.js应用程序中实现实时功能,并将其部署到Vercel。在这一过程中,你学到了一些关于认证和发布的最佳实践,以及如何使用Ably的存在感、历史和频道回放功能。
你肯定可以改进这个应用程序。首先,你可能会发现,某些带有查询参数的URL不能通过验证测试。你可以实施你自己的验证规则来解决这个问题。
而且,如果你想让它更上一层楼,这似乎是那种可以很好地作为黑客新闻类型的克隆应用。考虑添加一些投票按钮,甚至可以添加实时评论功能,就像我们在最近关于为博客评论创建实时更新的文章中所展示的那样。
我们很乐意看到你在这方面的努力。请在Twitter上联系我们@ablyrealtime, 这样我们就可以分享你的应用程序了!