半个小时,我开发了个短链接服务

前言

我在上一篇写了如何私有化部署我开发的短连接服务,这篇我们来讲解一下这个服务的具体实现。

当初开发这个短链接服务主要是为了我的个人博客。每次分享博客文章时,链接都特别冗长,因为链接格式是 https://域名/年/月/日/类别1/类别2/文件名。当然,这个格式本身是能修改的,但我觉得这种结构作为博客链接相当不错,因为地址不会重复。然而,这样的链接分享出去就显得有些"恶心"了,尤其是当包含中文名时,经过编码后往往会长达上百个字符,实在不够优雅。于是,我萌生了开发一个短链接服务的想法。

其实,免费的短链接服务市场上并不少见,但要么用不了,要么稳定性有些差,而且我需要为多个链接生成短网址,所以最终决定自己动手实现。毕竟,这个功能的核心逻辑并不复杂,完全可以在可控范围内完成。

实现思路

由于这主要是自用的服务,所以我并不需要考虑高并发、访问速度等问题,这使得开发过程可以更加专注于功能实现本身。

短链接主要思路

  • 生成唯一短链接
    • 输入长链接,通过哈希算法(如 MD5、SHA256)生成唯一标识。
    • 提取哈希值的部分内容(如前6位)作为短链接的标识符。
  • 存储映射关系
    • 在数据库中存储短链接标识符与原始长链接的映射关系。
    • 数据库表结构示例:idshorturlcreated_at
  • 访问短链接
    • 用户访问短链接时,根据短链接标识符查询数据库。
    • 找到对应的长链接后,通过 HTTP 3xx 重定向跳转到原链接。

可以看出,整个实现过程基本没有什么技术难点,简单来说就是围绕一个存储、一个查询的接口开发。这种轻量级的架构非常适合个人使用场景。

存储库选型

在写这个功能的时候,我主要是考虑该往哪里存储数据,虽然我有公网数据库,但我并不想在这个开放站点上使用,因为我实在没有精力去做数据库的安防,如果有爱搞事的人攻击我的数据库甚至服务器,我实在难以招架,也没有精力去防御。

那么我最初考虑的是白嫖 Git 平台,比如 Gitee、Github,通过 open api 读写仓库内的一个 json 文件,以此来实现云数据库的效果,为了避免频繁读写仓库文件带来的性能问题,通过缓存定时写入仓库 json 文件里。

但后来发现 Vercel 中有联合的一些免费云数据库额度可以用,这种云数据库服务不仅满足了我的存储需求,还避免了自行维护数据库的复杂性。

准备工作

初始化 Fastify 项目

因为我对 Vercel 的配置文件不是特别熟悉,所以为了简化工作流程,直接从 Vercel 官网提供的模板中选取一个 Fastify 模板项目,这样可以大大减少初始化工作量。

  1. 我们直接在创建项目的模板列表中搜索 Fastify, 然后点击进入模板
  1. 进入模板说明界面后,点击 Deploy 按钮进行部署
  1. 在部署界面需要先创建一个项目,这里可以修改项目名和可访问性,然后点击 Create 创建
  1. 等待创建完成,点击 Continue Dashboard 进入控制台。

配置 Supabase 数据库

  1. 进入项目控制台后,我们点击 Storage 进入存储页面
  2. 选择 Supabase 点击 Create 进行创建
  1. 选择 Free Plan 免费计划,点击 Continue
  1. 配置 Database Name 后点击 Create
  1. 创建完成后即可看到下面这个界面。

拉取项目

  1. 前面我们通过 Vercel 已经初始化了项目,在自己的 GitHub 中就能看到仓库了,我们拉取到本地。
  1. 由于我们需要用到 Vercel 的环境变量,所以我们需要全局安装 vercel
bash 复制代码
npm i -g vercel
  1. 然后连接到 vercel 项目
bash 复制代码
vercel link

基本一路回车就可以。

  1. 拉取环境变量文件
bash 复制代码
vercel env pull .env.development.local
  1. 安装依赖
bash 复制代码
pnpm install
pnpm add @supabase/supabase-js 

代码实现

封装数据库方法

初始化 supabase 实例

首先需要初始化supabase实例,这是连接数据库的基础:

js 复制代码
import { createClient } from '@supabase/supabase-js';
const supabase = createClient(process.env.SUPABASE_URL, process.env.SUPABASE_ANON_KEY)

封装 URL hash 值

使用 Node 原生的crypto库,将URL进行MD5签名,拿到 Hash 值后截取其中一部分作为短链接标识符。这种方法既保证了唯一性,又控制了标识符的长度:

js 复制代码
import crypto from 'node:crypto';
function generatorHash(url) {
  var md5 = crypto.createHash('md5');
  const hex = md5.update(url).digest('hex')
  return hex.slice(8, 24)
}

封装添加 URL 方法

这个方法负责处理URL的添加逻辑:先查询数据库中是否已存在该URL,如果存在则直接返回,避免重复;如果不存在则进行插入操作:

js 复制代码
export async function addUrl(req) {
  const link = req.body.url
  const short = generatorHash(link)
  const isExists = await getUrl(short)
  if (isExists.data.length) return { data: isExists.data[0] }
  return supabase.from('links').insert([{ link, short }]).select().single()
}

封装获取 URL 方法

这个方法根据短链查询对应的长链接URL,是短链接服务的核心查询功能:

js 复制代码
export function getUrl(short) {
  return supabase.from('links').select('*').eq('short', short)
}

实现 HTTP 接口

实现 addUrl 接口

这个接口处理用户提交的URL,生成短链接并返回给用户:

js 复制代码
app.post('/api/addUrl', async (req, reply) => {
  const result = await linkService.addUrl(req)
  return reply.status(200).type('application/json').send({
    code: 200,
    msg: 'success',
    url: `/u/${result.data.short}`
  })
})

实现短链接转发接口

当用户访问/u/:hash的短链时,系统查询短链映射的长链接URL,随后通过302重定向到长链接的网站地址,完成访问跳转:

js 复制代码
app.get('/u/:hash', async (req, reply) => {
  const result = await linkService.getUrl(req.params.hash)
  return reply.status(302).redirect(result.data[0].link)
})

前端页面实现

在根目录创建一个Public目录,这个目录下的文件可以直接访问。我们在这个目录下创建一个index.html,前端页面设计保持简约风格,只有一个输入框让用户输入URL地址,不需要特别复杂的内容:

html 复制代码
<!DOCTYPE html>
<html lang="en">

<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>Short Link Service</title>

  <style>
    * {
      margin: 0;
      padding: 0;
      box-sizing: border-box;
    }

    body {
      font-family: 'Arial', sans-serif;
      background: linear-gradient(135deg, #f5f7fa, #c3cfe2);
      color: #333;
      display: flex;
      justify-content: center;
      align-items: center;
      min-height: 100vh;
      margin: 0;
    }

    .wrapper {
      background: #fff;
      box-shadow: 0 4px 20px rgba(0, 0, 0, 0.1);
      border-radius: 12px;
      padding: 40px;
      min-width: 450px;
      text-align: center;
      position: relative;
    }

    header {
      font-size: 36px;
      margin-bottom: 24px;
      color: #444;
      font-weight: 700;
    }

    .url-wrapper {
      margin-top: 16px;
    }

    input[type="text"] {
      width: 100%;
      padding: 12px;
      margin-bottom: 12px;
      border: 1px solid #ccc;
      border-radius: 8px;
      font-size: 16px;
      transition: border-color 0.3s, box-shadow 0.3s;
    }

    input[type="text"]:focus {
      outline: none;
      border-color: #007bff;
      box-shadow: 0 0 5px rgba(0, 123, 255, 0.5);
    }

    input[type="button"] {
      width: 100%;
      padding: 12px;
      background: #007bff;
      color: #fff;
      border: none;
      border-radius: 8px;
      font-size: 16px;
      cursor: pointer;
      transition: background 0.3s, transform 0.2s;
    }

    input[type="button"]:hover {
      background: #0056b3;
    }

    input[type="button"]:active {
      transform: scale(0.98);
    }

    .response {
      margin-top: 16px;
      font-size: 16px;
      color: #28a745;
    }

    .response a {
      color: #007bff;
      text-decoration: none;
      font-weight: bold;
    }

    .response a:hover {
      text-decoration: underline;
    }
  </style>
</head>

<body>
  <div class="wrapper">
    <header>短链接服务</header>
    <div class="url-wrapper">
      <input type="text" id="url-input" placeholder="输入需要缩短的链接">
      <input type="button" value="添加" id="add-btn">
    </div>
    <div class="response" id="response-msg"></div>
  </div>

  <script>
    const urlInput = document.getElementById('url-input');
    const addBtn = document.getElementById('add-btn');
    const responseMsg = document.getElementById('response-msg');

    addBtn.addEventListener('click', () => {
      if (!urlInput.value.trim()) {
        responseMsg.style.color = 'red';
        responseMsg.textContent = '请输入有效链接';
        return;
      }

      responseMsg.textContent = '处理中...';
      responseMsg.style.color = '#007bff';

      fetch('/api/addUrl', {
        method: 'POST',
        headers: {
          'Content-Type': 'application/json;charset=utf-8'
        },
        body: JSON.stringify({ url: urlInput.value })
      })
        .then(res => res.json())
        .then((data) => {
          responseMsg.style.color = '#28a745';
          const url = window.location.origin + data.url;
          responseMsg.innerHTML = `短链接地址: <a href="${url}" target="_blank">${url}</a>`;
          urlInput.value = '';
        })
        .catch((error) => {
          responseMsg.style.color = 'red';
          responseMsg.textContent = `发生错误: ${error.msg}`;
        });
    });
  </script>
</body>

</html>

结语

通过以上步骤,我们实现了一个简单的短链接服务。可以看出整个功能确实很简单,技术难度也不高,唯一特殊点就是通过302转发链接的机制。

当然本篇只是简化后的版本,只保留了最纯粹的核心代码,没有做异常校验以及其他的拓展功能,完整代码可以查看我的 GitHub 仓库。

相关链接

相关推荐
Aphasia31120 分钟前
模式验证库——zod
前端·react.js
Mr Aokey1 小时前
Spring MVC参数绑定终极手册:单&多参/对象/集合/JSON/文件上传精讲
java·后端·spring
lexiangqicheng1 小时前
es6+和css3新增的特性有哪些
前端·es6·css3
地藏Kelvin1 小时前
Spring Ai 从Demo到搭建套壳项目(二)实现deepseek+MCP client让高德生成昆明游玩4天攻略
人工智能·spring boot·后端
拉不动的猪2 小时前
都25年啦,还有谁分不清双向绑定原理,响应式原理、v-model实现原理
前端·javascript·vue.js
烛阴2 小时前
Python枚举类Enum超详细入门与进阶全攻略
前端·python
孟孟~2 小时前
npm run dev 报错:Error: error:0308010C:digital envelope routines::unsupported
前端·npm·node.js
孟孟~2 小时前
npm install 报错:npm error: ...node_modules\deasync npm error command failed
前端·npm·node.js
菠萝012 小时前
共识算法Raft系列(1)——什么是Raft?
c++·后端·算法·区块链·共识算法
狂炫一碗大米饭2 小时前
一文打通TypeScript 泛型
前端·javascript·typescript