资讯动态

Vue 3 + Express 前后端分离项目模板:从跨域到部署的开箱即用实践

发布时间:2026/10/15 20:10:50 来源:尧图企业网站定制
1. 为什么要做一个开箱即用的前后端分离项目模板前后端分离这个概念在现在的团队开发里几乎已经是默认选项了。前端归前端后端归后端各自维护自己的仓库、自己的部署流程接口文档成了两个团队之间最重要的沟通契约。但真到自己动手搭一套项目的时候你会发现分离这两个字背后藏着的细节远比想象中多。很多同学第一次尝试前后端分离往往是这样的经历前端用Vue脚手架初始化一个工程后端用Express写几个接口然后就开始碰壁——前端页面上发请求控制台直接报跨域错误于是装上cors中间件跨域解决了cookie又带不过去需要登录认证token存localStorage里刷新页面就丢想部署上线又发现前端打包出来的静态文件不知道往哪放后端接口的地址还被写死在代码里。这些问题的根源不是某个具体技术不会用而是缺少一个整体的、完整的、可以直接跑起来的项目骨架做参考。我写这个项目的初衷就是把这个骨架补全一套Vue 3 Express的前后端分离模板它不是一个Demo级别的hello world而是包含用户认证、权限校验、统一响应封装、日志记录、异步任务、文件上传等常见业务能力克隆下来装好依赖就能跑通全流程。这个开箱即用不是说我替你写好了所有业务代码而是帮你把所有连接前后端的繁琐环节——跨域、代理、鉴权、环境变量、构建部署——都收拾利索了。你拿到手之后只需要关注自己的业务逻辑。对于刚入门前后端分离的开发者来说它能省掉大量试错时间对于想快速起一个内部工具或者外包项目的团队来说它也能省去重复搭脚手架的活。2. 整体架构与数据流一次请求从前端到数据库的完整旅程2.1 目录结构与职责划分项目的目录结构是前后端分离的第一步也是很多人一开始容易随便来的地方。我的建议是严格遵守前端一个目录、后端一个目录、公共文档在外的划分方式project-root/ ├── client/ # Vue 前端工程 │ ├── src/ │ │ ├── api/ # 接口请求封装 │ │ ├── assets/ # 静态资源 │ │ ├── components/ # 公共组件 │ │ ├── router/ # 路由配置 │ │ ├── stores/ # 全局状态管理 │ │ ├── utils/ # 工具函数 │ │ └── views/ # 页面 │ ├── .env.development # 开发环境变量 │ ├── .env.production # 生产环境变量 │ └── package.json ├── server/ # Express 后端工程 │ ├── src/ │ │ ├── controllers/ # 控制器层 │ │ ├── models/ # 数据模型层 │ │ ├── routes/ # 路由定义 │ │ ├── middlewares/ # 中间件 │ │ ├── services/ # 业务逻辑层 │ │ └── utils/ # 工具函数 │ ├── .env # 后端环境变量 │ └── package.json ├── deploy/ # 部署相关配置 └── README.md这个结构最大的好处是前后端各自内部的代码分层也能保持一致。前端通过api/目录统一管理所有接口请求后端通过controllers/services/models三层隔离业务逻辑排查问题时能直接定位到具体层不用在一堆回调里翻来翻去。2.2 从前端点击按钮到后端返回数据的完整链路弄清楚一次请求的完整路径是理解前后端分离项目最关键的一步。我拿用户登录这个最典型的功能来拆解用户在登录页填完表单点击登录按钮Vue组件触发login方法。组件调用api/user.js中封装好的login(credentials)方法这个方法的底层用的是axios实例。axios实例会在请求发出前经过一个请求拦截器自动加上Authorization请求头、附带统一的Content-Type还做了超时配置。请求经由Vite开发服务器的代理转发绕过跨域限制到达Express服务端。Express的路由先经过几个全局中间件——请求日志、CORS、JSON解析——然后命中/api/auth/login路由。路由对应的controller接收参数调用service层的业务逻辑对密码进行哈希比对生成JWT令牌。controller在拿到结果后封装成统一的JSON格式返回。前端axios的响应拦截器收到这个格式剥离出data部分交给组件使用。组件拿到token后存好调router.push跳转到首页。这一条链路走下来涵盖了前端请求封装、后端中间件体系、接口响应规范、状态管理、路由权限等各个方面。无论后续扩展多少功能数据流的框架都是这个骨架。2.3 开发环境下的联调方式开发环境下前后端分离项目最常见的痛点是跨域。表面上装一个cors插件就能解决但实际上开发环境代理和生产环境跨域要分开处理。开发阶段前端跑在Vite的5173端口后端跑在3000端口。如果前端直接给axios配baseURL: http://localhost:3000虽然后端开了cors请求能通但浏览器里那些带凭证的请求比如要传cookie还是会出问题。更稳妥的做法是让前端请求走相对路径比如 /api/user/login然后在Vite配置里做代理转发// vite.config.js export default { server: { port: 5173, proxy: { /api: { target: http://localhost:3000, changeOrigin: true } } } }这样做的好处是前端代码里永远只写/api/xxx这种相对路径不管后端地址怎么变只需改配置不用改代码。到生产环境前端打包后的静态文件可以直接交给后端Express托管或者部署在Nginx里做反向代理同样保持/api前缀的转发规则。3. 关键技术决策为什么选Vue 3 Express这套组合3.1 Vue生态为什么适合做这类项目在选择前端框架的时候Vue和React都能完成这个任务但Vue 3带来的组合式APIComposition API加上TypeScript支持让它在做功能完整前后端分离项目这件事上非常趁手。组合式API最直接的收益是逻辑复用更自然了。以封装一个带全局loading状态的请求hook为例如果用Options API需要给每个组件单独写data里的一堆状态和methods里的处理逻辑用组合式API可以抽出一个useRequest函数// client/src/composables/useRequest.ts import { ref } from vue import request from /utils/request export function useRequestT(apiFunc: () PromiseT) { const loading ref(false) const data refT | null(null) const error refError | null(null) const run async () { loading.value true error.value null try { data.value await apiFunc() } catch (e) { error.value e as Error } finally { loading.value false } } return { loading, data, error, run } }项目里的每个列表页、表单页都可以复用这个hook不用重复声明loading变量。加上TypeScript的泛型提示接口返回的数据结构在编辑器里就能看到不用翻文档。同时Vue生态里的Vue Router和Pinia都是官方维护的配套起来非常顺滑。路由守卫配合登录状态检查、Pinia里存用户信息和权限标识是构建权限控制体系的标准组合资料多、坑也已经被社区踩平了用起来心里有底。3.2 Express比NestJS更适合开箱即用的理由后端框架我在Express和NestJS之间犹豫过。NestJS确实规范、适合大型项目但它的学习成本和项目体积都不小依赖了一堆装饰器和抽象概念。对于开箱即用这个项目目标来说Express有着不可替代的优势理解门槛低Express的中间件模型是线性的一个请求按顺序经过各个中间件谁都能一眼看懂。自由度高想怎么写路由就怎么写适合快速搭建内部工具类项目。文档成熟遇到问题搜索引擎一查一大把解决方案新手也能快速定位。比如做一个简单的接口Express只需要几行代码而NestJS要配置控制器、模块、依赖注入还得留意各种装饰器。我做的这个项目在Express服务端单独封了一层service把业务逻辑从controller里抽出来这样即使以后项目负责到需要迁移到NestJS只需要把class和函数搬过去就行业务代码不用重写。这比一开始就上NestJS的框架成本低。3.3 几个不容忽视的技术选型细节除了框架本体有几个配套选型也值得说明。数据库方面我用的是SQLite而不是MySQL或PostgreSQL。原因很简单——开箱即用要求克隆下来就能跑SQLite零配置、文件型存储不需要额外安装数据库服务。真要换生产数据库只需要改ORM的dialect配置无需重构业务代码。ORM我选了Sequelize。虽然现在Prisma也很流行但Sequelize的模型定义方式更接近传统后端思维对从Express转过来的开发者更友好。它支持自动建表、迁移、数据校验功能完整也不重。密码加密用的是bcrypt。为什么不用MD5或者SHA256因为哈希算法设计时是针对一次性验证的速度快反而方便了暴力破解。bcrypt内置了加盐和成本因子可以人为控制哈希速度暴力破解的成本会指数级上升这才是存储用户密码的正确选择。4. 核心功能模块拆解从0到1复现的关键代码思路4.1 用户认证模块的完整闭环用户认证是几乎所有业务系统都需要的功能也是这个项目里最有参考价值的部分。整个模块包含三个环节注册、登录、身份验证。注册接口接收用户名和密码先做基础校验——用户名不能重复、密码长度至少6位——然后对密码做bcrypt哈希再写入数据库。关键点在于无论注册失败还是成功返回给前端的错误信息都不能太具体。比如说用户名已存在直接返回这个信息可能会泄漏系统的用户量安全要求不高的内部项目无所谓但公共服务就是要统一提示账号或密码错误。登录接口验证通过后签发一个JWT里面包含用户ID、用户名和一个过期时间。签名的密钥放在后端的环境变量里绝不允许出现在前端代码或者数据库里。前端拿到token存到localStorage每次请求通过axios拦截器自动带上。// server/src/middlewares/auth.js const jwt require(jsonwebtoken) module.exports function auth(req, res, next) { const token req.headers.authorization?.split( )[1] if (!token) return res.status(401).json({ code: 401, message: 未登录 }) try { const decoded jwt.verify(token, process.env.JWT_SECRET) req.userId decoded.userId next() } catch (err) { return res.status(401).json({ code: 401, message: 登录已过期 }) } }身份验证这一环我单独写了一个auth中间件。以后任何需要用户身份的接口只要在路由定义时加上这个中间件请求处理函数里就能直接通过req.userId拿到当前用户不用在每个controller里重复验证。这是Express中间件模式最典型的优势体现。4.2 权限控制的实现层次认证解决的是你是谁的问题权限解决的是你能干什么的问题。这个项目实现了两个层次的权限控制路由级权限和按钮级权限。路由级权限是前后端配合完成的。后端给每个接口标注了需要的权限码比如创建用户需要user:create权限删除用户需要user:delete权限。前端在登录接口返回的数据里携带了当前用户拥有的所有权限码列表路由守卫在跳转页面前检查权限码没有权限的页面直接跳回403页面。按钮级权限更细致一点。同样是用户管理页面有权限的人才看得到新增按钮没权限的人连按钮都看不到。这是用Vue的自定义指令实现的// client/src/plugins/permission.ts import type { Directive } from vue import { useUserStore } from /stores/user export const permission: Directive { mounted(el, binding) { const requiredPermission binding.value as string const userStore useUserStore() if (!userStore.permissions.includes(requiredPermission)) { el.parentNode?.removeChild(el) } } }使用方式也很简洁button v-permissionuser:create新增用户/button。4.3 通用CRUD接口怎么写得省心又安全在管理后台类的项目里大部分功能本质上是CRUD——增删改查。写这类接口最容易犯的错误是每个接口都单独复制粘贴一段逻辑代码膨胀得快还不易维护。我在这个项目里做了一层简单的接口封装后端提供标准的路由生成器只需要传入模型和配置就能生成一套完整的CRUD接口。但省心不能牺牲安全。有几个方面必须注意参数校验所有输入都经过Joi校验非法参数在到达业务逻辑前被拦截。分页参数列表接口必须做分页防止一次拉全量数据拖垮数据库。软删除删除操作一律用软删除也就是给记录打一个deleted_at标记而不是物理删除。这样误操作还能恢复审计也有迹可循。// server/src/routes/generateCrud.js const express require(express) const { Op } require(sequelize) function generateCrudRoutes(model, options {}) { const router express.Router() router.get(/, async (req, res) { const { page 1, pageSize 10, keyword } req.query const where { deleted_at: null } if (keyword options.searchFields?.length) { where[Op.or] options.searchFields.map(field ({ [field]: { [Op.like]: %${keyword}% } })) } const { count, rows } await model.findAndCountAll({ where, limit: Number(pageSize), offset: (page - 1) * pageSize }) res.json({ code: 0, data: { list: rows, total: count } }) }) // ... 其他增删改路由 return router }这样无论新增多少个业务实体CRUD接口只需要两三行代码就注册完成把时间省下来处理真正的业务逻辑。5. 开箱即用背后的构建与启动机制5.1 一键启动脚本的设计思路第一个克隆项目的同事一定会问我怎么跑起来如果README里只有先cd client再cd server再分别安装再分别启动体验已经打折了。我在项目根目录提供了一个start.sh脚本实现了真正的一键启动。#!/bin/bash # start.sh if [ ! -d server/node_modules ]; then echo 正在安装后端依赖... cd server npm install cd .. fi if [ ! -d client/node_modules ]; then echo 正在安装前端依赖... cd client npm install cd .. fi # 初始化数据库如果不存在 cd server npx sequelize-cli db:create 2/dev/null || true npm run migrate cd .. # 启动后端 cd server npm run dev # 启动前端 cd client npm run dev wait这里有个细节首次运行时如果检测到node_modules已存在就跳过依赖安装后续启动速度快很多。同时数据库迁移在每次启动时执行db:create容错处理保证重复执行不报错。整个脚本的核心思路是把一切可能出错的环节交给机器去做而不是让使用者跟着文档手打命令。5.2 环境变量的管理体系环境变量的管理是我见过最容易出问题的环节。有些人会把接口地址硬编码也有人不小心把密钥提交到Git仓库里。这个项目的做法是后端使用.env文件里面放PORT、JWT_SECRET、DB_PATH等本地开发变量同时提供一份.env.example模板。前端通过.env.development和.env.production区分环境关键变量命名统一以VITE_开头。.gitignore里把所有的.env文件排除掉只提交.env.example。# server/.env.example PORT3000 JWT_SECRETplease_change_me DB_PATH./data/app.sqlite这样不管是新人接手还是CI环境构建只需要复制.env.example为.env填上自己环境对应的值就能跑。JWT_SECRET在首次启动时会自动生成一个随机值这样就算有人忘了配置系统也能默认跑起来但日志里会打一行警告提醒生产环境必须改掉。5.3 生产环境的构建与部署注意点部署的时候开箱即用的价值体现得最明显。这个项目的部署方案是双模式模式一最省事的方式用Express直接托管前端构建产物。前端先执行npm run build生成dist目录后端在启动时通过一行静态文件服务把它托管起来// server/src/app.js const path require(path) const clientDist path.join(__dirname, ../../client/dist) if (require(fs).existsSync(clientDist)) { app.use(express.static(clientDist)) app.get(*, (req, res) { res.sendFile(path.join(clientDist, index.html)) }) }这样整个项目只需要部署一个Node服务非常适合小项目和个人工具。模式二前后端分开部署前端静态文件交给Nginx后端Node服务通过PM2管理进程中间用反向代理把/api请求转发给Node服务。这里必须提醒组件无论用哪种模式app.get(*)这个通配路由必须放在所有API路由的后面否则它会拦截所有请求导致API接口全部失效。6. 实战中反复踩过的坑与解决办法6.1 跨域问题看似解决换了个部署方式又冒出来开发环境的Vite代理和后端的cors插件同时存在一开始确实能跑通。但在某次生产部署时前端静态文件托管在Nginx后端服务跑在另一台机器的3000端口前端页面发请求直接失败。排查下来发现Nginx配置里已经加了/api的proxy_pass理论上应该没问题。但浏览器console里报的错是Response to preflight request doesnt pass access control check。问题在哪因为Nginx转发时把Origin头也带给了后端后端虽然开了cors但没有把新的Origin加入白名单。这个问题的深层原因是开发代理和后端cors是两条不同的跨越路径不能混为一谈。开发环境走代理时可以完全关掉后端cors生产环境一定要确认Nginx的proxy_set_header Origin $http_origin配置和后端cors白名单是对齐的。我最终的做法是后端cors白名单从环境变量读取部署时传入Nginx对应的域名两个环境各自维护不再靠猜测。6.2 Token过期与前端状态不同步这是整个项目里最容易被忽视的体验问题。用户登录后挂着页面不动等过了一会儿再操作请求返回401但前端页面还停留在已登录的假象里。用户会以为系统坏了而不是自己登录过期了。我在axios的响应拦截器里加了一个全局401处理// client/src/utils/request.ts request.interceptors.response.use( response response.data, error { if (error.response?.status 401) { const userStore useUserStore() userStore.clearToken() router.push({ name: login, query: { redirect: router.currentRoute.value.fullPath } }) } return Promise.reject(error) } )这个处理有三个细节值得说。第一拦截到401后立即清掉本地token避免下次请求带着过期token继续报错第二跳转登录页时带上redirect参数用户重新登录后能自动回到之前的页面第三不是所有401都要跳转。比如用户主动退出登录时后端也可能返回401这时候要根据请求路径做个排除判断否则会出现退登录又跳登录页页面一闪的诡异现象。6.3 接口联调期间的Mock策略前后端并行开发时前端经常处于接口还没写好的状态。很多人的解决方案是前端写一堆if判断或者直接注释掉真实请求开发完再改回来一不留神就漏了。我习惯在api层做一个简单的mock开关。核心逻辑是从环境变量VITE_USE_MOCK读取开关为true时axios的适配器直接返回static/mock目录下的JSON文件为false时走真实请求。这样后端接口挂掉或者还没实现时前端照样能正常开发页面和交互联调阶段再切回真实接口代码里不需要动一行。mock数据要尽量贴近真实接口返回结构。字段多了少了都不怕前端可以根据真实mock先定义好类型这样联调时的冲突会少很多。7. 这个模板还能怎么扩展 我的真实使用体会这套骨架搭完之后我们已经拿它快速起了两个内部项目一个数据工单管理系统一个简单的报表配置平台。两个项目从启动到第一个可用版本上线都用不了一周时间60%以上的基础能力是模板现成的剩下40%才是业务代码。如果要往上扩展优先级最高的几个方向是第一接入权限的角色-用户关联模型。目前项目里是用户直接绑权限码简单灵活但不够结构化。做成RBAC模型用户挂角色、角色挂权限权限管理逻辑会更清晰适合业务系统再复杂一点的情况。第二日志系统的结构化。目前后端日志用的是Morgon加自定义request logger只做控制台输出和简单文件保存。如果想做线上问题追踪接入日志采集服务、带上trace ID会体面得多。第三引入Socket服务。内部系统经常有通知、实时刷新这类需求目前模板只支持HTTP轮询。在Express基础上挂一个Socket.IO服务改动成本不高但用户体验会有本质提升。最后分享一个我个人的体会前后端分离这个模式的落地难点从来不在框架本身而是在前端和后端的缝合处——跨域怎么处理、token怎么传、接口返回结构怎么设计、环境变量怎么管理、构建产物怎么部署。这套模板真正的价值就是把所有缝合处的方案都定下来了你不需要每次换个项目就重新纠结一遍。哪怕你最后不会完整复用这里的代码把其中处理缝合处的思路带走以后搭建任何前后端项目都会顺利很多。

读完文章,也想定制专属网站?

尧图设计师 24 小时内与您沟通定制方案

免费获取报价 →
↑