nido:一台小服务器上的前端基建
过年闲得没事干,我买了台阿里云服务器。记得大概 69 元,2 核 CPU、2GB 内存,磁盘只有 40GB。资源紧巴巴的,偏偏还想在上面多折腾点东西。
前端要发布,单页应用(SPA)的路由得能正常打开,后端也得跑。我想在这台小服务器上放下这些网站,就自己写了一套管理前端站点的工具,先解决发布和 SPA 部署。
这个项目还没公开,文里先叫它 nido,也许以后就用这个名字。现在没开源,主要是没时间,之后可能会放出来。
我和老婆有个类似 X 的小网站,只有我们俩在用,记日常,也记当时在想什么、迷茫什么。以后我们自己回头看,或者未来有机会给孩子看,能知道那个时候的我们各在想些什么。我们还有个小应用,记录每天做了什么菜、花了多少钱。
我们还在 QQ 养了只 Doro,是放在腾讯云上的 OpenClaw 机器人。当时接的是豆包,豆包老师说话还挺有人味的。有次聊到服务器快到期,我问了它一些问题,差点被回答看哭,于是又续了一年腾讯云。
后来换了模型,Doro 就没那么有灵气了。现在更像定时任务执行器:告诉我们还有几天放假、天气怎么样,偶尔帮忙把视频转成 GIF,换个文件格式。大概明年到期就不续了吧,有点舍不得。
这些网站用的人很少,却有我想一直留着的东西。发布、访问和日后的维护,都是自己的事。
先给小服务器减点负担
现在前端交给 nido,业务后端还是 Node.js,用 PM2 管进程。前端构建通常在 GitHub Actions 上跑:没资源再养一套自己的 CI/CD,就借 GitHub 来做。构建产物放进 OSS 对象存储,静态资源可以通过 CDN 分发。
这两件事分走了构建的开销,也不用把每个网页版本都堆在那块 40GB 的磁盘上。CDN 能承担静态资源的分发,比让小服务器独自扛这件事合适。不过网站仍有需要服务端处理的请求,具体吞吐量得用压测说话,不能只凭这个设计就说快了多少。
nido 分成三部分:控制台里看项目、构建日志、版本、域名和脚本;管理服务保存配置,处理站点路由和页面;发布工具上传已经构建好的文件,再确认这一版准备好了。发布工具本身不负责把源码构建成网页。
GitHub Actions 只是其中一种接法。发布工具也有 CLI,可以在本地构建后上传现成的产物;需要知道版本和资源地址的项目,则先预留版本,再按那一版的地址构建。
这样发布前端静态文件,不需要顺便停掉业务后端。前后端发布少了一些互相牵连,但单机仍有单机的可用性限制。
首页能开,还不算部署完
SPA 是单页应用:先加载入口页面,再由浏览器里的 JavaScript 切换内容。从首页点进 /settings,地址变了,浏览器却不一定向服务器再要一张网页。
刷新就不一样了。浏览器会直接请求 /settings,但构建产物里可能只有 index.html,没有这个文件。按地址找文件,就是 404。这种路由需要服务器配合 (opens in a new tab),把应用路径送回入口,让前端决定显示什么。
nido 早期的兜底处理也是按路径取文件。后来才加了按项目区分的路由:history 模式的应用路径回入口,脚本、样式和图片继续找对应资源;hash 模式和静态网站分别处理。
不能图省事,所有找不到的东西都返回 index.html。/settings 拿到入口是合理的,缺失的 /assets/app.js 拿到一张 HTML,浏览器还是跑不起来。页面路由和资源错误得分开。
另一个修过的坑在代理响应里。Axios 在 Node.js 中默认会自动解压 (opens in a new tab),当时却把上游的 Content-Length 也照抄了过去。内容解压以后变长,长度头仍然是压缩后的大小,资源就可能被截断。修复时去掉了不再适用的长度头和编码头,不把上游响应头当作原封不动就能转发的东西。
找不到页面时,还能用自己设计的统一 404 页面,别让几个小网站各露出一种默认报错。不过 SPA 里哪个应用页面不存在,仍然要由前端判断;缺失的脚本也不能用漂亮的 404 网页冒充。
历史版本和候选版
每次构建都有独立版本和存放位置。文件上传完、结果确认成功以后,再决定要不要上线。传到一半的目录不会被当作准备好的版本,也不边上传边覆盖读者正在用的那一份。
旧版 A 继续给人看,新版 B 可以先留成候选版,打开预览地址检查。两版也能并排看,确认某个布局或交互到底改成了什么。
B 已经准备好,线上仍然可以用 A。
历史版本有点像时光机:可以重新打开过去那一版前端。在后台一个版本一个版本翻,像看幻灯片一样回顾网站的变化。额,正常人好像不会这么干吧。
历史版本地址现在还是很粗暴的 v-x 设计,后面想再改。要是用在公司的测试环境,也可以把候选版和线上版摆在一起,直观看看是不是改坏了。
但这台“时光机”保存的是前端构建,不是某一天完整的网站状态。旧页面如果还在请求当前后端,看到的可能仍是现在的数据。我和老婆以前写下的生活记录,得靠业务数据自己保存;OSS 里的旧网页不会替我们备份这些记录。
要做金丝雀发布,可以让符合规则的一部分访问先用候选版,其余继续用稳定版。nido 支持条件灰度。这里最要紧的是,一次页面加载中的 HTML、脚本和样式要保持同版,缓存也不能把某次选中的候选页发给所有人。
浏览器里已有的 Service Worker 缓存同样要考虑。切了版本引用,不代表它已经忘掉旧文件。版本一致性要连着资源地址和缓存一起设计,不能只顾首页显示了 B。
回滚也是重新指向保留的旧版,省掉重新构建和上传的时间。不过切引用省事,不代表各处缓存都能毫秒级同步。网页退回去,已写入后端的数据也不会跟着撤销。
拿到 HTML 以后,还想加点东西
做着做着,我发现 SPA 的可玩性其实很大。页面在返回给浏览器之前,还能加一些自己想要的东西。脚本插入这个功能很合我的偏好,也很好用。
比如几个站都需要的统计脚本、页面更新提醒,可以放在统一的脚本库里,按站点启用、调整插入位置。不用挨个项目改源码,再分别构建。能插入不等于所有脚本都能直接跑:第三方脚本有自己的配置,页面的 CSP (opens in a new tab) 也可能限制它。
更新提醒就是现成的例子。新版上线后,已经打开的页面不会自己换掉。提醒会比较页面记录的版本和服务端返回的版本,有变化才在角落提醒;愿意刷新就点刷新,也可以关掉,别再打扰这一页。
如果你开着我的博客,一直没关,也许见过下面这个可爱的提示(
实际提醒组件在空白测试页里的样式演示,版本变化是模拟的。
检查只取响应头,不用为了问一句“更新了吗”再下载整张网页。提醒用 Shadow DOM 隔开样式,免得网站把所有按钮改大,顺手把这里的刷新按钮也改了。请求失败就先放着,不把网络抖动当新版。
脚本和前端构建有各自的生命周期。改共用脚本会影响接下来返回的页面,切回旧网页却不会把脚本库一起退回。这份方便也要求我知道一段脚本正被哪些站点使用。
统计也放在控制台里:从来源看访客打开了哪些页面,或从一篇文章反查来源;几个项目可以放在同一刻度上比较。PV 是页面访问次数,UV 按浏览器标识去重,换浏览器、清存储,都可能换一个标识。它不能当准确人头数,更不能证明对方读完了文章。没有采集到的访问补不回来,统计脚本也会尊重浏览器的请勿跟踪信号。
域名、路由、脚本、版本和统一的 404 可以一起管理,基础站点门禁也已经有了。更细的业务权限仍要由各个应用负责。我开始觉得,这基本是面向自己这些小项目的前端基建平台了。
起初只是资源太紧,想把它尽量压榨出来。后来能折腾的东西越来越多,倒是比单纯把网页传上去有意思。业务后端以后可能会考虑 serverless,目前还是 Node.js 加 PM2。
那些小网站也许一直都没多少人用。我还是想把它们留着,里面记的是我们俩自己的日子。
评论