我是如何制作一个速度极快、运行在边缘的链接缩短器的
由 Mux 主办的 DEV 全球展示挑战赛:展示你的项目!
我最近开发了一款名为Deoxys 的链接缩短工具(名字来源于一只速度极快的宝可梦)。它速度非常快,因为它使用了Vercel Edge Functions。Edge Functions 本质上是运行在云端的函数,因此速度极快,无需冷启动,而且所有操作都在服务器端完成,客户端完全没有负担。在这篇博客中,我将概述 Deoxys 的架构。
堆
概要
前端使用 Next.js 构建,它是一个全栈React框架。我使用 tRPC 作为 API 层,以获得出色的类型安全。如果您不熟悉tRPC,可以阅读我之前写的一篇博客文章。数据库是PlanetScale 提供的 MySQL 数据库(准确来说是Vitess版本)。
每当有人缩短链接时,前端都会调用 tRPC mutation 将其存储到数据库中。我使用的是 Prisma ORM,因为它确实是最好的。
现在有趣的部分来了,每当有人访问一个缩短的 URL,比如说https://deoxys.nexxel.dev/cat,它就会运行一个边缘函数来检查提供的 slug(在本例中是 cat)是否是一个有效的 slug,如果是,它会将用户重定向到原来的 URL。
代码讲解
你可以在这里查看源代码。这只是一个标准的 Next.js 项目,我还设置了 tRPC 和 Prisma,并连接到了我的数据库。
// prisma/schema.prisma
model ShortLink {
id Int @id @default(autoincrement())
createdAt DateTime @default(now())
url String @db.VarChar(3000)
slug String @unique
@@index([slug])
}
这是数据库的架构图,非常简单。接下来,我创建了一个 API 端点,用于检查 slug 是否有效。为此,我使用了Next.js API 路由。之所以这样做,是因为 edge 函数无法使用 Prisma 客户端。请注意,这是一个动态路由。
//src/pages/api/get-link/[slug].ts
import type { NextApiRequest, NextApiResponse } from "next";
import { prisma } from "../../../db/client";
export default async (req: NextApiRequest, res: NextApiResponse) => {
const slug = req.query["slug"];
if (!slug || typeof slug !== "string") {
res.status(404).json({ message: "please provide a slug" });
return;
}
const data = await prisma.shortLink.findFirst({
where: {
slug: {
equals: slug,
},
},
});
if (!data) {
res.status(404).json({ message: "short link not found" });
return;
}
res.setHeader("Content-Type", "application/json");
res.setHeader("Access-Control-Allow-Origin", "*");
res.setHeader("Cache-Control", "s-maxage=1000000000, stale-while-revalidate");
res.json(data);
return;
};
如果 slug 有效,它还会将响应缓存 1000000000 秒。这使得边缘计算功能速度更快。
接下来,我编写了我的边缘函数,在 Next.js 中,边缘函数是用以下方式编写的:pages/_middleware.ts
// src/pages/_middleware.ts
import { NextFetchEvent, NextRequest, NextResponse } from "next/server";
export async function middleware(req: NextRequest, event: NextFetchEvent) {
if (
req.nextUrl.pathname.startsWith("/api/") ||
req.nextUrl.pathname === "/"
) {
return;
}
const slug = req.nextUrl.pathname.split("/").pop();
const fetchSlug = await fetch(`${req.nextUrl.origin}/api/get-link/${slug}`);
if (fetchSlug.status === 404) {
return NextResponse.redirect(req.nextUrl.origin);
}
const data = await fetchSlug.json();
if (data?.url) {
return NextResponse.redirect(data.url);
}
}
它会调用该端点并检查别名是否有效,如果有效,则将用户重定向到与该别名对应的 URL。大概就是这样。
现在我用 Tailwind 为它搭建了一个漂亮的 UI。我还创建了两个 tRPC 端点。第一个端点用于实时检查某个 slug 是否已被使用过。我觉得这种实时验证功能非常棒。看看这个。
第二个端点是创建新链接并将其写入数据库。代码如下所示。
// src/pages/api/trpc/[trpc].ts
import * as trpc from "@trpc/server";
import * as trpcNext from "@trpc/server/adapters/next";
import { z } from "zod";
import { prisma } from "../../../db/client";
export const appRouter = trpc
.router()
.query("checkSlug", {
input: z.object({ slug: z.string() }),
async resolve({ input }) {
const slugCount = await prisma.shortLink.count({
where: {
slug: {
equals: input.slug,
},
},
});
return { used: slugCount > 0 };
},
})
.mutation("createShortLink", {
input: z.object({ slug: z.string(), url: z.string() }),
async resolve({ input }) {
try {
await prisma.shortLink.create({
data: {
slug: input.slug,
url: input.url,
},
});
} catch (error) {
console.log(error);
}
},
});
export type AppRouter = typeof appRouter;
export default trpcNext.createNextApiHandler({
router: appRouter,
createContext: () => null,
});
我这里也用了zod做输入验证。真是个很棒的库。
剩下的部分很简单,我只需要创建一个表单组件来调用我的tRPC端点。首先,我为表单声明了一些状态。
const [form, setForm] = useState<Form>({ slug: "", url: "" });
我还在这里调用了我的 tRPC 端点。
const checkSlug = trpc.useQuery(["checkSlug", { slug: form.slug }], {
refetchOnReconnect: false,
refetchOnMount: false,
refetchOnWindowFocus: false,
});
const createShortLink = trpc.useMutation(["createShortLink"]);
表格来了。
<form
onSubmit={(event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
createShortLink.mutate({ ...form });
}}
className="mt-6"
>
{checkSlug.data?.used ? (
<span className="font-medium text-center text-red-500">
This link has already been used
</span>
) : (
<span className="font-medium text-center">
{url}/{form.slug}
</span>
)}
{/* ... */}
在这里,我将一个onSubmit函数传递给表单,该函数会调用 tRPC mutation 并将表单状态作为输入传递。此外,我还在这里实现了实时验证:如果端点返回usedtrue,则会将边框变为红色并显示错误消息。
表单内部只有一堆输入框,以下是它们的工作原理。
<input
type="url"
value={form.url}
maxLength={3000}
onChange={(e) => setForm({ ...form, url: e.target.value })}
placeholder="https://duckduckgo.com"
className="block w-full px-4 py-2 font-normal bg-black border-2 border-gray-200 rounded-md focus:outline-none placeholder:text-gray-400"
required
/>
此输入框用于输入需要缩短的 URL,这里我传递了一个onChange函数来设置表单状态。此外,它还type="url"有助于验证。
我用的是一个叫 random-word-slugs 的库来生成随机词条,挺不错的。这是随机按钮的代码。
<input
type="button
value="Random"
className="px-4 py-2 ml-2 font-medium transition-colors duration-300 bg-indigo-500 border-2 border-indigo-500 rounded cursor-pointer hover:bg-transparent"
onClick={() => {
const slug = generateSlug();
setForm({
...form,
slug,
});
checkSlug.refetch();
}}
/>
该generateSlug()函数来自 random-word-slugs 库。我还设置了状态,并检查该特定 slug 是否已被使用过。
如果短链接创建成功,则会显示此页面。
这是相应的代码。
if (createShortLink.status === "success") {
return (
<div className="flex flex-col items-center justify-center mx-3 mt-6">
<span className="pb-3 text-lg font-semibold">Here's your link!</span>
<div className="flex items-center gap-2">
<h1 className="text-lg text-center md:text-2xl">{`${url}/${form.slug}`}</h1>
<button
className="px-4 py-1.5 ml-3 font-medium transition-colors duration-300 bg-indigo-500 border-2 border-indigo-500 rounded hover:bg-transparent"
onClick={() => {
copy(`${url}/${form.slug}`);
}}
>
Copy
</button>
</div>
<button
className="px-4 mt-8 py-1.5 ml-3 font-medium transition-colors duration-300 bg-indigo-500 border-2 border-indigo-500 rounded hover:bg-transparent"
onClick={() => {
createShortLink.reset();
setForm({ slug: "", url: "" });
}}
>
Create New
</button>
</div>
);
}
tRPC 也会返回 mutation 的状态。因此,如果返回 true success,则会显示缩短后的 URL 和一个复制到剪贴板的按钮。此外,还有一个“创建新项”按钮,点击后会重置 tRPC mutation 并重置表单状态。
您可以在这里查看该组件的完整代码。
就是这样。这里面涉及很多环节,希望我已经让你对代欧奇希斯的运作方式有了大致的了解。
网站:https://deoxys.nexxel.dev
代码:https://github.com/nexxeln/deoxys
鸣谢
非常抱歉之前没有把这个也写进去。
感谢阅读!
文章来源:https://dev.to/nexxeln/how-i-made-a-really-fast-link-shortener-that-runs-on-the-edge-2gm




