做个人博客容易真正把它做成带社交生态的Nodejsvue个人博客社交系统才是大部分人栽坑的地方。这篇文章不聊虚的直接把我做过的“个人博客社交系统”拆给你看重点落在相册关注这两个模块上一个是内容沉淀一个是关系链两个合起来才是社区感的核心。适合正在做毕设、想做独立博客、或者想练手全栈项目的人参考。我这套方案踩过一轮又一轮坑最后沉淀下来是比较稳妥的组合Node.js 提供后端接口Vue 负责前端交互数据库用 MySQL 存结构化业务数据。文章、用户、相册、关注、点赞、评论这些功能拆成一块一块做比一上来就堆功能好维护得多。先说结论个人博客社交系统技术上没有多难难点在于你怎么把业务模块之间的关联理清楚。比如关注了别人之后首页动态流要能刷出对方的文章与相册更新比如上传相片之后封面怎么取、图片怎么压缩、权限怎么控制。这些细节才是项目真正花时间的地方。1. 系统定位与业务拆解1.1 做这套系统前先想清楚的三件事第一件事博客系统和社交系统到底怎么融合。博客本身是内容消费社交系统是关系维护。融合之后用户既要能看别人的博客也要能关注作者、评论互动、订阅更新。所以你不能只做一个文章发布网站得有用户体系、关注关系、动态流、互动反馈。第二件事相册模块到底是独立的图片管理工具还是个人主页的附属模块。我最后做成了“相册 图片”双层结构一个用户有多个相册一个相册下面有多张图片。这样用户在浏览作者主页的时候既能看文章列表也能点进相册看生活记录内容形式就丰富了。第三件事到底要不要做成前后端分离。如果你只是本地跑一个小 demo可以用模板渲染一把梭。但既然标题里写了 vue方向就已经明确了前端 Vue 单页应用后端 Node.js 提供 JSON API前后端通过 HTTP 通信。这种架构的好处是前端可以独立部署、独立调试后面想拆微服务、换前端框架也容易。缺点也非常现实跨域、登录态、接口鉴权这些都得自己处理。1.2 功能模块拆解文章、相册、关注三条主线用户模块注册、登录、个人信息修改、头像上传。这是整个系统的地基没有用户体系后面关注和评论无从谈起。文章模块发表文章、编辑删除、列表分页、文章详情、点赞、评论。相册模块新建相册、上传图片、浏览相册、删除图片、设置封面。关注模块关注用户、取消关注、粉丝列表、关注列表、访问对方主页时显示是否已关注。动态流模块基于关注关系在首页聚合展示“我关注的人”发的最新文章和相册动态。这里我不建议一开始就把五个模块全做完再联调正确的做法是先把用户注册登录跑通再把文章模块跑通然后做关注最后接相册和动态流。我第一版就是贪快先做相册结果用户体系没准备好上传图片不知道归属给谁返工了一大轮。2. 技术选型与整体架构2.1 为什么选 Node.js 加 Vue 的组合现在做全栈项目主流可选项很多Spring Boot 加 Vue、Python Django 加 Vue、Node.js 加 Vue。如果你本身熟悉 JavaScript 生态Node.js 全栈是个性价比极高的选择。Node.js 后端这边不一定要用什么重型框架Express 足够稳定Koa 也可以我更倾向 Express因为中间件生态成熟遇到问题搜索到的资料最多。前端则用的是 Vue 3 加 Vite配合 Pinia 做状态管理Vue Router 做路由。这套组合启动速度快开发体验好模板语法也足够简单适合快速搭建个人博客系统。有人会问Vue 2 行不行如果你是新项目建议直接上 Vue 3。Vue 2 虽然还有存量项目在用但已经进入维护末期新项目没必要给自己埋坑。Vue 3 的组合式 API 写业务逻辑会比 Vue 2 的选项式 API 更清晰尤其在做关注按钮、点赞按钮这种高频交互组件时组合式 API 的状态管理明显更顺手。2.2 前后端分离架构下的目录组织我所有项目都习惯用清晰的前后端目录分离而不是把前端直接混在后端项目里。根目录下建两个子目录server 和 web分别表示后端服务和前端应用。这样部署时后端跑在 Node 服务上前端打包成静态文件后交给 Nginx 托管双方通过 API 通信。目录参考blog-social-system/ ├── server/ # 后端源码 │ ├── app.js # 应用入口 │ ├── routes/ # 路由 │ ├── controllers/ # 控制器层 │ ├── services/ # 业务逻辑层 │ ├── models/ # 数据库模型 │ ├── middlewares/ # 中间件JWT校验、错误处理 │ ├── uploads/ # 本地上传文件目录 │ └── config/ # 配置文件 ├── web/ # 前端源码 │ ├── src/ │ │ ├── api/ # axios 接口封装 │ │ ├── views/ # 页面组件 │ │ ├── components/ # 通用组件 │ │ ├── stores/ # Pinia 状态 │ │ ├── router/ # 前端路由 │ │ └── utils/ # 工具函数 └── blog-social.sql # 数据库初始化脚本这种结构的优势在联调阶段特别明显你可以把后端接口在 Postman 里全部测好再对接前端页面出问题能一眼定位是哪边的锅不用两边代码混在一起找半天。另外前后端分开还有个好处就是前端页面走到哪里都能预览不必每次刷新都被后端服务重新渲染。2.3 数据库设计五张核心表数据库我用的是 MySQL业务数据用关系型数据库来存是最省心的。文章和用户是实体表关注关系是中间表相册用两张表连起来。核心表结构如下用户表 users字段类型说明idint主键自增usernamevarchar(50)用户名唯一passwordvarchar(100)加密后的密码nicknamevarchar(50)昵称avatarvarchar(255)头像地址biovarchar(255)个人简介created_atdatetime注册时间文章表 articles字段类型说明idint主键user_idint作者ID外键关联用户表titlevarchar(100)标题contenttext正文内容covervarchar(255)封面图地址tagsvarchar(255)标签逗号分隔viewsint浏览量created_atdatetime发布时间相册表 albums字段类型说明idint主键user_idint所属用户IDtitlevarchar(100)相册名称covervarchar(255)封面图descriptionvarchar(255)相册描述created_atdatetime创建时间相片表 photos字段类型说明idint主键album_idint所属相册IDuser_idint上传者IDurlvarchar(255)图片路径created_atdatetime上传时间关注表 follows字段类型说明idint主键follower_idint关注者IDfollowing_idint被关注者IDcreated_atdatetime关注时间需要注意一点关注表需要加唯一约束组合键设置为 follower_id 和 following_id。否则用户在页面上手快点了两次“关注”请求发出后数据库就会生成两条重复记录。这个问题我是在测试时发现的前端虽然做了 loading 禁用按钮但是防不住直接调接口的并发请求唯一索引才是底线保障。3. 后端核心实现从登录鉴权到动态流3.1 用户注册登录与 JWT 鉴权一开始我考虑用 session 方案服务端直接维护一份 session 表登录后把 sessionId 写入客户端 cookie。但当前后端分离接口越来越多移动端和前端都要用同一套登录态JWTJSON Web Token明显更简洁登录成功后服务端发一个 token 给前端前端后续请求在 Authorization 头里带上这个 token后端中间件校验成功后把用户信息挂到请求对象上即可。注册接口的密码处理是每个新手最容易出问题的地方。绝对不能明文存。我这里用 bcryptjs 做哈希它自带盐值同样的密码每次加密结果都不同安全性比单纯的 md5 强很多。实际注册逻辑如下const bcrypt require(bcryptjs) const jwt require(jsonwebtoken) const { createUser, findUserByUsername } require(../models/userModel) async function register(req, res) { const { username, password, nickname } req.body if (!username || !password) { return res.status(400).json({ msg: 用户名和密码不能为空 }) } const exists await findUserByUsername(username) if (exists) { return res.status(400).json({ msg: 用户名已被占用 }) } const hash await bcrypt.hash(password, 10) const userId await createUser({ username, password: hash, nickname }) res.json({ id: userId, username, nickname }) }登录成功后的 token 生成需要注意有效期设置。我做的是 7 天有效期同时在前端 axios 拦截器里对所有请求附上 token 头。退出登录时前端只需删除本地 token 并跳转登录页不一定要调后端接口因为 JWT 本身是无状态的服务端没法主动让一个 token 失效。如果你真的需要立即失效就得引入黑名单机制那就失去 JWT 的轻量优势了。JWT 鉴权中间件是整个后端接口安全的第一道大门function auth(req, res, next) { const authHeader req.headers.authorization || const token authHeader.startsWith(Bearer ) ? authHeader.slice(7) : null if (!token) { return res.status(401).json({ msg: 未登录 }) } try { const decoded jwt.verify(token, process.env.JWT_SECRET) req.userId decoded.id next() } catch (err) { return res.status(401).json({ msg: 登录已过期请重新登录 }) } }多个接口鉴权时不管有没有认证的接口都要处理好“没有 token 时”的行为不能让服务端直接崩。我见过不少项目在没带 token 的请求上抛 500而不是 401这是接口语义不规范。3.2 文章模块常用 CRUD 背后的细节文章模块表面上就是增删改查难点在于几个容易被忽略的细节。一是分页查询。前端列表页肯定会用到“加载更多”或者页码切换所以后端接口必须支持 page 和 pageSize 参数并返回 total 总数。使用 MySQL 的话就是 LIMIT 和 OFFSET 的配合。二是浏览量的处理。最简单的方案是每次文章详情接口被调用时就把 views 字段加一。但这样同时也会把作者自己的浏览加上去更严谨的做法是判断请求者 ID 是否等于文章作者 ID不一致才加浏览量。三是文章内容的存储。个人博客的内容可能包含 Markdown 或者富文本 HTML。我建议统一存 Markdown 文本前端渲染时再用一个 Markdown 解析库转成 HTML。这样做的好处很多文本体积小、方便编辑、不会出现直接拼 HTML 导致脚本注入的风险。如果前端直接渲染后端返回的 HTML后端必须做 XSS 过滤逃不掉的。文章发布接口的简化实现async function createArticle(req, res) { const { title, content, tags, cover } req.body if (!title || !content) { return res.status(400).json({ msg: 标题和内容必填 }) } const articleId await insertArticle({ userId: req.userId, title, content, tags, cover }) res.json({ id: articleId }) }返回 id 而不是仅返回成功消息是很细节但很有用的习惯。因为前端在创建完成后往往需要立即跳转到详情页这时候如果没有后端返回的 id前端就得再查一次列表才能拿到多一次请求还容易拿错。3.3 相册模块文件上传不算难难的是文件管理相册模块开始之前我建议先把图片存储方案想清楚。本地存储方案适合学习和小流量场景项目目录下建一个 uploads 文件夹Multer 中间件接收文件保存后用静态资源中间件直接暴露出去访问。对于一个个人博客系统这个方案完全够用。Multer 配置要考虑的核心点是文件的保存路径和随机文件名。直接用用户上传的原文件名会带来两个问题一是中文文件名可能存在编码问题二是重名文件会互相覆盖。最稳的方式是使用时间戳加随机数拼接作为文件名。const multer require(multer) const path require(path) const storage multer.diskStorage({ destination: (req, file, cb) cb(null, uploads/), filename: (req, file, cb) { const ext path.extname(file.originalname) const uniqueName Date.now() _ Math.round(Math.random() * 1e9) ext cb(null, uniqueName) } }) const upload multer({ storage, limits: { fileSize: 5 * 1024 * 1024 }, fileFilter: (req, file, cb) { const allowed [.jpg, .jpeg, .png, .gif, .webp] const ext path.extname(file.originalname).toLowerCase() if (!allowed.includes(ext)) { return cb(new Error(仅支持图片格式)) } cb(null, true) } })上传接口这里我同时做了两层控制文件格式和文件大小。图片后缀白名单可以直接过滤掉绝大多数非图片文件5MB 的限制则能防止用户上传超大图片把磁盘塞满。生产环境里更严谨的方案是读取文件的 MIME 类型并做压缩处理但对个人项目来说后缀白名单加大小限制已经足够。照片和相册的关系要小心设计。我选择让 photos 表里单独存 user_id 字段而不是每次通过 album_id 回查到用户。这里看着冗余实际是有用的当用户删除一个相册时要么把相册下的图片全部删除要么把图片保留成“未分类”状态。有 user_id 在图片表里统计用户的相片总数、做用户维度的相片查询都简单很多。另外相册封面的处理是相册页面的核心细节。一个相册创建时可能还没有照片封面可以先用默认图占位上传第一张照片后把这张照片的地址回写为相册封面。这个逻辑看起来简单如果不特意思考会出现相册封面空白的尴尬情况。3.4 关注关系与动态流的实现关注模块的数据结构很简单就是关注表里的 follower_id 和 following_id 两个外键。真正做起来需要处理三个层面的问题关注操作、关系查询、动态流聚合。关注操作就是插入一条记录取关操作就是删除一条记录。因为我在数据库加了唯一索引重复关注会直接报错所以接口里最好先查一下关注关系是否存在存在就返回“已经关注”不存在才插入。判断“我是否已关注某用户”在前端很常见用户访问对方主页页面上要显示“关注”还是“已关注”按钮个人主页的粉丝列表也要显示“互相关注”之类的标记。最实用的方案是写一个批量查询接口传入需要判断的目标用户 ID 列表返回一个 Set 集合前端拿到结果后直接做判断。避免每渲染一个用户卡片就去请求一次后端请求数量会爆炸。动态流是整个系统里最“社交”的功能。我实现的方案是首页动态流只显示当前用户关注的用户所发布的最新文章和最新相册。SQL 写法上用 JOIN 把用户表、文章表和关注表关联起来按发布时间倒序然后分页。SELECT articles.id, articles.title, articles.cover, articles.created_at, users.username, users.nickname, users.avatar FROM articles JOIN follows ON follows.following_id articles.user_id JOIN users ON users.id articles.user_id WHERE follows.follower_id ? ORDER BY articles.created_at DESC LIMIT ? OFFSET ?关注和相册的动态流可以用 UNION 把两个查询结果合并或者采用更工程化的做法在服务端维护一个 feed 表当用户发文章或传照片时往所有粉丝的 feed 里插入一条提醒记录。这个方案叫“写扩散”适合关注关系不怎么庞大的系统粉丝量大之后才有性能压力。我建议个人项目还是用实时 JOIN 查询简单直接不要为一个学习项目过早引入消息队列或写扩散架构。4. 前端核心页面与交互实现4.1 路由守卫与登录状态管理Vue 前端的第一个基础设施就是登录态。用户登录后把 token 存到 localStorage同时把用户信息放到 Pinia 的 store 里。每次刷新页面时通过一个接口获取当前登录用户信息并重新填充 store。Vue Router 的全局前置守卫是控制页面访问权限的关键router.beforeEach((to, from, next) { const token localStorage.getItem(token) if (to.meta.requiresAuth !token) { next(/login) } else { next() } })这里面容易踩的一个坑是只保存 token但用户信息在刷新后丢失了。解决办法是在路由守卫里加一个异步判断如果 store 里没有用户信息并且有 token就去调用一次“获取当前用户信息”接口等拿到结果后再放行。这里记得要考虑接口失败的情况比如 token 过期那么需要清除本地 token 并跳转登录页。4.2 相册上传页与图片列表组件相册页的前端拆成两层相册列表页负责展示当前用户或他人的所有相册每个相册显示封面图、标题、照片数量相册详情页展示某个相册下的所有图片。上传交互我做了两种入口新建相册时直接上传第一张图已有相册中追加图片。组件里用 el-uploadElement Plus 的上传组件或者自己写一个 input file核心是要做好上传前校验。一个合适的做法是选择文件后先在前端用 FileReader 读取图片并生成预览图用户确认后再真正调上传接口。这样避免了大文件上传失败浪费时间。上传过程中还要显示进度条前后端分离项目里进度条帮用户省下的等待焦虑是很明显的。照片删除操作需要前端确认弹窗毕竟删图不可逆。组件交互上我用的是图片右上角悬停出现删除按钮的模式。这里有个体验细节用户在上传完一批图片后要及时刷新相册的照片数量和封面很多博客项目在这里就漏了导致用户以为上传失败实际是前端视图没刷新。4.3 关注与取关的交互设计关注按钮本身很简单一个按钮两个状态点击后切换。但要从用户角度把逻辑想完整。如果访问的是自己的主页不应该显示“关注自己”按钮直接从组件层面判断当前访问用户 ID 和登录用户 ID 是否一致一致则隐藏。如果访问的是别人的主页按钮初始状态要通过接口查询获知。这里我建议封装成一个自定义 Hook 或者组合式函数把关注状态和切换逻辑都放进去任何页面只需要传入目标用户 ID 就能复用。具体关注切换逻辑在 Vue 里可以这样写const isFollowed ref(false) const loading ref(false) async function toggleFollow(targetUserId) { loading.value true try { if (isFollowed.value) { await unfollowUser(targetUserId) } else { await followUser(targetUserId) } isFollowed.value !isFollowed.value } finally { loading.value false } }这里有几个必要点按钮在请求过程中要进入 loading 状态防止用户疯狂点击触发多次重复请求关注和取关接口失败时要有错误提示不要把失败吞掉切换状态最好以后端返回结果为准而不是前端乐观更新。乐观更新虽然响应快但一旦接口失败状态还原的逻辑写起来很麻烦。个人项目我建议别加这个复杂度。4.4 axios 封装与接口统一管理前端每页都会调用大量接口如果不做封装代码里到处都是 axios 请求配置后期维护想哭。我用的封装方式是单独建一个 request.js 文件创建 axios 实例统一配置 baseURL 和超时时间用请求拦截器注入 token用响应拦截器统一处理后端返回的错误码。import axios from axios import { ElMessage } from element-plus import router from ../router const request axios.create({ baseURL: /api, timeout: 10000 }) request.interceptors.request.use((config) { const token localStorage.getItem(token) if (token) { config.headers.Authorization Bearer ${token} } return config }) request.interceptors.response.use( (response) response.data, (error) { const status error.response?.status if (status 401) { localStorage.removeItem(token) router.push(/login) } else { ElMessage.error(error.response?.data?.msg || 请求失败) } return Promise.reject(error) } ) export default request这里有一个需要单独说明的点baseURL 设置为 /api 而不是直接写死一个域名。开发环境下我通过 Vite 的 proxy 配置把 /api 接口转发到后端的 3000 端口生产环境下Nginx 也会配置 /api 的反向代理。这样前端代码里完全没有后端地址环境切换只需要改配置前端不需要重新打包。5. 环境配置与联调避坑实录5.1 Node.js 环境配置中高频出现的安装问题一个 Nodejsvue 项目第一个坑往往是开发环境本身搭不起来。很多新手在 Windows 上安装 Node.js 后执行 npm 命令会遇到下面的报错npm : 无法加载文件 D:\program files\nodejs\npm.ps1因为在此系统上禁止运行脚本这个报错不是 Node.js 本身坏掉了而是 Windows PowerShell 的执行策略默认禁用了脚本运行。解决办法是打开管理员权限的 PowerShell执行Set-ExecutionPolicy -ExecutionPolicy RemoteSigned执行策略改成 RemoteSigned 只允许本地脚本和已签名脚本运行比直接用 Unrestricted 更安全。另外还要注意Node.js 安装目录不要像报错里那样带着 Program Files 这类带空格的路径部分工具链解析路径时容易出问题。建议把 Node.js 直接装到 D:\nodejs 这类无空格目录下。安装完成后要立刻检查环境变量是否配置成功。在命令行执行node -v npm -v如果 node 命令能找到但 npm 找不到通常是环境变量里的 PATH 没有包含 npm 目录或者安装时没有勾选自动加入 PATH。手动把 Node.js 安装目录加到系统变量 PATH 里就能解决。5.2 前后端联调时常见的跨域与代理配置前后端分离开发时前端地址是 http://localhost:5173Vite 默认端口后端接口地址是 http://localhost:3000。浏览器跨域策略会直接拦截请求所以开发环境最常见的两种处理方案是后端配 CORS 中间件或者前端配代理。我推荐前端配代理。原因很简单后续生产环境部署时前端静态文件和接口通常都会走同一个域名本身就是“同源”不需要后端额外开放跨域。开发环境用代理模拟生产环境是最干净的方式。Vite 的代理配置// vite.config.js export default defineConfig({ server: { port: 5173, proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } } })后端接口统一以 /api 开头代理转发时就非常清爽。如果某个接口忘了写 /api 前缀就会变成直接请求 5173 端口结果返回的是一个 HTML 页面而不是 JSON这个错误非常好排查报错里绝不会是跨域而是拿到 200 状态但解析 JSON 失败。5.3 图片上传后无法访问的解决方案本地上传图片保存成功后浏览器访问图片地址 404是很多人会遇到的坑。原因通常是后端虽然把图片存进了 uploads 目录但没有用静态资源中间件把这个目录暴露出来。Express 里加一行代码就好app.use(/uploads, express.static(path.join(__dirname, uploads)))这样访问 http://localhost:3000/uploads/xxx.jpg 就能正常拿到图片。前端展示图片时直接使用后端返回的 /uploads/xxx.jpg 路径再配合代理走通整个链路。注意生产环境下构建好的前端文件本身打包了路由所以 Nginx 需要同时配置前端 history 模式路由回退和 /uploads 静态资源映射。路由回退的配置是 location / 下面加 try_files 指令否则刷新页面会出现 404。图片路径则把 /uploads 指到后端的 uploads 目录。5.4 部署上线时前端路由回退与接口代理个人博客系统部署不算复杂我用的是经典套餐后端用 PM2 守护进程跑在服务器上前端 build 成静态文件后用 Nginx 托管。后端启动npm run start生产环境我推荐用 PM2 管理 Node 进程崩溃自动重启日志管理也方便。pm2 start app.js --name blog-server pm2 save pm2 listNginx 配置里最核心的两个片段server { listen 80; server_name your-domain.com; root /var/www/blog-web/dist; index index.html; location / { try_files $uri $uri/ /index.html; } location /api/ { proxy_pass http://127.0.0.1:3000/api/; proxy_set_header Host $host; proxy_set_header X-Real-IP $remote_addr; proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; } location /uploads/ { proxy_pass http://127.0.0.1:3000/uploads/; } }try_files 的作用就是让前端路由在刷新时都指向 index.html由 Vue Router 再根据 URL 解析到对应页面。这个配置是第一优先级漏了它就出现“刷新页面就白屏”的灵异事件。6. 安全加固与性能优化细节6.1 密码安全、JWT 密钥与图片校验安全这块不用做得很复杂但最基础的三样必须做到。第一密码永远不能明文存储。bcryptjs 加盐哈希这个前面已经讲了属于最底线。如果用户密码用了 md5 直接入库跟明文几乎没有区别现在彩虹表一查就能还原。第二JWT_SECRET 不能写死在代码里。我见过很多人把 secret 放在 app.js 里然后在 GitHub 上把代码传了上去等于用钥匙挂在门外面。正确做法是用环境变量管理。开发环境下放在 .env 文件里生产环境从部署平台或者启动脚本中注入。第三文件上传除了校验后缀还要校验文件的大小。Multer 的 limits 里设置一个合理上限比如 5MB就能防住恶意上传大文件拖垮服务器磁盘。当然如果项目面向的用户量大更专业的做法是接入对象存储服务图片上传走云存储但个人项目没这个必要。6.2 数据库索引优化数据量小的时候有没有索引看起来区别不大。但一旦用户量到几千、文章到几万条无索引的查询会明显变慢这是必然发生的不需要等到性能真的出问题才处理。我的建议是趁早把索引加上。关注表给 follower_id 和 following_id 建联合唯一索引文章表给 user_id 建普通索引相片表给 album_id 建索引。这些索引在数据库表创建脚本里直接写好不要上线以后再来回补表里有数据后再加索引容易锁表。查看 MySQL 是否走了索引用 EXPLAIN 命令看查询计划即可如果看到 type 是 ALL 就是全表扫描说明索引没有建对。6.3 前端性能优化三件套前端性能优化里见效最快的是图片懒加载。相册列表和文章列表的图片数量必然不少一屏只显示几张其他图片完全可以到用户滚动到对应位置再加载。Element Plus 里图片组件自带 lazy 属性或者使用 v-lazy 指令这个优化成本极低收益极明显。其次是组件拆解。把文章卡片、相册卡片、关注按钮都拆成独立组件不仅是为了复用更是为了让 Vue 的响应式更新范围更小。同一个数据变了只有对应组件重渲染不会带着整页一起抖动。最后是接口数据按需加载。列表页先只加载文字信息和封面相册详情里的原图在点击后才加载。尽量后端接口就拆分细前端不要一把拉全量数据在本地筛选数据库层面做过滤永远比前端处理要省资源。7. 常见问题速查与个人心得做这套 Nodejsvue 个人博客社交系统我踩过不少坑这里挑几个高频问题整理成表格直接照着排查就行。问题现象可能原因解决方案npm 命令报错无法加载脚本Windows PowerShell 禁止脚本运行管理员执行 Set-ExecutionPolicy RemoteSigned前端请求后端接口拿到 HTML没有走代理或接口缺少 /api 前缀检查 vite proxy 配置与接口路径图片上传成功但访问 404后端没配置静态资源目录app.use(/uploads, express.static(uploads))刷新页面白屏前端 history 模式路由未回退Nginx 配置 try_files $uri /index.html重复关注了同一个用户关注表缺少唯一索引加联合唯一索引并查询后插入用户信息刷新后丢失没有在路由守卫中重新拉取Pinia 存储加异步获取用户信息逻辑上传大图片后页面卡顿前端没有压缩且后端无大小限制限制文件大小并加入前端预览压缩接口返回 401 后一直在登录页打转响应拦截器没有清除过期 token401 时清除本地 token 并跳转登录页最后补充一点我自己反复踩坑才意识到的经验任何跟“登录后操作”相关的接口比如发表文章、创建相册、关注别人都要在写代码时就把 auth 中间件加好而不是等接口联调完再补。补的时候很容易漏掉某个接口导致测试时接口可以访问上线后被人直接调用没有任何权限控制到时候再排查就很被动。我在实际开发过程中还有个体会相册和关注这两个模块看着互相独立其实是系统里最能体现“社交感”的部分。一个用户被关注了他的更新能推送到关注者的动态流里一个用户发了一组新照片粉丝第一时间能在首页看到。这种连接的建立会激励用户持续创作也让整个系统从“存文章”进化成“互动社区”。初版哪怕功能做浅一点也要保证这条关系链路是完整打通的。如果你现在刚开始动手我强烈建议第一步先装好 Node.js 环境并跑通一个最简单的 Express 接口别一开始就沉浸到页面布局里。环境跑通后按用户、文章、关注、相册的顺序推进每个模块完成就立即联调。项目不怕功能少就怕每个模块都半生不熟最后整个系统跑起来全是边界问题。
