返回首页

Hello World

这是我用 Nuxt 4 + MySQL 自己搭的个人博客 —— 来唠唠技术栈、踩过的坑和接下来要写的东西。

3 约 3 分钟 · 3405 字 随笔

这个博客是什么

如果你看到这篇,八成是被某个搜索引擎或者朋友拉过来的 —— 欢迎。

这是我自己折腾出来的一个纯手搓博客:从前端到后端、从数据库到部署、从代码高亮到访问日志,全是自己拼起来的。

技术栈很普通,选型基本是"哪个我用得顺手就用哪个":

  • 前端 / SSR:Nuxt 4 + Vue 3 + TypeScript + Tailwind CSS 3
  • 后端:Nitro(Nuxt 自带)+ MySQL(mysql2/promise)
  • 认证:JWT(jose)+ bcryptjs,Cookie HttpOnly
  • 服务器:阿里云 ECS 2C2G + Rocky Linux 9.5,PM2 cluster + Nginx 反代
  • 部署:本地构建 → tar → scp → SSH → pm2 reload,一条 pnpm run deploy 搞定

一开始用过 Hexo 和 Hugo,甚至想过 WordPress,最后还是觉得自己写一个最自由:想加什么功能加什么,不用受主题约束,而且顺便还能练手。

它现在能干嘛

除了"显示文章"这个基本功能,这一年陆陆续续给它加了不少东西:

  • 📝 后台编辑器:Markdown textarea + 实时预览,500ms 自动写 localStorage 草稿,断电不丢稿
  • 🔐 完整的 RBAC 后台:用户 / 角色 / 权限点 / 动态菜单,后期想多账号协作直接加
  • 📊 数据分析:ECharts 出图,看哪天 PV 高、哪篇文章被分享了
  • 🔍 访问日志:每个请求一行,IP / UA / 路径 / Query / Body / 状态码 / 耗时 / 设备 / 浏览器 / 30+ 种爬虫识别,密码 token 自动脱敏
  • 🛠️ 在线工具集 /tools:15 个开发者小工具,JSON / Base64 / 时间戳 / 正则 / 颜色 / 哈希 / UUID / JWT / 字数 / 密码生成 / 二维码 / AES / 图片上传 / JS 压缩混淆... 大部分纯前端,数据不上传
  • 🖼️ 自带图床:/admin/uploads 后台直接拖图,SHA256 命名,Markdown 一键复制粘贴到文章
  • 💬 评论:Giscus,基于 GitHub Discussions,垃圾评论自动挡,我也不用维护数据库
  • 性能优化:文章 HTML 进程内 LRU 缓存,shiki 双主题代码高亮预渲染,长文有 sticky TOC
  • 🤖 SEO:/sitemap.xml / /rss.xml / OG meta / Twitter Card / JSON-LD / canonical 全套

几段实际的代码

文章详情接口里,渲染好的 HTML 会进进程内 LRU 缓存,避免每次请求都去 marked + shiki 跑一遍:

const cache = new Map<string, { html: string; expiresAt: number }>()
const TTL_MS = 5 * 60 * 1000

export async function getRenderedPost(slug: string) {
  const hit = cache.get(slug)
  if (hit && hit.expiresAt > Date.now()) return hit.html

  const md = await fetchPostMarkdown(slug)
  const html = await renderMarkdown(md) // marked + shiki 双主题
  cache.set(slug, { html, expiresAt: Date.now() + TTL_MS })
  return html
}

数据库改 schema 不用写独立的 migration 文件,直接在 server/utils/db.ts 里追加 ALTER,启动时幂等执行:

ALTER TABLE access_logs
  ADD COLUMN request_body VARCHAR(2048) NULL AFTER query_string;

ALTER TABLE posts
  ADD FULLTEXT INDEX ft_posts_search (title, description, content)
  WITH PARSER ngram;

失败静默 —— 列已经存在就跳过,从不阻塞启动。这样代码版本和 schema 版本永远在一起走。

为什么不用现成的轮子

工具和自己造,一直是个取舍。我的逻辑大概是这样:

选项 优点 我为啥放弃
Hexo / Hugo 静态生成快、托管 0 成本 没有后台,改一篇文章都得本地编辑 + git push
WordPress 生态最全、有图床有评论 PHP + MySQL + 各种插件,光维护就够头疼
Vercel / Netlify Hosting 免运维、自动 HTTPS 想加自定义后端(图床 / 工具集 API)有限制,流量起来了也不便宜
自己写 + 阿里云 ECS 完全可控,想加啥加啥 要自己运维,但反正我也是写后端的

说白了,这博客不只是博客,也是我的练兵场。访问日志、限流、暴力破解防护、PM2 cluster 调优、shiki 集成、Rollup external、Nginx 反代、Nitro 静态文件运行时读盘... 每个功能都让我多懂了一点真实世界的工程问题。

写到这里突然想起一个真实踩过的坑:

javascript-obfuscator 这个包的 dist/index.js 是 webpack 预打包的复杂 CJS,Rollup 的 commonjs 插件静态分析它会直接 PARSE_ERROR
解法是 nuxt.config.tsnitro.externals.external + nitro.rollupConfig.external 双保险标记 external,业务里用 await import() 延迟加载,运行时由 Node 标准 module resolution 从 node_modules/ 找。

这种小坑全网搜不到几句中文资料,以后会单独写一篇详细聊。

接下来打算写什么

大方向是技术,但具体的话,目前在脑子里排队的有这么几类:

  • 真实踩坑 —— 工作或业余项目里遇到的怪问题,记录下来给后人省时间
  • 小工具开发笔记 —— 每加一个 /tools/<xxx>,顺手写一篇"它怎么实现的"
  • 运维笔记 —— 阿里云上的小服务器怎么省钱用 + 不被 OOM,从 1Panel 的坑到 PM2 cluster 的内存账
  • 零散思考 —— 看了什么书、听了什么播客、写代码时的小感悟

不许愿,先跑起来。


欢迎在下面评论区(用 GitHub 账号登录)留几句,或者去 归档 / 分类 / 标签 翻翻看有没有感兴趣的。

下次见。