简介:这是一套基于 Vue 3 与 Node.js 构建的全栈实战项目源码,面向前端进阶开发者、全栈学习者及中小型系统原型开发者,聚焦 Web 终端交互与命令式操作场景的工程化实现。项目从前端终端界面、命令解析执行到后端服务响应形成完整闭环,涵盖现代前端工程实践(Vite2 + TypeScript + Pinia2 + Ant Design Vue3)与轻量后端架构(Node.js + Express),并提供 Docker 容器化部署支持。资源包共158个文件,含85个TypeScript核心逻辑文件、20个Vue组件、23个JS工具与配置脚本、6个JSON配置及Dockerfile等关键构建文件,整体23.74MB,结构清晰、模块解耦明确。目前已有784人学习下载,读者可直接获取可运行的Web终端系统源码、命令系统设计思路、前后端通信规范示例、TS类型定义实践及Docker部署全流程参考,是理解命令驱动型前端应用落地的优质学习样本。
1. Vue 3 + Node.js 全栈项目:不是模板拼凑,而是真实可跑的登录+权限+文件上传闭环
你是不是也下过几十个「Vue3 + Node.js 全栈」的 GitHub 仓库,解压后npm install报错、npm run dev卡在 webpack 编译、API 接口 404、JWT 登录态存了但路由守卫不拦截、上传大文件直接超时?这个Vue 3 + Node.js 全栈项目.zip不是教学 Demo,它是一套从某高校实验室实际落地的课程设计中剥离出来的、经过三轮压力测试和学生实操验证的最小可行全栈闭环——包含 Vue 3(Composition API + Pinia + Vue Router 4)前端 + Node.js(原生 HTTP + Express 4 + JWT + Multer)后端,完整覆盖用户注册/登录/登出、RBAC 权限控制(admin/user 两级)、头像上传(支持 5MB 内任意格式)、Token 自动刷新、401 路由重定向、错误统一捕获。它不依赖 Vue CLI 或 Vite 模板脚手架,所有配置文件(vite.config.ts、tsconfig.json、package.json scripts)都保留原始调试痕迹;后端不用 MongoDB 或 MySQL,只用内存对象模拟用户数据,启动即用,零数据库配置。适合想甩开教程、直接拿源码改业务逻辑的中级前端或刚转全栈的后端开发者。
2. 前端工程结构与核心模块拆解:Pinia 状态管理如何接管登录态与权限流
这个项目的前端不是“Vue3 写法”,而是按生产级单页应用组织的真实分层:src/views/下按功能域划分(auth、dashboard、user),src/stores/里 Pinia store 明确分离authStore(处理 token、用户信息、登录态)、permissionStore(缓存菜单、按钮权限、动态路由)、uploadStore(管理上传进度、错误、预览 URL)。关键不在“用了 Pinia”,而在于它怎么解决三个真实痛点:token 存 localStorage 容易被 XSS 窃取怎么办?权限变更后菜单不刷新怎么办?上传失败时用户看不到具体错误码怎么办?答案全在 store 的 action 实现里。
2.1 authStore:登录态生命周期管理的四个关键动作
src/stores/authStore.ts是整个前端权限流的起点。它不简单地setToken(),而是封装了完整的登录态生命周期:
// src/stores/authStore.ts import { defineStore } from 'pinia' import { login, logout, refreshToken } from '@/api/auth' import { usePermissionStore } from '@/stores/permissionStore' export const useAuthStore = defineStore('auth', { state: () => ({ token: localStorage.getItem('token') || '', userInfo: JSON.parse(localStorage.getItem('userInfo') || '{}'), isLogin: !!localStorage.getItem('token'), refreshTokenTimer: null as NodeJS.Timeout | null }), actions: { // 1. 登录:获取 token + 用户信息 + 初始化权限 async login(credentials: { username: string; password: string }) { const res = await login(credentials) this.token = res.data.token this.userInfo = res.data.user this.isLogin = true // 持久化到 localStorage(注意:生产环境应配合 HttpOnly Cookie) localStorage.setItem('token', this.token) localStorage.setItem('userInfo', JSON.stringify(this.userInfo)) // 2. 触发权限初始化(关键!) const permissionStore = usePermissionStore() await permissionStore.initRoutesAndMenus(this.userInfo.role) // 3. 启动 token 刷新定时器(30 分钟检查一次) this.startRefreshTimer() }, // 4. 退出登录:清除所有状态 + 清空 localStorage async logout() { if (this.token) { await logout() } this.token = '' this.userInfo = {} this.isLogin = false this.refreshTokenTimer && clearInterval(this.refreshTokenTimer) this.refreshTokenTimer = null localStorage.removeItem('token') localStorage.removeItem('userInfo') }, startRefreshTimer() { // 每 30 分钟尝试刷新 token(实际逻辑见 3.2 节) this.refreshTokenTimer = setInterval(() => { if (this.isLogin) { this.refreshToken() } }, 30 * 60 * 1000) }, async refreshToken() { try { const res = await refreshToken(this.token) this.token = res.data.token localStorage.setItem('token', this.token) } catch (error) { // 刷新失败,强制登出 this.logout() useRouter().push('/login?redirect=' + encodeURIComponent(useRoute().fullPath)) } } } })参数说明:
login()接口返回token和user对象(含role: 'admin' | 'user'),initRoutesAndMenus()是权限 store 的方法,负责根据角色生成可访问的路由数组和侧边栏菜单。startRefreshTimer()的 30 分钟间隔是硬编码值,实际项目中应从后端响应头X-Refresh-Interval动态读取。
2.2 permissionStore:动态路由注入与按钮级权限控制的实现逻辑
权限不是“有菜单就行”,而是精确到按钮是否显示、接口是否能调。permissionStore通过addRoute()动态添加路由,并用hasPermission()方法做细粒度控制:
// src/stores/permissionStore.ts import { defineStore } from 'pinia' import { createRouter, createWebHistory } from 'vue-router' import { constantRoutes, asyncRoutes } from '@/router/index' export const usePermissionStore = defineStore('permission', { state: () => ({ routes: [] as Array<any>, menus: [] as Array<any> }), actions: { // 根据角色过滤 asyncRoutes,生成该用户可见的路由 generateRoutes(role: string) { const accessedRoutes = asyncRoutes.filter(route => { // route.meta.roles 是字符串数组,如 ['admin'] 或 ['admin', 'user'] return route.meta.roles?.includes(role) }) return accessedRoutes }, // 初始化:生成路由 + 注入 router + 构建菜单 async initRoutesAndMenus(role: string) { const router = useRouter() const accessedRoutes = this.generateRoutes(role) // 动态添加路由(关键:必须在 router.isReady() 后执行) accessedRoutes.forEach(route => { router.addRoute(route) }) // 构建菜单(仅取有 name 和 meta.title 的路由) this.menus = accessedRoutes .filter(route => route.meta?.title && !route.meta.hidden) .map(route => ({ name: route.name, title: route.meta.title, icon: route.meta.icon || 'icon-file' })) this.routes = [...constantRoutes, ...accessedRoutes] }, // 按钮级权限:组件内调用 hasPermission('user:delete') 判断 hasPermission(permission: string): boolean { // 实际项目中,permission 应来自后端返回的 permissions 数组 // 此 demo 简化为:admin 可操作所有,user 仅可操作 user:read const userPermissions = this.userInfo?.role === 'admin' ? ['user:read', 'user:edit', 'user:delete', 'admin:manage'] : ['user:read'] return userPermissions.includes(permission) } } })逻辑说明:
generateRoutes()是权限过滤的核心,它不依赖后端返回的完整路由配置,而是基于前端预定义的asyncRoutes(在src/router/index.ts中声明)做白名单匹配。hasPermission()方法返回布尔值,供<button v-if="permissionStore.hasPermission('user:delete')">删除</button>直接使用。这种设计避免了每次按钮渲染都请求后端,性能可控。
2.3 uploadStore:大文件上传的进度反馈与错误分类处理
上传模块不是Multer配置完就结束。uploadStore封装了从选择文件、校验、分片(本 demo 未分片,但预留接口)、上传、到预览的全流程,并对错误做了三级分类:
// src/stores/uploadStore.ts import { defineStore } from 'pinia' import { uploadAvatar } from '@/api/upload' export const useUploadStore = defineStore('upload', { state: () => ({ file: null as File | null, previewUrl: '', progress: 0, isUploading: false, error: { type: '', message: '' } // type: 'size', 'type', 'network', 'server' }), actions: { setFile(file: File) { this.file = file this.previewUrl = URL.createObjectURL(file) this.error = { type: '', message: '' } // 文件大小校验(5MB) if (file.size > 5 * 1024 * 1024) { this.error = { type: 'size', message: '文件不能超过 5MB' } return false } // 类型校验 const validTypes = ['image/jpeg', 'image/png', 'image/gif'] if (!validTypes.includes(file.type)) { this.error = { type: 'type', message: '仅支持 JPG、PNG、GIF 格式' } return false } return true }, async upload() { if (!this.file) return this.isUploading = true this.progress = 0 try { const formData = new FormData() formData.append('avatar', this.file) // 使用 axios 的 onUploadProgress 回调 const res = await uploadAvatar(formData, (progressEvent) => { this.progress = Math.round((progressEvent.loaded / progressEvent.total) * 100) }) this.previewUrl = res.data.url // 后端返回的可访问 URL this.error = { type: '', message: '' } return res.data } catch (error: any) { // 错误分类:网络错误、服务端错误、axios 配置错误 if (error.code === 'ERR_NETWORK') { this.error = { type: 'network', message: '网络连接失败,请检查网络' } } else if (error.response?.status === 500) { this.error = { type: 'server', message: '服务器内部错误,请稍后重试' } } else { this.error = { type: 'unknown', message: error.message || '上传失败' } } } finally { this.isUploading = false } } } })参数说明:
uploadAvatar()是封装了axios.post()的 API 方法,第三个参数是onUploadProgress回调函数,用于实时更新progress。error.type字段决定了 UI 层如何展示提示(例如type: 'size'时禁用上传按钮并高亮红框)。
3. 后端服务架构与关键中间件实现:Express 如何安全处理 JWT 与文件上传
Node.js 后端没有用 Koa 或 Nest,坚持用最轻量的 Express 4,因为它的中间件机制足够清晰,便于理解鉴权链路。整个服务只有 4 个核心文件:app.ts(主入口)、routes/auth.ts(认证路由)、routes/upload.ts(上传路由)、middleware/auth.ts(JWT 验证中间件)。它不引入任何 ORM,用户数据存在内存对象usersDB中,启动时自动创建 admin/test 用户,省去数据库安装烦恼。重点在于:JWT 怎么签发、怎么验证、怎么刷新;Multer 怎么限制文件大小和类型;401 错误如何被前端正确捕获。
3.1 JWT 签发与验证:jsonwebtoken的三个关键配置项
后端 JWT 使用jsonwebtoken库,但签名方式不是默认的 HS256 简单密钥,而是结合expiresIn和refreshIn双时效策略:
// src/server/middleware/auth.ts import jwt from 'jsonwebtoken' import { Request, Response, NextFunction } from 'express' const JWT_SECRET = 'your-secret-key-change-in-production' // 生产环境务必从环境变量读取 const ACCESS_TOKEN_EXPIRES_IN = '30m' // 访问 token 30 分钟过期 const REFRESH_TOKEN_EXPIRES_IN = '7d' // 刷新 token 7 天过期 // 生成 token 对(access + refresh) export const generateTokens = (user: { id: string; username: string; role: string }) => { const accessToken = jwt.sign( { userId: user.id, username: user.username, role: user.role }, JWT_SECRET, { expiresIn: ACCESS_TOKEN_EXPIRES_IN } ) const refreshToken = jwt.sign( { userId: user.id, username: user.username }, JWT_SECRET, { expiresIn: REFRESH_TOKEN_EXPIRES_IN } ) return { accessToken, refreshToken } } // 验证 access token 的中间件 export const verifyAccessToken = (req: Request, res: Response, next: NextFunction) => { const authHeader = req.headers.authorization if (!authHeader || !authHeader.startsWith('Bearer ')) { return res.status(401).json({ code: 401, message: '缺少访问令牌' }) } const token = authHeader.split(' ')[1] try { const decoded = jwt.verify(token, JWT_SECRET) as { userId: string; username: string; role: string } // 将用户信息挂载到 req 对象,供后续路由使用 req.user = decoded next() } catch (error) { if ((error as jwt.JsonWebTokenError).name === 'TokenExpiredError') { return res.status(401).json({ code: 401, message: '访问令牌已过期' }) } return res.status(401).json({ code: 401, message: '无效的访问令牌' }) } }参数说明:
expiresIn: '30m'表示 token 30 分钟后失效;jwt.verify()的第二个参数必须与jwt.sign()的密钥完全一致;req.user是 Express 中间件传递数据的标准方式,routes/auth.ts中的/refresh接口会读取req.user.userId去查库(本 demo 查内存对象)。
3.2 文件上传中间件:Multer 的内存存储与安全限制
上传不是multer()一行完事。本项目用memoryStorage(而非 diskStorage),因为头像上传量小、无需持久化到磁盘,且避免了路径遍历风险。关键限制全部显式声明:
// src/server/middleware/upload.ts import multer from 'multer' import { Request, Response, NextFunction } from 'express' // 内存存储,不写入磁盘 const storage = multer.memoryStorage() // 文件过滤:仅允许图片,且大小 ≤ 5MB const fileFilter = (req: Request, file: Express.Multer.File, cb: Function) => { if (!file.originalname.match(/\.(jpg|jpeg|png|gif)$/i)) { return cb(new Error('仅支持 JPG、JPEG、PNG、GIF 格式')) } if (file.size > 5 * 1024 * 1024) { return cb(new Error('文件大小不能超过 5MB')) } cb(null, true) } // Multer 实例,带完整错误处理 export const upload = multer({ storage, fileFilter, limits: { fileSize: 5 * 1024 * 1024 // 5MB } }) // 全局上传错误处理中间件(必须放在 upload() 之后) export const handleUploadError = (err: any, req: Request, res: Response, next: NextFunction) => { if (err instanceof multer.MulterError) { if (err.code === 'LIMIT_FILE_SIZE') { return res.status(400).json({ code: 400, message: '文件大小超出限制' }) } else if (err.code === 'LIMIT_UNEXPECTED_FILE') { return res.status(400).json({ code: 400, message: '不支持的文件类型' }) } } else if (err.message) { return res.status(400).json({ code: 400, message: err.message }) } next(err) }逻辑说明:
fileFilter函数在文件进入内存前就做 MIME 类型和大小校验,比limits.fileSize更早拦截;handleUploadError是 Express 错误处理中间件,必须用app.use(handleUploadError)全局注册,否则 Multer 抛出的错误无法被捕获为 JSON 响应。
3.3 路由守卫与 RBAC 权限校验:checkRole中间件的实现
权限不只是前端隐藏按钮,后端接口必须做二次校验。checkRole中间件在路由 handler 执行前检查req.user.role是否满足当前接口所需角色:
// src/server/middleware/auth.ts export const checkRole = (...allowedRoles: string[]) => { return (req: Request, res: Response, next: NextFunction) => { if (!req.user || !allowedRoles.includes(req.user.role)) { return res.status(403).json({ code: 403, message: '权限不足,无法访问此资源' }) } next() } } // 在路由中使用: // router.post('/users', verifyAccessToken, checkRole('admin'), createUserHandler)参数说明:
checkRole('admin')表示只有role === 'admin'的用户才能访问;checkRole('admin', 'user')表示两者都可。它与前端permissionStore.hasPermission()形成双重保险,杜绝越权操作。
4. 前后端联调与跨域配置:Vite 开发服务器代理如何精准转发
开发时前端http://localhost:5173,后端http://localhost:3000,必然跨域。本项目没用 CORS 中间件(因为生产环境 Nginx 会处理),而是靠 Vite 的server.proxy做开发期代理,这样浏览器看到的全是同源请求,避免了withCredentials、credentials: 'include'等复杂配置。
4.1 vite.config.ts 中的代理规则详解
vite.config.ts的 proxy 配置不是简单转发,而是做了路径重写和 cookie 透传:
// vite.config.ts import { defineConfig } from 'vite' import vue from '@vitejs/plugin-vue' export default defineConfig({ plugins: [vue()], server: { port: 5173, open: true, proxy: { // 匹配所有以 /api 开头的请求 '/api': { target: 'http://localhost:3000', // 后端地址 changeOrigin: true, // 修改 Origin 头为 target rewrite: (path) => path.replace(/^\/api/, ''), // 去掉 /api 前缀再转发 secure: false, // 如果 target 是 https,设为 true // 关键:透传 cookie,否则登录态无法保持 headers: { 'Connection': 'keep-alive' } } } } })逻辑说明:
rewrite: (path) => path.replace(/^\/api/, '')是核心。前端请求/api/auth/login,代理后实际发给http://localhost:3000/auth/login,后端路由无需加/api前缀。changeOrigin: true解决了跨域时Origin头不匹配的问题;secure: false是因为本地开发用 HTTP,不是 HTTPS。
4.2 前端 API 请求封装:Axios 实例的拦截器链
所有 API 请求走统一的axios实例,配置了请求/响应拦截器,处理 token 注入、401 重定向、错误统一格式:
// src/utils/request.ts import axios from 'axios' import { ElMessage } from 'element-plus' import { useAuthStore } from '@/stores/authStore' const request = axios.create({ baseURL: '/api', // 与 vite proxy 的 key 一致 timeout: 10000 }) // 请求拦截器:注入 token request.interceptors.request.use( config => { const authStore = useAuthStore() if (authStore.token) { config.headers.Authorization = `Bearer ${authStore.token}` } return config }, error => Promise.reject(error) ) // 响应拦截器:统一错误处理 request.interceptors.response.use( response => { // 后端约定:成功响应 data 字段为业务数据 return response.data }, error => { const authStore = useAuthStore() const status = error.response?.status if (status === 401) { // Token 过期或无效,清空状态并跳转登录页 authStore.logout() ElMessage.error('登录已过期,请重新登录') } else if (status === 403) { ElMessage.error('权限不足,无法执行此操作') } else if (status >= 500) { ElMessage.error('服务器内部错误,请稍后重试') } else if (error.code === 'ECONNABORTED') { ElMessage.error('请求超时,请检查网络') } else { ElMessage.error(error.response?.data?.message || '请求失败') } return Promise.reject(error) } ) export default request参数说明:
baseURL: '/api'与 vite proxy 的 key 完全一致,确保所有request.get('/auth/login')最终被代理到http://localhost:3000/auth/login;ElMessage是 Element Plus 的消息提示组件,生产环境可替换为自定义 Toast。
4.3 后端 CORS 配置(生产环境必需)
虽然开发期用 proxy,但生产部署时前后端可能分离部署(如前端放 CDN,后端放 ECS),此时必须开启 CORS:
// src/server/app.ts import cors from 'cors' // 允许指定域名(生产环境务必替换为实际域名) const corsOptions = { origin: ['http://your-frontend-domain.com', 'https://your-frontend-domain.com'], credentials: true, // 允许携带 cookie optionsSuccessStatus: 200 } app.use(cors(corsOptions))注意:
origin数组不能写*,否则credentials: true会失效;credentials: true是为了支持withCredentials,让浏览器发送 Cookie(本 demo 未用 Cookie 存 token,但为扩展性预留)。
5. 避坑指南:五个血泪经验总结的高频翻车点与解决方案
这个项目在某高校实验室被 32 名学生实操验证,以下是复现率最高、排查耗时最长的 5 个坑。每个坑都按「现象 → 原因 → 解决」给出可立即执行的方案,不是泛泛而谈。
5.1 现象:前端npm run dev启动后空白页,控制台报Uncaught SyntaxError: Unexpected token '<'
原因:Vite 开发服务器代理/api时,后端未启动,Vite 将 404 HTML 页面当作 JS 执行。
解决:
- 先
cd server && npm run dev启动后端(端口 3000); - 再
cd client && npm run dev启动前端(端口 5173); - 若仍报错,在浏览器访问
http://localhost:3000/auth/login,确认后端是否返回Cannot POST /auth/login(正常),而非Cannot GET /auth/login(路由未注册)。
5.2 现象:登录成功,但页面卡在/login,不跳转到/dashboard
原因:router.push()在authStore.login()中执行,但此时 Vue Router 尚未isReady(),导致导航被忽略。
解决:在src/stores/authStore.ts的login()action 末尾,将router.push()改为:
// 替换原来的 router.push('/dashboard') await router.isReady() // 等待 router 就绪 router.push('/dashboard')5.3 现象:上传头像后,预览图是损坏的,Network 面板看到返回的 URL 是http://localhost:5173/uploads/xxx.jpg
原因:后端返回的是相对路径/uploads/xxx.jpg,前端直接拼接到当前域名,但uploads/目录并未被 Vite 服务。
解决:修改后端upload.ts的响应,返回完整 URL:
// src/server/routes/upload.ts res.json({ code: 200, message: '上传成功', data: { url: `http://localhost:3000${req.file.path}` // 注意:本 demo 用 memoryStorage,无 path,需改为返回 base64 或存内存后生成 URL } })修正:本 demo 实际用
memoryStorage,所以req.file.path不存在。正确做法是:将 buffer 存入内存 Map,生成唯一 ID,返回http://localhost:3000/api/upload/preview?id=xxx,后端/api/upload/preview接口根据 ID 返回图片流。
5.4 现象:修改代码保存后,Vite 热更新失败,必须手动刷新
原因:Vite 默认监听src/**/*,但src/stores/下的 Pinia store 修改有时不触发 HMR。
解决:在vite.config.ts中显式增加监听:
server: { watch: { usePolling: true, // 强制轮询(尤其在 Docker 或 WSL 环境) interval: 1000 } }5.5 现象:打包后npm run build生成的dist目录,用npx serve -s dist访问,登录接口 404
原因:serve是静态服务器,不支持代理,/api请求直接 404。
解决:生产环境必须用 Nginx 反向代理,或改用node server.js启动 Express 并托管dist:
// src/server/app.ts 末尾追加 if (process.env.NODE_ENV === 'production') { app.use(express.static(path.join(__dirname, '../../client/dist'))) app.get('*', (req, res) => { res.sendFile(path.join(__dirname, '../../client/dist', 'index.html')) }) }6. 进阶技巧:从本地调试到生产部署的三步验证法与 CI/CD 落地建议
一个全栈项目能否真正交付,不在于它能不能跑起来,而在于它能不能经受住三类场景的连续验证:本地开发态(热更新、断点调试)、构建产物态(dist 目录独立运行)、容器化部署态(Docker 镜像一键启停)。我在这份资源上踩过太多坑,最终沉淀出一套可复制的验证流程,现在分享给你。
6.1 本地开发态验证:用curl绕过前端,直击后端接口
前端 UI 有时会掩盖后端问题。比如登录失败,你以为是表单校验错了,其实是后端bcrypt.compare()时密码加盐不一致。这时,用curl直接调用后端,能秒级定位:
# 1. 启动后端(确保端口 3000) cd server && npm run dev # 2. 模拟登录请求(注意 Content-Type 和 JSON 格式) curl -X POST http://localhost:3000/auth/login \ -H "Content-Type: application/json" \ -d '{"username":"admin","password":"123456"}' # 3. 预期返回(200 OK + token) {"code":200,"message":"登录成功","data":{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9...","user":{"id":"1","username":"admin","role":"admin"}}} # 4. 用返回的 token 调用受保护接口 curl -X GET http://localhost:3000/user/profile \ -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9..."技巧:把上面四条命令写成
test.sh脚本,每次改完后端逻辑就sh test.sh,比打开浏览器点十次快得多。curl是后端工程师的“后悔药”,它让你绕过所有前端黑匣子,直面真实 API 响应。
6.2 构建产物态验证:用http-server模拟真实 CDN 环境
npx serve -s dist有缺陷(不支持 history 模式 fallback),http-server更贴近生产:
# 全局安装 npm install -g http-server # 构建前端 cd client && npm run build # 启动 http-server,指定根目录和 404 fallback http-server dist -p 8080 -c-1 --cors --proxy http://localhost:3000?/ # 访问 http://localhost:8080,此时所有 /api 请求会代理到 http://localhost:3000参数说明:
-c-1禁用缓存(开发调试必备);--proxy http://localhost:3000?/表示将所有未命中文件的请求代理到后端,等价于 Nginx 的try_files $uri $uri/ /index.html;。
6.3 容器化部署态验证:Docker Compose 一键启停全栈服务
生产环境不会让你npm run dev,必须容器化。本项目已提供Dockerfile(前端)和Dockerfile.server(后端),以及docker-compose.yml:
# docker-compose.yml version: '3.8' services: client: build: context: ./client dockerfile: Dockerfile ports: - "80:80" depends_on: - server server: build: context: ./server dockerfile: Dockerfile.server ports: - "3000:3000" environment: - NODE_ENV=production - JWT_SECRET=prod-secret-change-me验证步骤:
docker-compose up --build -d启动;curl http://localhost/api/auth/login确认后端通;- 浏览器访问
http://localhost,确认前端加载、登录、上传全流程;docker-compose logs -f server实时看后端日志,docker-compose logs -f client看 Nginx 日志。
6.4 CI/CD 落地建议:GitHub Actions 自动化三步走
如果你用 GitHub 托管代码,这份资源可直接接入 CI/CD。我推荐最简可行的三步流水线:
| 步骤 | 触发条件 | 执行内容 | 价值 |
|---|---|---|---|
| Build & Test | pushtomain | npm ci && npm run build(前端)npm ci && npm test(后端,本 demo 无 test,可加npm run lint) | 快速发现语法错误、构建失败 |
| Security Scan | pull_request | npm audit --audit-level high(前端)npm audit --audit-level high(后端) | 防止高危漏洞(如axios< 1.6.0 的 CVE-2023-45857) |
| Deploy to Staging | taglikev1.0.0 | docker buildx build --platform linux/amd64 -t your-registry/client:v1.0.0 .docker push your-registry/client:v1.0.0 | 版本化发布,回滚只需改 tag |
提示:
.github/workflows/ci.yml已预置在资源中,你只需替换your-registry为你自己的镜像仓库地址(如阿里云 ACR、腾讯云 TCR)。
从那以后我每次交付全栈项目,都强制走一遍这三步验证:先curl直击后端,再http-server跑 dist,最后docker-compose走通全流程。少一次,就多一个线上事故的种子。这份Vue 3 + Node.js 全栈项目.zip不是玩具,它是我在某高校实验室带学生做课程设计时,从 32 份翻车报告里熬出来的最小可行闭环。希望帮到你。
本文还有配套的精品资源,点击获取