整个博客非得塞到网站源码吗?不!纯前端的前后端分离全部搞定!

视频

但不如这篇文章: https://www.bilibili.com/video/BV1kVNZ67EUz/

引言

众所周知,几乎所有人做网站都是从博客开始做起的。那么在博客写到令人厌烦之后,我们肯定需要为网站拓展一些其他功能,比如一些在线小工具或者其他增值服务。

image.png

不过这些都不重要。一旦你决定将网站从一个博客站转向为一个纯正宗的个人网站,你就会发现源码愈发臃肿。每一次修改代码,CI/CD 流水线都需要把博客和图片资源完整地拉取一遍。

image.png

在我的网站上,尤其是我已经写了 100 多篇博文,包括 1000 多张图片,整个资源加起来高达 300 MB。也就是说,每一次部署,CI/CD 流水线都沉在了来回 300 MB 的流量传输中。这不仅浪费了 CDN 的资源,也大大降低了开发、部署以及测试的速度。

image.png

image.png

那么,我们为什么不能将博客和其他业务分离开来呢?

第一次尝试:硬分割

一开始,我尝试的是各大厂都热衷于的“硬分离”。这边简单解释一下什么叫硬分离:比如说你在做博客站,后续想做一些其他的功能,又不想把这些功能一股脑全部塞到博客的站点上面,那么我们就可以来分域名做硬分割。

image.png

image.png

简单来说,就是博客站叫 blog.2x.nz,如果你还有论坛,那就叫 from.2x.nz,如果你还有其他服务,比如一些在线工具,就叫 tools.2x.nz

这种分割简单粗暴,易于管理。每个域名都是一个全新的项目,开发起来毫无依赖。你改你的文章,绝对不会波及到论坛的代码。更新和论坛的数据表,也不会把你正在编写的文章不小心删掉。

但是万事万物都有两面性,这么一个分离有点太彻底了。如果你是一个强迫症,热衷于统一高级的主题管理,那么你就可能会涉及到:每个仓库里面都引入了那一大坨同样的依赖,都有那一大坨同样的代码,都有那一大坨同样的调用方式,以保证样式正确和统一。

有的人就会说了:“你这人是不是傻叉呀?都他妈2026年了,不知道 monorepo 吗?”

好,这里给不知道这个东西的小伙伴解释一下,什么是 monorepo?

就是在同一个仓库里面放不同的子仓库,依赖由主仓库统一管理,而不同的子仓库就是不同的域名。只需要在主仓库编写一类方法,子仓库直接就可以去调用,不需要多个依赖,也不需要多次重写调用。

诚然,这种处理方式非常高级,也非常符合 UNIX 哲学。但是对于我这种喜欢大一统的人来说,依然不是很好,因为实际上你还是写了多个前端和多个后端,只不过它们看起来是只用写一遍罢了。

我一直秉持的思路就是:后端怎么乱没关系,只要前端优美即可。但是为了前端优美去大张旗鼓搞 monorepo,我觉得非常不合理。那么接下来看第二次尝试。

第二次尝试:单前端+多后端

让我们稍稍转变一下思路。我们最初的问题是什么?是博客的所有数据被塞到了一个代码库中,那我们为什么不把它分成两个代码库呢?

前端专门做 UI 界面,后端单独存储博客数据。这样我去改前端的样式,肯定能改变博客的渲染方式,但是却不需要动博客的数据,也不需要让 CI/CD 重新拉取全部的博客资源。

这是不是非常的聪明?那么就让我们开始实现吧。

正式开始

暂时抛开其他工具、论坛绘图等奇怪的服务不管,我们将网站专注于把博客前后端分离。

诚然,我们首先需要一个前端。前端肯定是用户访问越简单越好,所以就用短域名。那么前端就定好了,用 2x.nz

image.png

那么后端呢?由于用户几乎是不可知的,只有少数开发者会打开 F12 去抓,所以我们后端就来一个很有风味的 API 地址,叫做 raw-posts.2x.nz

那么前后端怎么交互呢?起初我在想,由于我们的后端(也就是真正存储博客数据的地方)实际上就是一个 GitHub 仓库,那么我们直接让前端去拉GitHub获取文章数据不就可以了吗?

诚然,这种思路简单直接,但如果你真的想要这么做,一方面你要解决 CORS,另一方面你需要解决大陆用户无法访问 GitHub 的痛点。

那么转变一下思路,我们嫁接一下呢?

在经常使用各类静态托管服务时,我意识到我们完全可以去写一个预构建脚本,将所有的文章做成一个索引,放在 /posts.json,然后让前端去抓这个索引,最后再让它触及原文章。

image.png

那么这个预构建由谁来做呢?就请出我们的 Cloudflare Worker 或者其他的,你开心就好。比如 EdgeOne Pages(现在叫 Maker 了),或者 Vercel、Netlify、Surge、Render,随你便。

image.png

那么思路一旦清晰了,实现起来就非常的简单。

我们马上上线了第一个版本,并且发布了第一篇视频,告诉大家我们的项目升级了! https://www.bilibili.com/video/BV182ND6rE2B/

疑难解答

但这种方式也会造成一些问题,最显然的就是由于你的前端不是预渲染好文章展示出来的,对于一些搜索引擎的 SEO 可能不太友好,因为它们需要通过 JavaScript 去解析你网站上的脚本,来动态渲染出文章。当然,在 2026 年,大部分搜索引擎都有执行 JavaScript 的能力。

image.png

最后就是 RSS 了,这是最头疼、最终也解决的一个问题。

到 2026 年,大家阅读博客一般都不会去每个博客主自己托管的网站,而是通过 https://folo.is ,或者各大第三方的 RSS 阅读器,去读取每个博客主的订阅源(也就是 /rss.xml),然后在一个统一的地方去看。

image.png

这样的好处是:

  1. 用户无需收藏多个网站,只需要收藏订阅器的网站。
  2. 订阅器会自动管理你读到哪里了。
  3. 还有 AI 摘要功能。
  4. 你还能看到你和谁一起读过这篇文章,找到你的同好。

那么问题就来了:我已经分离了我的前端,加上一个 /rss.xml ,那必然是 404 的呀。

如果你让订阅器去读取后端的 raw-posts.2x.nz/rss.xml

这就会导致很多问题

  1. 用户看起来就像是进入了另外一个网站,而且非常丑陋。
  2. 如果用户想要窥探一下你本身的网站,由于你的链接是后端的 API 地址,它只会被重定向到一个输出 Markdown 的地方,并没有优美的前端渲染,用户只会觉得一头雾水。

不过,这并不是不能解决。依旧是我们伟大的 Cloudflare 大人,它给了我们一个非常好的解决方案。还记得我们前面说的吗?我使用了 Cloudflare Worker 去预构建我们文章的索引,以及处理 Markdown 的提供。Cloudflare Worker 有一个非常牛逼的功能叫做 Worker 路由。它允许将另一个项目路由到我们的项目。简单来说,就是一个域名,但是不同路径实际上访问的是两个项目。

image.png

再简单一点来说,它可以做到用户访问 2x.nz/rss.xml 就等于访问 raw-posts.2x.nz/rss.xml

image.png

这个时候,我们只需要再加一条规则,让我们的渲染器在渲染 RSS 时把链接全部改成我们前端的地址,也就是 2x.nz,就可以做到完全无感的浏览。用户这个时候如果想要看我的网站,跳转的也是正确的前端页面。

这一切看起来都非常的完美,但是你听懂了吗?

超绝小插曲

神经主仓预渲染

不出意外就要出意外了。在这样运营了几周之后,我突然发现遇到了一个非常棘手的问题:当我后端添加了新的文章,前端访问会 404。

我立马去查看了一下 Next.js 的说法,得到的结果非常令人震惊,但其实也还好。简单来说,在 SSG 的情况下,Next.js 必须确保每个路径都打到一个 HTML 上。如果后端加了一篇文章,在前端看来这是一个新的路径,它没有对应的 HTML 就会 404。

解决方案有很多种,我们现在说最简单的一种,那就是预构建(也就是当前用的这种方式):在前端 SSG 生成页面的时候,先查一下后端到底有多少文章,然后预生成好这些 HTML 壳子,这样访问就不会 404。

image.png

不过这个方案的弊端也非常明显:在后端更新了文章之后,我还是得把前端重新部署一遍。这又回到了原来的那个样子,对吧?所以第二种方案是这样的:由于我们是 SSG 静态导出,没办法让服务端去解释这个 path name,所以我们可以试着曲线救国,使用各大静态部署服务的 404 fallback。这是什么意思呢?就是将所有的路径、所有的请求全部路由到 404,在 404 里面包含整个网站的所有逻辑,也包含解析 path name 并渲染文章。但这会导致两个问题:

  1. SEO 的下降。
  2. 整个项目都要完全重写。它需要从一个 SEO 良好的路径级别的网站改为一个单页的 SPA 项目。

那么最终我选择第三种方案,就是通过和论坛一样的查询 query 来实现。由于 Next.js 要求每一个路径、每一个 path name 都需要对应一个唯一的 HTML,那么我们就把 posts.html 写好,然后通过 ?slug=某一篇文章,这样来将所有文章都定向到 posts。通过动态的查询参数来获取不同的文章。

Giscus评论区无锚定

但这又导致一个问题,但这个问题我们是可以解决的,那就是我们的评论区,也就是 giscus 依赖于 pathname。这样改完以后,所有的文章的 pathname 都是 posts,因为 pathname 严格意义上并不会把 query 包含在内。

首先我想到的就是通过 URL 来鉴别。但是这又会导致两个问题:

  1. 批量更新评论区标题:由于之前用的是 pathname,所有的 discussion 都存储的是 pathname。如果这么改的话,还需要批量去更新一下每篇文章所对应的评论区的标题,否则 giscus 无法匹配。
  2. 域名变更风险:如果说我们后续的 2x.nz 这个域名它不再继续使用了,我们还需要再更改一遍,更改为之后的域名。

那么我们想到一个最好的办法,就是使用 giscus 自带的新的锚定方式,叫做 ogtitle。这是一个被定义在每篇文章的 HTML 中的 head 里面的 Meta 元数据。它可以定义任何内容,然后 giscus 根据定义的元数据来锚定每篇文章的评论区。那么这个时候,我们就可以将 ogtitle 设为原来的 pathname 的值。这样的话,新文章在浏览器的地址栏看起来是 query 查询,用户访问没有任何问题;同时,giscus 评论区也会按照之前的 pathname 的规则去锚定它。

image.png

旧路径兼容重定向

最后,我们需要做一个收尾工作:如果之前有其他网站刊登了我的文章,它用的还是 pathname,我们需要通过 项目本身有静态重定向,但由于我们在开头提到了 Next.js,必须要保证每个路径都有一个唯一的 HTML。

对于我们这些存量文章,当然可以生成一堆乱七八糟的壳子 HTML,但这个时候我们就可以复用 404.html 了。因为逻辑非常少,改动点也非常少,只需要:

  1. 让 404 重定向到 404.html;
  2. 解析 pathname 是不是旧格式,如果是,就重定向。

天才啊!我们想到的所有方法和所有模块都被复用到了,让它重定向到我们新的 query 的形式(注意,根据部署平台,可能需要显式指定404行为)。

访问量上报

和Giscus一致,改为读 og:title 上报即可

至此,所有工作才得以结束。你听懂了吗?

OK,那么这期文章就到这里结束。如果有问题,欢迎留言,也欢迎加群。