尧图网站设计 尧图网站设计YAOTU DESIGN
ARTICLE DETAIL

资讯详情

深耕网站设计与一线实操的经验洞察。

Node.js+Vue个人博客社交系统实战:相册、关注与部署全攻略

Node.js+Vue个人博客社交系统实战:相册、关注与部署全攻略 个人博客做了这么多年最头疼的事从来不是写不出内容而是写到一半发现读者是“路过”的——看完就走留不下关系。所以当我要发起这个“Nodejsvue个人博客社交系统”项目时刻意把“相册”和“关注”这两个功能放在和文章同等级的位置上不只是让访客看文章还要让大家有地方放图片、有办法持续追踪彼此。这套小系统实际开发了大改小改两个多月过程中踩过的坑不少但整体方向最后走通了。本文就把这套系统的技术选型、拆解下来的模块设计、前端实现和相册相关的几个易错点详细写一遍给想自己做社交化博客、或者正在为课程设计/毕业设计找参考的人提供一份可落地的实战记录。先给个结论性交代后端用 Node.jsExpress Sequelize前端用 Vue 3Vite Pinia Vue Router数据库用 MySQL文件存储用 MinIO。前后端完全分离部署时用 Nginx 把静态页面和 API 反代到一台服务器上。看完下文你会发现这套组合对个人项目和中等体量的小站来说性价比远高于重型的 Java 系框架。1. 为什么做成社交博客而不是传统博客选型的真实原因1.1 “关注”这个功能带来的连锁反应传统博客的形态一般是首页文章列表、文章详情、评论区。内容再好读者也只是“过客”。加了关注功能之后站点逻辑会彻底变化你关注的作者发布新文章你的“动态/时间线”里能直接刷到你的粉丝列表变成了一个可以展示的小社群作者能知道自己到底被谁关注个人主页从“我写过的文章”扩展成“我的文章、我的相册、我的粉丝、我关注的人”。“相册”看起来比博客好做但实际上它是极大拉升系统复杂度的一个功能。因为相册里不仅有图片还可能有视频。图片要压缩、要缩略图视频如果直接拿video标签播 mp4 倒还好但一旦涉及 m3u8 流媒体或者视频文件较大就必须引入对象存储和播放器适配层。所以我是在动手写代码之前先按“内容 相册 社交关系”三条线把功能拆干净后面开发效率才提得上来。1.2 为什么放弃 Spring Boot 转用 Node.js很多博客系统的项目参考会直接写成 “Spring Boot Vue”这类方案我试过不是说不好而是对这个场景而言偏重了。个人博客社交系统的用户量一般就是几百到几千人大量接口是普通的增删改查业务模型并不复杂真正吃资源的反而是图片视频这类静态文件。这时用 Node.js 的优势就很明显前后端都是 JavaScript心智负担小后端一个models文件对应数据库表前端组件一样用对象和数组处理数据不需要切换语言思维部署体积小服务器上一行命令装好依赖就能跑不像 JVM 那套需要关注堆内存和启动参数常见的 Express 中间件生态非常成熟JWT 鉴权、CORS、文件上传这些都能直接找到稳定库写起来很快。顺便回应一个老在网上被搜的问题Node.js 底层是不是用的 V8是。它靠 V8 引擎做即时编译所以虽然不允许你直接写 C 去死磕性能但对博客社交这种 IO 密集型服务来说完全够用。真要瓶颈了前面再加一层缓存和对象存储就够了不会一上来就被性能卡住。1.3 系统功能清单与整体架构我最终确认的范围大概是这样的模块功能点用户系统注册、登录、JWT 鉴权、头像、个人资料博客内容文章发布、编辑、列表、详情、软删除相册系统创建相册、上传图片/视频、懒加载列表关注体系关注、取消关注、粉丝列表、互关判断消息/动态作者发文后关注者可见对应动态简单时间线管理能力自己的内容管理、文件清理架构层面不复杂Vue 页面通过 REST API 请求 Node 服务Node 负责读写 MySQL文件统一走 MinIO 对象存储。前端和 API 之间用 JSON 通信Nginx 负责把/指向前端静态目录把/api和/files指向后端服务。这套架构决定了后面每一步怎么设计“社交系统”里的任何操作都和用户身份有关因此鉴权要放在所有写接口前面“相册”里的数据量大、类型杂因此文件上传绝不能放在 Node 进程里直接读成 buffer 再存本地磁盘必须做的事独立存储。搞清楚这些再开始搭环境。2. 从装环境开始就把我绊倒Node.js 配置 npm.ps1 被禁的完整排障2.1 Node.js 版本选择和环境变量配置先解决最基础的。我现在建议用 Node.js 的 LTS 版本而不是追求最新版。原因很简单最新版对一些老项目的原生依赖兼容性未必好而 LTS 版本在本地开发和生产环境里踩坑的人最多报错一搜基本都有答案。安装包在官网下载对应的 Windows 安装包就行。想省事的可以用“免安装版”也就是 GitHub 上发布的 zip 包解压后手动配环境变量适合经常给多台电脑折腾 Node 环境的人。注意一个细节如果你用的是免安装版务必把解压路径记好不用放进系统盘的 Program Files 也可以但不要带奇怪的空格和中文字符。环境变量配置分两步新建NODE_HOME指向 Node.js 实际解压目录在Path中加入%NODE_HOME%\。配置完之后重新开一个新的终端窗口执行node -v和npm -v能正常输出版本号就说明环境变量已经生效。如果输出“node 不是内部或外部命令”优先检查路径写没写进去以及终端是不是没重启。2.2 这条报错终于不吓人了npm.ps1 无法加载文件项目开始第一天我就卡住过这个报错相信很多人在 Windows 上也和我一样见过npm : 无法加载文件 D:\Program Files\nodejs\npm.ps1因为在此系统上禁止运行脚本。我先解释一下原因。Windows 默认的 PowerShell 执行策略是Restricted也就是说它不允许执行任何.ps1脚本文件。而npm在 PowerShell 里的入口恰恰是一个npm.ps1所以你想敲npm installPowerShell 直接给你拦住了。解决办法是修改当前用户的执行策略。不用管理员权限也可以用管理员或者普通用户身份打开 PowerShell执行Set-ExecutionPolicy -Scope CurrentUser RemoteSignedRemoteSigned的含义是本地创建的脚本可以运行从网上下载的脚本必须经过签名。它比放开全部限制要安全所以日常开发设置成这样最合适。如果你公司电脑的安全策略比较严格不允许改执行策略还有两个替代方案直接在 VS Code 里把默认终端改成 cmd 或 Git Bashcmd 调用的是npm.cmd不会触发这个检查每次运行. \node_modules\.bin\npm.ps1也可以但太麻烦。我建议你直接在终端配置里把 shell 改掉这才是立竿见影的。另外有一个很隐蔽的问题如果你的 Node.js 安装路径里带空格比如D:\Program Files (x86)\nodejs个别脚本在拼接路径时可能会出幺蛾子。排查时可以把NODE_HOME放进系统变量同时注意脚本里是否存在不规范的引号拼接。2.3 npm 镜像源、Vue 项目脚手架和 Vue DevTools环境能用之后先把 npm 源换成国内镜像不要等装到一半才后悔。npm config set registry https://registry.npmmirror.com npm config get registry装 Vue 生态我推荐直接用 Vite 模板比传统的 Webpack 方案启动速度快太多了npm create vitelatest blog-fe -- --template vue cd blog-fe npm install npm run dev这里提醒一句Vite 不同版本对 Node 版本有要求遇到创建项目后启动报错时先回过头看 Node 版本够不够新。LTS 版本一般都能满足。Vue 开发过程中vue-devtools是强烈建议装的。它能直接查看组件树、修改响应式数据、看路由跳转记录。如果应用商店装不了就去官网下载对应版本的离线 crx 文件再拖进浏览器扩展页面启用。装好之后基本能省掉一半瞎猜的时间。与此配套的 VS Code 插件也比较固定Vue Language FeaturesVolar负责语法高亮和类型提示ESLint 保持代码规范Prettier 统一格式。2.4 顺手回答“VS Code Vue 怎么做手机软件”搜这个问题的人非常多我一开始也是这么想的。实际做下来最稳妥的是先把网页做成响应式布局也就是通过断点让页面在手机宽度下自动改版。相册列表和关注按钮都可以靠 CSS 媒体查询适配好。之后如果想上手机应用可以再用 Electron 或者 Capacitor 套壳而不是一开始就进入原生开发流程不然项目会变得异常重。Electron 打包 Vue 的原理其实很简单Vue 构建出静态文件Electron 的主进程加载这些文件渲染进程负责展示。很多人关心的“主进程、渲染进程、IPC 通信”本质是让渲染进程通过 IPC 去请求主进程做文件读写等系统级操作。比如博客后台要导入导出压缩包就可以让渲染进程发 IPC 消息给主进程由主进程用 Node 的 fs 去解压或打包。我后来做桌面端的直接部署时确实是这么办的。3. 后端拆分设计三个核心模块的表结构、接口和防坑点3.1 从用户表到关注关系的数据建模无论后端用什么框架表结构都得先定清楚。我这边选用 MySQL 加 Sequelize ORM。最开始没用 ORM直接写 SQL 也跑得通但博客系统的字段多、关联复杂后期维护直接写成 ORM 模型会更省心。用户表、文章表、相册表、图片/视频表是最基础的。社交关注关系尤其值得单独设计我用一张follows表记录“谁关注了谁”CREATE TABLE IF NOT EXISTS follows ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL COMMENT 关注者, follow_user_id INT UNSIGNED NOT NULL COMMENT 被关注者, status TINYINT NOT NULL DEFAULT 1 COMMENT 1正常 0取消, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, UNIQUE KEY uk_user_follow (user_id,follow_user_id), KEY idx_follow_user (status, created_at) ) ENGINEInnoDB DEFAULT CHARSETutf8mb4 COLLATEutf8mb4_unicode_ci;这张表看起来简单但有两个关键点加UNIQUE KEY防止用户疯狂点击按钮导致重复关注user_id是“关注者”follow_user_id是“被关注者”这个方向千万别搞反。很多人写粉丝列表时就是把user_id当被关注者去查结果关注变成了粉丝我第一版也踩过。判断“互相关注”也很简单查当前用户是否关注了对方之后再反向查一下对方是否关注了当前用户即可不需要设计复杂的关联表。3.2 文章与相册的存储设计文章表我建议保留一个status字段发表和草稿分开删除时用is_deleted软删除。因为博客系统里如果真删了文章评论和粉丝动态的关系会变得不好处理软删除是最低成本方案。CREATE TABLE articles ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, title VARCHAR(255) NOT NULL, content MEDIUMTEXT NOT NULL, status TINYINT NOT NULL DEFAULT 1 COMMENT 1发布 0草稿, is_deleted TINYINT NOT NULL DEFAULT 0, created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP, updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP );相册结构稍微复杂一点因为“相册”和“媒体文件”是一对多关系CREATE TABLE albums ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, user_id INT UNSIGNED NOT NULL, name VARCHAR(100) NOT NULL, description VARCHAR(255) DEFAULT , created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ); CREATE TABLE album_medias ( id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY, album_id INT UNSIGNED NOT NULL, media_url VARCHAR(500) NOT NULL, media_type TINYINT NOT NULL DEFAULT 0 COMMENT 0图片 1视频, cover_url VARCHAR(500) DEFAULT , created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP );如果你决定支持视频media_type和缩略图字段就是必须的因为视频列表里需要展示封面而不是把所有视频都加载出来。3.3 后端接口设计与 JWT 鉴权中间件接口按模块划分我整理一份对照表模块接口说明用户POST /api/auth/register注册密码 bcrypt 加密用户POST /api/auth/login登录返回 JWT文章GET /api/articles分页列表文章POST /api/articles创建文章需要登录相册GET /api/albums相册列表相册POST /api/albums/:id/medias上传媒体需要登录关注POST /api/follow/:userId关注某人关注DELETE /api/follow/:userId取消关注关注GET /api/users/:id/followers粉丝列表关注GET /api/users/:id/following关注列表写关注接口时最需要防两个问题自关注和重复关注。自关注很简单前端可以禁掉但真正安全的地方还是在后端app.post(/api/follow/:userId, auth, async (req, res, next) { try { const targetId Number(req.params.userId); if (targetId req.userId) { return res.status(400).json({ code: 400, message: 不能关注自己 }); } const [follow, created] await Follow.findOrCreate({ where: { userId: req.userId, followUserId: targetId }, defaults: { status: 1 } }); res.json({ code: 0, data: { created } }); } catch (err) { // 如果唯一索引兜底挡掉数据冲突这里直接读异常返回即可 next(err); } });JWT 中间件是所有受保护接口统一定鉴权的地方const jwt require(jsonwebtoken); function auth(req, res, next) { const header req.headers.authorization || ; const token header.replace(/^Bearer\s/, ); if (!token) return res.status(401).json({ code: 401, message: 未登录 }); try { const payload jwt.verify(token, process.env.JWT_SECRET); req.userId payload.uid; req.username payload.name; next(); } catch (err) { return res.status(401).json({ code: 401, message: 登录过期 }); } }3.4 参数校验、响应规范和数据一致性后端最影响开发效率的不是选择了什么框架而是一开始有没有定好全局规则。我定了三条所有响应统一格式{ code, message, data }前端不需要每个接口单独处理字段结构所有写接口都做参数校验不能用“前端已经校验过了”来省事上传的媒体文件名统一用 UUID 重新生成避免用户上传同名文件互相覆盖。序号和分页字段也必须在接口层做默认值处理。比如列表接口的page和size前端可能传负数、传字符串甚至传空后端要统一clamp。这些小原则能让你后续写前端时顺畅一大半。4. 前端 Vue 3 落地路由、组件和关注交互的踩坑记录4.1 Vite 项目的目录规划和依赖安装前端我用的目录大概长这样src/ api/ # axios 请求封装 stores/ # Pinia 状态库 router/ # 路由配置 views/ # 页面 components/ # 复用组件 assets/依赖方面在创建好的项目里额外安装npm install axios vue-router4 piniaaxios负责发请求vue-router管路由pinia管全局状态。这三个几乎是 Vue 项目标配没有太多挑选空间。4.2 动态路由和路由参数的正确打开方式博客和社交系统离不开个人主页和相册详情页这两类页面都需要靠动态路由承载。配置如下{ path: /user/:id, name: user-profile, component: () import(/views/UserProfile.vue), props: true, meta: { keepAlive: false } }, { path: /album/:id, name: album-detail, component: () import(/views/AlbumDetail.vue), props: true }我强烈建议在这里把props: true打开这样组件内就可以直接用 defineProps 接收id而不是到处写route.params.id。好处是组件更纯净测试的时候直接传 props 就能渲染。接收参数之后还要监听参数变化。比如你在/user/1页面里点了一个用户卡片要跳到/user/2同一个组件实例会被复用onMounted不会再次触发。这种情况下最好用 watch 监听const props defineProps({ id: { type: String, required: true } }); watch( () props.id, () { loadUserDetail(props.id); }, { immediate: true } );动态路由的另一个典型场景是权限控制。后台管理、写文章页面这类功能不能直接放给所有人。用路由守卫集中拦截router.beforeEach((to) { const userStore useUserStore(); if (to.meta.requiresAuth !userStore.token) { return { name: login, query: { redirect: to.fullPath } }; } return true; });因为个人站的前端路由数量不多动态路由通常只是“登录前/登录后可见不同菜单”的程度使用meta.requiresAuth就足够了不需要做很重的菜单动态生成。4.3 用 Pinia 管理登录状态和用户资料登录状态是最核心的全局数据。我在stores/user.js里这样写import { defineStore } from pinia; export const useUserStore defineStore(user, { state: () ({ token: localStorage.getItem(blog_token) || , userInfo: JSON.parse(localStorage.getItem(blog_user) || {}) }), actions: { setLogin(token, userInfo) { this.token token; this.userInfo userInfo; localStorage.setItem(blog_token, token); localStorage.setItem(blog_user, JSON.stringify(userInfo)); }, logout() { this.token ; this.userInfo {}; localStorage.removeItem(blog_token); localStorage.removeItem(blog_user); } } });这里有一个细节token 存 localStorage 会面临 XSS 风险但个人博客系统的权限边界相对简单大家基本都这么干。真正要注意的是每次请求都要把 token 塞进请求头。我在api/index.js里做了 axios 统一拦截api.interceptors.request.use((config) { const token localStorage.getItem(blog_token); if (token) { config.headers.Authorization Bearer ${token}; } return config; });4.4 关注按钮组件与自定义 v-model 的用法关注按钮是社交系统的脸面前端体验要做顺。我把它写成一个独立组件支持v-model的followed状态。先说自定义 v-model 本身它其实就是组件内部触发update:followed事件script setup const props defineProps({ followed: { type: Boolean, default: false }, mutual: { type: Boolean, default: false } }); const emit defineEmits([update:followed, change]); /script template button classfollow-btn :class{ followed, mutual } clicktoggleFollow {{ followed ? (mutual ? 互相关注 : 已关注) : 关注 }} /button /template点击事件里我用“乐观更新”策略也就是先改界面再发请求失败再回滚async function toggleFollow() { const current props.followed; emit(update:followed, !current); try { if (!current) { await api.post(/api/follow/${targetUserId}); } else { await api.delete(/api/follow/${targetUserId}); } } catch (err) { emit(update:followed, current); console.error(err); } }这样体验很好但因为界面比接口先变用户可能快速连续点两下。防抖动要加在组件外层或者接口层做一个简单的相互排斥。我当时是在父组件里做了个loading状态点击后按钮禁用一秒稳妥多了。4.5 相册页面的懒加载和无限滚动相册列表最容易出现的问题是图片多、视频多一次性全渲染出来页面上会有几百个请求同时发出去浏览器直接卡死。我采用的方案是“分页 触底加载 图片懒加载”。触底加载可以用IntersectionObserver在列表末尾放一个占位元素被观察到时加载下一页const sentinelRef ref(null); const observer new IntersectionObserver((entries) { if (entries[0].isIntersecting !loading.value page.value totalPages.value) { page.value 1; loadMedias(); } });图片懒加载Vue 3 可以直接用浏览器原生loadinglazy配合v-lazy插件也行。原生方案胜在零依赖但列表项如果频繁变化最好还是用专门的懒加载指令避免那些已经滑过的图片又被加载一遍。后来项目稳定在一个方案对象存储返回的缩略图地址配合原生 lazy性能已经够用了。5. 相册才是这套系统的重头戏上传、MinIO 与 m3u8 播放5.1 前端压缩图片不能啥都往服务器丢我第一版想得很天真用户传原图后端直接存。结果服务器磁盘几天就满了页面加载还慢得要命。后来老老实实在前端做压缩读取图片到一个canvas上按最长边 1920px 缩放再用canvas.toBlob()输出 JPEG质量参数设 0.85。function compressImage(file) { return new Promise((resolve, reject) { const img new Image(); const url URL.createObjectURL(file); img.onload () { const scale Math.min(1920 / img.width, 1); const canvas document.createElement(canvas); canvas.width Math.round(img.width * scale); canvas.height Math.round(img.height * scale); canvas.getContext(2d).drawImage(img, 0, 0, canvas.width, canvas.height); canvas.toBlob((blob) { URL.revokeObjectURL(url); resolve(blob); }, image/jpeg, 0.85); }; img.onerror reject; img.src url; }); }压缩后宽超过 1920 的图片不会超过 300KB 左右加载速度会有肉眼可见的提升。当然如果你做的是摄影类博客用户追求原图质量那就不压缩改成分辨率转换和 WebP 格式转换。5.2 上传直传 MinIO 的流程对象存储我选 MinIO原因是它兼容 S3 协议可以本地私有化部署也可以几个命令迁移到云厂商的服务。尤其很多人搜过“Vue Java MinIO”但在 Node 里接入逻辑和 Java 是一样的都是通过预签名 URL 让前端直传。流程是这样的前端请求上传接口带上文件名和类型Node 后端生成一个唯一对象名比如albums/20240612/uuid.jpg后端用 MinIO 客户端生成一个 PUT 预签名 URL有效期设为 60 分钟前端直接把压缩后的文件 PUT 到这个 URL 上上传成功后后端再把对象 URL 写进数据库。Node 端代码关键部分const Minio require(minio); const client new Minio.Client({ endPoint: process.env.MINIO_ENDPOINT, port: Number(process.env.MINIO_PORT), useSSL: false, accessKey: process.env.MINIO_ACCESS_KEY, secretKey: process.env.MINIO_SECRET_KEY }); async function generateUploadUrl(objectName) { return client.presignedPutObject(process.env.MINIO_BUCKET, objectName, 60 * 60); }这是我认为目前最优雅的上传方案。Node 进程不直接接触大文件不会因为上传大视频导致事件循环卡顿前端直传的速度也更快因为文件流不过 Node 这一层。5.3 视频播放与 m3u8 这件事相册里如果放视频直接存 mp4 并用video播技术上没问题但文件大、拖动不快。为了兼容性更好很多站点会把视频转成 HLS 的分片格式也就是生成一个.m3u8索引文件和一堆.ts分片。前端播放时浏览器并不会原生支持这个协议特别是 Chrome必须用hls.js这个库import Hls from hls.js; function playVideo(videoElement, src) { if (videoElement.canPlayType(application/vnd.apple.mpegurl)) { // Safari 原生支持 HLS videoElement.src src; } else if (Hls.isSupported()) { const hls new Hls(); hls.loadSource(src); hls.attachMedia(videoElement); } }这里有三个坑要挨个说第一m3u8 内部的 ts 分片地址需要和 m3u8 文件能组成相对路径。如果分片是绝对地址就要求重新生成时写对域名。第二跨域。MinIO 桶如果设置成私有预签名 URL 对每个分片要单独签名太麻烦。我的做法是把相册视频设置成公开只读桶然后在 Nginx 层面向 MinIO 转发时统一加 CORS 响应头。第三m3u8 播放器本身不负责转码。如果你手里只有 mp4先用 ffmpeg 转成 HLSffmpeg -i input.mp4 -c:v libx264 -c:a aac -hls_time 10 -hls_list_size 0 output.m3u8转换后记得把输出目录里的.m3u8和.ts一起上传到存储桶只传 m3u8 文件是播不了的。5.4 相册的“位置”这个加分项如果能记录照片或视频的拍摄地点整个相册的质感会提升一大截。我给相册详情页加了一个位置模块前端接腾讯地图 JavaScript SDK在页面里初始化地图实例逆地址解析出城市和街道。这里务必注意组件销毁时一定要销毁地图实例否则反复进入离开页面内存会持续增长。我一开始没处理某次开会开脑洞测试了很久标签页卡到爆。在 Vue 3 里可以这样onBeforeUnmount(() { if (map) { map.destroy(); } });这是那种“不做没感觉做了才知道少了一块”的细节功能建议时间充裕的话一定要保留。6. 上线前和上线后的复盘几个藏在细节里的硬坑6.1 关注计数和互关状态的二次修正第一版上线后我发现两个数字总对不上粉丝数有时候自增有时候负数。查了一圈问题出在我没把“自关注”完全挡死。虽然后端校验了targetId req.userId但前端的个人主页也可以通过修改请求参数来调接口。后来我把校验挪到了关注接口入口并且给头像卡片上的关注按钮加了一层判断当前用户 id 等于目标用户 id 时直接不显示按钮。之后再遇到计数不准基本都是并发重复请求。用户在高延迟环境里连续点两下两条请求同时进数据库一个改了前端状态另一个又被唯一索引挡了。前端锁按钮只是降低概率真正压住的是数据库的UNIQUE KEY所以别把数据库约束省掉。6.2 软删除带来的级联问题我一开始给文章和相册都做了软删除当时觉得挺好后来发现相册里的文件很容易变成“孤儿文件”用户删了相册但 MinIO 里的图片和视频没人管。我专门写了一个每日清理脚本扫描数据库里所有is_deleted1的记录拿到对象名去 MinIO 做删除。别觉得这是小事我项目上线第四天就发现存储桶里多了几个 G 的无效视频。清理脚本不多但一定得有。6.3 部署与运维PM2 不是说说的部署方面我的选择很传统Nginx 托管前端静态文件Node 服务由 PM2 守护。PM2 这个工具也是很多人搜索“nodejs 做运维工具”时最终会撞见的答案它其实就是一个进程守护和管理器常用命令就这么几个pm2 start ecosystem.config.js pm2 logs pm2 restart blog-api pm2 saveecosystem.config.js里我放了环境变量和 max_memory_restart 配置。这里有一个容易被忽略的问题如果 Node 服务跑在 Nginx 后面而你在代码里用req.ip做限流或日志分析需要让 Nginx 把X-Forwarded-For头传过去同时在应用里设置app.set(trust proxy, true)否则拿到的 IP 全是内网地址。我最后一次复盘觉得最容易影响这套系统体验的三个点是数据库唯一约束必须设、关注按钮必须做防抖、相册里的文件必须和 MinIO 保持同步删除。只要这三个点稳住整个项目就算完成八成了。如果现在让我从头再做一遍我会先把follows表里的方向字段画清楚并且老老实实从第一天就给所有列表接口加上分页和缓存预留而不是等页面卡到不行了才返工。这套 Node.js Vue 的组合说到底还是让一个人能快速完成从“想法”到“可用产品”的最好路径之一踩过这些坑之后做同类系统你会比我快很多。
返回列表