今天干了三件事:买服务器、搬博客、加评论。
搬完顺手点了一下文章左上角的「返回」——它一次退了好几页,直接给我退到浏览器外面去了。
追下去才发现是我自己埋的坑,而且是那种「不查源码根本想不到」的坑。
一、为什么要搬
以前博客放在 Cloudflare Pages 上,免费、快、不用管。听起来完美,但它有条规矩:
单个文件不能超过 25 MB。
我那个「更多的音乐唱片」是 26.41 MB,超了 194 KB。而超了的后果不是「这一个文件传不上去」,是整次部署都失败。
(试过重新压,只省得下 700 字节 —— 里面全是 ogg 音频,本来就已经压过了,压不动。)
还有一个更实在的问题:下载。
我在页面上放了自己的安装包,别人点一下就直接从我站上拿。在 Cloudflare 那边没事,但换成自己的服务器之后,这台机器带宽只有 5M,大概 625 KB 一秒。一个 5.3 MB 的 APK,一个人下就要占满 8.5 秒。
这 8.5 秒里,别人打开我的博客就是转圈。
所以就买了台香港的小机器:2 核 2G,38 块钱一个月。香港不用备案,这是最省事的一点。
二、下载:我不该自己发文件
想通一件事:问题不在我代码写得怎么样,在「自己发文件」这件事本身。
文件大了传不上去,有人一直下还会把带宽占满。这两条跟我写代码的水平没关系。
所以改成:点「下载」不下载,弹个窗,把人送到网盘。
点「下载」之后,先是这么两下:
[[ 服务器繁忙,请使用备用地址或稍后再试 ]] ← 先出来┌──────────────────────────────────────────────┐│ 选择下载方式 ││ Home Chat 安卓端 APP ││ ││ ┌────────────────────────────────────────┐ ││ │ GitHub [推荐] │ ││ └────────────────────────────────────────┘ ││ ││ ┌────────────────────────────────────────┐ ││ │ ☁ 蓝奏云 │ ││ └────────────────────────────────────────┘ ││ ││ ┌────────────────────────────────────────┐ ││ │ 🔑 提取密码 0808 │ ││ └────────────────────────────────────────┘ │└──────────────────────────────────────────────┘ ← 250 毫秒后跟上「服务器繁忙」那条先冒出来,「选择下载方式」晚 250 毫秒才跟上来。 先说自己忙、再给备用地址,读起来才顺 —— 反过来会像是自己没事找事。
「服务器繁忙」那条先冒出来,「选择下载方式」晚 250 毫秒才跟上来。 先说自己忙、再给备用地址,读起来才顺 —— 反过来会像是自己没事找事。
每样东西最多给两个选择:GitHub(拿源码,推荐) 和蓝奏云(拿安装包)。源码放 GitHub 上最权威,也不会哪天链接失效。
这里有个坑:弹窗里的按钮不能”拼字符串”
弹窗里的按钮是按数据现场拼出来的,不是写死在 HTML 里的 —— 因为下载页、文章页、HomeChat 页三个地方要弹同一个窗,我不可能每处都画一遍。
那问题来了:按钮上有个图标(一朵云,或者 GitHub 那只猫),脚本画不出来。
我第一个想到的写法是拼字符串:
box.innerHTML = `<a href="${href}"><svg>…</svg>${label}</a>`;千万别这么写。
只要 href 或者 label 里混进一段别人能控制的文字,这就等于把一段字符串当代码执行 —— 这就是 XSS,课本级的漏洞。
正确的写法是:提前把图标画好,藏在一个看不见的池子里,用的时候复制一份。
const svg = pool.querySelector('[data-dl-icon="pan"] svg');a.appendChild(svg.cloneNode(true));
const label = document.createElement("span");label.textContent = choice.label; // ← 用 textContent,不是 innerHTMLtextContent 是把内容当纯文字塞进去的,里面写什么都不会被当成代码。
再加一道保险:链接只认 http:// 和 https:// 开头的,javascript: 那种一律丢掉。
三、评论:为什么选了 giscus
评论区我没自己写,用的是 giscus —— 它把评论存在 GitHub Discussions 里。
好处很明显:不用我维护、不怕人刷广告、不要钱,而且数据不在我服务器上(万一我那台机器炸了,评论还在)。
代价只有一个,但很关键:想评论的人必须有 GitHub 账号。
我为什么还是选了它?因为我这个博客的读者,基本都是小学同学 —— 打编程比赛、创客比赛那批人,他们有账号。
换成别的读者群体,我可能就得自己写一套了。
又一个小坑:评论框不跟着主题变色
giscus 自带一个「跟随系统深浅色」的选项。但它认的是操作系统的设置,不认我博客上那个开关。
也就是说:我把博客切成白色,评论框还是黑的,特别突兀。
修法是给它发消息:
frame.contentWindow.postMessage( { giscus: { setConfig: { theme: "dark_dimmed" } } }, "https://giscus.app");再挂两个监听:一个盯着 <html> 的 class 变没变(我切主题就是改这个),一个盯着系统设置变没变。
颜色我挑的是 dark_dimmed,不是 dark。因为 GitHub 的纯黑是 #0d1117,而我的卡片底色偏灰蓝,两个放一起会显得凹进去一块。
四、藏得最深的那个 bug:返回键
这是今天最有意思的一个。
文章左上角有个「返回」按钮。逻辑是:从站内点进来的,就退回上一页;从站外点进来的,就回归档页。
写完我测了一下 —— 好使。就放那儿了。
结果今天翻了几篇文章之后点它,一次退了好几页。
追下去
那段逻辑挂在 document 上的点击监听里。一个监听,一次 history.back(),不可能退好几页。
除非……不止一个监听。
我数了一下:翻三篇文章,就有三个监听。点一次返回,history.back() 就被调用好几次。
多出来的监听哪来的
fuwari 这个主题用 swup 做无刷新换页 —— 点链接的时候不整页刷新,只把 <main> 里的内容换掉,所以翻页很顺。
swup 有个插件叫 ScriptsPlugin,作用是:换页之后,把新内容里的 <script> 重新塞一遍,让它们再执行一次。
这个想法是合理的(新页面里的脚本当然该跑)。问题是它不去重。
而我的脚本里写的是:
document.addEventListener("click", handler);每执行一次,就多挂一个监听,旧的永远不会解绑。
翻 N 页 → N 个监听 → 点一次返回 → history.back() 跑 N 次。
修法:把逻辑挪出 main
既然重复执行是因为「脚本待在一个会被换掉的容器里」,那就别放在那儿。
挪到布局文件里。那个脚本每页都会加载,但一个完整页面里只执行一次,所以监听也只挂一个。
顺手搞懂的一件事:为什么挪出去就安全
这里有个我一开始没想明白的点:ScriptsPlugin 不是「把文档里的脚本全重塞一遍」吗?那布局里那个脚本不也一样被重塞、被重新执行?
答案是浏览器有模块缓存:
| 脚本长什么样 | 换页时会不会再执行 |
|---|---|
<script type="module">代码直接写在这儿</script>(内联) | ✅ 会,每换一页执行一次 |
<script type="module" src="/xxx.js"></script>(外链) | ❌ 不会,浏览器按网址缓存,一个页面只执行一次 |
而 Astro 打包出来的脚本全是第二种。
所以:逻辑写在 Layout.astro 的 <script> 里只跑一次;写在会被替换的区域里的内联 <script>,就会反复跑。
一句话:要挂全局监听,就放布局。
同一个毛病还有一处
代码块右上角那个「复制」按钮,也是内联脚本、也是挂 document 监听、也会叠。
它的后果轻一点(连着复制同一段代码,结果是一样的),但监听还是一直在涨。
swup 留了开关:给 <script> 加一个 data-swup-ignore-script,它就会跳过这个脚本。加上就好了。
五、搬家的几个实操细节
记几个容易卡住的地方:
① 端口填 80。
添加站点的时候要填端口。填 80 —— 那是浏览器不写端口时默认去敲的那扇门。填别的(比如 8080),网址就得写成 http://xxx:8080,又长又容易记错。
② gzip 是默认开着的。
我一开始看到首页 131 KB 吓了一跳,其实服务器真正传出去的只有 21 KB 左右 —— 剩下的都是压缩省下来的。
③ 404 页要自己接一根线。
Astro 会生成一个自己的 404.html,但 nginx 不知道要用它,默认吐自己那个白底黑字的。要在站点配置里加一行:
error_page 404 /404.html;④ 香港服务器不用备案。
域名解析过去就能用,省掉了最麻烦的一步。
最后
今天加起来就三件事:搬家、下载改网盘、加评论。
但真正学到东西的,是最后那个返回键的 bug。
它不是语法写错了,是我根本不知道「换页的时候脚本会重跑」这件事。
这也解释了为什么之前播放器也出过类似的毛病 —— 我总想把逻辑写在「离功能最近的地方」,觉得那样看着清楚、好改。
可这个主题是无刷新换页的。在这个前提下,「离功能最近」恰恰就是「最容易被换掉、最容易被重复执行」的地方。
有些东西的作用域,比它看起来的位置大得多。