
前阵子我把自己的博客系统从零重构了一遍前端选了 Vue3后端用 Python 写接口整体走了一遍数据库设计 → 后端 API → 前端页面 → 部署上线的完整流程。这篇东西不是官方文档式的教程而是我踩完坑之后想分享给同样在做博客系统、或者打算用 Vue3 Python 组合做点东西的人的一些实际经验。标题里的关键词我都覆盖到了包括 Vue3 的项目创建、生命周期理解、父子组件通信、登录注册、数据库设计、Python 环境配置甚至部署时遇到的 Nginx 问题。项目本身不复杂但把它跑通、跑稳、跑得舒服中间有不少细节值得记录下来。1. 为什么选 Vue3 Python 这个组合从技术选型的真实考量说起很多人问过我一个问题做博客系统前端选 Vue 就算了后端为什么不用 Node.js非要选 Python这个问题的答案其实跟你做这个项目的动机有直接关系。1.1 前端选 Vue3 而不是 Vue2 的核心理由我上一个项目用的还是 Vue2这次换到 Vue3最直接的感受是组合式 API 让组件代码的组织方式发生了根本变化。在 Vue2 里一个复杂组件的逻辑被拆散在 data、methods、computed、watch 这些选项里一个功能相关的代码往往要上下跳好几个地方才能看全。Vue3 的 setup 函数可以把某个功能的状态、计算属性和方法集中在一起代码的可维护性确实好了很多。Vue3 的响应式系统也换了基于 Proxy 实现相比 Vue2 的 Object.defineProperty它能够监听对象属性的新增和删除处理数组下标操作也更自然。对于博客这种有大量动态数据结构文章列表、分类、标签的场景这个特性实际用起来省了不少事。还有一个现实原因生态方向。Vue3 现在已经是 Vue 官方的主推版本Element Plus、Vant 4、Naive UI 这些组件库都是围绕 Vue3 做的新项目如果再选 Vue2很多第三方库的兼容性会越来越尴尬。我做博客系统不是一次性玩具后续还想加后台管理就是热搜词里那个 Vue3 后台管理系统相关的功能从 Vue3 起步是更稳的选择。1.2 后端到底用 Python 的哪套框架Python 做 Web 常见的三个框架是 Flask、Django、FastAPI。博客系统这个体量我用的是 FastAPI理由主要有三个性能不错基于 ASGI支持异步接口响应速度对于个人博客来说绰绰有余。自动生成 OpenAPI 文档前后端联调的时候接口文档直接可看可试省掉了很多沟通成本。用 Pydantic 做参数校验非常顺手请求参数的合法性判断再也不用手写一堆 if。当然如果你是刚接触 Python 不久用 Flask 也可以逻辑更简单学习曲线更低。但如果你跟我一样是从零搭一个前后端分离项目FastAPI 的类型提示配合 IDE 的自动补全开发体验真的比 Flask 舒服。Python 环境这块多说一句新手最容易卡住的点有两个一是 Python 版本选择博客系统建议直接用 3.10 以上版本不仅语法特性新一些第三方库的新版本也对低版本不太友好二是虚拟环境项目依赖一定要用 venv 或 conda 隔离不要直接装到全局环境里。我遇到过多次因为全局环境依赖冲突导致项目跑不起来的尴尬情况后来学乖了每个项目都单独建虚拟环境。1.3 这套组合适合什么人、解决什么问题如果你属于下面几类人这个组合会很适合你前端想入门全栈但不想一上来就啃 Java、Go 这类相对繁重的后端技术栈。需要快速搭建一个内容管理型网站而且希望技术栈比较统一好维护。对 Python 本身有兴趣想通过一个实际项目练手博客系统的用户模块、文章模块都很典型。博客系统这个项目形态其实是很经典的全栈练习项目它包含了用户注册登录、数据表设计、增删改查、文件/富文本处理、前后端交互、部署运维几乎覆盖了一个业务系统的大部分常见环节。做完一个完整的博客系统你再去接触电商类、管理后台类的项目会发现很多思路是相通的。2. 项目地基数据库表结构设计与后端接口规划博客系统看起来功能不多但表结构如果设计不好后期写接口会非常痛苦。数据库设计这个环节我推荐大家一定不要跳过直接上来写代码后面八成要返工。2.1 用户信息表怎么设计才不返工热搜词里有个数据库设计 - 博客系统第1关数据库表设计 - 用户信息表很多人在学习时就把表设计当作一个简单的建表练习其实这里面坑不少。用户信息表我最后的字段设计是字段名类型说明idINT / BIGINT主键自增usernameVARCHAR(50)登录用户名唯一索引password_hashVARCHAR(255)密码哈希值绝不存明文emailVARCHAR(100)邮箱可空但建议唯一avatarVARCHAR(255)头像地址bioVARCHAR(255)个人简介roleTINYINT角色0 普通用户1 管理员statusTINYINT状态0 禁用1 正常created_atDATETIME注册时间updated_atDATETIME更新时间几个容易被忽略的点password_hash 一定要留够长度。bcrypt 生成的哈希一般是 60 位如果设计字段时只给 50 位写注册功能时就会报数据太长的错误。username 一定要加唯一索引。注册接口并发下可能会有重复用户名写入的风险唯一索引就是兜底。role 字段从一开始就要加不要觉得我的博客就我一个人用。后期如果要开放注册、加审核权限没有 role 字段就要改表。2.2 文章表、评论表和分类表的关联关系文章表是博客系统的核心表我的设计是id、title、content文章正文用长文本类型、summary摘要列表页用、cover_image封面图、category_id关联分类表、tags可以用单独的表来做多对多也可以简化成逗号分隔字符串、status草稿/已发布、views浏览量、created_at、updated_at。关于 tags我给的建议是如果你没有做标签筛选、标签聚合这种复杂需求直接在文章表里存逗号分隔的字符串就够了。我一开始按教科书式设计建了 tag 表 文章标签关联表后来发现博客场景下标签功能大多数只是文章详情的展示做三张表反而复杂。但如果你的标签页要做点击标签查看相关文章列表 标签数量统计那还是宁可多建一张关联表。评论表比较简单id、article_id、user_id、parent_id回复的是哪条评论、content、created_at。parent_id 支持楼中楼回复很多博客系统的评论其实是无限层级但实际展示时最多做两层就够第三层开始阅读体验就很差了。分类表就两个核心字段id、name再加一个 sort_order 控制排序。分类数量少不需要做复杂的树形结构。2.3 后端接口清单与 RESTful 设计思路我在写接口之前先把所有需要的前后端交互点列了一个清单这也是我比较推荐的做法避免前端开发到一半发现缺接口。博客系统核心接口POST /api/auth/register —— 注册POST /api/auth/login —— 登录返回 TokenGET /api/auth/me —— 获取当前登录用户信息GET /api/articles —— 文章列表分页、按分类/标签筛选GET /api/articles/{id} —— 文章详情POST /api/articles —— 发布文章需要登录PUT /api/articles/{id} —— 编辑文章DELETE /api/articles/{id} —— 删除文章管理员或作者GET /api/categories —— 分类列表GET /api/comments?article_idxx —— 某篇文章的评论列表POST /api/comments —— 发表评论需要登录RESTful 设计的原则就是URL 里用名词不用动词操作通过 HTTP 方法表达。比如发布文章是 POST /api/articles而不是 POST /api/createArticle。这个习惯其实很多新手不在意但你接口多了之后规范命名会减少大量沟通成本。3. 前端工程搭建从创建 Vue3 项目到路由与状态管理前端这块是很多刚接触 Vue3 的人最容易卡住的地方因为创建项目的工具链变化比较大。热搜词里创建 Vue3 项目vue3 安装vue3 教程都是高频词说明确实有不少人卡在第一步。3.1 用 Vite 创建 Vue3 项目的正确姿势现在创建 Vue3 项目推荐直接用 Vite不用 Vue CLI 了。Vite 开发服务器的启动速度比 Webpack 时代快一个量级热更新基本是毫秒级的。命令很简单npm create vitelatest blog-frontend -- --template vue然后进入项目目录安装依赖cd blog-frontend npm install npm run dev这个模版生成的是 Vue3 JS 的基础结构。如果你熟悉 TypeScript也可以加 --template vue-ts但我个人建议博客这个规模先用 JS 把逻辑跑通TS 后面再引入不迟。创建完之后我习惯第一时间把项目目录整理一下别所有组件都堆在 components 下。我的目录结构是src/ ├── api/ # 所有接口请求封装 ├── assets/ # 静态资源 ├── components/ # 通用组件 ├── router/ # 路由配置 ├── stores/ # Pinia 状态管理 ├── views/ # 页面组件 ├── utils/ # 工具函数 └── App.vue这个结构不是必须的但前后端项目一旦进入功能迭代期清晰的目录结构能帮你省下大量找代码的时间。还有一个容易踩的坑Vite 默认的 别名代表 src 目录不是自动配好的。需要在 vite.config.js 里配置import { defineConfig } from vite import vue from vitejs/plugin-vue import path from path export default defineConfig({ plugins: [vue()], resolve: { alias: { : path.resolve(__dirname, src) } } })不然你写import xxx from /api/xxx的时候会一直报错找不到模块。3.2 Vue3 生命周期与组合式 API 的实践理解Vue3 的生命周期和 Vue2 有对应关系但名字变了而且组合式 API 里要显式导入生命周期函数比如import { onMounted, onUnmounted, ref } from vue setup() { const articleList ref([]) onMounted(() { fetchArticleList() }) }我自己的理解方式是onMounted 相当于 Vue2 的 mounted页面里要拉数据的逻辑基本都放这里onUnmounted 相当于 destroyed用来清理定时器、取消订阅onBeforeUnmount 相当于 beforeDestroy适合做保存草稿之类的收尾操作。使用 setup 语法糖的话代码会更简洁script setup import { ref, onMounted } from vue const articleList ref([]) onMounted(async () { const res await getArticleList() articleList.value res.data }) /script在实际做博客项目时我最常遇到的困惑是接口数据到底应该存在组件的 ref 里还是存到全局的 Pinia store 里。我的经验规则是如果是多个页面/组件都要使用的数据比如用户登录信息存 Pinia如果只是当前页面自己用的比如文章列表、评论列表直接用 ref 就够了。不需要把所有数据都全局化否则状态管理会变得非常混乱。3.3 父子组件与非父子组件通信博客场景里的实际用法这部分是 Vue3 面试题里的高频考点也是实际开发中一定会遇到的。我在博客系统里就有几个很典型的应用场景。父子组件通信最好是简单直接父组件通过 props 传数据子组件通过 emit 发事件通知父组件。比如博客列表页 PostCard 组件它只需要从父组件接收 article 对象用户点击卡片时 emit 一个 view 事件父组件去跳转详情页script setup const props defineProps({ article: { type: Object, required: true } }) const emit defineEmits([view]) const handleClick () { emit(view, props.article.id) } /script非父子组件通信我有三个方案可以选Vuex/Pinia、EventBus、Provide/Inject。博客系统里我一般这样做登录状态用户信息、Token用 Pinia 管理登录页去修改 store 状态导航栏和文章列表页去读取这两个完全不相关的组件就是通过 Pinia 共享数据的。如果只是祖先组件和后代组件之间传数据用 Provide/Inject 会更轻量不需要引入全局状态。比如文章详情页需要把当前文章的作者信息传给深层的评论组件中间隔了好几层这种用 Provide/Inject 就很省事。这里有个要提醒的EventBus全局事件总线在 Vue3 里已经不是官方推荐的做法了因为 Vue3 不再提供 $on/$off 方法。如果你看到某些旧教程还在教 EventBus那多半是 Vue2 时代的方案。非父子通信优先想 Pinia这是现在最主流也最可维护的方式。4. 登录注册与 Token 鉴权前后端联调的血泪经验热搜词里有博客系统 - 登录注册界面和博客系统 - 用户模块头歌看来不少做博客的人都在这一块被卡过。登录注册看起来简单但真正做完一遍里面涉及的东西挺多的。4.1 密码加密与 JWT 生成逻辑密码绝对不能明文存储这是在所有安全常识里我必须反复强调的一条。我在 FastAPI 后端用 passlib 库的 bcrypt 算法做密码哈希from passlib.context import CryptContext pwd_context CryptContext(schemes[bcrypt], deprecatedauto) def hash_password(password: str) - str: return pwd_context.hash(password) def verify_password(plain_password: str, hashed_password: str) - bool: return pwd_context.verify(plain_password, hashed_password)注册的时候把明文密码 hash 之后存库登录校验的时候用 verify_password 比对。bcrypt 会自动加盐相同密码在不同用户下的哈希值不同这是它比直接做 MD5 安全得多的原因。登录成功之后后端返回 JWT Token。JWT 由三部分组成Header、Payload、Signature核心思路是前端拿着这个 Token 访问受保护的接口后端验证签名有效即可确认用户身份。FastAPI 里我用的是 python-jose 库from jose import jwt, JWTError SECRET_KEY 项目自己的密钥 ALGORITHM HS256 def create_access_token(data: dict): to_encode data.copy() expire datetime.utcnow() timedelta(hours24) to_encode.update({exp: expire}) return jwt.encode(to_encode, SECRET_KEY, algorithmALGORITHM)JWT 里我一般只放用户 id 和 username不放密码、邮箱这类敏感信息。到期时间设置成 24 小时博客这种场景下用户在页面上停留时间不会太长24 小时还算合理业务系统的话这个值要另外考虑。4.2 Axios 请求拦截与响应拦截的正确写法前端所有接口请求我用 Axios 封装了一层统一处理 Token 和错误状态。这块写得不好后面联调会非常痛苦。请求拦截器的作用是每次发请求前从 localStorage 或 Pinia 里取出 Token加到请求头里import axios from axios const service axios.create({ baseURL: /api, timeout: 15000 }) service.interceptors.request.use( config { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }, error Promise.reject(error) )响应拦截器则统一处理返回的状态码。后端约定业务状态码 200 表示成功401 表示未登录或 Token 过期403 表示没有权限service.interceptors.response.use( response { return response.data }, error { if (error.response error.response.status 401) { // Token 过期清除登录态跳转登录页 localStorage.removeItem(token) router.push(/login) } return Promise.reject(error) } )这里最容易踩的坑是后端返回的 401 或者业务错误码如果不在拦截器里统一处理每个页面都要写一遍如果失败怎么办的逻辑不仅代码冗余还很容易漏处理。4.3 登录态刷新与 401 处理的边界问题实际使用中登录态失效的触发场景比想象中多。最常见的情况是用户写一篇长文章写了半小时点击发布的时候 Token 刚好过期接口返回 401结果他写了半天的内容直接丢了。我的解决方案是两件事前端在文章编辑页做content的自动保存草稿每 30 秒把当前内容存到 localStorage即使提交失败也能恢复。对于 Token 过期我做了一个简单的静默续期处理响应拦截器捕获到 401 时尝试用后端提供的 refresh_token 接口换一个新的 access_token成功则重放原请求失败才跳转登录页。这个逻辑写起来要小心因为并发请求同时 401 时不能发起多次 refresh 请求否则会互相把 Token 覆盖。我的做法是加一个 flag 标记是否正在刷新中let isRefreshing false let waitingQueue [] function handle401(response) { const { config } response if (!isRefreshing) { isRefreshing true return refreshToken().then(newToken { localStorage.setItem(token, newToken) waitingQueue.forEach(cb cb(newToken)) waitingQueue [] config.headers.Authorization Bearer ${newToken} return service(config) }).finally(() { isRefreshing false }) } else { return new Promise(resolve { waitingQueue.push(newToken { config.headers.Authorization Bearer ${newToken} resolve(service(config)) }) }) } }这个做法的核心思路是同一时间只有一个 refresh 请求在飞其他 401 请求先排队等新 Token 回来后统一重放。虽然博客系统用不用得上这么复杂的逻辑另说但如果你打算往全栈开发方向走这个模式值得理解。5. 文章发布与 Markdown 渲染从编辑器到前端展示链路博客系统里最有产品感的部分就是文章编辑和展示了。用户真正每天用的功能就是写文章、看文章这个链路的体验直接决定项目质量。5.1 编辑器选型与二次开发写文章用纯 textarea 肯定不行至少要支持 Markdown。我评估过几个方案mavon-editorVue2 时代用得很多Vue3 版本有 kangc/v-md-editor。bytemd基于 Svelte 的 Markdown 编辑器提供了 Vue3 的封装UI 清爽功能边界清楚。vditor国产项目功能很全支持所见即所得和分屏模式。我最后选了 vditor因为它的中文化支持和上传图片的处理比较顺。图片上传我会单独做一个接口前端拿到图片 URL 后回填到 Markdown 文本里不要在 Markdown 里直接存 base64不然文章表字段很容易爆掉。5.2 文章内容的转义与 XSS 防护这是做博客系统必须严肃对待的一个安全问题用户输入的 Markdown 最终会渲染成 HTML 展示给其他人看如果不做防护别人在你的博客里插入一段恶意脚本所有访问者都会中招。我的处理分三层后端在接收文章内容时做html.escape转义但 Markdown 本身包含 #、* 这些字符不能无脑转义。正确做法是后端接收原文渲染的时候在前端处理。前端渲染 Markdown 时用 DOMPurify 对生成的 HTML 做清洗把 script、onerror 这类危险内容滤掉import DOMPurify from dompurify import { marked } from marked const renderMarkdown (content) { const rawHtml marked.parse(content) return DOMPurify.sanitize(rawHtml) }如果允许用户输入自定义 HTMLMarkdown 里可以嵌入 HTML 标签那风险更高建议直接用 DOMPurify 白名单模式默认只保留 p、strong、em、code、pre、blockquote 这些安全标签iframe、script、style 一律删除。5.3 代码高亮、目录生成与阅读体验优化博客文章里的代码块如果没有高亮阅读体验会差很多。我用的方案是 highlight.js它支持语言自动检测配合 CSS 主题使用import hljs from highlight.js import highlight.js/styles/github.css marked.setOptions({ highlight: function (code, lang) { if (lang hljs.getLanguage(lang)) { return hljs.highlight(code, { language: lang }).value } return hljs.highlightAuto(code).value } })目录生成也是文章详情页体验的重要环节。我用了一个简单方案遍历渲染后的 DOM找出所有 h2/h3 标签给它们加上 id然后生成锚点目录const generateToc (contentDom) { const headings Array.from(contentDom.querySelectorAll(h2, h3)) const toc headings.map((el, index) { const id heading-${index} el.id id return { id, text: el.textContent, level: el.tagName.toLowerCase() } }) return toc }目录组件放在文章详情页的右侧点击时通过scrollIntoView平滑滚动到对应位置。我用了 IntersectionObserver 来监听当前阅读到哪个标题让目录自动高亮。这个功能实现起来不算难但对阅读体验的提升非常明显。6. 部署上线与踩坑记录从本地跑通到服务器稳定运行项目在本地跑通只是第一步上线部署才是真正考验。我这次部署过程中踩了不少坑挑三个印象最深的写下来。6.1 前后端分离部署的 Nginx 配置细节博客前端构建后是纯静态文件部署用 Nginx 最省事。我服务器上的 Nginx 配置核心思路是前端静态文件由 Nginx 直接托管接口请求反向代理到 Python 后端进程。server { listen 80; server_name your-blog-domain.com; # 前端静态文件 root /var/www/blog-frontend/dist; index index.html; # history 模式路由重写 location / { try_files $uri $uri/ /index.html; } # 接口代理到后端 location /api/ { proxy_pass http://127.0.0.1:8000; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } }有两个地方特别容易弄错一是前端路由用了 history 模式的话必须配置try_files否则用户直接访问 /article/123 这样的地址会返回 404。很多新手一上线就遇到首页能打开刷新二级页面就 404就是少了这一行。二是proxy_pass后面的 URL 如果带了路径比如http://127.0.0.1:8000/Nginx 会把/api部分替换掉如果没有带路径会把完整路径传给后端。这个细节会导致接口路径对不上我调了半天才明白是斜杠的锅。6.2 Python 环境迁移与依赖管理本地 Python 环境跑得好好的一部署到服务器就各种问题。我遇到最多的是 Python 版本不一致。本地 Python 3.12 用的特性在服务器 Python 3.8 上不兼容运行直接报语法错误。解决方式是做好依赖锁定和环境一致。本地开发时在虚拟环境里导出依赖清单pip freeze requirements.txt服务器上安装依赖时严格指定版本pip install -r requirements.txt另外我强烈建议在服务器上给每个项目单独建虚拟环境别用系统的全局 Python。我见过很多服务器因为全局环境装得乱七八糟最后连系统自带工具都被搞坏了。用 venv 的话python3 -m venv blog-env source blog-env/bin/activate pip install -r requirements.txt后端进程管理我用的是 systemd uvicorn保证服务崩了能自动重启[Unit] DescriptionBlog Backend Service Afternetwork.target [Service] Userwww-data WorkingDirectory/var/www/blog-backend ExecStart/var/www/blog-backend/blog-env/bin/uvicorn main:app --host 0.0.0.0 --port 8000 Restartalways [Install] WantedBymulti-user.target6.3 我踩过的三个印象最深的坑第一个坑是文件上传大小限制。文章封面图用的是服务器本地存储默认 FastAPI 没有限制请求体大小但 Nginx 默认限制 1MB导致图片稍微大一点就上传失败前端报 413 Request Entity Too Large。解决方式是在 Nginx 配置里加client_max_body_size 10m;。第二个坑是跨域问题。本地开发时前端跑在 5173 端口后端跑在 8000 端口直接请求接口会被浏览器的同源策略拦截。我在开发环境用 Vite 的 proxy 解决生产环境则用 Nginx 反向代理让所有请求看起来都是同源的。这里有个常见误区后端用 CORS 中间件直接放开跨域也能解决但生产环境更推荐让所有请求走同一个域名省心很多。第三个坑是时区问题。文章发布时间在本地测试时显示的北京时间上线后全部变成了 UTC 时间差 8 个小时。原因是 Python 后端默认用 UTC 时间写入数据库而前端展示直接用了这个值。我的解决方式是后端统一存 UTC 时间前端拿到时间戳后通过dayjs转成本地时区显示。这样不管用户在中国还是国外看到的时间都是当地时间。最后一个个人体会博客系统的技术栈不算复杂但它把前后端开发的整个链路都串起来了。做完这个项目你对 Vue3 的组件化设计、Python 后端接口开发、数据库设计、部署运维都会有一个完整的认知。这个项目里踩过的坑其实也是很多中小型前后端分离项目共通的坑。如果你正在做类似的系统希望这篇东西能帮你少走一些弯路。