1. 这套技术栈到底适合做什么样的项目
先把结论摆在前面:Node.js + Express + Vue + MySQL + axios 这套组合,本质上是一套“前后端分离、轻量级、全 JavaScript 打通”的 Web 应用方案。它特别适合中小型管理系统、内部工具、内容管理后台、数据看板、表单流转类应用,以及个人开发者想快速把想法落地的场景。如果你要做的是高并发实时通信、复杂微服务治理或者超大规模电商,这套方案不是不能做,但需要额外引入消息队列、缓存层、服务拆分等组件,那是另一个话题了。
我见过太多人一上来就纠结“用哪个框架最好”,结果环境搭了三天还没跑起来。其实技术选型这件事,核心就一句话:匹配你的团队规模和项目阶段。这套组合最大的优势在于语言统一——前端 Vue、后端 Node.js、数据库 MySQL,中间用 axios 做 HTTP 通信,整个链路都是 JavaScript 生态,一个人可以从前写到后,调试成本低,上手速度快。
具体来说,这套架构的分工是这样的:Vue 负责页面渲染和用户交互,跑在浏览器里;axios 是前端发请求的工具,把数据以 JSON 格式发给后端;Express 是 Node.js 上的 Web 框架,接收请求、处理业务逻辑、操作数据库;MySQL 负责持久化存储。四者各司其职,边界清晰。
提示:如果你的项目需要服务端渲染做 SEO,Vue 单页应用可能不是最优解,可以考虑 Nuxt 这类方案。但如果是后台管理系统,SPA 完全够用。
我在实际搭建过程中发现,很多人卡住的地方不是某个技术本身,而是“项目结构怎么组织”“前后端怎么联调”“跨域怎么处理”这些工程化问题。所以这篇内容不会只给你一堆命令,而是把每个决策背后的原因讲清楚,让你搭完之后知道为什么这么搭,出了问题知道往哪查。
2. 动手之前先把环境和目录结构想明白
2.1 环境准备:版本选择比安装本身更重要
安装 Node.js 这件事本身没什么难度,但版本选择有讲究。截至我写这篇内容时,Node.js 的 LTS 版本已经到 20.x,建议直接用 LTS 版本,不要追最新的 Current 版本。原因很简单:LTS 版本经过长时间验证,生态兼容性最好,很多 npm 包对最新版 Node 的支持往往滞后几个月。
MySQL 建议用 8.0 及以上版本,因为 8.0 之后默认字符集变成了 utf8mb4,对中文和 emoji 的支持更完整。如果你用的是 5.7,建库时一定要手动指定字符集,否则中文乱码问题会让你排查到怀疑人生。
安装完成后,用下面几条命令验证环境:
node -v npm -v mysql --version三条命令都能正常输出版本号,说明基础环境没问题。这里有个小细节:npm 的默认源在国内访问可能较慢,可以换成国内镜像源加速依赖安装,但不要用那些来路不明的第三方源,用主流大厂提供的镜像即可。
2.2 目录结构:前后端分离不等于两个毫无关系的文件夹
很多人搭项目时习惯把前端和后端放在两个完全独立的目录,甚至两个不同的代码仓库。这样做不是不行,但对于个人项目或小团队来说,我建议用一个根目录把前后端包起来,结构大致如下:
project-root/ ├── client/ # Vue 前端 │ ├── src/ │ ├── public/ │ └── package.json ├── server/ # Express 后端 │ ├── routes/ │ ├── controllers/ │ ├── models/ │ ├── config/ │ └── package.json └── README.md这样组织的好处是:前后端代码在同一个仓库里,版本管理方便,联调时切换目录快,部署时也可以统一用脚本处理。当然,如果团队规模大了,前后端由不同人负责,拆成两个仓库也合理,这是团队协作层面的考量,不是技术限制。
2.3 后端初始化:Express 项目的最小骨架
进入 server 目录,执行npm init -y生成 package.json,然后安装核心依赖:
npm install express mysql2 cors dotenv npm install -D nodemon这里解释一下每个包的作用。express 是 Web 框架本体;mysql2 是 MySQL 驱动,相比老的 mysql 包,它支持 Promise 和预处理语句,性能更好;cors 用来处理跨域请求;dotenv 用来管理环境变量,避免把数据库密码硬编码在代码里;nodemon 是开发工具,文件改动后自动重启服务,省去手动重启的麻烦。
在 package.json 里加一行启动脚本:
"scripts": { "dev": "nodemon app.js", "start": "node app.js" }然后创建入口文件 app.js,先写一个最小可运行版本:
const express = require('express'); const cors = require('cors'); require('dotenv').config(); const app = express(); app.use(cors()); app.use(express.json()); app.get('/api/health', (req, res) => { res.json({ status: 'ok', timestamp: Date.now() }); }); const PORT = process.env.PORT || 3000; app.listen(PORT, () => { console.log(`Server running on port ${PORT}`); });跑起来之后访问/api/health,能看到返回的 JSON 就说明后端骨架通了。这个健康检查接口看起来简单,但它在后续排查“到底是前端问题还是后端问题”时非常有用,建议每个项目都保留。
3. 数据库连接层的设计决定了后期维护成本
3.1 为什么推荐用连接池而不是单连接
MySQL 连接是一个相对昂贵的资源,每次请求都新建连接、用完关闭,在高频访问下性能损耗非常明显。连接池的思路是预先创建一批连接放在池子里,请求来了直接取,用完还回去,省去了反复建立连接的开销。
用 mysql2 创建连接池的代码大概是这样:
const mysql = require('mysql2'); const pool = mysql.createPool({ host: process.env.DB_HOST, user: process.env.DB_USER, password: process.env.DB_PASSWORD, database: process.env.DB_NAME, waitForConnections: true, connectionLimit: 10, queueLimit: 0, charset: 'utf8mb4' }); module.exports = pool.promise();connectionLimit: 10这个数字不是随便写的。它取决于你的数据库服务器配置和预期并发量。对于个人项目或小团队内部系统,10 到 20 足够了。设太大反而会拖垮数据库,因为每个连接都占用内存和文件句柄。charset: 'utf8mb4'必须显式指定,这是避免中文乱码的关键一步。
3.2 环境变量管理:别把密码写进代码里
.env文件放在 server 根目录,内容格式如下:
DB_HOST=localhost DB_USER=your_user DB_PASSWORD=your_password DB_NAME=your_database PORT=3000这个文件必须加入.gitignore,绝对不能提交到代码仓库。我见过有人把数据库密码直接推到公开仓库,结果被扫描工具抓到,数据库被清空勒索,这种教训太惨痛了。团队协作时,可以提供一个.env.example文件作为模板,里面只写键名不写真实值,新人拉下代码后自己复制一份改成.env再填值。
3.3 封装一个通用的查询工具函数
直接在路由里写 SQL 不是不可以,但项目一大就会乱。建议在 models 层封装一层,把常用的查询操作抽象出来:
const pool = require('../config/db'); async function query(sql, params) { const [rows] = await pool.execute(sql, params); return rows; } module.exports = { query };用pool.execute而不是pool.query,是因为 execute 使用预处理语句,能有效防止 SQL 注入。参数用?占位,实际值通过数组传入,数据库驱动会自动做转义处理。这个习惯从项目第一天就要养成,后期改起来成本很高。
4. 前端 Vue 工程与 axios 请求层的配合
4.1 Vue 项目创建:用官方脚手架最省心
前端部分用 Vue 官方推荐的脚手架创建,进入 client 目录后执行创建命令,选择 Vue 3 + Vite 的组合。Vite 的冷启动速度比老一代构建工具快一个数量级,开发体验提升明显。
创建完成后安装 axios:
npm install axiosVue 3 的组合式 API 写起来更灵活,逻辑复用也方便,新项目建议直接用组合式 API,不要再用选项式了。当然这不是强制要求,如果你团队里有人对选项式更熟悉,混用也不是不行,但统一风格对后期维护更友好。
4.2 axios 实例化:统一配置比到处写重复代码强
不要在每个组件里直接import axios from 'axios'然后发请求,那样 baseURL、超时时间、请求头这些配置要重复写很多遍。正确做法是创建一个 axios 实例:
import axios from 'axios'; const request = axios.create({ baseURL: import.meta.env.VITE_API_BASE_URL || 'http://localhost:3000/api', timeout: 10000, headers: { 'Content-Type': 'application/json' } }); request.interceptors.request.use( config => { const token = localStorage.getItem('token'); if (token) { config.headers.Authorization = `Bearer ${token}`; } return config; }, error => Promise.reject(error) ); request.interceptors.response.use( response => response.data, error => { console.error('Request failed:', error.message); return Promise.reject(error); } ); export default request;请求拦截器里统一加 token,响应拦截器里统一处理错误和剥离外层数据,这样业务组件里拿到的就是干净的数据,不用每次都res.data.data这样层层剥。
4.3 跨域问题的本质与两种解法
开发阶段前端跑在 5173 端口,后端跑在 3000 端口,浏览器同源策略会拦截请求,这就是跨域问题。解法有两种:
第一种是后端开 CORS,也就是前面 app.js 里那行app.use(cors())。这种方式简单直接,适合开发阶段。但生产环境不建议无差别放开,应该配置白名单,只允许特定域名访问。
第二种是在前端构建工具的配置里做代理转发。以 Vite 为例,在vite.config.js里配置:
export default defineConfig({ server: { proxy: { '/api': { target: 'http://localhost:3000', changeOrigin: true } } } });代理的原理是:浏览器请求发给同源的开发服务器,开发服务器再转发给后端,这样浏览器看来请求始终是同源的,就不存在跨域了。生产环境通常用 Nginx 做同样的反向代理。
注意:CORS 和代理不要同时用,否则可能出现请求头重复、预检失败等奇怪问题。开发阶段选一种就行,我一般推荐代理方式,因为更接近生产环境的部署形态。
5. 前后端联调时最容易踩的几个坑
5.1 请求发出去了但后端收不到数据
这个问题十有八九是请求体解析中间件没配好。Express 从 4.16 版本开始内置了express.json(),但如果你忘了加这行,req.body就是 undefined。还有一种情况是前端发的是表单格式而不是 JSON,那后端需要用express.urlencoded({ extended: true })来解析。
排查方法很简单:在后端加一行日志console.log(req.method, req.url, req.body),看看请求到底有没有到、body 是什么。如果日志根本没打印,说明请求没到后端,问题在前端或网络层;如果打印了但 body 是空的,问题在解析中间件。
5.2 数据库查出来是 Buffer 或者乱码
这通常是字符集问题。检查三个地方:数据库建库时有没有指定 utf8mb4,数据表有没有指定,连接配置里有没有指定。三个地方缺一不可。另外 mysql2 返回的某些字段类型(比如 BIT、BINARY)默认是 Buffer 对象,需要手动转成字符串或数字,这个在写查询逻辑时要注意。
5.3 nodemon 频繁重启导致数据库连接泄漏
开发阶段用 nodemon 自动重启很方便,但如果你的数据库连接池没有正确处理进程退出事件,每次重启都会残留一批连接,时间长了数据库连接数就被占满。解决办法是在 app.js 里监听退出信号:
process.on('SIGINT', async () => { await pool.end(); process.exit(0); });这样按 Ctrl+C 停止服务时,连接池会被正确关闭。虽然开发阶段这个问题不致命,但养成好习惯,生产环境部署时就不会出大问题。
5.4 前端打包后接口 404
开发时用代理没问题,打包部署后代理配置不生效了,因为代理是开发服务器提供的功能。生产环境需要 Nginx 配置反向代理,把/api开头的请求转发到后端服务。这个问题几乎每个新手都会遇到一次,提前知道就能少走弯路。
6. 从能跑到好用还差哪些工程化细节
6.1 统一响应格式让前端处理更省心
后端返回的数据格式应该统一,建议约定成这样的结构:
{ code: 0, message: 'success', data: { ... } }code 为 0 表示成功,非 0 表示各种错误。这样前端拦截器里可以统一判断,成功就返回 data,失败就弹提示。如果不统一,前端每个接口都要写不同的判断逻辑,维护起来很痛苦。
6.2 错误处理中间件是后端的兜底网
Express 的错误处理中间件有四个参数(err, req, res, next),必须放在所有路由之后。任何路由里next(err)抛出的错误都会汇聚到这里:
app.use((err, req, res, next) => { console.error(err.stack); res.status(err.status || 500).json({ code: err.status || 500, message: err.message || 'Internal Server Error', data: null }); });有了这层兜底,即使某个路由忘了 try-catch,服务也不会直接崩溃,而是返回一个结构化的错误响应。
6.3 用路由分层把业务逻辑从入口文件里拆出去
app.js 只负责中间件注册和路由挂载,具体业务逻辑放到 routes 和 controllers 里。比如用户相关的接口:
// routes/user.js const router = require('express').Router(); const userController = require('../controllers/userController'); router.get('/', userController.list); router.post('/', userController.create); module.exports = router;然后在 app.js 里app.use('/api/users', require('./routes/user'))。这样项目规模变大时,每个模块的代码都在自己的文件里,找起来快,改起来也不容易影响其他模块。
6.4 部署时的进程管理
开发用 nodemon,生产环境不能再用它了。生产环境建议用 pm2 这类进程管理工具,它能在服务崩溃时自动重启,还支持多进程负载均衡和日志管理。启动命令大概是pm2 start app.js --name my-api,然后pm2 save保存进程列表,服务器重启后能自动恢复。
前端打包用npm run build,产物在 dist 目录,把 dist 目录交给 Nginx 托管,同时配置/api的反向代理指向后端服务端口。这样一套下来,整个项目就从“本地能跑”变成了“线上能用”。
7. 我在多次搭建中总结的几条实用经验
第一条,先跑通最小闭环再扩展功能。不要一上来就把用户认证、权限管理、日志系统全搭好,先让一个最简单的接口从前端调到后端再查到数据库,这条链路通了,剩下的都是在这个骨架上加东西。我见过太多项目死在“想一次性搭完美”上。
第二条,数据库表结构设计要留余地。比如用户表除了基本的用户名密码,建议加上 created_at、updated_at、status 这些字段,后期做软删除、数据统计时不用改表。字段类型上,能用 varchar 就别用 text,能用 int 就别用 varchar,索引该加就加,但也不要滥用,每个索引都会拖慢写入速度。
第三条,接口文档越早写越好。哪怕只是在一个 Markdown 文件里列出接口路径、请求方法、参数、返回值,也比没有强。前后端联调时,文档就是契约,能省掉大量“你这个字段叫什么”“返回的是数组还是对象”这类沟通成本。
第四条,日志要分级。开发阶段 console.log 随便打,但生产环境要有 info、warn、error 分级,错误日志要单独收集。不然出了问题翻日志像大海捞针。可以用 winston 或 pino 这类日志库,配置简单,输出格式也规范。
第五条,环境变量分环境管理。开发、测试、生产三套环境,数据库地址、端口、密钥都可能不同。用.env.development、.env.production这样的文件区分,配合 dotenv 的加载逻辑,部署时切换环境不用改代码。
这套技术栈我前后搭过不下十次,每次都会根据项目特点做微调,但核心骨架和上面讲的这些原则基本没变过。真正决定项目成败的从来不是用了多新的框架,而是工程结构是否清晰、错误处理是否到位、联调流程是否顺畅。把这些基础打牢,后面加什么功能都是水到渠成的事。