不必复杂,静态博客也可以做短链
立泉为什么需要短链?
https://mudan.me/post/original/2022/05/17/不必复杂-静态博客也可以做短链.html
这是我博客里一篇文章的完整链接,除去协议和域名,后面一长串包含文章类型“original”
、写作时间“20220517”
和文章标题“不必复杂-静态博客也可以做短链”
,它们共同构成一个指向此HTML
文档的URL
,很详细也很长。
我的博客站点是用Jekyll
生成的,它在转换Markdown
文章到静态网页时支持设定输出的URL
结构,确实能做到很短。只需给每个Markdown
文章都指定一个自定义Front Matter,然后用它做输出文件名,可以实现类似https://mudan.me/lxzlysh.html
的短链。
# Jekyll配置文件 _config.yml
# 指定HTML页面的输出目录格式,也就是最终文章的URL格式
# https://mudan.me/post/original/2022/05/17/不必复杂-静态博客也可以做短链.html
permalink: /post/:categories/:year/:month/:day/:title:output_ext
# 可以很短,比如 https://mudan.me/bfjbyzd.html
permalink: /:custom_front_matter:output_ext
但这样的短接不包含任何文章相关信息,只有一串手动指定的字符,会不会碰撞是一个问题,另一个问题是permalink
实际上是Jekyll
文章页面的输出目录。我的长链是把文章输出到/post/original/2022/05/17/
目录下,按类型、日期分类存储,而上面那种短链显然把所有文章都输出到了根目录,堆成一团。如果硬要做「短结构」也可以是/p/o/22/5/17/
,但显然不如长链清晰,而且这也不是通常意义上的「短链」。
理解为什么链接会这么长之后,再回到一开始的问题,为什么需要短链?
URL
,Universal Resource Locator
,统一资源定位符,是一个能在茫茫网络中准确指向一个指定资源的字符串。当把一篇文章的URL
分享出去,任何人都可以通过它访问到文章页面,就是这么“神奇”,而我则是遇到了几个不太“神奇”的问题。
问题
不友好的中文解析
我的博文URL
包含中文(文章标题),而执行网页“跳转”的HTTP
协议并不允许URL
中含有ASCII
码表以外字符,所以浏览器在访问中文URL
时会先用URL Encoder
将其转换为合法ASCII
字符,然后才能执行HTTP
请求。另一方面,中文URL
的分享传播并不需要转码,可以直接发在微博、朋友圈的帖子里,等点击的时候再转码跳转。
问题是,很多平台在解析链接时会以非ASCII
字符作为URL
结束的标志,它们解析出的可点击链接只是中文URL
的前一部分,并不完整,比如微博:
# 原始URL
https://mudan.me/post/original/2022/05/17/不必复杂-静态博客也可以做短链.html
# 解析出可以点击跳转的只是`https://mudan.me/post/original/2022/05/17/`这部分
# 自然不能跳转到正确页面
`https://mudan.me/post/original/2022/05/17/`不必复杂-静态博客也可以做短链.html
如果使用URL Encoder
把中文部分编码后再分享倒是能正确解析,但又太长了,每一个汉字都被编码成十几个ASCII
字符,很容易触及帖子的字数限制。
# 原始URL
https://mudan.me/post/original/2022/05/17/不必复杂-静态博客也可以做短链.html
# 编码后的URL,明显变长
https://mudan.me/post/original/2021/06/10/%E4%B8%8D%E5%BF%85%E5%A4%8D%E6
%9D%82-%E9%9D%99%E6%80%81%E5%8D%9A%E5%AE%A2%E4%B9%9F%E5%8F%AF%E4%BB%A5%
E5%81%9A%E7%9F%AD%E9%93%BE.html
URL
变化需要重定向
使用短链的另一个原因也与博文URL
里的文章标题有关,我有时候修订文章可能会修改标题,导致其URL
也随之变化,之前分享出去的链接也会因此失效。虽然Jekyll
提供的重定向插件jekyll-redirect-from
可以在原位置生成一个指向文章最新地址的HTML
网页进行跳转,但毕竟会创建多余目录和多余文件,不如直接维护一个短链与原始链的映射表简单直观。无论文章URL
怎么变化,只需要保证映射表里用于分享的短链始终指向最新地址即可。
<!-- jekyll-redirect-from 生成的重定向网页 -->
<!DOCTYPE html>
<html lang="en-US">
<meta charset="utf-8">
<title>Redirecting…</title>
<link rel="canonical" href="http://mudan.me/target.html">
<script>location="http://mudan.me/target.html"</script>
<meta http-equiv="refresh" content="0; url=http://mudan.me/target.html">
<meta name="robots" content="noindex">
<h1>Redirecting…</h1>
<a href="http://mudan.me/target.html">Click here if you are not redirected.</a>
</html>
映射
短链服务的实现本质就是查询映射表,从中找到短链对应的长链然后跳转,这要求从长链生成短链的算法必须保证短链的唯一性,不能出现多个长链生成同一个短链的情况。
首先哈希
算法是不合适的,虽然概率很低,但哈希碰撞
不可避免。一种流行实现是多位62进制自增
,比如定义短链为5个字符,每个字符都是62进制,那么1位就可以对应62个长链,5位对应62^5=916,132,832
个长链。短短5位的指数级增长就已经接近10亿,足够应对大量短链映射。
为什么是62进制?因为URL
必须是ASCII
码表里的字符,常用的数字[0-9]
和大小写字母[a-z,A-Z]
加起来就是62。当然如果把+
、-
这类合法字符也算上,64进制也可以。
一个生成短链的自增函数示例:
val table = charArrayOf('0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z')
/**
* 把传入的字符串按[table]数组的62个字符顺序自增,比如0000Z,自增后为00010
*/
fun selfIncreaseBase62(baseStr: String): String {
val sb = StringBuilder()
// 进位记录
var carry = 1
for (i in (baseStr.length - 1) downTo 0) {
var index = table.indexOf(baseStr[i])
if (index == -1) throw InvalidParameterException("invalid character")
index += carry
if (index >= table.size) {
// 进位
carry = 1
// index归零
index = 0
} else {
// 不进位
carry = 0
}
sb.insert(0, table[index])
}
// 跳出循环依然有进位,则已经超出了所能表示的范围
if (carry == 1) {
throw IndexOutOfBoundsException()
}
return sb.toString()
}
需要数据库吗
已经有生成映射表的方法,要如何存储这个映射表并快速查询呢?数据库当然是能想到的首选方案,但对一个托管在GitHub Pages
上区区数十网页的静态博客来说,有些太“重”了,而且还要单独租用服务器,并不合适。
我不会对外提供短链服务,仅仅为博客里的一部分文章生成短链,所以更现实的方法是在api
目录下创建一个保存映射表的url-map.json文件,手动添加映射,我估摸着以自己的文章产出大概是不会超过百条的🙄️。
// api/url-map.json
// 短链我只用2位36进制就足够,36^2=1296,千条容量,如果有幸真的不够,后期还可以扩充
// 这里的op、og、rp是我的文章分类标志
// 最终的短链格式 https://mudan.me/op00
{
"map": [
{
"id": "op00",
"target": {
"path": "/post/opera/essay_00.html",
"title": "title_00"
}
},
{
"id": "op01",
"target": {
"path": "/post/opera/essay_01.html",
"title": "title_01"
}
},
{
"id": "og00",
"target": {
"path": "/post/original/essay_00.html",
"title": "title_00"
}
},
{
"id": "og01",
"target": {
"path": "/post/original/essay_01.html",
"title": "title_01"
}
},
{
"id": "rp00",
"target": {
"path": "/post/repost/essay_00.html",
"title": "title_00"
}
}
]
}
跳转
GitHub Pages
静态博客并不能像自建Nginx
服务器那样可以处理每一个请求并灵活返回要展示的网页,所谓“静态”是指每个网页都是已经存在的HTML
文件,不能动态生成。Pages
只会返回请求URL
的对应页面,但允许用户创建域名下URL不存在时的404页面,它本质也是一个HTML
文件,是实现短链跳转的关键。
对于短链https://mudan.me/op00,它对应的HTML
文件不存在,所以Pages
会重定向到自定义的404
页面,在这个页面里就可以通过JavaScript
获取URL
中的短链ID
,随后下载含有映射表的JSON
文件,从中查找原链跳转。这就是静态博客实现短链的机制,没那么复杂,也不需要额外花费。
适当添加一些好看的跳转动画,一切都由开发者随意控制。