基于 Cloudflare D1 的新版本(代号:shURL2)已经上线。由于 leancloud 垮掉,旧版本(代号:shURL1)已经不再适用,仅作为历史版本,不再提供支持。

新版本相较于旧版本,反应速度更快,部署更便捷,操作更优雅;部署在 cloudflare pages 上,更加稳定。

「shURL 短网址」

Github 仓库

欢迎使用shURL\texttt{shURL}
这是shURL\texttt{shURL} 工具的使用文档。

图:最终效果


综述

此工具为一「短网址」工具。

何为短网址?简单来讲,就是以工具将原网址缩短长度,使访问更加便捷。

例子:

  • 原网址:https://www.bilibili.com/video/BV1DK4y1z7wq/?spm_id_from=333.1387.upload.video_card.click
  • 短网址:https://shurl.daoxi365.dpdns.org/78yy

显然,shURL1 实现链接转换是需要后端服务器、数据库的。但是,此工具选择了基于 LeanCloud 的线上储存,因此可以运行于静态网站,如 Github PagesNetlify 等。

shURL2 采用 Cloudflare D1,基于 cloudflare 的稳定性和性能,部署在 cloudflare pages 上,更加稳定,且能享受赛博菩萨 cf 的慷慨的免费额度。

shURL 部署操作步骤

shURL1

警告:这个版本程序所涉及的 LeanCloud 即将停止对外服务,这意味着此程序将不再适用。
部署的 demo 将会下线。

所以这里对其做一个折叠。

shURL1 的部署方法
  1. 注册或登录一个 LeanCloud​ 帐号。LeanCloud 账号分为国际、国内两种,如果你使用国外网络环境或用户面向国外,推荐使用国际版。反之,则建议使用国内版。
    本网站当前使用国内版 LeanCloud,可大幅提升访问速度。但是注册国内版 LeanCloud 账号需要实名认证。

    图:国际版账号注册图

  2. 创建一个新应用。应用名称随便写;计价方案选择开发版,否则会被收费。

  3. 进入刚刚创建的应用,左侧边栏下划到最后,在“设置”中找到“应用凭证”,在右侧按如图方式复制凭证信息。
    注意:第四步 国内版 用户必须保留。国际版用户可以不使用此 rest api 地址。

    图:我已配置好的一个国内版应用的凭证

  4. 打开 /js/app.js ,按注释(绿字)要求修改相关内容。

  5. 本地测试。
    如果有 Python 环境,可以使用以下命令打开一个开发环境:

    1
    2
    python -m http.server 8000
    # 接下来你可以在 127.0.0.1:8000 访问到你的页面。

    图:测试

    图:测试

  6. 关于个性化:

    • 您可以修改 media 文件夹下的网站图标(favicon.ico)和标志(logo.png)。
    • 您可以自行修改网页样式,但是我希望您能保留帮助文档的链接。
  7. push 到远端。打开 pages 服务或用其他平台发布。

    图:提交后的样子

  8. 修改安全网址。

    图:修改安全网址的操作(用的旧图)

  9. 测试,完成。

补充

您可以在结构化数据控制台上增、删短网址。

shURL2

不放图片了,因为操作比较简单。

我不会说是因为图片和放在一起导致博客的体积有点太大了

  1. 在 github 上 fork 我的仓库:https://github.com/PanDaoxi/shURL
  2. 在 cloudflare 上,创建 D1 数据库:
    左侧边栏:构建 -> 存储和数据库 -> D1 SQLite 数据库
    然后是右边操作台:创建数据库 -> 起名字(例如:shurl-db-> 位置:“亚太地区” -> 创建
    接下来在数据库的仪表盘,点击上面“控制台” -> 在下面的命令依次输入命令,每输入一条,按一次运行。
1
2
DROP TABLE IF EXISTS links;
CREATE TABLE links ( id INTEGER PRIMARY KEY AUTOINCREMENT, code TEXT UNIQUE NOT NULL, url TEXT NOT NULL, visits INTEGER DEFAULT 0, created_at DATETIME DEFAULT CURRENT_TIMESTAMP );
  1. 在 cloudflare 上,创建 pages 项目:
    左侧边栏:构建 -> 计算 -> Workers 和 Pages
    然后是右边操作台:创建应用程序 -> 下面 “想要部署 Pages?开始使用” -> 导入现有 Git 存储库 “开始使用” -> 选你刚才的仓库 -> 开始设置
  2. 部署:
条目
生产分支shURL2
框架预设不选(无)
构建命令exit 0
构建输出目录public
  1. 部署成功后:继续处理项目。

  2. 在项目首页,右边的操作台的上部:设置 -> 绑定 -> 添加 -> D1 数据库

    条目
    变量名称DB
    D1 数据库你刚刚创建的数据库

    然后重试部署。

  3. 部署完成。可以绑定自定义域名,然后大功告成了。

原理解析

shURL1 是我最初认识 LeanCloud 和 tailwindcss 的项目。以上技术栈均在原理中涉及。

shURL1 的原理是建立一个长网址与随机字符串的映射关系,并存储在 LeanCloud 数据库中。
shURL2 的原理也是建立一个长网址与随机字符串的映射关系,但存储在 D1 数据库中。

也就是说,用户每次提交原网址,shURL 都会生成一个随机字符串,并将其与长网址映射,存储在数据库中。每当用户将字符串通过接口传入 shURL 时,就会依据数据库存储重定向到原网址。

拆解源程序

shURL1

shURL1 原理

拆解 app.js

初始化 LC 对象,检测是否有 shURL 类:

1
2
3
4
5
6
7
AV.init({
appId: APPID,
appKey: APPKEY,
serverURL: REST_API,
});
AV.debug.enable();
const SHURL = AV.Object.extend("shURL");

这是一个创建最大长度为4848 的随机字符串的方法。该字符串就是待生成的短网址链接代号。

1
2
3
4
5
6
7
8
9
10
function randomStr(len) {
if (len > 48) len = 48;
var $chars = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678";
var maxPos = $chars.length;
var pwd = "";
for (var i = 0; i < len; i++) {
pwd += $chars.charAt(Math.floor(Math.random() * maxPos));
}
return pwd;
}

在前端给出提示的方法:

1
2
3
4
function setNote(s) {
var x = document.getElementById("newURL");
x.innerHTML = s;
}

检测是否已经存在用户给出的原长网址的方法。如果有就返回其长度。

1
2
3
4
5
6
7
function checker(s) {
const query = new AV.Query("shURL");
query.equalTo("shortURL", s);
query.find().then((link) => {
return link.length;
});
}

生成短网址的方法。首先检测是否符合 URL 格式,然后检查是否已经存在该长网址的短网址,如果有就直接返回,否则生成随机字符串,并上传到数据库,同时把短网址返回到前端。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
function make() {
try {
var ago = document.getElementById("ago");
var agoURL = ago.value;
var pattern =
/^((ht|f)tps?):\/\/([\w\-]+(\.[\w\-]+)*\/)*[\w\-]+(\.[\w\-]+)*\/?(\?([\w\-\.,@?^=%&:\/~\+#]*)+)?/;
if (!pattern.test(agoURL)) {
setNote(
'<b class="text-purple-700">您的输入似乎不是 URL,请重新输入。</b>',
);
return;
}
setNote(`<b class="text-state-800">请稍等!</b>`);

const query = new AV.Query("shURL");
query.equalTo("agoURL", agoURL);
query.find().then((link) => {
if (link.length >= 1) {
flag = true;
var shLink = link[0].get("shortURL");
var shURL =
`${WEBSITE}/` +
(GITHUB ? `${GITHUB_REPOSITORY}/` : ``) +
`?x=${shLink}`;
setNote(
`<b><a href="${shURL}" class="text-blue-700 hover:text-blue-800"><code>${shURL}</code></a></b>`,
);
return;
} else {
var shLink = randomStr(LENGTH);
while (checker(shLink)) shLink = randomStr(LENGTH);
const todo = new SHURL();
todo.set("agoURL", agoURL);
todo.set("shortURL", shLink);
todo.save().then(
(todo) => {
var shURL =
`${WEBSITE}/` +
(GITHUB ? `${GITHUB_REPOSITORY}/` : ``) +
`?x=${shLink}`;
setNote(
`<b><a href="${shURL}" class="text-blue-700 hover:text-blue-800"><code>${shURL}</code></a></b>`,
);
},
(error) => {
setNote(
`<b class="text-rose-600">发生错误(<code>CODE = ${error.code}</code>),请稍后再试。</b>`,
);
},
);
}
});
} catch (err) {
setNote(
`<b class="text-rose-600">发生错误(<code>${err}</code>),请稍后再试。</b>`,
);
}
}

如果用户给出了短网址,则需要根据数据库查询原来的长网址,并重定向到原网址。若不存在则报错。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
function getName(name) {
var reg = new RegExp("(^|&)" + name + "=([^&]*)(&|$)", "i");
var r = window.location.search.substr(1).match(reg);
var context = "";
if (r != null) context = r[2];
reg = null;
r = null;
return context == null || context == "" || context == "undefined"
? "ERROR ERROR ERROR!!!"
: context;
}

function loadTo() {
try {
var x = getName("x");
if (x == "ERROR ERROR ERROR!!!") return;
var w = `${WEBSITE}/` + (GITHUB ? `${GITHUB_REPOSITORY}/` : ``);
setNote(
`<b class="text-amber-700">正在跳转:<code>${x}</code>;<a href="${w}" class="text-sky-600 hover:text-sky-700">取消此操作</a>。</b>`,
);
const query = new AV.Query("shURL");
query.equalTo("shortURL", x);
query.find().then((link) => {
if (!link.length)
setNote(
`<b class="text-rose-600">发生错误,此链接尚未被注册或在数据库中被删除。</b>`,
);
window.location.href = link[0].get("agoURL");
});
} catch (err) {
setNote(
`<b class="text-rose-600">发生错误(<code>${err}</code>),请稍后再试。</b>`,
);
}
}

创建一个 SHURL 对象,完成初始化。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const initial = new SHURL();
try {
const query = new AV.Query("shURL");
} catch (err) {
initial.set("agoURL", "index");
initial.set("shortURL", "index1");
initial.save().then(
(todo) => {},
(error) => {
setNote(
`<b class="text-rose-600">初始化失败(<code>CODE = ${err.code}</code>),请联系网站开发者。</b>`,
);
},
);
}

shURL2

shURL2 采用 Pages Functions,也是 serverless 程序,这样便于操作数据库且不暴露在前台,更现代、更便捷、更安全。我们一起来看看:

结构
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
.
│ .gitignore
│ LICENSE
│ package.json
│ README.md
│ schema.sql
│ wrangler.toml

├─functions
│ │ [code].js
│ │
│ └─api
│ shorten.js

└─public
index.html
核心 1:functions/[code].js

这是一个路由,负责处理 /xxxx 这类请求,根据数据库把它们跳转到对应的页面,并统计访问量。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
export async function onRequestGet(context) {
const { env, params } = context;
const code = params.code;

if (!code) {
return context.next();
}
const link = await env.DB.prepare(
"UPDATE links SET visits = visits + 1 WHERE code = ? RETURNING url",
)
.bind(code)
.first();

if (!link) {
return new Response("404 Link not found", { status: 404 });
}
return Response.redirect(link.url, 302);
}
核心 2:functions/api/shorten.js

负责创建新的链接并写入数据库。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
export async function onRequestPost(context) {
const { request, env } = context;
const { url } = await request.json();

if (!url) {
return Response.json({ error: "Missing url" }, { status: 400 });
}
try {
new URL(url);
} catch {
return Response.json({ error: "Invalid URL" }, { status: 400 });
}
const existing = await env.DB.prepare(
"SELECT code FROM links WHERE url = ?",
)
.bind(url)
.first();

if (existing) {
return Response.json({ code: existing.code });
}
let code;
while (true) {
code = generateCode(4);
const exists = await env.DB.prepare(
"SELECT code FROM links WHERE code = ?",
)
.bind(code)
.first();
if (!exists) break;
}
await env.DB.prepare("INSERT INTO links (code, url) VALUES (?, ?)")
.bind(code, url)
.run();

return Response.json({ code });
}

function generateCode(length) {
const chars = "ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678";
let result = "";
for (let i = 0; i < length; i++) {
result += chars.charAt(Math.floor(Math.random() * chars.length));
}
return result;
}
数据库 schema.sql

高一微机课学到的 SQL,现在还真是有用武之地了。

这个文件是用于本地测试的,需要使用 wrangler 操作。如果你用 wrangler 远程部署,也可以利用此文件建表。

1
2
3
4
5
6
7
8
DROP TABLE IF EXISTS links;
CREATE TABLE links (
id INTEGER PRIMARY KEY AUTOINCREMENT,
code TEXT UNIQUE NOT NULL, -- 短链码 (x参数)
url TEXT NOT NULL, -- 原始长链接
visits INTEGER DEFAULT 0, -- 访问次数
created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

关于前端

前端使用的是 tailwindcss 作为 CSS 框架。确实好使。

shURL1 前端
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
<!DOCTYPE html>

<html>
<head>
<meta charset="utf-8" />
<title>shURL 短网址</title>
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<link rel="shortcut icon" href="./media/favicon.ico" />
<!--
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/leancloud-storage@3.15.0/dist/av-min.js"></script>
-->
<script src="./js/leancloud-javascript-sdk-6f78e1c/dist/av-min.js"></script>
<script src="./js/tailwindcss_v3.4.1.js"></script>
</head>

<body>
<div
class="flex flex-col items-center justify-center h-screen bg-sky-100"
>
<img
src="./media/logo.png"
alt="logo"
class="w-20 h-auto mb-8 rounded-lg"
/>
<form
action="javascript:make();"
class="flex items-center justify-center w-1/2 bg-white rounded-full shadow-xl"
>
<input
type="text"
placeholder="转换为短网址"
id="ago"
class="w-full px-4 py-2 rounded-l-full focus:outline-none"
/>
<button
type="submit"
class="flex items-center justify-center px-4 py-2 text-white bg-emerald-700 rounded-r-full hover:bg-emerald-900 focus:outline-none"
>
<b>Go!</b>
</button>
</form>
<br /><br />
<p id="newURL">
<a
class="text-teal-500 hover:text-teal-600"
href="https://blog.daoxi365.top/posts/43801/"
><b>帮助文档:我也想创建一个这样的网站。</b></a
>
</p>
</div>
<script src="./js/app.js"></script>
<script>
loadTo();
</script>
</body>
</html>

现在 shURL2 仍然使用 tailwind。但是前端有更重要的任务,就是把表单提交给 api。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>shURL 短网址</title>
<link rel="shortcut icon" href="<base64 图片>" />
<script src="https://cdn.tailwindcss.com"></script>
<style>
@font-face {
font-family: "HYTMR";
src: url("https://npm.elemecdn.com/fontcdn-ariasaka@1.0.0/HYTangMeiRen55W.woff2")
format("woff2");
font-weight: normal;
font-style: normal;
font-display: swap;
}

body,
input,
button {
font-family:
"HYTMR", "PingFang SC", "Microsoft YaHei", sans-serif;
}

/* 让输入框和按钮继承字体,同时保持其他样式 */
input::placeholder {
font-family:
"HYTMR", "PingFang SC", "Microsoft YaHei", sans-serif;
}
</style>
</head>

<body class="h-screen bg-sky-100 flex flex-col items-center justify-center">
<img src="<base64 图片>" class="w-20 h-auto mb-8 rounded-lg" />

<form
id="form"
class="flex items-center justify-center w-1/2 bg-white rounded-full shadow-xl"
>
<input
id="url"
type="text"
placeholder="输入长链接…"
class="w-full px-4 py-2 rounded-l-full outline-none bg-transparent"
/>
<button
type="submit"
class="flex items-center justify-center px-4 py-2 text-white bg-emerald-700 rounded-r-full hover:bg-emerald-900 focus:outline-none"
>
<b>Go!</b>
</button>
</form>

<p id="result" class="mt-4 font-bold"></p>

<p id="newURL" class="mt-2">
<a
class="text-teal-500 hover:text-teal-600"
href="https://blog.daoxi365.top/posts/43801/"
>
<b>帮助文档:我也想创建一个这样的网站。</b>
</a>
</p>

<script src="./js/app.js"></script>
<script>
(function () {
const form = document.getElementById("form");
const resultEl = document.getElementById("result");
const website = window.location.origin;

form.addEventListener("submit", async (e) => {
e.preventDefault();
const urlInput = document.getElementById("url");
const rawUrl = urlInput.value.trim();
if (!rawUrl) {
resultEl.textContent = "请输入链接";
return;
}
resultEl.innerHTML = "生成中…";

try {
const res = await fetch("/api/shorten", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ url: rawUrl }),
});
const data = await res.json();
if (data.code) {
const shortUrl = website + "/" + data.code;
resultEl.innerHTML =
'<a href="' +
shortUrl +
'" class="text-blue-700 hover:underline">' +
shortUrl +
"</a>";
} else {
resultEl.textContent =
"生成失败:" + (data.error || "未知错误");
}
} catch (err) {
resultEl.textContent = "请求失败,请检查网络";
}
});
})();
</script>
</body>
</html>

本地测试与贡献

如果你希望为本项目再添加一些新功能 / 改 bug,可以自行操作,安装 wrangler 后,进行本地测试:

  1. git clone "https://github.com/PanDaoxi/shURL.git" 之后,在根目录创建一个 wrangler.toml
1
2
3
4
5
6
7
8
9
name = "shurl"
compatibility_date = "2024-05-21"
pages_build_output_dir = "public"

[[d1_databases]]
binding = "DB"
database_name = "shurl-db"
database_id = ""
preview_database_id = "DB"
  1. 运行命令,建表:
1
wrangler d1 execute shurl-db --local --file=schema.sql

wrangler 会给出类似如下提示:

1
2
3
4
5
6
7
8
9
 ⛅️ wrangler 4.87.0 (update available 4.118.0)
──────────────────────────────────────────────
Resource location: local

Use --remote if you want to access the remote instance.

🌀 Executing on local database shurl-db (DB) from .wrangler\state\v3\d1:
🌀 To execute on your remote database, add a --remote flag to your wrangler command.
🚣 2 commands executed successfully.
  1. 启动本地服务器,可以进行你的开发了。
1
wrangler pages dev public --d1=DB