全栈实战:Node.js+MySQL+Vue3用户管理系统实践
在前2篇中,我将这次代码你需要学会的前置知识都总结了一下,感兴趣的话,可以去搜索一下,补充知识,避免下方的代码看不懂,我在下面使用对应知识不会在讲一遍,默认你们都会。
第一篇:SQL完整入门指南:基础语法+实战技巧+安全规范
第二篇:从零搭建Node.js后台系统:连接MySQL、Express与CORS配置详解
下面的图,是数据库和连接数据库的配置,在上面2篇文章中都有讲。
const mysql = require('mysql2')
const Mock = require('mockjs')
const db = mysql.createPool({
host:'127.0.0.1',//数据库的IP地址
user:'xinghe',//登录数据库的账号
password:'Cqy@2005',//登录数据库的密码
database:'lists'//要操作的数据库
})
const length = 200

//导入
const express = require('express')
const app = express()
//导入数据库数据和对应的方法
const list = require('./list')
//导入cors
const cors = require('cors')
app.use(express.json()) // 解析JSON请求体
app.use(express.urlencoded({ extended: true })) // 解析URL编码请求体
app.use(cors({
origin: ['http://localhost:5173', 'http://127.0.0.1:5173'], // 明确指定允许的源
credentials: true,
methods: ['GET', 'POST', 'PUT', 'DELETE', 'OPTIONS'],
allowedHeaders: ['Content-Type', 'Authorization', 'X-Requested-With']
}))
list 是增删改查初始化函数等代码存放的文件名字,之所以这样安排是因为这样对萌新比较友好,当然你可以选择使用路由,使用路由会有更好的维护性。
1、初始化函数:
在第一篇中,我们曾经提到过初始化的问题,下面我们就仔细讲解如何在 Node.js 中对数据库中的数据进行初始化。
首先,我们可以使用数据库中的 count 指令,来统计你所连接的数据库的数据是否为空。
下面是全部的代码:
function init() {
return new Promise((resolve, reject) => {
// 检查是否已有数据
const sqlStr = 'SELECT COUNT(*) as count FROM users'
db.query(sqlStr, (err, results) => {
if (err) {
reject(err)
return
}
console.log(results)
const count = results[0].count
if (count === 0) {
// 生成模拟数据
const users = []
for (let i = 0; i < length; i++) {
users.push(Mock.mock({
id: i + 1,
username: Mock.Random.cname(),
address: Mock.Random.county(true),
status: 0,
sex: Mock.Random.integer(0, 1),
'age|18-60': 1,
birthday: Mock.Random.date()
}))
}
// 插入模拟数据
let completed = 0
for (let i = 0; i < length; i++) {
const sqlStr1 = 'INSERT INTO users (id, username, address, status, sex, age, birthday) VALUES (?, ?, ?, ?, ?, ?, ?)'
db.query(sqlStr1, [users[i].id, users[i].username, users[i].address, users[i].status, users[i].sex, users[i].age, users[i].birthday], (err, results) => {
if (err) {
reject(err)
return
}
completed++
if (completed === length) {
console.log('初始化数据成功')
resolve() // 我们不返回users数组,因为之后从数据库取
}
})
}
} else {
console.log('数据库中已有数据,跳过初始化')
resolve()
}
})
})
}
1.1、count返回值:
我们慢慢解析,首先我们先创建一个空表 person (目的是详细查看数据库会返回给后端什么东西),然后连接。
const mysql = require('mysql2')
const db = mysql.createPool({
host:'127.0.0.1',//数据库的IP地址
user:'xinghe',//登录数据库的账号
password:'Cqy@2005',//登录数据库的密码
database:'lists'//要操作的数据库
})
const sqlStr = 'select count(*) as count from person'
db.query(sqlStr,(err,results)=>{
if(err) return
if(results){
const count = results
console.log(count)
}
})

运行结果是 [{count:0}],随后我们可以,让 count = results[0]。

最后让 count = results[0].count 即可得到表中数据的全部数量,当 count = 0 时调用初始化函数,达到只调用一次初始化函数,不会有过多的数据(不会多次调用初始化函数)。
我们在函数的最外面定义了全局变量 length = 200 ,这是我们预设的模拟数据的总量。
1.2、mock.js--模拟数据:
在使用 if 来判断数据表的数据为空后,我们使用了 for 循环去进行模拟200条数据,在这里我们使用之前安装的 Mock.js (官方文档里面有对应的模拟数据方式),对数据进行模拟,使用 push 方法将模拟数据放入到 users 数组里面,里面就放着我们模拟的全部数据。
随后就是插入数据了,在这里我们有 2 种方式去插入数据,一种是使用循环一条一条的插入,一种是一次性全部插入,在这里我们选择前者,论性能的话,在数据量庞大时,后者性能远大于前者,但我们是萌新,求稳和理解,并且我们模拟的数据量比较小,所以选择前者来进行插入。
1.3、insert--插入数据:
const sqlStr1 = 'INSERT INTO users (id, username, address, status, sex, age, birthday) VALUES (?, ?, ?, ?, ?, ?, ?)'
在这里我们使用占位符进行占位,所以在使用 db.query() 时我们需要使用一个数组,去将占位符中的数据进行一 一对位,确保数据不会插入错误。

里面的 affectedRows 表示受影响的数据数量,这里的 affectedRows = 1 表示插入成功 1 条数据, 所以在判断插入是否成功是 便可以是 if ( affectedRows > 0) 说明操作执行成功。
我们使用 completed 来做计数器来记录插入的数据,当它和我们预定数量一样时,就完成插入操作。
到这里 初始化函数就讲完了,里面的 Promise 会在下面讲解,这涉及到一个小 bug ,放在那里讲更合适。
2、获取用户数据--getUser:
这是全部的代码,当然这个是写在 module.exports 里面暴露出去的,所以结尾有逗号。
注意:这里先查询总数再查询数据,在极高并发场景下可能会出现数据不一致的情况,对应于大多数项目,这种方式已经足够,如果对一致性要求极高的话,可以考虑其他方案。
getUsers: function(page = 1, pageSize = 10) {
return new Promise((resolve, reject) => {
const offset = (page - 1) * pageSize;
// 只查询 status=0 的数据
const countSql = 'SELECT COUNT(*) as count FROM users WHERE status = 0';
const dataSql = 'SELECT * FROM users WHERE status = 0 LIMIT ? OFFSET ?';
console.log(`获取用户数据 - page: ${page}, pageSize: ${pageSize}, offset: ${offset}`);
// 先查询总数
db.query(countSql, (err, countResults) => {
if (err) {
console.error('查询总数失败:', err);
reject(err);
return;
}
const totalCount = countResults[0].count;
console.log(`用户总记录数: ${totalCount}`);
// 再查询分页数据
db.query(dataSql, [parseInt(pageSize), parseInt(offset)], (err, results) => {
if (err) {
console.error('查询数据失败:', err);
reject(err);
} else {
console.log(`返回数据条数: ${results.length}`);
resolve({
list: results,
count: totalCount,
page: parseInt(page),
pageSize: parseInt(pageSize)
});
}
});
});
});
},
2.1、分页公式:
我们使用了分页公式 offset = (page - 1) * pageSize ,但需要注意,我们传入该函数中的参数 page 是指当前的数据所在页数,pageSize 是当前页面要展示的数据量。
2.2、记录总数据:
我们在这里没有使用 select * from users 查询总数据,是因为我们使用了软删除的思想,即我们并没有真正的把数据从数据库中删除而是以 status 这个的值来规定该数据是否被删除了。
所以我们在这里需要使用 :
const countSql = 'SELECT COUNT(*) as count FROM users WHERE status = 0';
来查询前端可以渲染的数据量是多少。
2.3、分页渲染数据:
我们在前面定义了 offset 来为下面的分页渲染做准备。
const dataSql = 'SELECT * FROM users WHERE status = 0 LIMIT ? OFFSET ?';
我们使用这行代码来查询接下来不同页面的数据做处理,也就是分页功能。

2.4、调试代码:
为了保证下面我们查询的数据是否有误,我们下面的代码下面,添加了 console.log 取打印重要的数据是否有误,如果有误的话方便后面代码的修改。
这些 console.log 在开发阶段很有用,可以帮助我们快速定位问题,在生产环境中可以移除或改用专业的日志工具。
const dataSql = 'SELECT * FROM users WHERE status = 0 LIMIT ? OFFSET ?';

3、异步代码:
3.1、隐藏bug:
写这一篇对应了上文提到的 Promise 的小bug。
list.init().then(()=>{
app.get('/',(req,res)=>{
list.getUsers().then(users=>{
console.log('我在这里',users[1]||{})
res.send(users[1]||{})
}).catch(err=>{
res.status(500).send('数据库错误')
})
})
})
在原先我写的代码里面,仅仅只 list.init() 这样看代码是没有问题的,但我在最初运行的时候拿不到一点数据,在查找资料后得知,list.init() 它是同步代码,而我们上面写的数据库的初始化函数是异步代码(我最开始的版本代码初始化函数不是一个 Promise 对象)。
为此我们就需要 Promise 帮助,他会强制让代码在 list.init() 这里停止运行(简单来讲是这样的),等里面的数据库数据初始化完成之后才去运行接下来的代码,这样这个隐藏的 bug 就可以解决了。
3.2、测试:
而这里的 list.init().then() 是为了测试,我们的初始化代码是否被成功的执行,如果成的执行那么对应的数据库会有数据,我们可以在后端上看到对应的数据,只要不为空,就代表着我们成功了。
4、getUserData--前端接口:
在上面的讲解下,我们知道了函数是怎么去写的,但在写后端接口前,我们需要先写前端的接口,这样会更夹方便后面对后端接口的讲解。
这是前端对应模块的部分代码:
import {ref,getCurrentInstance,onMounted,reactive} from 'vue'
import {ElMessage,ElMessageBox} from 'element-plus'
const {proxy}=getCurrentInstance()
const config = reactive({
total:0,
page:1,
limit:10,
})
const transformUserData = (dataArray) => {
return dataArray.map(item => ({
...item,
sexLabel: item.sex === 1 ? '男' : '女',
name: item.username || item.name,
age: item.age,
birth: item.birthday || item.birth,
addr: item.address || item.addr,
status: item.status || 0,
}))
}
const getUserData = async () => {
try {
const params = {
page: config.page,
limit: config.limit,
name: formInline.keyWord || '' // 搜索关键词
}
console.log('请求参数:', params)
const data = await proxy.$api.getUserData(params)
console.log('响应数据:', data)
if (data && data.list) {
const activeUsers = data.list.filter(item => item.status === 0 || item.status === undefined)
tableData.value = transformUserData(activeUsers)
config.total = data.count
console.log('分页信息 - 总数:', config.total, '当前页:', config.page)
// 如果当前页没有有效数据且不是第一页,自动跳转到上一页
if (activeUsers.length === 0 && config.page > 1) {
console.log('当前页无有效数据,跳转到上一页')
config.page -= 1
getUserData() // 重新获取
return
}
} else {
console.error('返回数据格式错误:', data)
tableData.value = []
config.total = 0
}
} catch (err) {
console.error('获取数据失败:', err)
ElMessage.error('获取数据失败,请重试')
}
}
4.1、try / catch:
我们在代码中使用 try / catch 来捕捉代码运行时的错误,便于随时得到错误信息,方便修改。
4.2、config:
这是我们定义的全部变量,目的就是记录当前的页数(翻页功能的当前页数),以及数据总量,和每一页的限制数据是多少。
4.3、params:
我们在代码开头就定义了 params 这个变量,为的就是记录当前的页面的页数、最大渲染数据量、以及搜索的关键词(这个在后面搜索功能会用到)。
我们定义的这个变量是为了给后端的 getUsers 传参数,方便后端返回对应页数的数据,实现展示数据页面的分页功能。
4.4、console.log:
在 const data = await proxy.$api.getUserData(params) 代码运行前后都添加 console.log 是为了查看参数是否按照我们想要的模式去传递,以及后端返回给我们的数据是什么格式,主要是查看后端返回的数据(一般是对象)上面有什么属性。
可以看到,当我们使用 console.log 打印后,我们可以很直观的看到数据里面有什么属性,这样方便我们之后的使用和开发,可以提高我们开发的效率。
4.5、data&&data.list :
这是一个常见的 js 安全访问模式,首先是需要 data 变量不为空或者 undefined ,这样是为了防止在 data 为 null/undefined 时访问其属性,data.list 则是确保 data 对象里面含义我们需要的数据,这样可以有效避免运行时出错,确保代码在数据异常时不会崩溃。
4.6、filter:
我们使用 filter 来确保我们渲染的数据没有被删除(status=0)。
4.7、赋值:
我们将 getUserData 和 getSearch 函数里面的公用部分给写出一个函数,来减少项目的体积,优化我们的项目。
我们将 data.count 赋值给 config.total 确保我们的分页功能不会出现问题,这个主要是和 element plus 里面的翻页功能有关,里面的 total 是翻页功能里面渲染的数据总条数,layout 则对应上一页、省略、下一页、跳转功能,current-page 则是记录当前的页数,page-size 则是当前页面渲染允许的最大数据量。

<el-pagination
class="pager"
background="true"
layout="prev, pager, next, jumper"
size="small"
:total="config.total"
:current-page="config.page"
:page-size="config.limit"
@current-change="handleChange"
/>
Pagination 分页 | Element Plus 这是 element plus 中文官网。
5、api接口:
里面的 request 文件是 axios 请求的封装后的文件,api 这个文件存储了我们项目里面需要 axios 请求的所有函数,集中在这个文件是因为这样方便维护。
// 整个项目api的统一管理
import request from './request'
//请求首页左侧的表格的数据
export default {
getTableData() {
return request({
url: '/home/getTableData',
method: 'get',
})
},
getCountData() {
return request({
url: '/home/getCountData',
method: 'get',
})
},
getChartData() {
return request({
url: '/home/getChartData',
method: 'get',
})
},
getUserData(data){
return request({
url:'/home/getUserData',
method:'post',
data,
mock:false,
}).then(response => {
return response
}).catch(error => {
throw error
})
},
getMenu(params){
return request({
url:'/permission/getMenu',
method:'post',
data:params,
})
},
deleteUser(data) {
return request({
url: '/home/deleteUser',
method: 'post',
data,
})
},
addUser(data) {
return request({
url: '/home/addUser',
method: 'post',
data,
})
},
// 编辑用户(新增接口,Mock 中也要对应配置)
updateUser(data) {
return request({
url: '/home/updateUser',
method: 'post',
data,
})
},
getSearch(data) {
return request({
url: '/home/getSearch',
method: 'post',
data,
})
},
}
在这里我们可以很容易看到里面的 getUserData 的请求路径和方法,这样我们就可以去写对应的后端接口了。
6、getUserData--后端接口:
// 在后端路由中包装数据结构
app.post('/home/getUserData', (req, res) => {
console.log('收到用户数据请求:', req.body)
const { page = 1, limit = 10, name = '' } = req.body
console.log(`用户数据请求参数 - page: ${page}, limit: ${limit}, name: "${name}"`);
// 如果有搜索条件,调用搜索接口
if (name && name.trim() !== '') {
console.log('检测到搜索条件,调用搜索接口');
list.getSearch(name, page, limit).then(data => {
res.json(data);
}).catch(err => {
console.error('搜索失败:', err);
res.status(500).json({
code: 500,
message: '搜索失败: ' + err.message
});
});
} else {
// 没有搜索条件,调用普通分页接口
list.getUsers(page, limit).then(data => {
console.log(`用户数据获取成功 - 返回数据: ${data.list.length}条, 总数: ${data.count}`);
res.json(data);
}).catch(err => {
console.error('获取用户数据失败:', err);
res.status(500).json({
code: 500,
message: '获取用户数据失败: ' + err.message
});
});
}
});
我们的路径接口可以从 api 里面直接找到。
6.1、请求内容:
我们使用 req.body 来看到前端给后端的数据内容,我们可以只管看到前端给后端成功的传递了 page、limit、name。

我们首先看 else 这部分,因为这是我们一开始不进行任何操作就能看到数据的关键,前面讲过的函数我就在这里不在讲一遍了,值得一提的是我们将 后端放回的数据通过 res.json 转换成立 json 字符串的格式。
7、getSearch--搜索:
函数里面的部分代码我们在上面的 getUsers 中讲过部分,重复的我们就不会在这里继续讲。
getSearch: function(uname, page = 1, pageSize = 10){
return new Promise((resolve,reject) => {
const offset = (page - 1) * pageSize;
console.log(`搜索参数 - uname: ${uname}, page: ${page}, pageSize: ${pageSize}, offset: ${offset}`);
// 只查询 status=0 的数据
const countSql = "SELECT COUNT(*) as count FROM users WHERE username LIKE CONCAT('%',?,'%') AND status = 0";
const dataSql = "SELECT * FROM users WHERE username LIKE CONCAT('%',?,'%') AND status = 0 LIMIT ? OFFSET ?";
db.query(countSql, [uname], (err, countResults) => {
if(err) {
console.error('查询总数失败:', err);
reject(err);
return;
}
const totalCount = countResults[0].count;
console.log(`搜索到总记录数: ${totalCount}`);
// 再查询分页数据
db.query(dataSql, [uname, parseInt(pageSize), parseInt(offset)], (err, results) => {
if(err) {
console.error('查询数据失败:', err);
reject(err);
} else {
console.log(`返回数据条数: ${results.length}`);
resolve({
list: results,
count: totalCount,
page: parseInt(page),
pageSize: parseInt(pageSize)
});
}
});
});
})
},
const countSql = "SELECT COUNT(*) as count FROM users WHERE username LIKE CONCAT('%',?,'%') AND status = 0";
这行代码会返回你搜索的名字中含有这个字的数据。
const dataSql = "SELECT * FROM users WHERE username LIKE CONCAT('%',?,'%') AND status = 0 LIMIT ? OFFSET ?";
这行代码是确保搜索出来的数据按照之前设定的进行渲染(每次最多是渲染 10 条数据)。
下面的代码不是不讲,是上面基本都讲过了类似的,剩下的你可以推出了。
7.1、接口代码:
app.post('/home/getSearch', (req, res) => {
console.log('搜索请求数据:', req.body)
const { uname = '', page = 1, limit = 10 } = req.body
console.log(`搜索参数 - uname: "${uname}", page: ${page}, limit: ${limit}`);
list.getSearch(uname, page, limit).then(data => {
console.log(`搜索成功 - 返回数据: ${data.list.length}条, 总数: ${data.count}`);
const responseData = {
list: data.list,
count: data.count,
page: data.page,
pageSize: data.pageSize
}
res.json(responseData)
}).catch(err => {
console.error('搜索失败:', err)
res.status(500).json({
code: 500,
message: '搜索失败: ' + err.message
})
})
})
7.2、前端接口代码:
const getSearch = async() => {
config.page = 1 // 搜索时重置到第一页
getUserData()
}
8、前端--提交函数:
<el-dialog
v-model="show"
:title="dialogTitle"
width="500px"
align-center
class="custom-dialog"
>
<el-form
ref="ruleFormRef"
style="max-width: 600px ; margin: 0 auto;"
:model="ruleForm"
status-icon
:rules="rules"
label-width="auto"
class="custom-form"
>
<el-form-item label="姓名:" prop="username" class="form-item">
<el-input placeholder="请输入你的名字" type="text" autocomplete="off" v-model="ruleForm.username"/>
</el-form-item>
<el-form-item label="年龄:" prop="userage" class="form-item">
<el-input placeholder="请输入你的年龄" type="number" autocomplete="off" v-model="ruleForm.userage" @input="() => ruleFormRef.value?.clearValidate('userage')" @change="val => ruleForm.userage = Number(val)"/>
</el-form-item>
<el-form-item label="性别:" prop="usersex" class="form-item">
<el-select v-model="ruleForm.usersex" placeholder="请选择性别" class="custom-select">
<el-option label="男" value="男"></el-option>
<el-option label="女" value="女"></el-option>
</el-select>
</el-form-item>
<el-form-item label="出生日期:" prop="userbirthday" class="form-item">
<el-date-picker placeholder="选择出生日期" autocomplete="off" v-model="ruleForm.userbirthday" value-format="YYYY-MM-DD" type="date"/>
</el-form-item>
<el-form-item label="地址:" prop="useradress" class="form-item">
<el-input placeholder="请输入你的地址" type="text" autocomplete="off" v-model="ruleForm.useradress"/>
</el-form-item>
</el-form>
<div class="button-container">
<el-form-item class="create-body">
<el-button type="primary" @click="submitForm(ruleFormRef)">
提交
</el-button>
<el-button @click="resetForm(ruleFormRef)">全部重填</el-button>
</el-form-item>
</div>
</el-dialog>
const rules = reactive({
username: [{ required: true, message: '请输入姓名', trigger: 'blur' }],
userage: [
{ required: true, message: '请输入年龄', trigger: 'blur' },
{ type: 'number', message: '年龄必须为数字', trigger: 'blur' }
],
usersex: [{ required: true, message: '请选择性别', trigger: 'change' }],
userbirthday: [{ required: true, message: '请输入出生日期', trigger: 'blur' }],
useradress: [{ required: true, message: '请输入地址', trigger: 'blur' }]
})
const submitForm = (formEl) => {
if (!formEl) return
formEl.validate(async (valid) => {
if (valid) {
const formData = {
name: ruleForm.username,
age: ruleForm.userage,
sex: ruleForm.usersex === '男' ? 1 : 0,
birth: ruleForm.userbirthday,
addr: ruleForm.useradress
}
try {
if (currentEditId.value) {
formData.id = currentEditId.value
await proxy.$api.updateUser(formData)
ElMessage.success('修改成功')
} else {
await proxy.$api.addUser(formData)
ElMessage.success('新增成功')
}
show.value = false
resetForm(formEl)
getUserData() // 刷新数据
} catch (error) {
console.error('操作失败:', error)
ElMessage.error(currentEditId.value ? '修改失败' : '新增失败')
}
} else {
ElMessage.error('表单验证失败')
}
})
}

代码 1 是图片中的运行效果,而我们之前的数据库数据都绑定到了 ruleFormRef 对象上面,提交函数的第一个 if 就是判断是否对象为空,为空就不执行(无法进行提交)。

在 element plus 里面的 form 表单里的 api 他规定了我们表单输入数据的类型,也就是 年龄必须是数字等,都在上面的代码里面规定的有。
formEl.validate()是 element plus 里面的内置 api 他会自己检验数据类型是否正确,正确则 vaild 的值就为真,否则为假,之后就是我们通过使用 currentEditId.value 添加到我们定义的对象里面,对象的其他属性里的值,里面是我们更改的值,将 currentEditId.value 添加进去是为了传递给后端,为更改数据做索引。
app.post('/home/updateUser',(req,res)=>{
console.log('我拿到数据了',req.body)
const target = {
username:req.body.name,
age:req.body.age,
sex:req.body.sex,
birthday:req.body.birth,
address:req.body.addr,
id:req.body.id
}
list.updateUser(target).then((data)=>{
console.log(data)
res.send('更改成功')
})
})
updateUser: function(target){
return new Promise((resolve,reject)=>{
const sqlStr = "update users set ? where id = ?"
db.query(sqlStr,[target,target.id],(err,results)=>{
if(err){
reject(err)
return
}
else resolve(results)
})
})
},
剩下的增加,删除这两个接口我就不写了,他们可以使用我们上面讲的知识写出来,这是最简单的接口,写这么多也很累,见谅哈。
9、下期预告:
《进阶实战:TypeScript重构与数据可视化大屏》
🚀 技术升级
-
使用TypeScript重构前后端代码,提升类型安全和开发体验
-
接口类型定义、泛型应用、装饰器实战
-
从JavaScript到TypeScript的平滑迁移策略
📊 数据可视化
-
集成ECharts实现用户数据统计大屏
-
年龄分布饼图、地域分布地图、注册趋势折线图
-
实时数据更新与动画效果优化
🔧 工程化增强
-
更严格的ESLint配置与Prettier代码格式化
-
环境变量管理与多环境部署配置
-
性能优化与错误监控实践
🎯 新功能拓展
-
用户行为分析报表生成
-
数据导出功能(Excel/PDF)
-
权限管理模块设计与实现
更多推荐
所有评论(0)