CaiYa_Blog:Vue 3 个人博客原型
记录早期纯前端博客的路由、Markdown 渲染、主题状态与部署经验。
用 Vue 3 + Vite 从零搭建个人博客 —— CaiYa_Blog 开发全记录
菜牙爱吃菜 · 前端 / Vue 3 / 个人项目
前言:为什么要自己写博客?
说实话,用 Hexo、Hugo 这些现成的静态博客框架,十分钟就能搭出一个漂亮的站点。但我偏不——我想自己写一个。
原因有三:
- 学习目的:我在学 AI Agent 开发,天天和 Go、Wails、Vue 打交道,想通过一个完整的前端项目把 Vue 3 的组合式 API、路由、状态管理系统地练一遍。
- 可控性:自己写的博客,想加什么功能加什么功能,不用等框架作者排期。
- 折腾的快乐:从零到上线,踩坑、填坑、再踩坑——这种体验用现成框架是得不到的。
于是就有了 CaiYa_Blog——一个基于 Vue 3 + Vite 的个人博客网站。
一、项目介绍
CaiYa_Blog 是一个纯前端的个人博客系统,核心功能:
- 文章列表与详情页
- 文章分类与标签
- 搜索功能
- 深色模式切换
- 响应式布局(移动端适配)
项目地址:CaiYa_Blog(公开仓库)
CaiYa_Blog/
├── caiya_bg/ # 主项目(Vue 3 + Vite)
│ ├── src/
│ │ ├── components/ # 通用组件
│ │ ├── views/ # 页面
│ │ ├── router/ # 路由
│ │ ├── stores/ # Pinia 状态管理
│ │ ├── assets/ # 静态资源
│ │ └── utils/ # 工具函数
│ ├── public/
│ ├── vite.config.ts
│ ├── package.json
│ └── tsconfig.json
├── BUILD_AND_DEPLOY.md # 构建与部署文档
├── FAVICON_SETUP.md # Favicon 配置指南
└── UPLOAD_GUIDE.md # 上传指南
二、技术栈
| 层面 | 选型 | 理由 |
|---|---|---|
| 框架 | Vue 3(Composition API) | 组合式逻辑复用比 mixin 强太多 |
| 构建 | Vite 5 | 冷启动毫秒级,HMR 秒级,开发体验拉满 |
| 语言 | TypeScript | 配合 Volar 插件,类型提示非常舒服 |
| 路由 | Vue Router 4 | 官方亲儿子,支持路由懒加载 |
| 状态管理 | Pinia | 比 Vuex 简洁,TS 支持一流 |
| 样式 | SCSS + CSS 变量 | 深色模式靠 CSS 变量无缝切换 |
| Markdown | marked + highlight.js | 文章用 Markdown 写,前端渲染 |
| 部署 | GitHub Pages / Vercel | 免费,push 即部署 |
三、核心实现
1. Vite 配置:多页面与路径别名
博客项目需要多个入口(文章页、关于页等),Vite 的 build.rollupOptions.input 可以配置多页面:
// vite.config.ts
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import { resolve } from 'path'
export default defineConfig({
plugins: [vue()],
resolve: {
alias: {
'@': resolve(__dirname, 'src'),
},
},
build: {
rollupOptions: {
input: {
main: resolve(__dirname, 'index.html'),
about: resolve(__dirname, 'about.html'),
},
},
},
})
2. 路由设计:文章详情页
用动态路由匹配文章 slug,配合懒加载减少首屏体积:
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const routes = [
{
path: '/',
name: 'Home',
component: () => import('@/views/Home.vue'),
},
{
path: '/post/:slug',
name: 'Post',
component: () => import('@/views/Post.vue'),
},
{
path: '/about',
name: 'About',
component: () => import('@/views/About.vue'),
},
{
path: '/:pathMatch(.*)*',
name: 'NotFound',
component: () => import('@/views/404.vue'),
},
]
const router = createRouter({
history: createWebHistory(import.meta.env.BASE_URL),
routes,
scrollBehavior(to, _from, savedPosition) {
if (savedPosition) return savedPosition
return { top: 0 }
},
})
export default router
3. Markdown 渲染
用 marked 解析 Markdown,highlight.js 做代码高亮:
// utils/markdown.ts
import { marked } from 'marked'
import DOMPurify from 'dompurify'
import hljs from 'highlight.js'
marked.setOptions({
highlight(code, lang) {
if (lang && hljs.getLanguage(lang)) {
return hljs.highlight(code, { language: lang }).value
}
return code
},
})
export function renderMarkdown(content: string): string {
return DOMPurify.sanitize(marked.parse(content) as string)
}
<!-- views/Post.vue -->
<template>
<article class="post-content" v-html="renderedContent" />
</template>
<script setup lang="ts">
import { ref, onMounted } from 'vue'
import { useRoute } from 'vue-router'
import { renderMarkdown } from '@/utils/markdown'
const route = useRoute()
const renderedContent = ref('')
onMounted(async () => {
const slug = route.params.slug as string
const response = await fetch(`${import.meta.env.BASE_URL}posts/${encodeURIComponent(slug)}.md`)
const markdown = await response.text()
renderedContent.value = renderMarkdown(markdown)
})
</script>
4. 深色模式:CSS 变量切换
不用第三方库,纯 CSS 变量 + 一个 class 切换就搞定了:
// assets/styles/variables.scss
:root {
--bg-primary: #ffffff;
--bg-secondary: #f5f5f5;
--text-primary: #333333;
--text-secondary: #666666;
--accent: #42b883;
}
html.dark {
--bg-primary: #1a1a1a;
--bg-secondary: #2d2d2d;
--text-primary: #e0e0e0;
--text-secondary: #aaaaaa;
--accent: #42b883;
}
// stores/theme.ts
import { defineStore } from 'pinia'
export const useThemeStore = defineStore('theme', {
state: () => ({
isDark: localStorage.getItem('theme') === 'dark',
}),
actions: {
toggle() {
this.isDark = !this.isDark
document.documentElement.classList.toggle('dark', this.isDark)
localStorage.setItem('theme', this.isDark ? 'dark' : 'light')
},
},
})
四、部署经验
本地构建
## build-all.ps1
cd caiya_bg
npm install
npm run build
build-all.ps1 是个简单的 PowerShell 脚本,一键安装依赖 + 构建。可在 PowerShell 中运行,能否直接双击取决于文件关联和执行策略。
部署到 GitHub Pages
- 构建产物在
caiya_bg/dist/目录下 - 在
vite.config.ts里设置base: '/CaiYa_Blog/'(仓库名) - 用
gh-pages分支推送:
npm install -D gh-pages
npx gh-pages -d caiya_bg/dist
- 在 GitHub 仓库 Settings → Pages 里选
gh-pages分支
踩坑:GitHub Pages 的 404 问题
部署后发现子路由(如 /post/hello-world)直接 404 了。原因是 GitHub Pages 的静态服务器没有 SPA 回退。
解决方案:在 public/ 目录下加一个 404.html,内容与 index.html 相同,这样所有路径都会返回首页,由前端路由接管。
五、经验教训
1. Vue 3 的 ref 和 reactive 别滥用
一开始我图省事,所有状态都用 reactive。结果在模板里解构后失去响应式,排查了半小时。
// 错误写法 ❌
const state = reactive({ count: 0 })
const { count } = state // 失去响应式!
// 正确写法 ✅
const count = ref(0)
// 或者用 toRefs
const state = reactive({ count: 0 })
const { count } = toRefs(state)
2. TypeScript 严格模式值得开
tsconfig.json 里 "strict": true 一定要开。虽然前期写起来痛苦,但能避免大量运行时错误。我项目里因为开了 strict,至少提前发现了 3-4 个 undefined 访问的 bug。
3. Vite 的 base 路径在开发和生产环境不同
开发时 base: '/',部署到 GitHub Pages 时 base: '/CaiYa_Blog/'。如果忘了改,部署后所有静态资源都会 404。
建议:用环境变量区分:
// vite.config.ts
export default defineConfig({
base: process.env.VITE_BASE_URL || '/',
})
4. Pinia 的 store 设计要克制
一开始我把所有逻辑都塞进 store,结果 store 越写越大。后来学乖了——store 只存状态和方法,业务逻辑写在组件或 composable 里。
5. 别过度设计
博客项目不需要 SSR、不需要 PWA、不需要微前端。先把核心功能跑通,再考虑优化。我一开始想加 SSR,研究了半天 Nuxt 3 发现完全没必要——个人博客的 SEO 用静态生成就够了。
6. 写文档是给自己看的
项目写了 BUILD_AND_DEPLOY.md、FAVICON_SETUP.md、UPLOAD_GUIDE.md 三份文档。半年后回头看,如果没有这些文档,我可能连构建命令都忘了。写文档的成本远低于重新踩坑的成本。
六、后续计划
- [ ] 接入 Gitalk 或 Giscus 评论系统
- [ ] 添加文章 RSS 订阅
- [ ] 接入 Algolia 全文搜索
- [ ] 写一个简单的静态生成器(类似 VitePress),构建时把 Markdown 编译成 HTML
- [ ] 支持文章草稿与定时发布
写在最后
CaiYa_Blog 从构思到上线大概花了两周的业余时间。代码量不大(Vue 306KB + JS 33KB),但五脏俱全——路由、状态管理、Markdown 渲染、深色模式、响应式布局,该有的都有。
这个项目最大的收获不是"我会用 Vue 3 了",而是我完整地走了一遍从开发到部署的全流程。从写代码、构建、部署、到排查线上问题——每一步都有新的教训。
如果你也想自己写一个博客,别犹豫,动手就完了。踩坑不可怕,可怕的是从未开始。
我是菜牙爱吃菜,一个在 Go/Vue/Python 之间反复横跳的开发者。保持学习,保持分享。
版本与部署补充
marked 的 highlight 选项在部分新版本已移除,需按版本使用扩展或独立高亮流程。v-html 前必须清洗不可信 HTML;原型加入清洗示意后仍需安装依赖并测试。GitHub Pages 的 404.html 回退仍可能返回 HTTP 404,不能当作真正的服务端路由。只有实现预渲染/静态生成后才具备相应 SEO 内容,当前纯 SPA 不自动获得这些能力。