· 17 分钟 · 建站 / Next.js / Cloudflare / 工程化
从零到上线:Seabloom 是怎么搭起来的
一个空文件夹,怎么变成全世界都能访问的网站?这篇把用到的每一项技术、每个页面的实现、上线的每一步和踩过的坑都讲清楚。
这篇文章记录 Seabloom 从一个空文件夹到全世界都能访问的全过程。它写给两类人:一是想自己搭网站、但看到一堆名词就发懵的人;二是以后的我自己——面试时被问到"这个项目你是怎么做的",可以讲得清清楚楚。
读法建议:先看第一节的全貌图,后面每一节都是在放大图里的某一块。
一、先看全貌
整个系统只有四个角色:
- 你的电脑:写代码、本地预览、把改动"存档"(commit)。
- GitHub:放代码的云端仓库。代码推上来后,它会自动跑一遍检查(CI),同时通知 Cloudflare。
- Cloudflare:收到通知后,自己把代码拉下来、构建成网页文件,然后发到它遍布全球的服务器上。
- 访客浏览器:任何人输入网址,离他最近的 Cloudflare 服务器就把网页发给他。
一句话概括:我只负责写代码和 git push,剩下的检查、构建、上线全是自动的。 这就是常说的 CI/CD(持续集成 / 持续部署)。
后面的内容按"从底层到上层,再从开发到上线"的顺序展开。
二、起点:域名、托管、DNS 和 IP
刚开始最容易混淆的是这几个概念,用寄信来类比:
| 概念 | 是什么 | 类比 |
|---|---|---|
| IP 地址 | 服务器在网络上的数字地址,比如 104.21.x.x | 门牌号 |
| 域名 | 好记的名字,比如 haiyangliu.com | 店名 |
| DNS | 把域名翻译成 IP 的系统 | 查号台 / 地图 |
| 托管 | 把网站文件放在一台一直开着、连着网的机器上 | 租的店面 |
| CDN | 在全世界很多城市都放一份网站副本,谁近就由谁送 | 连锁分店 |
为什么我们没有"自己的 IP"? 因为我们没租服务器(VPS),而是把网站放在 Cloudflare 上。网站文件在 Cloudflare 的几百个机房里,IP 由它管理。我们只需要告诉 DNS:"这个域名请交给 Cloudflare"。
为什么不一开始就租服务器? 对一个展示型网站来说,租服务器要自己装系统、配 Nginx、申请 HTTPS 证书、打补丁、防攻击,还要付钱。托管到 Cloudflare 这些都不用管,而且免费。等以后需要后端(比如留言板要存数据),再单独租一台服务器做 api.haiyangliu.com 就行,前端完全不用动——这叫前后端分离。
目前网站用的是 Cloudflare 免费分配的
workers.dev网址。这个域名在中国大陆经常访问不了,绑定自己的域名后情况会好很多。
三、工程地基:每个工具各管什么
打开项目文件夹,会看到一堆工具。它们的分工是这样的:
| 工具 | 一句话职责 | 在项目里的体现 |
|---|---|---|
| Node.js | 让 JavaScript 能在电脑上(而不只是浏览器里)运行 | 所有命令都靠它跑 |
| pnpm | 包管理器:下载和管理第三方代码库 | package.json、pnpm-lock.yaml |
| React | 用"组件"拼出界面的 UI 库 | 每个 .tsx 文件 |
| Next.js | 基于 React 的完整框架:路由、构建、优化都管 | src/app/ 目录、next.config.ts |
| TypeScript | 给 JavaScript 加上"类型",写错能提前发现 | .ts、.tsx 文件,tsconfig.json |
| Tailwind CSS | 用短小的 class 名直接写样式 | className="mt-4 text-sm" 这类写法 |
为什么这么选
- React + Next.js + TypeScript 是目前国内外大厂前端的主流组合。会它,简历上最好写。
- pnpm 比 npm 更快、更省硬盘:同一个库在电脑上只存一份,多个项目共用。
package.json里的"packageManager": "pnpm@10.34.9"锁定了版本,保证我的电脑、GitHub、Cloudflare 用的是同一个 pnpm。 - Tailwind 不用在 CSS 文件和组件之间来回跳,而且只会打包实际用到的样式,最终 CSS 文件很小。
项目目录
cc_web/
├─ src/
│ ├─ app/ # 页面:一个文件夹 = 一个网址
│ │ ├─ page.tsx # 首页 → /
│ │ ├─ about/ # 关于 → /about/
│ │ ├─ blog/[slug]/ # 每篇文章 → /blog/xxx/
│ │ ├─ feed.xml/ # RSS 订阅 → /feed.xml
│ │ ├─ layout.tsx # 所有页面共用的外壳(页头、页脚、字体)
│ │ └─ globals.css # 全局样式和配色
│ ├─ components/ # 可复用的组件(页头、和弦图、相册……)
│ ├─ content/ # 内容数据:文章、项目、照片、吉他曲目
│ ├─ config/site.ts # 站点名字、导航、链接
│ └─ lib/ # 工具函数(以及它们的测试)
├─ scripts/photos.mjs # 照片处理脚本
├─ public/ # 原样发布的静态文件(照片)
├─ .github/workflows/ # GitHub 自动检查的配置
└─ wrangler.jsonc # Cloudflare 部署配置
最重要的规则是 "文件夹即路由":在 src/app/ 下建一个 guitar/page.tsx,网站就自动多出 /guitar/ 这个网址。不需要额外配置。
四、设计系统:配色、深浅色和字体
两层颜色变量
所有颜色都定义在 globals.css 里,分成两层:
/* 第一层:原始色板,只描述"是什么颜色" */
--color-sea-500: #1e9aa8;
--color-sand-50: #fbf7ef;
/* 第二层:语义色,描述"用在哪" */
:root {
--bg: var(--color-sand-50);
--accent: var(--color-sea-700);
}
.dark {
--bg: #0f1e2b;
--accent: #6cc9cf;
}
组件里只写语义色,比如 bg-bg、text-accent,从不直接写 #1e9aa8。好处是:切换深色模式时,只需要换掉第二层的变量值,整站自动变色,组件一行都不用改。大厂的设计系统(Design Token)也是这个思路。
深浅色是怎么切换的
next-themes这个库读取你的系统设置(或你上次的选择),在<html>标签上加或去掉dark这个 class。- CSS 里
.dark { ... }那组变量就生效了。 - 右上角的按钮调用
setTheme("dark"),并把选择记在浏览器的 localStorage 里,下次打开还记得。
这里有个细节:服务器生成 HTML 时并不知道你喜欢深色还是浅色,所以 layout.tsx 里给 <html> 加了 suppressHydrationWarning,告诉 React"这一处服务器和浏览器不一致是正常的,别报警"。
字体
- 英文标题用 Fraunces(一款有手作感的衬线字体),正文用 Geist。
- 中文没有单独引入字体,而是回落到系统自带的宋体和苹方或微软雅黑。因为一套完整的中文字体有好几 MB,会拖慢加载。
- 字体通过
next/font引入:它会在构建时把字体文件下载下来,和网站放在一起,访客不用再去 Google 下载,也就不会因为访问不了 Google 而加载失败。
五、逐页拆解
先理解 Next.js 里最核心的一个概念,后面每一页都会用到:
| 服务端组件(默认) | 客户端组件(文件开头写 "use client") | |
|---|---|---|
| 在哪运行 | 构建时,在 Node.js 里 | 在访客的浏览器里 |
| 能做什么 | 读文件、算数据,产出 HTML | 响应点击、滚动、键盘,保存状态 |
| 会不会给浏览器增加 JS | 不会 | 会 |
原则是:能在服务端做完的就在服务端做完,只有需要交互的小块才用客户端组件。 这样页面又快又轻。整个网站只有 4 个客户端组件:主题按钮、导航高亮、深度计、相册。
首页 /
- 插画(
BloomScene.tsx):纯手写的 SVG,太阳、海浪和花都是几何图形,颜色用fill-sun、stroke-sea这样的主题色。花轻轻摇摆是一个 CSS 动画:以茎的底部为轴,在 -2° 到 2° 之间旋转。 - 分区:近况、项目、文章、摄影、海底,每一区左边标一个深度。数据直接来自其他页面的数据文件(文章列表、项目列表、照片列表),所以内容只写一次,多处复用。
- 背景变深:页面背景是一个从沙色到浅海色的渐变,越往下滚颜色越深,模拟"下潜"。
- 深度计(
DepthMeter.tsx,客户端组件):- 监听页面滚动,算出滚动进度
进度 = 已滚动距离 / 可滚动总距离,范围是 0 到 1。 - 把进度换算成深度:
深度 = 4000 × 进度²。用平方是为了"前面潜得慢、后面潜得快",更像真的下潜。 - 滚动事件一秒能触发上百次,所以用
requestAnimationFrame节流:每一帧最多算一次,避免卡顿。 - 换算函数
progressToDepth放在lib/depth.ts,配了单元测试,保证 0 对应海面、1 对应 4000 米。
- 监听页面滚动,算出滚动进度
关于 /about/ 和项目 /projects/
这两页体现的是数据驱动:把内容写成数据,页面只负责"怎么展示"。
// src/content/projects.ts
export const projects: Project[] = [
{ title: "Seabloom", status: "building", tags: ["Next.js"], featured: true },
// 加新项目只需要在这里加一行
];
页面里用 projects.map(...) 把每一项渲染成一行。featured: true 的项目会自动出现在首页。Project 这个类型由 TypeScript 定义:如果哪天把 status 写成了不存在的 "finish",编辑器会立刻标红。
博客 /blog/
这是技术含量最高的一块。一篇文章从 .mdx 文件变成网页,要经过这几步:
01
写文章
在 src/content/blog/ 里新建一个 .mdx 文件:开头 export 一个 metadata(标题、日期、摘要),下面写 Markdown。
02
列出所有文章
lib/posts.ts 用 Node.js 的 fs 模块读取这个文件夹,得到所有文章的文件名(slug)。
03
生成每篇文章的页面
blog/[slug]/page.tsx 里的 generateStaticParams 返回所有 slug,Next.js 就为每篇文章各生成一个 HTML。
04
Markdown 转换
@next/mdx 把 Markdown 解析成语法树,再交给一串插件加工:remark-gfm 处理表格和任务列表,remark-math + rehype-katex 渲染公式,Shiki 给代码上色。
05
套上样式
mdx-components.tsx 规定每种元素长什么样,比如所有 h2 都用衬线字体、所有链接都有下划线。
06
输出 HTML
最终每篇文章都是一个现成的 HTML 文件,访客打开时不需要任何计算。
几个值得一说的细节:
[slug]是动态路由:方括号表示"这一段网址是变量"。配合dynamicParams = false,不存在的文章会直接显示 404。- 代码高亮在构建时完成:Shiki 用的是 VS Code 同款的语法规则,在构建时就把代码拆成带颜色的片段,浏览器不需要再加载高亮库。它会同时生成浅色和深色两套颜色,深色模式下用 CSS 切换。
- 公式:写
$\sqrt{d_k}$这样的 LaTeX,KaTeX 在构建时把它变成 HTML。比如 就是这么渲染出来的。 - 阅读时长:
readingMinutes按中文每分钟 400 字、英文每分钟 200 词估算,并且会排除代码块和 metadata,配了单元测试。 - RSS(
/feed.xml):一个"路由处理器",在构建时把文章列表拼成 RSS 格式的 XML 文件。别人用 RSS 阅读器订阅后,有新文章会自动收到。
摄影 /photos/
照片是网页里最"重"的东西,一张手机原图就有 5 到 10 MB。所以专门写了一个处理脚本 scripts/photos.mjs,运行 pnpm photos 时:
- 读取
photos/originals/里的原图。 - 按照片自带的方向信息把它摆正(手机竖拍的照片常常是"躺着"存的)。
- 用 sharp(一个很快的图片处理库)输出 800 和 1600 像素宽两种尺寸,格式是 WebP,体积大约只有 JPEG 的一半。
- 再生成一张 16 像素宽的超小图,转成一串文字(base64)直接写进页面,照片加载出来之前先显示它的模糊色块。
- 用 exifr 读出相机、光圈、快门、ISO 这些参数,写进
photos.generated.json。 - 输出的图片会去掉全部元数据,包括 GPS 定位。不然别人下载照片,就可能知道是在哪里拍的。原图很大,不提交到 Git,只提交压缩后的版本。
页面这边:
- 响应式图片:
<img>同时给出srcSet(两种尺寸)和sizes(在什么屏幕宽度下占多宽),浏览器会自己挑最合适的一张。手机上下载 800 像素的版本,大屏才下载 1600 像素的版本。 - 提前写好宽高:
width和height让浏览器在图片下载完之前就把位置留好,页面不会"跳"一下。这个指标叫 CLS(累积布局偏移),是 Google 衡量网页体验的指标之一。 - 瀑布流:用 CSS 的
columns属性实现,不需要任何 JS。 - 大图浏览:用浏览器原生的
<dialog>元素,自带遮罩层、Esc 关闭、焦点锁定,再加上 ← → 键切换。
吉他 /guitar/
和弦图(ChordDiagram.tsx)是用代码"画"出来的 SVG。和弦的数据长这样:
{ name: "C", frets: [null, 3, 2, 0, 1, 0] }
// 6 根弦从低音 E 到高音 e:null 不弹,0 空弦,数字是按第几品
组件根据这组数字算出每个圆点的坐标:第 s 根弦、第 f 品的圆点画在 (左边距 + s × 弦距, 上边距 + (f - 0.5) × 品距)。F 和弦这样的横按用一根圆角长条表示。因为是 SVG,放大不会模糊,颜色也跟着主题走。演奏视频预留了 B 站播放器的位置,填上视频的 BV 号就会自动嵌入。
六、静态导出:next build 到底产出了什么
next.config.ts 里最关键的一行是:
output: "export",
意思是在构建时把每个页面都提前生成好 HTML 文件,输出到 out/ 文件夹,大概是这样:
out/
├─ index.html
├─ about/index.html
├─ blog/building-seabloom/index.html
├─ feed.xml
├─ photos/*.webp
└─ _next/static/ # CSS、JS、字体
这就叫静态网站。和"动态网站"的区别是:
| 静态网站(我们) | 动态网站 | |
|---|---|---|
| 页面什么时候生成 | 构建时,一次生成好 | 每次有人访问时现场生成 |
| 需要服务器运行代码吗 | 不需要,发文件就行 | 需要 |
| 速度 | 极快,可以放在 CDN 上 | 取决于服务器 |
| 成本 | 几乎免费 | 要付服务器的钱 |
| 能不能存用户数据 | 不能(以后靠单独的后端) | 能 |
为了静态导出,我们还配置了:trailingSlash: true(把 /about 输出成 /about/index.html,静态托管下更稳);images.unoptimized(静态网站没有服务器帮忙实时压缩图片,所以由 photos.mjs 预先处理)。
七、代码质量的六道防线
每次改代码,都有六道关卡依次检查:
| 关卡 | 工具 | 拦住什么 | 什么时候运行 |
|---|---|---|---|
| 1 | TypeScript | 类型错误:传错参数、拼错属性名 | 写代码时、构建时、CI 里 |
| 2 | ESLint | 可疑写法:比如 React Hook 用错 | 提交前、CI 里 |
| 3 | Prettier | 格式不统一:缩进、引号、换行 | 提交前自动修好 |
| 4 | Vitest | 逻辑错误:函数算错了 | pnpm test、CI 里 |
| 5 | Commitlint | 提交信息不规范 | 每次 git commit 时 |
| 6 | Husky | 负责在提交时触发 2、3、5 | 每次 git commit 时 |
- Husky + lint-staged:Git 有一个"钩子"机制,可以在提交前后自动运行脚本。Husky 管理这些钩子,lint-staged 让检查只针对这次改动的文件,所以很快。
- Commitlint 要求提交信息遵循 Conventional Commits 规范:
feat:表示新功能,fix:表示修 bug,build:表示构建相关。真实发生过一次:我把提交信息写成了feat: M3 内容页,它报错subject must not be sentence-case,因为冒号后面不能以大写字母开头。改成feat: 完成 M3 内容页才通过。统一格式的好处是,以后可以按类型自动生成更新日志。 - 单元测试目前有 13 个,覆盖深度换算、阅读时长、文章排序、日期格式、和弦数据合法性这些纯逻辑函数。
- Prettier 还装了 Tailwind 插件,会自动把 class 名排成统一的顺序。
八、Git 和 GitHub
- Git 是装在电脑上的版本管理工具。
git commit给当前代码拍一张"快照",以后随时可以回到任何一张快照。 - GitHub 是放 Git 仓库的网站。
git push把本地的快照上传上去。 - commit 只存在本地,push 之后别人(和自动化系统)才看得到。 这是最常见的误解。
写这篇文章时,仓库里的提交是这样的,每次都对应一个阶段(fix(about) 里括号中的 about 表示改动范围):
feat: 初始化 Seabloom 个人网站 ← 工程搭建 + 设计系统
build: 添加 Cloudflare Workers 静态资源部署配置 ← 上线准备
feat: 完成 M3 内容页(博客、关于、项目……) ← 内容
fix(about): 补上考研二战的空档年…… ← 修正经历
docs: 扩写建站全过程长文 ← 就是这篇
九、CI/CD:一次 push 之后发生了什么
这是把所有东西串起来的一节。我在电脑上执行 git push 之后:
01 · 0s
代码到达 GitHub
main 分支更新,GitHub 同时通知两个地方:GitHub Actions 和 Cloudflare。
02 · 约 50s
GitHub Actions 跑 CI
按 .github/workflows/ci.yml 的步骤,在一台全新的 Linux 虚拟机上:安装依赖 → lint → 检查格式 → 类型检查 → 单元测试 → 构建。全部通过显示绿色对勾,任何一步失败显示红叉。
03 · 2s
Cloudflare 初始化、克隆代码
Cloudflare 的构建服务(Workers Builds)在自己的机器上把仓库克隆下来。
04 · 13s
安装依赖
执行 pnpm install,严格按照 pnpm-lock.yaml 里锁定的版本安装,保证和我电脑上一模一样。
05 · 11s
构建
执行 pnpm build,也就是 next build,产出 out/ 文件夹。
06 · 17s
部署
执行 npx wrangler deploy。wrangler 读取 wrangler.jsonc,把 out/ 里的文件上传,几秒钟内同步到全球的节点。
07 · 约 30s
全世界都能看到新版本
从 push 到线上更新,总共大约半分钟。整个过程我一次都没有登录服务器。
这里有两条并行的流水线:GitHub Actions 负责"检查",Cloudflare 负责"发布"。它们互不依赖。更严格的做法是让 CI 通过之后才允许部署,这是以后可以加的改进。
部署配置 wrangler.jsonc 只有几行:
{
"name": "haiyangliu", // 必须和 Cloudflare 后台的项目名一致
"assets": {
"directory": "./out", // 发布哪个文件夹
"not_found_handling": "404-page", // 找不到页面时用我们自己的 404.html
},
}
一个访客打开网站时发生了什么
01
DNS 查询
浏览器问 DNS:这个域名对应哪个 IP?DNS 返回离访客最近的 Cloudflare 节点的地址。
02
建立 HTTPS 连接
和这个节点建立加密连接。证书由 Cloudflare 自动签发和续期,我们完全不用管。
03
拿到 HTML
节点直接返回已经生成好的 index.html。因为是静态文件,几乎不需要等待。
04
加载 CSS、字体、图片
浏览器读 HTML,发现还需要样式、字体和图片,于是继续下载。这些文件的名字里带着内容哈希,可以长期缓存。
05
水合(Hydration)
最后下载少量 JS,React 把交互能力接到已经显示出来的页面上:主题按钮能点了,深度计开始跟着滚动走了。
"先显示 HTML,再接上交互"——这就是为什么页面打开时几乎瞬间就能看到内容。
十、我们踩过的坑
真实项目一定会遇到问题。下面每个坑都按"现象 → 原因 → 解决"来写,面试时讲这些往往比讲"我用了什么技术"更加分。
1. 装不上 pnpm
现象:npm install -g pnpm 报 EPERM。原因:电脑上 npm 的全局目录在 D:\develop\node,普通权限没法写入。解决:改用 Node 自带的 corepack 运行 pnpm(corepack pnpm ...)。它会把 pnpm 下载到用户目录,不需要管理员权限。
2. 构建报错 PPR cannot be enabled in export mode
原因:新版 Next.js 默认开启了 Cache Components,它依赖 PPR(部分预渲染),而 PPR 需要服务器在运行时参与,和"纯静态导出"天然冲突。解决:关掉这两个选项,并在配置里写上注释,以后上服务器时再打开。
3. 页脚的年份导致构建失败
现象:new Date() 被报 unstable value。原因同上:缓存模式不允许在渲染时读取"当前时间"这种每次都不一样的值。关掉 Cache Components 之后,年份会在构建时确定下来。
4. 改了配色,浏览器里还是旧颜色
原因:开发服务器缓存了旧的 CSS 编译结果。解决:删掉 .next 缓存目录,重启开发服务器。
5. 代码块的边框不见了
原因:Shiki 会给 <pre> 加上它自己的 className,而我们的组件把它传下去时,覆盖掉了我们写的样式。解决:把两边的 class 合并,而不是互相覆盖。
6. 中文里多出空格
现象:"喜欢弹吉他, 也喜欢……",逗号后面多了一个空格。原因:JSX 会把"换行 + 缩进"压缩成一个空格,英文里这正好合适,中文里就多余了。解决:长段中文写成 {"..."} 表达式,就不会被插入空格。
7. 相册切换后标题变了,图片没变
原因:React 复用了同一个 <img> 元素,只改了 src,在新图加载完之前仍显示旧图。解决:给 <img> 加上 key={照片 id},换照片时就创建一个全新的元素。
8. 提交被拦下
Commitlint 报 subject-case,见第七节。这恰恰说明防线在起作用。
十一、面试怎么讲
一分钟介绍
我独立搭建并上线了一个个人网站 Seabloom,技术栈是 Next.js、React、TypeScript 和 Tailwind CSS,采用静态导出,部署在 Cloudflare 的全球 CDN 上。工程上配置了 ESLint、Prettier、Vitest 单元测试,通过 Husky 在提交时自动检查,GitHub Actions 做 CI。推送代码后约 30 秒自动部署上线。内容方面,博客基于 MDX,支持代码高亮、LaTeX 公式和 RSS;摄影页用 Node 脚本预处理图片,生成多尺寸 WebP 和模糊占位,并去除 GPS 信息;首页有一个跟随滚动的深度计,用 requestAnimationFrame 做了节流。
可能被问到的问题
Q:为什么选 Next.js,而不是直接写 React? A:React 只负责 UI,路由、构建、代码分割、字体优化都要自己配。Next.js 把这些都做好了,还支持静态生成,非常适合内容型网站。以后要加服务端功能,也不用换框架。
Q:服务端组件和客户端组件有什么区别?你怎么决定用哪个? A:服务端组件在构建时运行,不会给浏览器增加 JS;客户端组件在浏览器里运行,能处理交互。我默认用服务端组件,只有需要状态、事件或浏览器 API 的地方才用客户端组件,比如主题切换、滚动深度计、相册。全站只有 4 个客户端组件。
Q:静态导出有什么限制? A:没有服务器在运行时参与,所以不能做需要实时数据的功能,比如用户登录、存储留言,也不能用 Next.js 的图片实时优化和 PPR。我的处理办法是:图片在构建前用脚本预处理;以后需要后端时,单独做一个 API 服务。
Q:怎么优化图片加载?
A:四点:WebP 格式减小体积;srcSet 加 sizes 让浏览器按屏幕宽度选尺寸;写明宽高,避免布局偏移(CLS);用 base64 内联的模糊小图做占位。另外,处理时去掉了 EXIF 里的 GPS 信息,保护隐私。
Q:深色模式是怎么实现的?怎么避免闪烁?
A:用 CSS 变量分两层,原始色板加语义色,.dark 只覆盖语义层。next-themes 会在页面渲染之前注入一小段脚本,提前设置好 class,所以不会先闪一下浅色再变深色。
Q:滚动监听会不会卡?
A:滚动事件触发很频繁,所以我用 requestAnimationFrame 节流,每帧最多计算一次,并把监听设为 passive,告诉浏览器我不会阻止滚动,它就可以放心优化。
Q:你的 CI/CD 流程是怎样的? A:提交时 Husky 跑 lint-staged 和 commitlint;推送后 GitHub Actions 在干净的环境里跑 lint、格式检查、类型检查、测试和构建;同时 Cloudflare Workers Builds 拉取代码、构建并部署,大约 30 秒上线。依赖版本由 lockfile 和 packageManager 字段锁定,保证三个环境一致。
Q:遇到过什么技术难点? A:可以挑第十节里的一两个来讲。比如 PPR 和静态导出冲突:先看懂报错,再在 Next.js 文档里查到 Cache Components 依赖 PPR,权衡之后,在静态部署阶段关掉它,并记下以后上服务器时再开启。
Q:如果访问量变大,你会怎么扩展? A:静态部分本来就在 CDN 上,几乎可以无限扩展。真正需要扩展的是以后的后端:用 Docker 部署 API 服务,数据库用 PostgreSQL,前面接 Cloudflare 做缓存和防护,压力再大就做水平扩展和读写分离。
十二、下一步
- 绑定自己的域名:在 Cloudflare 买
haiyangliu.com,加到 Worker 的自定义域名里,DNS 和 HTTPS 都是自动的。 - 第一个后端项目:比如"潮汐留言板"。可以先用 Cloudflare Workers 加 D1 数据库试水,再过渡到自己的 VPS(Linux、Nginx、Docker、PostgreSQL)。前端调用
api.haiyangliu.com,正好就是第二节说的前后端分离。 - 质量打磨:sitemap、社交分享卡片、Lighthouse 性能评分、无障碍检查。
- 让 CI 卡住部署:CI 不通过就不发布。
从一个空文件夹到现在,最大的收获是:每个工具都只解决一个具体问题,把它们一个一个接起来,就是一个完整的工程。 搞清楚每一环在解决什么问题,就不会再被一堆名词吓住了。