技术 2026年7月16日 78 次阅读

从零到上线 Build-my-blog

C

Chris

作者

从零到上线:我如何搭建了自己的个人博客 | chrislll.cn
Today I Learned

从零到上线

我如何搭建了自己的个人博客网站 chrislll.cn —— 从本地开发到阿里云部署的完整踩坑记录

📅 2026年7月 ⏱️ 阅读约 15 分钟 🏷️ Nuxt 3 · Express · 阿里云
Chapter 01

为什么我要自己搭建博客

不是选择一个平台,而是从头开始掌控一切。

作为一个技术爱好者,我一直想拥有一个完全属于自己的内容空间。不是 Medium,不是微信公众号,不是知乎专栏 —— 而是一个我拥有完全控制权的个人网站。

我的需求很明确:

  • Apple 风格的简洁设计,内容用简体中文展示
  • 自定义域名 chrislll.cn(已完成 ICP 备案)
  • 后台可以增删改查文章,支持配图上传
  • 部署在阿里云,国内访问速度快
搭建一个网站最好的时间是十年前,其次是现在。但十年前我还在用 Dreamweaver 做静态页面,现在则有了一套成熟的全栈方案。 —— 我对自己说
Chapter 02

技术选型:为什么要这样组合

全栈不是堆砌技术,而是让每个工具做它最擅长的事。

层级 技术 选择理由
前端框架 Vue 3 + Nuxt 3 SSR 首屏渲染快,SEO 友好,文件路由省心
样式 Tailwind CSS 原子化 CSS,不用写一堆自定义类名
后端 API Node.js + Express 轻量、生态丰富、前后端同语言
ORM Prisma TypeScript 类型安全,迁移管理方便
数据库 MySQL 8.0 阿里云 RDS,100GB 弹性存储
部署 Nginx + PM2 Nginx 做反向代理和静态文件,PM2 进程保活
关于数据库的选择

阿里云 RDS 和 ECS 本地 MySQL 的区别:RDS 是独立服务,按量付费,提供弹性存储和自动备份;本地 MySQL 受限于 ECS 硬盘空间。对于博客文章这类文本数据,RDS 的 100GB 基本等同于"无限"。

Chapter 03

本地开发:MacBook 上的第一步

在部署到云服务器之前,先在本地把整个流程跑通。

我的开发环境是 MacBook Pro。首先需要安装 MySQL:

# 通过 Homebrew 安装 MySQL brew install mysql # 启动服务 brew services start mysql # 创建数据库 mysql -u root -p CREATE DATABASE chrislll_blog;

然后初始化 Prisma:

# 生成 Prisma Client npx prisma generate # 执行数据库迁移 npx prisma migrate dev # 填充种子数据(示例文章、分类、标签) npx prisma db seed

启动后端 API(端口 3000):

cd blog-api npm install npm run dev

启动前端(端口 3001):

cd blog-web npm install npm run dev
本地验证通过

打开 http://localhost:3001,看到首页正常显示文章列表,后台管理页面可以登录 —— 本地环境准备完毕。

Chapter 04

部署到阿里云:踩坑开始

ECS 只有 2GB 内存,每一步都是挑战。

我的阿里云配置:

  • ECS 实例:Alibaba Cloud Linux 3,2GB 内存
  • RDS:MySQL 8.0,100GB 存储
  • 域名:chrislll.cn(已 ICP 备案)
  • 管理面板:宝塔面板

坑 1:Git 未安装

第一次在 ECS 上执行 git clone,报错 git: command not found

解决:Alibaba Cloud Linux 用 dnf 而不是 apt

dnf install -y git

坑 2:Node.js 环境

ECS 上没有 Node.js。我通过宝塔面板安装了 v20.20.2,但需要手动添加到 PATH:

export PATH=/www/server/nodejs/v20.20.2/bin:$PATH

坑 3:Nginx 配置

宝塔面板生成的 Nginx 配置是自动化的,但我需要添加自定义的 location 规则。一开始我直接把 location 块贴到了 server 块外面,报错 "location" directive is not allowed here

Nginx 配置的正确姿势

location 块必须放在 server { } 内部。修改前先用 nginx -t 测试配置是否正确,再重载。

坑 4:500 错误与 SSR

网站能打开,但首页显示 500 Server Error。查看日志发现 Cannot read properties of null (reading 'length')

根因:Nuxt 3 的 SSR(服务端渲染)会在服务器上直接请求后端 API。如果 NUXT_PUBLIC_API_BASE 没设置,Nuxt 默认使用 /api,而服务器上没有对应的代理,请求失败返回 null。

// ecosystem.config.js — PM2 配置 module.exports = { apps: [{ name: 'blog-web', cwd: './blog-web', script: './.output/server/index.mjs', env: { NODE_ENV: 'production', PORT: 3001, // 关键:SSR 时用这个地址访问后端 API NUXT_PUBLIC_API_BASE: 'http://127.0.0.1:3000/api' } }] }

坑 5:内存不足,构建失败

执行 npm run build 时,ECS 的 2GB 内存直接被打满,进程被系统杀掉。

解决:在 MacBook 本地构建,上传构建产物到服务器:

# MacBook 本地构建 cd blog-web NUXT_PUBLIC_API_BASE=/api npm run build tar czf output.tar.gz .output # 上传到 ECS scp output.tar.gz root@8.163.80.16:/www/wwwroot/chrislll-blog/blog-web/ # ECS 上解压 cd /www/wwwroot/chrislll-blog/blog-web rm -rf .output tar xzf output.tar.gz
本地构建 vs 服务器构建

本地构建时 NUXT_PUBLIC_API_BASE=/api(相对路径),因为浏览器端会通过 Nginx 代理到后端。服务器构建时如果用了 http://127.0.0.1:3000/api,浏览器端会尝试直接访问这个地址,导致 API 请求失败。

Chapter 05

功能迭代:从能用到好用

网站跑起来只是开始,真正的工作量在于打磨细节。

定制 1:备案号和联系方式

底部加上 ICP 备案号,替换社交链接为微信二维码:

  • 备案号:吉ICP备2025025523号-2
  • 微信:nonojay
  • 视频号:chris来唠唠

定制 2:自定义标签

把默认的技术标签(Vue、React、Node.js)替换为内容导向的标签:

  • 英文阅读理解
  • 人工智能前沿
  • 用英文讲好中国

Bug 修复 1:文章排序

更新文章后,首页"最新文章"没有按最新发布排序。

根因:更新文章时 publishedAt 字段保持不变。修复:更新时自动将 publishedAt 设为当前时间。

// posts.ts — 更新文章时 publishedAt: data.status === 'PUBLISHED' ? new Date() // 自动更新为当前时间 : null

Bug 修复 2:数据验证失败

保存文章时提示"数据验证失效"。

根因:HTML select 元素返回的 categoryId 是字符串,而 Zod schema 要求数字。修复:提交前强制转换类型。

// PostEditor.vue — handleSubmit if (data.categoryId === '' || data.categoryId === null) { data.categoryId = null; } else { data.categoryId = Number(data.categoryId); // 字符串转数字 }

Bug 修复 3:图片上传

上传图片后不能正常显示,URL 是相对路径 /uploads/xxx.jpg

修复:上传 API 返回完整 URL(包含协议和域名):

const protocol = (req.headers['x-forwarded-proto'] as string) || req.protocol; const host = req.headers.host || 'localhost:3000'; const url = `${protocol}://${host}/uploads/${req.file.filename}`;

Bug 修复 4:客户端 Hydration 后文章消失

最诡异的一个 bug:页面刚加载时能看到文章,一闪之后变成"暂无文章"。

根因:Nuxt SSR 时成功渲染了文章(服务器能访问 127.0.0.1:3000),但浏览器客户端 hydration 后重新请求 API,如果 apiBase 被硬编码为 http://127.0.0.1:3000/api,浏览器无法访问这个地址,导致数据变为空。

关键教训

客户端的 API baseURL 必须用相对路径 /api(由 Nginx 代理),只有 SSR 服务端才用完整 URL http://127.0.0.1:3000/api

Chapter 06

缓存与多端同步

电脑端更新了,手机端还是旧内容 —— 缓存问题比代码 bug 更隐蔽。

第一次发现这个问题时,我以为是部署没成功。但用 curl 检查服务器返回的是最新内容,手机浏览器加 ?t=1 参数也能看到新内容。

问题出在多个层面:

缓存层级分析

1. 手机浏览器缓存:浏览器保存了旧页面的 HTML 和静态资源。

2. Nuxt SSR 数据获取:useAsyncData 可能复用了之前的数据。

3. Nginx 代理缓存:如果配置了缓存,会缓存 API 响应。

我的解决方案:在所有 API 请求上加时间戳参数,同时禁用 Nitro 服务端缓存:

// nuxt.config.ts nitro: { routeRules: { '/**': { headers: { 'Cache-Control': 'no-cache, no-store, must-revalidate' } } } } // 每个 API 请求加时间戳 $api('/posts?limit=6&_t=' + Date.now())

同时,在页面挂载时强制刷新数据,避免客户端导航时复用旧数据:

// index.vue const { data: posts, refresh } = await useAsyncData('home-posts', ...); onMounted(() => { refresh(); // 从后台返回前台时强制刷新 });
Chapter 07

经验总结

如果让我重新搭一次,我会怎么做?

给后来者的建议

1. 本地验证优先:在 MacBook 上把整个流程跑通再部署,能节省大量调试时间。

2. 内存规划:2GB 内存的 ECS 跑不了 Nuxt 构建,本地构建 + 上传是更务实的方案。

3. 环境变量管理:NUXT_PUBLIC_API_BASE 在开发和生产环境的值完全不同,一定要理解 SSR 和 CSR 的区别。

4. 数据类型严格:HTML 表单返回的永远是字符串,提交到 Zod 验证前一定要做类型转换。

5. 缓存策略:公开内容站点要考虑多端缓存同步,时间戳 + Cache-Control 是简单有效的方案。

6. 微信验证:国内网站一定要做 ICP 备案,微信分享链接需要域名验证才能去掉安全警告。

搭建一个个人网站,技术难度其实不高。真正花时间的是那些"看起来很简单"的细节 —— 一个字段的类型转换、一个环境变量的配置、一个缓存策略的选择。正是这些细节,决定了用户体验的天花板。 —— 写在 chrislll.cn 上线后

这篇文章的 HTML 也是用同样的技术栈生成的 —— 递归的快乐。

Written by chrislll · 吉ICP备2025025523号-2

Built with Nuxt 3 + Express + Prisma + MySQL · Styled with Apple Design Language

上一篇
返回文章列表
下一篇