跳到正文

· 17 分钟 · 建站 / Next.js / Cloudflare / 工程化

从零到上线:Seabloom 是怎么搭起来的

一个空文件夹,怎么变成全世界都能访问的网站?这篇把用到的每一项技术、每个页面的实现、上线的每一步和踩过的坑都讲清楚。

这篇文章记录 Seabloom 从一个空文件夹到全世界都能访问的全过程。它写给两类人:一是想自己搭网站、但看到一堆名词就发懵的人;二是以后的我自己——面试时被问到"这个项目你是怎么做的",可以讲得清清楚楚。

读法建议:先看第一节的全貌图,后面每一节都是在放大图里的某一块。

一、先看全貌

整个系统只有四个角色:

你的电脑写代码pnpm dev 预览git commitGitHub存代码仓库Actions 跑 CI通知 CloudflareCloudflare拉代码、构建发布 out/全球 CDN 节点访客浏览器输入网址下载 HTML/CSS/JS渲染页面git push自动触发HTTPS 响应
图 1:整个系统只有四个角色,代码沿着箭头单向流动
  • 你的电脑:写代码、本地预览、把改动"存档"(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)也是这个思路。

深浅色是怎么切换的

  1. next-themes 这个库读取你的系统设置(或你上次的选择),在 <html> 标签上加或去掉 dark 这个 class。
  2. CSS 里 .dark { ... } 那组变量就生效了。
  3. 右上角的按钮调用 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,客户端组件):
    1. 监听页面滚动,算出滚动进度 进度 = 已滚动距离 / 可滚动总距离,范围是 0 到 1。
    2. 把进度换算成深度:深度 = 4000 × 进度²。用平方是为了"前面潜得慢、后面潜得快",更像真的下潜。
    3. 滚动事件一秒能触发上百次,所以用 requestAnimationFrame 节流:每一帧最多算一次,避免卡顿。
    4. 换算函数 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 文件变成网页,要经过这几步:

  1. 01

    写文章

    在 src/content/blog/ 里新建一个 .mdx 文件:开头 export 一个 metadata(标题、日期、摘要),下面写 Markdown。

  2. 02

    列出所有文章

    lib/posts.ts 用 Node.js 的 fs 模块读取这个文件夹,得到所有文章的文件名(slug)。

  3. 03

    生成每篇文章的页面

    blog/[slug]/page.tsx 里的 generateStaticParams 返回所有 slug,Next.js 就为每篇文章各生成一个 HTML。

  4. 04

    Markdown 转换

    @next/mdx 把 Markdown 解析成语法树,再交给一串插件加工:remark-gfm 处理表格和任务列表,remark-math + rehype-katex 渲染公式,Shiki 给代码上色。

  5. 05

    套上样式

    mdx-components.tsx 规定每种元素长什么样,比如所有 h2 都用衬线字体、所有链接都有下划线。

  6. 06

    输出 HTML

    最终每篇文章都是一个现成的 HTML 文件,访客打开时不需要任何计算。

图 2:一篇 MDX 文章变成网页的过程(全部发生在构建时)

几个值得一说的细节:

  • [slug] 是动态路由:方括号表示"这一段网址是变量"。配合 dynamicParams = false,不存在的文章会直接显示 404。
  • 代码高亮在构建时完成:Shiki 用的是 VS Code 同款的语法规则,在构建时就把代码拆成带颜色的片段,浏览器不需要再加载高亮库。它会同时生成浅色和深色两套颜色,深色模式下用 CSS 切换。
  • 公式:写 $\sqrt{d_k}$ 这样的 LaTeX,KaTeX 在构建时把它变成 HTML。比如 softmax(QK⊤/dk)V\mathrm{softmax}(QK^\top/\sqrt{d_k})V 就是这么渲染出来的。
  • 阅读时长:readingMinutes 按中文每分钟 400 字、英文每分钟 200 词估算,并且会排除代码块和 metadata,配了单元测试。
  • RSS(/feed.xml):一个"路由处理器",在构建时把文章列表拼成 RSS 格式的 XML 文件。别人用 RSS 阅读器订阅后,有新文章会自动收到。

摄影 /photos/

照片是网页里最"重"的东西,一张手机原图就有 5 到 10 MB。所以专门写了一个处理脚本 scripts/photos.mjs,运行 pnpm photos 时:

  1. 读取 photos/originals/ 里的原图。
  2. 按照片自带的方向信息把它摆正(手机竖拍的照片常常是"躺着"存的)。
  3. 用 sharp(一个很快的图片处理库)输出 800 和 1600 像素宽两种尺寸,格式是 WebP,体积大约只有 JPEG 的一半。
  4. 再生成一张 16 像素宽的超小图,转成一串文字(base64)直接写进页面,照片加载出来之前先显示它的模糊色块。
  5. 用 exifr 读出相机、光圈、快门、ISO 这些参数,写进 photos.generated.json。
  6. 输出的图片会去掉全部元数据,包括 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 预先处理)。

七、代码质量的六道防线

每次改代码,都有六道关卡依次检查:

关卡工具拦住什么什么时候运行
1TypeScript类型错误:传错参数、拼错属性名写代码时、构建时、CI 里
2ESLint可疑写法:比如 React Hook 用错提交前、CI 里
3Prettier格式不统一:缩进、引号、换行提交前自动修好
4Vitest逻辑错误:函数算错了pnpm test、CI 里
5Commitlint提交信息不规范每次 git commit 时
6Husky负责在提交时触发 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 之后:

  1. 01 · 0s

    代码到达 GitHub

    main 分支更新,GitHub 同时通知两个地方:GitHub Actions 和 Cloudflare。

  2. 02 · 约 50s

    GitHub Actions 跑 CI

    按 .github/workflows/ci.yml 的步骤,在一台全新的 Linux 虚拟机上:安装依赖 → lint → 检查格式 → 类型检查 → 单元测试 → 构建。全部通过显示绿色对勾,任何一步失败显示红叉。

  3. 03 · 2s

    Cloudflare 初始化、克隆代码

    Cloudflare 的构建服务(Workers Builds)在自己的机器上把仓库克隆下来。

  4. 04 · 13s

    安装依赖

    执行 pnpm install,严格按照 pnpm-lock.yaml 里锁定的版本安装,保证和我电脑上一模一样。

  5. 05 · 11s

    构建

    执行 pnpm build,也就是 next build,产出 out/ 文件夹。

  6. 06 · 17s

    部署

    执行 npx wrangler deploy。wrangler 读取 wrangler.jsonc,把 out/ 里的文件上传,几秒钟内同步到全球的节点。

  7. 07 · 约 30s

    全世界都能看到新版本

    从 push 到线上更新,总共大约半分钟。整个过程我一次都没有登录服务器。

图 3:一次 git push 之后的完整链条(时间为实际耗时)

这里有两条并行的流水线:GitHub Actions 负责"检查",Cloudflare 负责"发布"。它们互不依赖。更严格的做法是让 CI 通过之后才允许部署,这是以后可以加的改进。

部署配置 wrangler.jsonc 只有几行:

{
  "name": "haiyangliu", // 必须和 Cloudflare 后台的项目名一致
  "assets": {
    "directory": "./out", // 发布哪个文件夹
    "not_found_handling": "404-page", // 找不到页面时用我们自己的 404.html
  },
}

一个访客打开网站时发生了什么

  1. 01

    DNS 查询

    浏览器问 DNS:这个域名对应哪个 IP?DNS 返回离访客最近的 Cloudflare 节点的地址。

  2. 02

    建立 HTTPS 连接

    和这个节点建立加密连接。证书由 Cloudflare 自动签发和续期,我们完全不用管。

  3. 03

    拿到 HTML

    节点直接返回已经生成好的 index.html。因为是静态文件,几乎不需要等待。

  4. 04

    加载 CSS、字体、图片

    浏览器读 HTML,发现还需要样式、字体和图片,于是继续下载。这些文件的名字里带着内容哈希,可以长期缓存。

  5. 05

    水合(Hydration)

    最后下载少量 JS,React 把交互能力接到已经显示出来的页面上:主题按钮能点了,深度计开始跟着滚动走了。

图 4:访客从输入网址到看到页面

"先显示 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 不通过就不发布。

从一个空文件夹到现在,最大的收获是:每个工具都只解决一个具体问题,把它们一个一个接起来,就是一个完整的工程。 搞清楚每一环在解决什么问题,就不会再被一堆名词吓住了。