基于 Cloudflare D1 的新版本(代号:shURL2)已经上线。由于 leancloud 垮掉,旧版本(代号:shURL1)已经不再适用,仅作为历史版本,不再提供支持。
新版本相较于旧版本,反应速度更快,部署更便捷,操作更优雅;部署在 cloudflare pages 上,更加稳定。
「shURL 短网址」Github 仓库
欢迎使用shURL \texttt{shURL} shURL ! 这是shURL \texttt{shURL} 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 Pages、Netlify 等。
shURL2 采用 Cloudflare D1,基于 cloudflare 的稳定性和性能,部署在 cloudflare pages 上,更加稳定,且能享受赛博菩萨 cf 的慷慨的免费额度。
shURL 部署操作步骤 shURL1警告:这个版本程序所涉及的 LeanCloud 即将停止对外服务,这意味着此程序将不再适用。 部署的 demo 将会下线。
所以这里对其做一个折叠。
shURL1 的部署方法 注册或登录一个 LeanCloud 帐号。LeanCloud 账号分为国际、国内两种,如果你使用国外网络环境或用户面向国外,推荐使用国际版 。反之,则建议使用国内版。 本网站当前使用国内版 LeanCloud,可大幅提升访问速度。但是注册国内版 LeanCloud 账号需要实名认证。
创建一个新应用。应用名称随便写;计价方案选择开发版,否则会被收费。
进入刚刚创建的应用,左侧边栏下划到最后,在“设置”中找到“应用凭证”,在右侧按如图方式复制凭证信息。 注意:第四步 国内版 用户必须保留。国际版用户可以不使用此 rest api 地址。
打开 /js/app.js ,按注释(绿字)要求修改相关内容。
本地测试。 如果有 Python 环境,可以使用以下命令打开一个开发环境:
1 2 python -m http.server 8000 # 接下来你可以在 127 .0 .0 .1 :8000 访问到你的页面。
关于个性化:
您可以修改 media 文件夹下的网站图标(favicon.ico)和标志(logo.png)。 您可以自行修改网页样式,但是我希望您能保留帮助文档的链接。 push 到远端。打开 pages 服务或用其他平台发布。
修改安全网址。
测试,完成。
补充
您可以在结构化数据控制台 上增、删短网址。
shURL2不放图片了,因为操作比较简单。
我不会说是因为图片和放在一起导致博客的体积有点太大了 。
在 github 上 fork 我的仓库:https://github.com/PanDaoxi/shURL 。 在 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 );
在 cloudflare 上,创建 pages 项目: 左侧边栏:构建 -> 计算 -> Workers 和 Pages 然后是右边操作台:创建应用程序 -> 下面 “想要部署 Pages?开始使用” -> 导入现有 Git 存储库 “开始使用” -> 选你刚才的仓库 -> 开始设置 部署: 条目 值 生产分支 shURL2框架预设 不选(无) 构建命令 exit 0构建输出目录 public
部署成功后:继续处理项目。
在项目首页,右边的操作台的上部:设置 -> 绑定 -> 添加 -> D1 数据库
然后重试部署。
部署完成。可以绑定自定义域名,然后大功告成了。
原理解析shURL1 是我最初认识 LeanCloud 和 tailwindcss 的项目。以上技术栈均在原理中涉及。
shURL1 的原理是建立一个长网址与随机字符串的映射关系,并存储在 LeanCloud 数据库中。 shURL2 的原理也是建立一个长网址与随机字符串的映射关系,但存储在 D1 数据库中。
也就是说,用户每次提交原网址,shURL 都会生成一个随机字符串,并将其与长网址映射,存储在数据库中。每当用户将字符串通过接口传入 shURL 时,就会依据数据库存储重定向到原网址。
拆解源程序 shURL1shURL1 原理 拆解 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" );
这是一个创建最大长度为48 48 4 8 的随机字符串的方法。该字符串就是待生成的短网址链接代号。
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>` , ); }, ); }
shURL2shURL2 采用 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 , 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 ="./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 后,进行本地测试:
在 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 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 wrangler pages dev public --d1=DB