前言
我在上一篇写了如何私有化部署我开发的短连接服务,这篇我们来讲解一下这个服务的具体实现。
当初开发这个短链接服务主要是为了我的个人博客。每次分享博客文章时,链接都特别冗长,因为链接格式是 https://域名/年/月/日/类别1/类别2/文件名
。当然,这个格式本身是能修改的,但我觉得这种结构作为博客链接相当不错,因为地址不会重复。然而,这样的链接分享出去就显得有些"恶心"了,尤其是当包含中文名时,经过编码后往往会长达上百个字符,实在不够优雅。于是,我萌生了开发一个短链接服务的想法。
其实,免费的短链接服务市场上并不少见,但要么用不了,要么稳定性有些差,而且我需要为多个链接生成短网址,所以最终决定自己动手实现。毕竟,这个功能的核心逻辑并不复杂,完全可以在可控范围内完成。
实现思路
由于这主要是自用的服务,所以我并不需要考虑高并发、访问速度等问题,这使得开发过程可以更加专注于功能实现本身。
短链接主要思路
- 生成唯一短链接
- 输入长链接,通过哈希算法(如 MD5、SHA256)生成唯一标识。
- 提取哈希值的部分内容(如前6位)作为短链接的标识符。
- 存储映射关系
- 在数据库中存储短链接标识符与原始长链接的映射关系。
- 数据库表结构示例:
id
、short
、url
、created_at
。
- 访问短链接
- 用户访问短链接时,根据短链接标识符查询数据库。
- 找到对应的长链接后,通过 HTTP 3xx 重定向跳转到原链接。
可以看出,整个实现过程基本没有什么技术难点,简单来说就是围绕一个存储、一个查询的接口开发。这种轻量级的架构非常适合个人使用场景。
存储库选型
在写这个功能的时候,我主要是考虑该往哪里存储数据,虽然我有公网数据库,但我并不想在这个开放站点上使用,因为我实在没有精力去做数据库的安防,如果有爱搞事的人攻击我的数据库甚至服务器,我实在难以招架,也没有精力去防御。
那么我最初考虑的是白嫖 Git 平台,比如 Gitee、Github,通过 open api 读写仓库内的一个 json 文件,以此来实现云数据库的效果,为了避免频繁读写仓库文件带来的性能问题,通过缓存定时写入仓库 json 文件里。
但后来发现 Vercel 中有联合的一些免费云数据库额度可以用,这种云数据库服务不仅满足了我的存储需求,还避免了自行维护数据库的复杂性。
准备工作
初始化 Fastify 项目
因为我对 Vercel 的配置文件不是特别熟悉,所以为了简化工作流程,直接从 Vercel 官网提供的模板中选取一个 Fastify 模板项目,这样可以大大减少初始化工作量。
- 我们直接在创建项目的模板列表中搜索
Fastify
, 然后点击进入模板

- 进入模板说明界面后,点击 Deploy 按钮进行部署

- 在部署界面需要先创建一个项目,这里可以修改项目名和可访问性,然后点击
Create
创建

- 等待创建完成,点击
Continue Dashboard
进入控制台。

配置 Supabase 数据库
- 进入项目控制台后,我们点击 Storage 进入存储页面
- 选择 Supabase 点击 Create 进行创建

- 选择 Free Plan 免费计划,点击
Continue

- 配置
Database Name
后点击Create

- 创建完成后即可看到下面这个界面。

拉取项目
- 前面我们通过 Vercel 已经初始化了项目,在自己的 GitHub 中就能看到仓库了,我们拉取到本地。

- 由于我们需要用到 Vercel 的环境变量,所以我们需要全局安装
vercel
bash
npm i -g vercel
- 然后连接到 vercel 项目
bash
vercel link
基本一路回车就可以。

- 拉取环境变量文件
bash
vercel env pull .env.development.local
- 安装依赖
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 仓库。
相关链接
- 在线示例: short.pangcy.cn/
- 项目源码: github.com/Alessandro-...