发布于 2026-01-06 2 阅读
0

我是如何制作一个速度超快的链接缩短器,它在边缘上运行 DEV 的全球展示与讲述挑战赛由 Mux 呈现:展示你的项目!

我是如何制作一个速度极快、运行在边缘的链接缩短器的

由 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])
}
Enter fullscreen mode Exit fullscreen mode

这是数据库的架构图,非常简单。接下来,我创建了一个 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;
};
Enter fullscreen mode Exit fullscreen mode

如果 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);
  }
}
Enter fullscreen mode Exit fullscreen mode

它会调用该端点并检查别名是否有效,如果有效,则将用户重定向到与该别名对应的 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,
});
Enter fullscreen mode Exit fullscreen mode

我这里也用了zod做输入验证。真是个很棒的库。

剩下的部分很简单,我只需要创建一个表单组件来调用我的tRPC端点。首先,我为表单声明了一些状态。

const [form, setForm] = useState<Form>({ slug: "", url: "" });
Enter fullscreen mode Exit fullscreen mode

我还在这里调用了我的 tRPC 端点。

const checkSlug = trpc.useQuery(["checkSlug", { slug: form.slug }], {
    refetchOnReconnect: false,
    refetchOnMount: false,
    refetchOnWindowFocus: false,
  });

const createShortLink = trpc.useMutation(["createShortLink"]);
Enter fullscreen mode Exit fullscreen mode

表格来了。

<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>
      )}
{/* ... */}
Enter fullscreen mode Exit fullscreen mode

在这里,我将一个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
/>
Enter fullscreen mode Exit fullscreen mode

此输入框用于输入需要缩短的 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();
    }}
/>
Enter fullscreen mode Exit fullscreen mode

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>
    );
  }
Enter fullscreen mode Exit fullscreen mode

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