简体中文 繁體中文 English Deutsch 한국 사람 بالعربية TÜRKÇE português คนไทย Français Japanese

站内搜索

搜索

活动公告

通知:本站资源由网友上传分享,如有违规等问题请到版务模块进行投诉,将及时处理!
10-23 09:31

Vue3项目中连接MySQL数据库的最佳实践与常见问题解决方案

SunJu_FaceMall

3万

主题

153

科技点

3万

积分

大区版主

碾压王

积分
32103
发表于 2025-9-29 11:40:00 | 显示全部楼层 |阅读模式 [标记阅至此楼]

马上注册,结交更多好友,享用更多功能,让你轻松玩转社区。

您需要 登录 才可以下载或查看,没有账号?立即注册

x
引言

在现代Web开发中,Vue3作为一款流行的前端框架,通常与后端数据库配合使用以构建全栈应用。MySQL作为最流行的关系型数据库之一,经常被选为数据存储解决方案。然而,由于Vue3是一个前端框架,它不能直接连接MySQL数据库,而是需要通过后端服务作为中间层。本文将详细介绍在Vue3项目中连接MySQL数据库的最佳实践,以及解决常见问题的方案。

Vue3连接MySQL的基本架构

在前后端分离的架构中,Vue3作为前端框架运行在浏览器中,而MySQL数据库运行在服务器上。它们之间的通信必须通过后端服务进行。基本架构如下:
  1. Vue3前端应用 -> HTTP请求 -> 后端API服务 -> MySQL数据库
复制代码

这种架构有以下几个优势:

• 安全性:数据库不会直接暴露给客户端
• 解耦:前后端可以独立开发和部署
• 可扩展性:后端可以服务多个前端应用

最佳实践

1. 使用后端API作为中间层

在Vue3项目中,最佳实践是使用后端服务作为API中间层来处理数据库操作。常用的后端技术栈包括:

• Node.js + Express/Koa/NestJS
• PHP + Laravel
• Java + Spring Boot
• Python + Django/FastAPI

下面以Node.js + Express为例,介绍如何搭建后端服务:
  1. // server.js
  2. const express = require('express');
  3. const mysql = require('mysql2/promise');
  4. const cors = require('cors');
  5. const app = express();
  6. const port = 3000;
  7. // 中间件
  8. app.use(cors());
  9. app.use(express.json());
  10. // MySQL连接池配置
  11. const pool = mysql.createPool({
  12.   host: 'localhost',
  13.   user: 'root',
  14.   password: 'password',
  15.   database: 'vue3_mysql_demo',
  16.   waitForConnections: true,
  17.   connectionLimit: 10,
  18.   queueLimit: 0
  19. });
  20. // 测试数据库连接
  21. app.get('/api/test-connection', async (req, res) => {
  22.   try {
  23.     const connection = await pool.getConnection();
  24.     await connection.ping();
  25.     connection.release();
  26.     res.json({ success: true, message: '数据库连接成功' });
  27.   } catch (error) {
  28.     console.error('数据库连接失败:', error);
  29.     res.status(500).json({ success: false, message: '数据库连接失败', error: error.message });
  30.   }
  31. });
  32. app.listen(port, () => {
  33.   console.log(`服务器运行在 http://localhost:${port}`);
  34. });
复制代码

2. RESTful API设计

设计良好的RESTful API可以使前后端交互更加清晰和高效。以下是一些设计原则:

• 使用合适的HTTP方法(GET、POST、PUT、DELETE等)
• 使用名词复数形式表示资源集合
• 使用清晰的URL结构
• 返回适当的HTTP状态码

例如,对于一个用户资源,可以设计如下API:
  1. // 用户相关API
  2. // 获取所有用户
  3. app.get('/api/users', async (req, res) => {
  4.   try {
  5.     const [rows] = await pool.query('SELECT * FROM users');
  6.     res.json({ success: true, data: rows });
  7.   } catch (error) {
  8.     res.status(500).json({ success: false, message: '获取用户列表失败', error: error.message });
  9.   }
  10. });
  11. // 获取单个用户
  12. app.get('/api/users/:id', async (req, res) => {
  13.   try {
  14.     const { id } = req.params;
  15.     const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [id]);
  16.    
  17.     if (rows.length === 0) {
  18.       return res.status(404).json({ success: false, message: '用户不存在' });
  19.     }
  20.    
  21.     res.json({ success: true, data: rows[0] });
  22.   } catch (error) {
  23.     res.status(500).json({ success: false, message: '获取用户信息失败', error: error.message });
  24.   }
  25. });
  26. // 创建用户
  27. app.post('/api/users', async (req, res) => {
  28.   try {
  29.     const { name, email, age } = req.body;
  30.    
  31.     // 验证请求数据
  32.     if (!name || !email) {
  33.       return res.status(400).json({ success: false, message: '姓名和邮箱是必填项' });
  34.     }
  35.    
  36.     const [result] = await pool.query(
  37.       'INSERT INTO users (name, email, age) VALUES (?, ?, ?)',
  38.       [name, email, age || null]
  39.     );
  40.    
  41.     res.status(201).json({
  42.       success: true,
  43.       message: '用户创建成功',
  44.       data: { id: result.insertId, name, email, age }
  45.     });
  46.   } catch (error) {
  47.     // 处理唯一键冲突等错误
  48.     if (error.code === 'ER_DUP_ENTRY') {
  49.       return res.status(409).json({ success: false, message: '邮箱已存在' });
  50.     }
  51.    
  52.     res.status(500).json({ success: false, message: '创建用户失败', error: error.message });
  53.   }
  54. });
  55. // 更新用户
  56. app.put('/api/users/:id', async (req, res) => {
  57.   try {
  58.     const { id } = req.params;
  59.     const { name, email, age } = req.body;
  60.    
  61.     // 检查用户是否存在
  62.     const [existingUsers] = await pool.query('SELECT * FROM users WHERE id = ?', [id]);
  63.     if (existingUsers.length === 0) {
  64.       return res.status(404).json({ success: false, message: '用户不存在' });
  65.     }
  66.    
  67.     // 构建更新语句
  68.     const updateFields = [];
  69.     const updateValues = [];
  70.    
  71.     if (name !== undefined) {
  72.       updateFields.push('name = ?');
  73.       updateValues.push(name);
  74.     }
  75.    
  76.     if (email !== undefined) {
  77.       updateFields.push('email = ?');
  78.       updateValues.push(email);
  79.     }
  80.    
  81.     if (age !== undefined) {
  82.       updateFields.push('age = ?');
  83.       updateValues.push(age);
  84.     }
  85.    
  86.     if (updateFields.length === 0) {
  87.       return res.status(400).json({ success: false, message: '没有提供要更新的字段' });
  88.     }
  89.    
  90.     updateValues.push(id);
  91.    
  92.     await pool.query(
  93.       `UPDATE users SET ${updateFields.join(', ')} WHERE id = ?`,
  94.       updateValues
  95.     );
  96.    
  97.     // 获取更新后的用户信息
  98.     const [updatedUsers] = await pool.query('SELECT * FROM users WHERE id = ?', [id]);
  99.    
  100.     res.json({ success: true, message: '用户更新成功', data: updatedUsers[0] });
  101.   } catch (error) {
  102.     if (error.code === 'ER_DUP_ENTRY') {
  103.       return res.status(409).json({ success: false, message: '邮箱已存在' });
  104.     }
  105.    
  106.     res.status(500).json({ success: false, message: '更新用户失败', error: error.message });
  107.   }
  108. });
  109. // 删除用户
  110. app.delete('/api/users/:id', async (req, res) => {
  111.   try {
  112.     const { id } = req.params;
  113.    
  114.     // 检查用户是否存在
  115.     const [existingUsers] = await pool.query('SELECT * FROM users WHERE id = ?', [id]);
  116.     if (existingUsers.length === 0) {
  117.       return res.status(404).json({ success: false, message: '用户不存在' });
  118.     }
  119.    
  120.     await pool.query('DELETE FROM users WHERE id = ?', [id]);
  121.    
  122.     res.json({ success: true, message: '用户删除成功' });
  123.   } catch (error) {
  124.     res.status(500).json({ success: false, message: '删除用户失败', error: error.message });
  125.   }
  126. });
复制代码

3. GraphQL作为替代方案

除了RESTful API,GraphQL也是一种现代的API设计方式,它允许客户端精确地请求需要的数据。下面是使用Express和Apollo Server实现GraphQL的示例:
  1. // 安装依赖: npm install apollo-server-express graphql
  2. const { ApolloServer, gql } = require('apollo-server-express');
  3. // GraphQL schema定义
  4. const typeDefs = gql`
  5.   type User {
  6.     id: ID!
  7.     name: String!
  8.     email: String!
  9.     age: Int
  10.   }
  11.   type Query {
  12.     users: [User]
  13.     user(id: ID!): User
  14.   }
  15.   type Mutation {
  16.     createUser(name: String!, email: String!, age: Int): User
  17.     updateUser(id: ID!, name: String, email: String, age: Int): User
  18.     deleteUser(id: ID!): Boolean
  19.   }
  20. `;
  21. // Resolver实现
  22. const resolvers = {
  23.   Query: {
  24.     users: async () => {
  25.       const [rows] = await pool.query('SELECT * FROM users');
  26.       return rows;
  27.     },
  28.     user: async (_, { id }) => {
  29.       const [rows] = await pool.query('SELECT * FROM users WHERE id = ?', [id]);
  30.       return rows[0];
  31.     }
  32.   },
  33.   Mutation: {
  34.     createUser: async (_, { name, email, age }) => {
  35.       const [result] = await pool.query(
  36.         'INSERT INTO users (name, email, age) VALUES (?, ?, ?)',
  37.         [name, email, age]
  38.       );
  39.       return { id: result.insertId, name, email, age };
  40.     },
  41.     updateUser: async (_, { id, name, email, age }) => {
  42.       // 检查用户是否存在
  43.       const [existingUsers] = await pool.query('SELECT * FROM users WHERE id = ?', [id]);
  44.       if (existingUsers.length === 0) {
  45.         throw new Error('用户不存在');
  46.       }
  47.       
  48.       // 构建更新语句
  49.       const updateFields = [];
  50.       const updateValues = [];
  51.       
  52.       if (name !== undefined) {
  53.         updateFields.push('name = ?');
  54.         updateValues.push(name);
  55.       }
  56.       
  57.       if (email !== undefined) {
  58.         updateFields.push('email = ?');
  59.         updateValues.push(email);
  60.       }
  61.       
  62.       if (age !== undefined) {
  63.         updateFields.push('age = ?');
  64.         updateValues.push(age);
  65.       }
  66.       
  67.       if (updateFields.length === 0) {
  68.         throw new Error('没有提供要更新的字段');
  69.       }
  70.       
  71.       updateValues.push(id);
  72.       
  73.       await pool.query(
  74.         `UPDATE users SET ${updateFields.join(', ')} WHERE id = ?`,
  75.         updateValues
  76.       );
  77.       
  78.       // 获取更新后的用户信息
  79.       const [updatedUsers] = await pool.query('SELECT * FROM users WHERE id = ?', [id]);
  80.       return updatedUsers[0];
  81.     },
  82.     deleteUser: async (_, { id }) => {
  83.       // 检查用户是否存在
  84.       const [existingUsers] = await pool.query('SELECT * FROM users WHERE id = ?', [id]);
  85.       if (existingUsers.length === 0) {
  86.         throw new Error('用户不存在');
  87.       }
  88.       
  89.       await pool.query('DELETE FROM users WHERE id = ?', [id]);
  90.       return true;
  91.     }
  92.   }
  93. };
  94. // 创建Apollo Server
  95. const server = new ApolloServer({ typeDefs, resolvers });
  96. // 应用中间件
  97. server.applyMiddleware({ app, path: '/graphql' });
复制代码

4. 数据库连接池配置

使用数据库连接池可以显著提高应用性能,避免频繁创建和销毁连接的开销。以下是MySQL连接池的配置示例:
  1. const pool = mysql.createPool({
  2.   host: 'localhost',
  3.   user: 'root',
  4.   password: 'password',
  5.   database: 'vue3_mysql_demo',
  6.   waitForConnections: true,    // 当连接池没有可用连接时,是否等待(而不是抛出错误)
  7.   connectionLimit: 10,         // 连接池大小
  8.   queueLimit: 0,               // 排队等待的最大连接数(0表示不限制)
  9.   namedPlaceholders: true,     // 启用命名占位符
  10.   dateStrings: true,           // 返回日期作为字符串而不是Date对象
  11.   supportBigNumbers: true,     // 支持大数字(BIGINT和DECIMAL列)
  12.   bigNumberStrings: true       // 将大数字作为字符串返回
  13. });
  14. // 使用连接池执行查询
  15. async function query(sql, params) {
  16.   let connection;
  17.   try {
  18.     connection = await pool.getConnection();
  19.     const [rows] = await connection.query(sql, params);
  20.     return rows;
  21.   } finally {
  22.     if (connection) connection.release();
  23.   }
  24. }
  25. // 使用示例
  26. app.get('/api/users', async (req, res) => {
  27.   try {
  28.     const users = await query('SELECT * FROM users');
  29.     res.json({ success: true, data: users });
  30.   } catch (error) {
  31.     res.status(500).json({ success: false, message: '获取用户列表失败', error: error.message });
  32.   }
  33. });
复制代码

5. 安全性考虑

在连接MySQL数据库时,安全性是一个重要考虑因素。以下是一些最佳实践:
  1. // 安装依赖: npm install dotenv
  2. // 创建.env文件
  3. DB_HOST=localhost
  4. DB_USER=root
  5. DB_PASSWORD=your_password
  6. DB_NAME=vue3_mysql_demo
  7. // 在服务器代码中加载环境变量
  8. require('dotenv').config();
  9. const pool = mysql.createPool({
  10.   host: process.env.DB_HOST,
  11.   user: process.env.DB_USER,
  12.   password: process.env.DB_PASSWORD,
  13.   database: process.env.DB_NAME,
  14.   // 其他配置...
  15. });
复制代码

使用参数化查询或预处理语句来防止SQL注入攻击:
  1. // 不安全的方式 - 容易受到SQL注入攻击
  2. app.get('/api/users/unsafe', async (req, res) => {
  3.   const { name } = req.query;
  4.   const [rows] = await pool.query(`SELECT * FROM users WHERE name = '${name}'`); // 危险!
  5.   res.json(rows);
  6. });
  7. // 安全的方式 - 使用参数化查询
  8. app.get('/api/users/safe', async (req, res) => {
  9.   const { name } = req.query;
  10.   const [rows] = await pool.query('SELECT * FROM users WHERE name = ?', [name]); // 安全
  11.   res.json(rows);
  12. });
  13. // 使用命名占位符(需要配置namedPlaceholders: true)
  14. app.get('/api/users/named', async (req, res) => {
  15.   const { name, email } = req.query;
  16.   const [rows] = await pool.query(
  17.     'SELECT * FROM users WHERE name = :name AND email = :email',
  18.     { name, email }
  19.   );
  20.   res.json(rows);
  21. });
复制代码
  1. // 安装依赖: npm install jsonwebtoken bcryptjs
  2. const jwt = require('jsonwebtoken');
  3. const bcrypt = require('bcryptjs');
  4. // 用户登录
  5. app.post('/api/login', async (req, res) => {
  6.   try {
  7.     const { email, password } = req.body;
  8.    
  9.     // 查找用户
  10.     const [users] = await pool.query('SELECT * FROM users WHERE email = ?', [email]);
  11.     if (users.length === 0) {
  12.       return res.status(401).json({ success: false, message: '邮箱或密码错误' });
  13.     }
  14.    
  15.     const user = users[0];
  16.    
  17.     // 验证密码
  18.     const isPasswordValid = await bcrypt.compare(password, user.password);
  19.     if (!isPasswordValid) {
  20.       return res.status(401).json({ success: false, message: '邮箱或密码错误' });
  21.     }
  22.    
  23.     // 生成JWT令牌
  24.     const token = jwt.sign(
  25.       { id: user.id, email: user.email },
  26.       process.env.JWT_SECRET || 'your-secret-key',
  27.       { expiresIn: '24h' }
  28.     );
  29.    
  30.     res.json({
  31.       success: true,
  32.       message: '登录成功',
  33.       data: {
  34.         token,
  35.         user: {
  36.           id: user.id,
  37.           name: user.name,
  38.           email: user.email
  39.         }
  40.       }
  41.     });
  42.   } catch (error) {
  43.     res.status(500).json({ success: false, message: '登录失败', error: error.message });
  44.   }
  45. });
  46. // 认证中间件
  47. function authenticateToken(req, res, next) {
  48.   const authHeader = req.headers['authorization'];
  49.   const token = authHeader && authHeader.split(' ')[1];
  50.   
  51.   if (!token) {
  52.     return res.status(401).json({ success: false, message: '访问令牌缺失' });
  53.   }
  54.   
  55.   jwt.verify(token, process.env.JWT_SECRET || 'your-secret-key', (err, user) => {
  56.     if (err) {
  57.       return res.status(403).json({ success: false, message: '无效的访问令牌' });
  58.     }
  59.     req.user = user;
  60.     next();
  61.   });
  62. }
  63. // 受保护的路由
  64. app.get('/api/profile', authenticateToken, async (req, res) => {
  65.   try {
  66.     const [users] = await pool.query('SELECT id, name, email, age FROM users WHERE id = ?', [req.user.id]);
  67.     if (users.length === 0) {
  68.       return res.status(404).json({ success: false, message: '用户不存在' });
  69.     }
  70.    
  71.     res.json({ success: true, data: users[0] });
  72.   } catch (error) {
  73.     res.status(500).json({ success: false, message: '获取用户信息失败', error: error.message });
  74.   }
  75. });
复制代码

Vue3前端调用API

在Vue3项目中,我们可以使用Axios或Fetch API来调用后端API。以下是使用Axios的示例:

1. 安装和配置Axios
  1. npm install axios
复制代码
  1. // src/utils/api.js
  2. import axios from 'axios';
  3. // 创建axios实例
  4. const api = axios.create({
  5.   baseURL: 'http://localhost:3000/api',
  6.   timeout: 10000,
  7.   headers: {
  8.     'Content-Type': 'application/json'
  9.   }
  10. });
  11. // 请求拦截器 - 添加认证令牌
  12. api.interceptors.request.use(
  13.   config => {
  14.     const token = localStorage.getItem('token');
  15.     if (token) {
  16.       config.headers.Authorization = `Bearer ${token}`;
  17.     }
  18.     return config;
  19.   },
  20.   error => {
  21.     return Promise.reject(error);
  22.   }
  23. );
  24. // 响应拦截器 - 处理错误
  25. api.interceptors.response.use(
  26.   response => {
  27.     return response.data;
  28.   },
  29.   error => {
  30.     if (error.response) {
  31.       // 服务器返回了错误状态码
  32.       const { status, data } = error.response;
  33.       
  34.       if (status === 401) {
  35.         // 未授权,清除令牌并跳转到登录页
  36.         localStorage.removeItem('token');
  37.         window.location.href = '/login';
  38.       }
  39.       
  40.       return Promise.reject({
  41.         success: false,
  42.         message: data.message || '请求失败',
  43.         status
  44.       });
  45.     } else if (error.request) {
  46.       // 请求已发出但没有收到响应
  47.       return Promise.reject({
  48.         success: false,
  49.         message: '服务器无响应,请检查网络连接'
  50.       });
  51.     } else {
  52.       // 请求设置出错
  53.       return Promise.reject({
  54.         success: false,
  55.         message: '请求设置错误: ' + error.message
  56.       });
  57.     }
  58.   }
  59. );
  60. export default api;
复制代码

2. 创建API服务
  1. // src/services/userService.js
  2. import api from '@/utils/api';
  3. export default {
  4.   // 获取所有用户
  5.   getUsers() {
  6.     return api.get('/users');
  7.   },
  8.   
  9.   // 获取单个用户
  10.   getUser(id) {
  11.     return api.get(`/users/${id}`);
  12.   },
  13.   
  14.   // 创建用户
  15.   createUser(userData) {
  16.     return api.post('/users', userData);
  17.   },
  18.   
  19.   // 更新用户
  20.   updateUser(id, userData) {
  21.     return api.put(`/users/${id}`, userData);
  22.   },
  23.   
  24.   // 删除用户
  25.   deleteUser(id) {
  26.     return api.delete(`/users/${id}`);
  27.   },
  28.   
  29.   // 用户登录
  30.   login(credentials) {
  31.     return api.post('/login', credentials);
  32.   },
  33.   
  34.   // 获取用户资料
  35.   getProfile() {
  36.     return api.get('/profile');
  37.   }
  38. };
复制代码

3. 在Vue组件中使用API
  1. <!-- src/views/UserList.vue -->
  2. <template>
  3.   <div class="user-list">
  4.     <h1>用户列表</h1>
  5.    
  6.     <div v-if="loading" class="loading">加载中...</div>
  7.    
  8.     <div v-else-if="error" class="error">{{ error }}</div>
  9.    
  10.     <div v-else>
  11.       <table class="user-table">
  12.         <thead>
  13.           <tr>
  14.             <th>ID</th>
  15.             <th>姓名</th>
  16.             <th>邮箱</th>
  17.             <th>年龄</th>
  18.             <th>操作</th>
  19.           </tr>
  20.         </thead>
  21.         <tbody>
  22.           <tr v-for="user in users" :key="user.id">
  23.             <td>{{ user.id }}</td>
  24.             <td>{{ user.name }}</td>
  25.             <td>{{ user.email }}</td>
  26.             <td>{{ user.age || '未设置' }}</td>
  27.             <td>
  28.               <button @click="editUser(user)" class="btn-edit">编辑</button>
  29.               <button @click="deleteUser(user.id)" class="btn-delete">删除</button>
  30.             </td>
  31.           </tr>
  32.         </tbody>
  33.       </table>
  34.       
  35.       <button @click="showAddForm = true" class="btn-add">添加用户</button>
  36.     </div>
  37.    
  38.     <!-- 添加/编辑用户表单 -->
  39.     <div v-if="showAddForm || editingUser" class="user-form">
  40.       <h2>{{ editingUser ? '编辑用户' : '添加用户' }}</h2>
  41.       
  42.       <div class="form-group">
  43.         <label for="name">姓名:</label>
  44.         <input
  45.           type="text"
  46.           id="name"
  47.           v-model="formData.name"
  48.           required
  49.         />
  50.       </div>
  51.       
  52.       <div class="form-group">
  53.         <label for="email">邮箱:</label>
  54.         <input
  55.           type="email"
  56.           id="email"
  57.           v-model="formData.email"
  58.           required
  59.         />
  60.       </div>
  61.       
  62.       <div class="form-group">
  63.         <label for="age">年龄:</label>
  64.         <input
  65.           type="number"
  66.           id="age"
  67.           v-model="formData.age"
  68.           min="1"
  69.         />
  70.       </div>
  71.       
  72.       <div class="form-actions">
  73.         <button @click="saveUser" class="btn-save">保存</button>
  74.         <button @click="cancelForm" class="btn-cancel">取消</button>
  75.       </div>
  76.     </div>
  77.   </div>
  78. </template>
  79. <script>
  80. import { ref, reactive, onMounted } from 'vue';
  81. import userService from '@/services/userService';
  82. export default {
  83.   name: 'UserList',
  84.   setup() {
  85.     const users = ref([]);
  86.     const loading = ref(false);
  87.     const error = ref(null);
  88.     const showAddForm = ref(false);
  89.     const editingUser = ref(null);
  90.     const formData = reactive({
  91.       name: '',
  92.       email: '',
  93.       age: null
  94.     });
  95.     // 获取用户列表
  96.     const fetchUsers = async () => {
  97.       loading.value = true;
  98.       error.value = null;
  99.       
  100.       try {
  101.         const response = await userService.getUsers();
  102.         if (response.success) {
  103.           users.value = response.data;
  104.         } else {
  105.           error.value = response.message || '获取用户列表失败';
  106.         }
  107.       } catch (err) {
  108.         error.value = err.message || '获取用户列表失败';
  109.         console.error('获取用户列表错误:', err);
  110.       } finally {
  111.         loading.value = false;
  112.       }
  113.     };
  114.     // 编辑用户
  115.     const editUser = (user) => {
  116.       editingUser.value = user;
  117.       formData.name = user.name;
  118.       formData.email = user.email;
  119.       formData.age = user.age;
  120.       showAddForm.value = true;
  121.     };
  122.     // 删除用户
  123.     const deleteUser = async (id) => {
  124.       if (!confirm('确定要删除这个用户吗?')) {
  125.         return;
  126.       }
  127.       
  128.       try {
  129.         const response = await userService.deleteUser(id);
  130.         if (response.success) {
  131.           // 从列表中移除已删除的用户
  132.           users.value = users.value.filter(user => user.id !== id);
  133.           alert('用户删除成功');
  134.         } else {
  135.           alert(response.message || '删除用户失败');
  136.         }
  137.       } catch (err) {
  138.         alert(err.message || '删除用户失败');
  139.         console.error('删除用户错误:', err);
  140.       }
  141.     };
  142.     // 保存用户
  143.     const saveUser = async () => {
  144.       // 验证表单
  145.       if (!formData.name || !formData.email) {
  146.         alert('姓名和邮箱是必填项');
  147.         return;
  148.       }
  149.       
  150.       try {
  151.         let response;
  152.         
  153.         if (editingUser.value) {
  154.           // 更新用户
  155.           response = await userService.updateUser(editingUser.value.id, {
  156.             name: formData.name,
  157.             email: formData.email,
  158.             age: formData.age
  159.           });
  160.          
  161.           if (response.success) {
  162.             // 更新列表中的用户数据
  163.             const index = users.value.findIndex(u => u.id === editingUser.value.id);
  164.             if (index !== -1) {
  165.               users.value[index] = response.data;
  166.             }
  167.             alert('用户更新成功');
  168.           }
  169.         } else {
  170.           // 创建新用户
  171.           response = await userService.createUser({
  172.             name: formData.name,
  173.             email: formData.email,
  174.             age: formData.age
  175.           });
  176.          
  177.           if (response.success) {
  178.             // 添加新用户到列表
  179.             users.value.push(response.data);
  180.             alert('用户创建成功');
  181.           }
  182.         }
  183.         
  184.         if (!response.success) {
  185.           alert(response.message || '保存用户失败');
  186.         } else {
  187.           cancelForm();
  188.         }
  189.       } catch (err) {
  190.         alert(err.message || '保存用户失败');
  191.         console.error('保存用户错误:', err);
  192.       }
  193.     };
  194.     // 取消表单
  195.     const cancelForm = () => {
  196.       showAddForm.value = false;
  197.       editingUser.value = null;
  198.       formData.name = '';
  199.       formData.email = '';
  200.       formData.age = null;
  201.     };
  202.     // 组件挂载时获取用户列表
  203.     onMounted(() => {
  204.       fetchUsers();
  205.     });
  206.     return {
  207.       users,
  208.       loading,
  209.       error,
  210.       showAddForm,
  211.       editingUser,
  212.       formData,
  213.       editUser,
  214.       deleteUser,
  215.       saveUser,
  216.       cancelForm
  217.     };
  218.   }
  219. };
  220. </script>
  221. <style scoped>
  222. .user-list {
  223.   max-width: 1000px;
  224.   margin: 0 auto;
  225.   padding: 20px;
  226. }
  227. .loading, .error {
  228.   padding: 20px;
  229.   text-align: center;
  230.   background-color: #f5f5f5;
  231.   border-radius: 4px;
  232.   margin: 20px 0;
  233. }
  234. .error {
  235.   color: #f44336;
  236.   background-color: #ffebee;
  237. }
  238. .user-table {
  239.   width: 100%;
  240.   border-collapse: collapse;
  241.   margin: 20px 0;
  242. }
  243. .user-table th, .user-table td {
  244.   padding: 12px 15px;
  245.   text-align: left;
  246.   border-bottom: 1px solid #ddd;
  247. }
  248. .user-table th {
  249.   background-color: #f5f5f5;
  250.   font-weight: bold;
  251. }
  252. .user-table tr:hover {
  253.   background-color: #f9f9f9;
  254. }
  255. .btn-edit, .btn-delete, .btn-add, .btn-save, .btn-cancel {
  256.   padding: 8px 12px;
  257.   border: none;
  258.   border-radius: 4px;
  259.   cursor: pointer;
  260.   margin-right: 5px;
  261. }
  262. .btn-edit {
  263.   background-color: #2196f3;
  264.   color: white;
  265. }
  266. .btn-delete {
  267.   background-color: #f44336;
  268.   color: white;
  269. }
  270. .btn-add {
  271.   background-color: #4caf50;
  272.   color: white;
  273.   margin-top: 20px;
  274. }
  275. .btn-save {
  276.   background-color: #4caf50;
  277.   color: white;
  278. }
  279. .btn-cancel {
  280.   background-color: #9e9e9e;
  281.   color: white;
  282. }
  283. .user-form {
  284.   margin-top: 30px;
  285.   padding: 20px;
  286.   border: 1px solid #ddd;
  287.   border-radius: 4px;
  288.   background-color: #f9f9f9;
  289. }
  290. .form-group {
  291.   margin-bottom: 15px;
  292. }
  293. .form-group label {
  294.   display: block;
  295.   margin-bottom: 5px;
  296.   font-weight: bold;
  297. }
  298. .form-group input {
  299.   width: 100%;
  300.   padding: 8px;
  301.   border: 1px solid #ddd;
  302.   border-radius: 4px;
  303. }
  304. .form-actions {
  305.   margin-top: 20px;
  306. }
  307. </style>
复制代码

常见问题及解决方案

1. CORS问题

当Vue3前端应用和后端API服务运行在不同的域或端口时,会遇到跨域资源共享(CORS)问题。以下是解决方案:
  1. // 使用cors中间件
  2. const cors = require('cors');
  3. app.use(cors());
  4. // 或者自定义CORS配置
  5. app.use(cors({
  6.   origin: 'http://localhost:8080', // 允许的前端域名
  7.   methods: ['GET', 'POST', 'PUT', 'DELETE'],
  8.   allowedHeaders: ['Content-Type', 'Authorization'],
  9.   credentials: true // 允许发送cookie
  10. }));
复制代码

在Vue项目的vue.config.js文件中配置代理:
  1. // vue.config.js
  2. module.exports = {
  3.   devServer: {
  4.     proxy: {
  5.       '/api': {
  6.         target: 'http://localhost:3000',
  7.         changeOrigin: true,
  8.         pathRewrite: {
  9.           '^/api': '/api' // 不重写路径
  10.         }
  11.       }
  12.     }
  13.   }
  14. };
复制代码

然后修改API实例的baseURL:
  1. // src/utils/api.js
  2. const api = axios.create({
  3.   baseURL: '/api', // 使用相对路径,通过代理转发
  4.   timeout: 10000,
  5.   headers: {
  6.     'Content-Type': 'application/json'
  7.   }
  8. });
复制代码

2. 数据库连接泄露

数据库连接泄露是指应用程序没有正确释放数据库连接,导致连接池中的连接被耗尽。以下是解决方案:
  1. // 不安全的做法 - 可能导致连接泄露
  2. app.get('/api/users/unsafe', async (req, res) => {
  3.   const connection = await pool.getConnection();
  4.   const [rows] = await connection.query('SELECT * FROM users');
  5.   // 如果在查询和释放连接之间发生错误,连接可能不会被释放
  6.   connection.release();
  7.   res.json(rows);
  8. });
  9. // 安全的做法 - 使用try-finally确保连接释放
  10. app.get('/api/users/safe', async (req, res) => {
  11.   let connection;
  12.   try {
  13.     connection = await pool.getConnection();
  14.     const [rows] = await connection.query('SELECT * FROM users');
  15.     res.json(rows);
  16.   } catch (error) {
  17.     console.error('获取用户列表失败:', error);
  18.     res.status(500).json({ success: false, message: '获取用户列表失败' });
  19.   } finally {
  20.     if (connection) connection.release(); // 确保连接总是被释放
  21.   }
  22. });
复制代码
  1. // 使用连接池的query方法,它会自动获取和释放连接
  2. app.get('/api/users/auto', async (req, res) => {
  3.   try {
  4.     const [rows] = await pool.query('SELECT * FROM users');
  5.     res.json(rows);
  6.   } catch (error) {
  7.     console.error('获取用户列表失败:', error);
  8.     res.status(500).json({ success: false, message: '获取用户列表失败' });
  9.   }
  10. });
复制代码
  1. // 定期记录连接池状态
  2. setInterval(() => {
  3.   console.log('连接池状态:', {
  4.     totalConnections: pool.pool.allConnections.length,
  5.     activeConnections: pool.pool.allConnections.length - pool.pool.freeConnections.length,
  6.     freeConnections: pool.pool.freeConnections.length,
  7.     queuedRequests: pool.pool.acquiringConnections.length
  8.   });
  9. }, 30000); // 每30秒记录一次
复制代码

3. SQL注入防护

SQL注入是一种常见的安全漏洞,攻击者可以通过恶意输入来执行未授权的SQL命令。以下是防护措施:
  1. // 不安全的做法 - 容易受到SQL注入攻击
  2. app.get('/api/users/search/unsafe', async (req, res) => {
  3.   const { term } = req.query;
  4.   // 直接拼接SQL字符串,容易受到SQL注入攻击
  5.   const sql = `SELECT * FROM users WHERE name LIKE '%${term}%'`;
  6.   const [rows] = await pool.query(sql);
  7.   res.json(rows);
  8. });
  9. // 安全的做法 - 使用参数化查询
  10. app.get('/api/users/search/safe', async (req, res) => {
  11.   const { term } = req.query;
  12.   // 使用参数化查询,防止SQL注入
  13.   const [rows] = await pool.query(
  14.     'SELECT * FROM users WHERE name LIKE ?',
  15.     [`%${term}%`]
  16.   );
  17.   res.json(rows);
  18. });
复制代码

使用ORM(对象关系映射)工具如Sequelize、TypeORM等可以进一步减少SQL注入的风险:
  1. // 安装Sequelize: npm install sequelize mysql2
  2. const { Sequelize, DataTypes } = require('sequelize');
  3. // 初始化Sequelize
  4. const sequelize = new Sequelize(
  5.   process.env.DB_NAME,
  6.   process.env.DB_USER,
  7.   process.env.DB_PASSWORD,
  8.   {
  9.     host: process.env.DB_HOST,
  10.     dialect: 'mysql'
  11.   }
  12. );
  13. // 定义用户模型
  14. const User = sequelize.define('User', {
  15.   name: {
  16.     type: DataTypes.STRING,
  17.     allowNull: false
  18.   },
  19.   email: {
  20.     type: DataTypes.STRING,
  21.     allowNull: false,
  22.     unique: true
  23.   },
  24.   age: {
  25.     type: DataTypes.INTEGER,
  26.     allowNull: true
  27.   }
  28. });
  29. // 使用ORM进行查询
  30. app.get('/api/users/orm', async (req, res) => {
  31.   try {
  32.     const users = await User.findAll();
  33.     res.json({ success: true, data: users });
  34.   } catch (error) {
  35.     res.status(500).json({ success: false, message: '获取用户列表失败', error: error.message });
  36.   }
  37. });
  38. // 使用ORM进行条件查询
  39. app.get('/api/users/search/orm', async (req, res) => {
  40.   try {
  41.     const { term } = req.query;
  42.     const users = await User.findAll({
  43.       where: {
  44.         name: {
  45.           [Sequelize.Op.like]: `%${term}%`
  46.         }
  47.       }
  48.     });
  49.     res.json({ success: true, data: users });
  50.   } catch (error) {
  51.     res.status(500).json({ success: false, message: '搜索用户失败', error: error.message });
  52.   }
  53. });
复制代码

4. 性能优化

在Vue3项目连接MySQL数据库时,性能优化是一个重要考虑因素。以下是一些优化策略:
  1. // 使用索引优化查询
  2. // 确保在常用查询条件的列上创建索引
  3. // 例如:CREATE INDEX idx_users_email ON users(email);
  4. // 避免N+1查询问题
  5. // 不好的做法 - 可能导致N+1查询问题
  6. app.get('/api/users-with-posts/inefficient', async (req, res) => {
  7.   try {
  8.     const [users] = await pool.query('SELECT * FROM users');
  9.    
  10.     // 对每个用户执行额外的查询获取帖子
  11.     for (const user of users) {
  12.       const [posts] = await pool.query('SELECT * FROM posts WHERE userId = ?', [user.id]);
  13.       user.posts = posts;
  14.     }
  15.    
  16.     res.json({ success: true, data: users });
  17.   } catch (error) {
  18.     res.status(500).json({ success: false, message: '获取用户和帖子失败', error: error.message });
  19.   }
  20. });
  21. // 好的做法 - 使用JOIN或子查询一次性获取所有数据
  22. app.get('/api/users-with-posts/efficient', async (req, res) => {
  23.   try {
  24.     // 使用JOIN一次性获取所有数据
  25.     const [rows] = await pool.query(`
  26.       SELECT u.*, p.id as postId, p.title, p.content
  27.       FROM users u
  28.       LEFT JOIN posts p ON u.id = p.userId
  29.       ORDER BY u.id, p.id
  30.     `);
  31.    
  32.     // 处理结果,将帖子分组到用户对象中
  33.     const users = [];
  34.     let currentUser = null;
  35.    
  36.     for (const row of rows) {
  37.       if (!currentUser || currentUser.id !== row.id) {
  38.         currentUser = {
  39.           id: row.id,
  40.           name: row.name,
  41.           email: row.email,
  42.           age: row.age,
  43.           posts: []
  44.         };
  45.         users.push(currentUser);
  46.       }
  47.       
  48.       if (row.postId) {
  49.         currentUser.posts.push({
  50.           id: row.postId,
  51.           title: row.title,
  52.           content: row.content
  53.         });
  54.       }
  55.     }
  56.    
  57.     res.json({ success: true, data: users });
  58.   } catch (error) {
  59.     res.status(500).json({ success: false, message: '获取用户和帖子失败', error: error.message });
  60.   }
  61. });
复制代码
  1. // 实现分页查询
  2. app.get('/api/users/paginated', async (req, res) => {
  3.   try {
  4.     // 获取分页参数
  5.     const page = parseInt(req.query.page) || 1;
  6.     const limit = parseInt(req.query.limit) || 10;
  7.     const offset = (page - 1) * limit;
  8.    
  9.     // 获取总记录数
  10.     const [countResult] = await pool.query('SELECT COUNT(*) as total FROM users');
  11.     const total = countResult[0].total;
  12.    
  13.     // 获取分页数据
  14.     const [users] = await pool.query(
  15.       'SELECT id, name, email, age FROM users LIMIT ? OFFSET ?',
  16.       [limit, offset]
  17.     );
  18.    
  19.     // 计算总页数
  20.     const totalPages = Math.ceil(total / limit);
  21.    
  22.     res.json({
  23.       success: true,
  24.       data: {
  25.         users,
  26.         pagination: {
  27.           page,
  28.           limit,
  29.           total,
  30.           totalPages
  31.         }
  32.       }
  33.     });
  34.   } catch (error) {
  35.     res.status(500).json({ success: false, message: '获取分页用户列表失败', error: error.message });
  36.   }
  37. });
复制代码
  1. // 安装内存缓存: npm install memory-cache
  2. const cache = require('memory-cache');
  3. // 实现简单的缓存中间件
  4. function cacheMiddleware(duration) {
  5.   return (req, res, next) => {
  6.     const key = '__express__' + req.originalUrl || req.url;
  7.     const cachedBody = cache.get(key);
  8.    
  9.     if (cachedBody) {
  10.       res.send(cachedBody);
  11.       return;
  12.     }
  13.    
  14.     res.sendResponse = res.send;
  15.     res.send = (body) => {
  16.       cache.put(key, body, duration * 1000);
  17.       res.sendResponse(body);
  18.     };
  19.    
  20.     next();
  21.   };
  22. }
  23. // 使用缓存中间件
  24. app.get('/api/users/cached', cacheMiddleware(60), async (req, res) => {
  25.   try {
  26.     const [users] = await pool.query('SELECT * FROM users');
  27.     res.json({ success: true, data: users });
  28.   } catch (error) {
  29.     res.status(500).json({ success: false, message: '获取用户列表失败', error: error.message });
  30.   }
  31. });
复制代码

5. 错误处理

良好的错误处理可以提高应用的健壮性和用户体验。以下是一些错误处理的最佳实践:
  1. // 全局错误处理中间件
  2. app.use((err, req, res, next) => {
  3.   console.error('全局错误:', err);
  4.   
  5.   // 根据错误类型返回不同的状态码
  6.   if (err.code === 'ER_DUP_ENTRY') {
  7.     return res.status(409).json({ success: false, message: '数据已存在' });
  8.   }
  9.   
  10.   if (err.name === 'ValidationError') {
  11.     return res.status(400).json({ success: false, message: '数据验证失败', details: err.message });
  12.   }
  13.   
  14.   if (err.name === 'UnauthorizedError') {
  15.     return res.status(401).json({ success: false, message: '未授权访问' });
  16.   }
  17.   
  18.   // 默认服务器错误
  19.   res.status(500).json({ success: false, message: '服务器内部错误' });
  20. });
  21. // 在路由中使用try-catch捕获错误
  22. app.get('/api/users/error', async (req, res, next) => {
  23.   try {
  24.     // 可能出错的代码
  25.     const [users] = await pool.query('SELECT * FROM non_existent_table');
  26.     res.json({ success: true, data: users });
  27.   } catch (error) {
  28.     // 将错误传递给全局错误处理中间件
  29.     next(error);
  30.   }
  31. });
复制代码
  1. // 处理需要多个数据库操作的事务
  2. app.post('/api/transfer', async (req, res) => {
  3.   const { fromUserId, toUserId, amount } = req.body;
  4.   
  5.   if (!fromUserId || !toUserId || !amount) {
  6.     return res.status(400).json({ success: false, message: '缺少必要参数' });
  7.   }
  8.   
  9.   if (fromUserId === toUserId) {
  10.     return res.status(400).json({ success: false, message: '转出和转入用户不能相同' });
  11.   }
  12.   
  13.   let connection;
  14.   
  15.   try {
  16.     // 获取连接并开始事务
  17.     connection = await pool.getConnection();
  18.     await connection.beginTransaction();
  19.    
  20.     // 检查转出用户余额
  21.     const [fromUsers] = await connection.query(
  22.       'SELECT * FROM accounts WHERE userId = ? FOR UPDATE',
  23.       [fromUserId]
  24.     );
  25.    
  26.     if (fromUsers.length === 0) {
  27.       throw new Error('转出用户不存在');
  28.     }
  29.    
  30.     const fromUser = fromUsers[0];
  31.    
  32.     if (fromUser.balance < amount) {
  33.       throw new Error('余额不足');
  34.     }
  35.    
  36.     // 检查转入用户
  37.     const [toUsers] = await connection.query(
  38.       'SELECT * FROM accounts WHERE userId = ? FOR UPDATE',
  39.       [toUserId]
  40.     );
  41.    
  42.     if (toUsers.length === 0) {
  43.       throw new Error('转入用户不存在');
  44.     }
  45.    
  46.     const toUser = toUsers[0];
  47.    
  48.     // 更新转出用户余额
  49.     await connection.query(
  50.       'UPDATE accounts SET balance = balance - ? WHERE userId = ?',
  51.       [amount, fromUserId]
  52.     );
  53.    
  54.     // 更新转入用户余额
  55.     await connection.query(
  56.       'UPDATE accounts SET balance = balance + ? WHERE userId = ?',
  57.       [amount, toUserId]
  58.     );
  59.    
  60.     // 记录交易
  61.     await connection.query(
  62.       'INSERT INTO transactions (fromUserId, toUserId, amount, createdAt) VALUES (?, ?, ?, NOW())',
  63.       [fromUserId, toUserId, amount]
  64.     );
  65.    
  66.     // 提交事务
  67.     await connection.commit();
  68.    
  69.     res.json({ success: true, message: '转账成功' });
  70.   } catch (error) {
  71.     // 发生错误,回滚事务
  72.     if (connection) {
  73.       await connection.rollback();
  74.     }
  75.    
  76.     console.error('转账失败:', error);
  77.     res.status(500).json({ success: false, message: error.message || '转账失败' });
  78.   } finally {
  79.     if (connection) {
  80.       connection.release();
  81.     }
  82.   }
  83. });
复制代码

完整示例:一个简单的CRUD应用

下面是一个完整的Vue3项目连接MySQL数据库的CRUD(创建、读取、更新、删除)应用示例。

1. 数据库准备

首先,创建MySQL数据库和表:
  1. -- 创建数据库
  2. CREATE DATABASE vue3_mysql_demo;
  3. -- 使用数据库
  4. USE vue3_mysql_demo;
  5. -- 创建用户表
  6. CREATE TABLE users (
  7.   id INT AUTO_INCREMENT PRIMARY KEY,
  8.   name VARCHAR(100) NOT NULL,
  9.   email VARCHAR(100) NOT NULL UNIQUE,
  10.   age INT,
  11.   created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
  12.   updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
  13. );
  14. -- 插入一些示例数据
  15. INSERT INTO users (name, email, age) VALUES
  16. ('张三', 'zhangsan@example.com', 25),
  17. ('李四', 'lisi@example.com', 30),
  18. ('王五', 'wangwu@example.com', 28);
复制代码

2. 后端服务

创建一个Node.js/Express后端服务:
  1. // server.js
  2. require('dotenv').config();
  3. const express = require('express');
  4. const mysql = require('mysql2/promise');
  5. const cors = require('cors');
  6. const bcrypt = require('bcryptjs');
  7. const jwt = require('jsonwebtoken');
  8. const app = express();
  9. const port = process.env.PORT || 3000;
  10. // 中间件
  11. app.use(cors());
  12. app.use(express.json());
  13. // MySQL连接池配置
  14. const pool = mysql.createPool({
  15.   host: process.env.DB_HOST || 'localhost',
  16.   user: process.env.DB_USER || 'root',
  17.   password: process.env.DB_PASSWORD || '',
  18.   database: process.env.DB_NAME || 'vue3_mysql_demo',
  19.   waitForConnections: true,
  20.   connectionLimit: 10,
  21.   queueLimit: 0,
  22.   namedPlaceholders: true
  23. });
  24. // 测试数据库连接
  25. app.get('/api/test-connection', async (req, res) => {
  26.   try {
  27.     const connection = await pool.getConnection();
  28.     await connection.ping();
  29.     connection.release();
  30.     res.json({ success: true, message: '数据库连接成功' });
  31.   } catch (error) {
  32.     console.error('数据库连接失败:', error);
  33.     res.status(500).json({ success: false, message: '数据库连接失败', error: error.message });
  34.   }
  35. });
  36. // 用户相关API
  37. // 获取所有用户
  38. app.get('/api/users', async (req, res) => {
  39.   try {
  40.     const [rows] = await pool.query('SELECT id, name, email, age FROM users');
  41.     res.json({ success: true, data: rows });
  42.   } catch (error) {
  43.     res.status(500).json({ success: false, message: '获取用户列表失败', error: error.message });
  44.   }
  45. });
  46. // 获取单个用户
  47. app.get('/api/users/:id', async (req, res) => {
  48.   try {
  49.     const { id } = req.params;
  50.     const [rows] = await pool.query('SELECT id, name, email, age FROM users WHERE id = ?', [id]);
  51.    
  52.     if (rows.length === 0) {
  53.       return res.status(404).json({ success: false, message: '用户不存在' });
  54.     }
  55.    
  56.     res.json({ success: true, data: rows[0] });
  57.   } catch (error) {
  58.     res.status(500).json({ success: false, message: '获取用户信息失败', error: error.message });
  59.   }
  60. });
  61. // 创建用户
  62. app.post('/api/users', async (req, res) => {
  63.   try {
  64.     const { name, email, age } = req.body;
  65.    
  66.     // 验证请求数据
  67.     if (!name || !email) {
  68.       return res.status(400).json({ success: false, message: '姓名和邮箱是必填项' });
  69.     }
  70.    
  71.     const [result] = await pool.query(
  72.       'INSERT INTO users (name, email, age) VALUES (?, ?, ?)',
  73.       [name, email, age || null]
  74.     );
  75.    
  76.     res.status(201).json({
  77.       success: true,
  78.       message: '用户创建成功',
  79.       data: { id: result.insertId, name, email, age }
  80.     });
  81.   } catch (error) {
  82.     // 处理唯一键冲突等错误
  83.     if (error.code === 'ER_DUP_ENTRY') {
  84.       return res.status(409).json({ success: false, message: '邮箱已存在' });
  85.     }
  86.    
  87.     res.status(500).json({ success: false, message: '创建用户失败', error: error.message });
  88.   }
  89. });
  90. // 更新用户
  91. app.put('/api/users/:id', async (req, res) => {
  92.   try {
  93.     const { id } = req.params;
  94.     const { name, email, age } = req.body;
  95.    
  96.     // 检查用户是否存在
  97.     const [existingUsers] = await pool.query('SELECT * FROM users WHERE id = ?', [id]);
  98.     if (existingUsers.length === 0) {
  99.       return res.status(404).json({ success: false, message: '用户不存在' });
  100.     }
  101.    
  102.     // 构建更新语句
  103.     const updateFields = [];
  104.     const updateValues = [];
  105.    
  106.     if (name !== undefined) {
  107.       updateFields.push('name = ?');
  108.       updateValues.push(name);
  109.     }
  110.    
  111.     if (email !== undefined) {
  112.       updateFields.push('email = ?');
  113.       updateValues.push(email);
  114.     }
  115.    
  116.     if (age !== undefined) {
  117.       updateFields.push('age = ?');
  118.       updateValues.push(age);
  119.     }
  120.    
  121.     if (updateFields.length === 0) {
  122.       return res.status(400).json({ success: false, message: '没有提供要更新的字段' });
  123.     }
  124.    
  125.     updateValues.push(id);
  126.    
  127.     await pool.query(
  128.       `UPDATE users SET ${updateFields.join(', ')} WHERE id = ?`,
  129.       updateValues
  130.     );
  131.    
  132.     // 获取更新后的用户信息
  133.     const [updatedUsers] = await pool.query('SELECT id, name, email, age FROM users WHERE id = ?', [id]);
  134.    
  135.     res.json({ success: true, message: '用户更新成功', data: updatedUsers[0] });
  136.   } catch (error) {
  137.     if (error.code === 'ER_DUP_ENTRY') {
  138.       return res.status(409).json({ success: false, message: '邮箱已存在' });
  139.     }
  140.    
  141.     res.status(500).json({ success: false, message: '更新用户失败', error: error.message });
  142.   }
  143. });
  144. // 删除用户
  145. app.delete('/api/users/:id', async (req, res) => {
  146.   try {
  147.     const { id } = req.params;
  148.    
  149.     // 检查用户是否存在
  150.     const [existingUsers] = await pool.query('SELECT * FROM users WHERE id = ?', [id]);
  151.     if (existingUsers.length === 0) {
  152.       return res.status(404).json({ success: false, message: '用户不存在' });
  153.     }
  154.    
  155.     await pool.query('DELETE FROM users WHERE id = ?', [id]);
  156.    
  157.     res.json({ success: true, message: '用户删除成功' });
  158.   } catch (error) {
  159.     res.status(500).json({ success: false, message: '删除用户失败', error: error.message });
  160.   }
  161. });
  162. // 全局错误处理中间件
  163. app.use((err, req, res, next) => {
  164.   console.error('全局错误:', err);
  165.   
  166.   if (err.code === 'ER_DUP_ENTRY') {
  167.     return res.status(409).json({ success: false, message: '数据已存在' });
  168.   }
  169.   
  170.   res.status(500).json({ success: false, message: '服务器内部错误' });
  171. });
  172. // 启动服务器
  173. app.listen(port, () => {
  174.   console.log(`服务器运行在 http://localhost:${port}`);
  175. });
复制代码

3. Vue3前端应用

创建Vue3前端应用,实现用户管理功能:
  1. <!-- src/App.vue -->
  2. <template>
  3.   <div id="app">
  4.     <nav class="navbar">
  5.       <div class="nav-brand">Vue3 & MySQL Demo</div>
  6.       <div class="nav-links">
  7.         <router-link to="/" class="nav-link">首页</router-link>
  8.         <router-link to="/users" class="nav-link">用户管理</router-link>
  9.       </div>
  10.     </nav>
  11.    
  12.     <div class="container">
  13.       <router-view />
  14.     </div>
  15.    
  16.     <footer class="footer">
  17.       <p>&copy; 2023 Vue3 & MySQL Demo. All rights reserved.</p>
  18.     </footer>
  19.   </div>
  20. </template>
  21. <script>
  22. export default {
  23.   name: 'App'
  24. }
  25. </script>
  26. <style>
  27. * {
  28.   margin: 0;
  29.   padding: 0;
  30.   box-sizing: border-box;
  31. }
  32. body {
  33.   font-family: 'Arial', sans-serif;
  34.   line-height: 1.6;
  35.   color: #333;
  36.   background-color: #f5f5f5;
  37. }
  38. #app {
  39.   display: flex;
  40.   flex-direction: column;
  41.   min-height: 100vh;
  42. }
  43. .navbar {
  44.   display: flex;
  45.   justify-content: space-between;
  46.   align-items: center;
  47.   background-color: #333;
  48.   color: white;
  49.   padding: 1rem 2rem;
  50. }
  51. .nav-brand {
  52.   font-size: 1.5rem;
  53.   font-weight: bold;
  54. }
  55. .nav-links {
  56.   display: flex;
  57. }
  58. .nav-link {
  59.   color: white;
  60.   text-decoration: none;
  61.   margin-left: 1rem;
  62.   padding: 0.5rem;
  63.   border-radius: 4px;
  64.   transition: background-color 0.3s;
  65. }
  66. .nav-link:hover {
  67.   background-color: #555;
  68. }
  69. .router-link-active {
  70.   background-color: #555;
  71. }
  72. .container {
  73.   flex: 1;
  74.   max-width: 1200px;
  75.   margin: 2rem auto;
  76.   padding: 0 1rem;
  77.   width: 100%;
  78. }
  79. .footer {
  80.   background-color: #333;
  81.   color: white;
  82.   text-align: center;
  83.   padding: 1rem;
  84.   margin-top: auto;
  85. }
  86. </style>
复制代码
  1. <!-- src/views/Home.vue -->
  2. <template>
  3.   <div class="home">
  4.     <h1>Vue3 & MySQL 集成示例</h1>
  5.    
  6.     <div class="card">
  7.       <h2>项目介绍</h2>
  8.       <p>这是一个展示如何在Vue3项目中连接MySQL数据库的示例应用。本项目采用前后端分离架构,使用Vue3作为前端框架,Node.js/Express作为后端服务,MySQL作为数据库。</p>
  9.     </div>
  10.    
  11.     <div class="card">
  12.       <h2>技术栈</h2>
  13.       <ul>
  14.         <li>前端:Vue3, Vue Router, Axios</li>
  15.         <li>后端:Node.js, Express</li>
  16.         <li>数据库:MySQL</li>
  17.       </ul>
  18.     </div>
  19.    
  20.     <div class="card">
  21.       <h2>功能特点</h2>
  22.       <ul>
  23.         <li>用户管理(增删改查)</li>
  24.         <li>RESTful API设计</li>
  25.         <li>数据库连接池</li>
  26.         <li>错误处理</li>
  27.         <li>安全性考虑</li>
  28.       </ul>
  29.     </div>
  30.    
  31.     <div class="card">
  32.       <h2>快速开始</h2>
  33.       <ol>
  34.         <li>克隆项目代码</li>
  35.         <li>安装依赖:npm install</li>
  36.         <li>配置数据库连接信息</li>
  37.         <li>启动后端服务:node server.js</li>
  38.         <li>启动前端应用:npm run serve</li>
  39.       </ol>
  40.     </div>
  41.    
  42.     <router-link to="/users" class="btn-primary">查看用户管理</router-link>
  43.   </div>
  44. </template>
  45. <script>
  46. export default {
  47.   name: 'Home'
  48. }
  49. </script>
  50. <style scoped>
  51. .home {
  52.   max-width: 800px;
  53.   margin: 0 auto;
  54. }
  55. .card {
  56.   background-color: white;
  57.   border-radius: 8px;
  58.   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  59.   padding: 1.5rem;
  60.   margin-bottom: 1.5rem;
  61. }
  62. h1 {
  63.   color: #2c3e50;
  64.   margin-bottom: 1.5rem;
  65.   text-align: center;
  66. }
  67. h2 {
  68.   color: #2c3e50;
  69.   margin-bottom: 1rem;
  70.   border-bottom: 2px solid #eee;
  71.   padding-bottom: 0.5rem;
  72. }
  73. ul {
  74.   padding-left: 1.5rem;
  75. }
  76. li {
  77.   margin-bottom: 0.5rem;
  78. }
  79. .btn-primary {
  80.   display: inline-block;
  81.   background-color: #42b983;
  82.   color: white;
  83.   padding: 0.75rem 1.5rem;
  84.   border-radius: 4px;
  85.   text-decoration: none;
  86.   font-weight: bold;
  87.   margin-top: 1rem;
  88.   transition: background-color 0.3s;
  89. }
  90. .btn-primary:hover {
  91.   background-color: #3aa876;
  92. }
  93. </style>
复制代码
  1. <!-- src/views/UserList.vue -->
  2. <template>
  3.   <div class="user-list">
  4.     <h1>用户管理</h1>
  5.    
  6.     <div class="actions">
  7.       <button @click="showAddForm = true" class="btn-add">添加用户</button>
  8.       <div class="search">
  9.         <input
  10.           type="text"
  11.           v-model="searchTerm"
  12.           placeholder="搜索用户..."
  13.           @input="searchUsers"
  14.         />
  15.       </div>
  16.     </div>
  17.    
  18.     <div v-if="loading" class="loading">加载中...</div>
  19.    
  20.     <div v-else-if="error" class="error">{{ error }}</div>
  21.    
  22.     <div v-else-if="filteredUsers.length === 0" class="no-data">
  23.       {{ searchTerm ? '没有找到匹配的用户' : '暂无用户数据' }}
  24.     </div>
  25.    
  26.     <div v-else>
  27.       <div class="user-grid">
  28.         <div v-for="user in filteredUsers" :key="user.id" class="user-card">
  29.           <div class="user-avatar">
  30.             {{ user.name.charAt(0).toUpperCase() }}
  31.           </div>
  32.           <div class="user-info">
  33.             <h3>{{ user.name }}</h3>
  34.             <p>{{ user.email }}</p>
  35.             <p v-if="user.age">年龄: {{ user.age }}</p>
  36.           </div>
  37.           <div class="user-actions">
  38.             <button @click="editUser(user)" class="btn-edit">编辑</button>
  39.             <button @click="deleteUser(user.id)" class="btn-delete">删除</button>
  40.           </div>
  41.         </div>
  42.       </div>
  43.     </div>
  44.    
  45.     <!-- 添加/编辑用户表单 -->
  46.     <div v-if="showAddForm || editingUser" class="modal">
  47.       <div class="modal-content">
  48.         <div class="modal-header">
  49.           <h2>{{ editingUser ? '编辑用户' : '添加用户' }}</h2>
  50.           <button @click="cancelForm" class="close-btn">&times;</button>
  51.         </div>
  52.         
  53.         <div class="modal-body">
  54.           <div class="form-group">
  55.             <label for="name">姓名:</label>
  56.             <input
  57.               type="text"
  58.               id="name"
  59.               v-model="formData.name"
  60.               required
  61.             />
  62.           </div>
  63.          
  64.           <div class="form-group">
  65.             <label for="email">邮箱:</label>
  66.             <input
  67.               type="email"
  68.               id="email"
  69.               v-model="formData.email"
  70.               required
  71.             />
  72.           </div>
  73.          
  74.           <div class="form-group">
  75.             <label for="age">年龄:</label>
  76.             <input
  77.               type="number"
  78.               id="age"
  79.               v-model="formData.age"
  80.               min="1"
  81.             />
  82.           </div>
  83.         </div>
  84.         
  85.         <div class="modal-footer">
  86.           <button @click="saveUser" class="btn-save">保存</button>
  87.           <button @click="cancelForm" class="btn-cancel">取消</button>
  88.         </div>
  89.       </div>
  90.     </div>
  91.    
  92.     <!-- 删除确认对话框 -->
  93.     <div v-if="showDeleteConfirm" class="modal">
  94.       <div class="modal-content confirm-dialog">
  95.         <div class="modal-header">
  96.           <h2>确认删除</h2>
  97.           <button @click="showDeleteConfirm = false" class="close-btn">&times;</button>
  98.         </div>
  99.         
  100.         <div class="modal-body">
  101.           <p>确定要删除用户 "{{ userToDelete?.name }}" 吗?此操作不可撤销。</p>
  102.         </div>
  103.         
  104.         <div class="modal-footer">
  105.           <button @click="confirmDelete" class="btn-delete">确认删除</button>
  106.           <button @click="showDeleteConfirm = false" class="btn-cancel">取消</button>
  107.         </div>
  108.       </div>
  109.     </div>
  110.   </div>
  111. </template>
  112. <script>
  113. import { ref, reactive, computed, onMounted } from 'vue';
  114. import userService from '@/services/userService';
  115. export default {
  116.   name: 'UserList',
  117.   setup() {
  118.     const users = ref([]);
  119.     const loading = ref(false);
  120.     const error = ref(null);
  121.     const searchTerm = ref('');
  122.     const showAddForm = ref(false);
  123.     const editingUser = ref(null);
  124.     const showDeleteConfirm = ref(false);
  125.     const userToDelete = ref(null);
  126.     const formData = reactive({
  127.       name: '',
  128.       email: '',
  129.       age: null
  130.     });
  131.     // 过滤用户列表
  132.     const filteredUsers = computed(() => {
  133.       if (!searchTerm.value) return users.value;
  134.       
  135.       const term = searchTerm.value.toLowerCase();
  136.       return users.value.filter(user =>
  137.         user.name.toLowerCase().includes(term) ||
  138.         user.email.toLowerCase().includes(term)
  139.       );
  140.     });
  141.     // 获取用户列表
  142.     const fetchUsers = async () => {
  143.       loading.value = true;
  144.       error.value = null;
  145.       
  146.       try {
  147.         const response = await userService.getUsers();
  148.         if (response.success) {
  149.           users.value = response.data;
  150.         } else {
  151.           error.value = response.message || '获取用户列表失败';
  152.         }
  153.       } catch (err) {
  154.         error.value = err.message || '获取用户列表失败';
  155.         console.error('获取用户列表错误:', err);
  156.       } finally {
  157.         loading.value = false;
  158.       }
  159.     };
  160.     // 搜索用户
  161.     const searchUsers = () => {
  162.       // 使用计算属性filteredUsers实现搜索
  163.     };
  164.     // 编辑用户
  165.     const editUser = (user) => {
  166.       editingUser.value = user;
  167.       formData.name = user.name;
  168.       formData.email = user.email;
  169.       formData.age = user.age;
  170.       showAddForm.value = true;
  171.     };
  172.     // 删除用户
  173.     const deleteUser = (id) => {
  174.       const user = users.value.find(u => u.id === id);
  175.       if (user) {
  176.         userToDelete.value = user;
  177.         showDeleteConfirm.value = true;
  178.       }
  179.     };
  180.     // 确认删除
  181.     const confirmDelete = async () => {
  182.       if (!userToDelete.value) return;
  183.       
  184.       try {
  185.         const response = await userService.deleteUser(userToDelete.value.id);
  186.         if (response.success) {
  187.           // 从列表中移除已删除的用户
  188.           users.value = users.value.filter(user => user.id !== userToDelete.value.id);
  189.           showDeleteConfirm.value = false;
  190.           userToDelete.value = null;
  191.         } else {
  192.           alert(response.message || '删除用户失败');
  193.         }
  194.       } catch (err) {
  195.         alert(err.message || '删除用户失败');
  196.         console.error('删除用户错误:', err);
  197.       }
  198.     };
  199.     // 保存用户
  200.     const saveUser = async () => {
  201.       // 验证表单
  202.       if (!formData.name || !formData.email) {
  203.         alert('姓名和邮箱是必填项');
  204.         return;
  205.       }
  206.       
  207.       try {
  208.         let response;
  209.         
  210.         if (editingUser.value) {
  211.           // 更新用户
  212.           response = await userService.updateUser(editingUser.value.id, {
  213.             name: formData.name,
  214.             email: formData.email,
  215.             age: formData.age
  216.           });
  217.          
  218.           if (response.success) {
  219.             // 更新列表中的用户数据
  220.             const index = users.value.findIndex(u => u.id === editingUser.value.id);
  221.             if (index !== -1) {
  222.               users.value[index] = response.data;
  223.             }
  224.             alert('用户更新成功');
  225.           }
  226.         } else {
  227.           // 创建新用户
  228.           response = await userService.createUser({
  229.             name: formData.name,
  230.             email: formData.email,
  231.             age: formData.age
  232.           });
  233.          
  234.           if (response.success) {
  235.             // 添加新用户到列表
  236.             users.value.push(response.data);
  237.             alert('用户创建成功');
  238.           }
  239.         }
  240.         
  241.         if (!response.success) {
  242.           alert(response.message || '保存用户失败');
  243.         } else {
  244.           cancelForm();
  245.         }
  246.       } catch (err) {
  247.         alert(err.message || '保存用户失败');
  248.         console.error('保存用户错误:', err);
  249.       }
  250.     };
  251.     // 取消表单
  252.     const cancelForm = () => {
  253.       showAddForm.value = false;
  254.       editingUser.value = null;
  255.       formData.name = '';
  256.       formData.email = '';
  257.       formData.age = null;
  258.     };
  259.     // 组件挂载时获取用户列表
  260.     onMounted(() => {
  261.       fetchUsers();
  262.     });
  263.     return {
  264.       users,
  265.       loading,
  266.       error,
  267.       searchTerm,
  268.       filteredUsers,
  269.       showAddForm,
  270.       editingUser,
  271.       showDeleteConfirm,
  272.       userToDelete,
  273.       formData,
  274.       searchUsers,
  275.       editUser,
  276.       deleteUser,
  277.       confirmDelete,
  278.       saveUser,
  279.       cancelForm
  280.     };
  281.   }
  282. };
  283. </script>
  284. <style scoped>
  285. .user-list {
  286.   max-width: 1200px;
  287.   margin: 0 auto;
  288. }
  289. h1 {
  290.   color: #2c3e50;
  291.   margin-bottom: 1.5rem;
  292.   text-align: center;
  293. }
  294. .actions {
  295.   display: flex;
  296.   justify-content: space-between;
  297.   align-items: center;
  298.   margin-bottom: 1.5rem;
  299. }
  300. .search input {
  301.   padding: 0.5rem;
  302.   border: 1px solid #ddd;
  303.   border-radius: 4px;
  304.   width: 250px;
  305. }
  306. .loading, .error, .no-data {
  307.   padding: 2rem;
  308.   text-align: center;
  309.   background-color: #f5f5f5;
  310.   border-radius: 8px;
  311.   margin: 1rem 0;
  312. }
  313. .error {
  314.   color: #f44336;
  315.   background-color: #ffebee;
  316. }
  317. .user-grid {
  318.   display: grid;
  319.   grid-template-columns: repeat(auto-fill, minmax(300px, 1fr));
  320.   gap: 1.5rem;
  321. }
  322. .user-card {
  323.   background-color: white;
  324.   border-radius: 8px;
  325.   box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
  326.   padding: 1.5rem;
  327.   display: flex;
  328.   align-items: center;
  329.   transition: transform 0.3s, box-shadow 0.3s;
  330. }
  331. .user-card:hover {
  332.   transform: translateY(-5px);
  333.   box-shadow: 0 5px 15px rgba(0, 0, 0, 0.1);
  334. }
  335. .user-avatar {
  336.   width: 60px;
  337.   height: 60px;
  338.   border-radius: 50%;
  339.   background-color: #42b983;
  340.   color: white;
  341.   display: flex;
  342.   align-items: center;
  343.   justify-content: center;
  344.   font-size: 1.5rem;
  345.   font-weight: bold;
  346.   margin-right: 1rem;
  347.   flex-shrink: 0;
  348. }
  349. .user-info {
  350.   flex: 1;
  351. }
  352. .user-info h3 {
  353.   margin-bottom: 0.5rem;
  354.   color: #2c3e50;
  355. }
  356. .user-info p {
  357.   margin: 0.25rem 0;
  358.   color: #666;
  359. }
  360. .user-actions {
  361.   display: flex;
  362.   flex-direction: column;
  363.   gap: 0.5rem;
  364. }
  365. .btn-add, .btn-edit, .btn-delete, .btn-save, .btn-cancel {
  366.   padding: 0.5rem 1rem;
  367.   border: none;
  368.   border-radius: 4px;
  369.   cursor: pointer;
  370.   font-weight: bold;
  371.   transition: background-color 0.3s;
  372. }
  373. .btn-add {
  374.   background-color: #42b983;
  375.   color: white;
  376. }
  377. .btn-edit {
  378.   background-color: #2196f3;
  379.   color: white;
  380. }
  381. .btn-delete {
  382.   background-color: #f44336;
  383.   color: white;
  384. }
  385. .btn-save {
  386.   background-color: #42b983;
  387.   color: white;
  388. }
  389. .btn-cancel {
  390.   background-color: #9e9e9e;
  391.   color: white;
  392. }
  393. .modal {
  394.   position: fixed;
  395.   top: 0;
  396.   left: 0;
  397.   right: 0;
  398.   bottom: 0;
  399.   background-color: rgba(0, 0, 0, 0.5);
  400.   display: flex;
  401.   align-items: center;
  402.   justify-content: center;
  403.   z-index: 1000;
  404. }
  405. .modal-content {
  406.   background-color: white;
  407.   border-radius: 8px;
  408.   width: 90%;
  409.   max-width: 500px;
  410.   max-height: 90vh;
  411.   overflow-y: auto;
  412. }
  413. .modal-header {
  414.   display: flex;
  415.   justify-content: space-between;
  416.   align-items: center;
  417.   padding: 1rem;
  418.   border-bottom: 1px solid #eee;
  419. }
  420. .modal-header h2 {
  421.   margin: 0;
  422.   color: #2c3e50;
  423. }
  424. .close-btn {
  425.   background: none;
  426.   border: none;
  427.   font-size: 1.5rem;
  428.   cursor: pointer;
  429.   color: #999;
  430. }
  431. .close-btn:hover {
  432.   color: #333;
  433. }
  434. .modal-body {
  435.   padding: 1rem;
  436. }
  437. .form-group {
  438.   margin-bottom: 1rem;
  439. }
  440. .form-group label {
  441.   display: block;
  442.   margin-bottom: 0.5rem;
  443.   font-weight: bold;
  444.   color: #333;
  445. }
  446. .form-group input {
  447.   width: 100%;
  448.   padding: 0.75rem;
  449.   border: 1px solid #ddd;
  450.   border-radius: 4px;
  451.   font-size: 1rem;
  452. }
  453. .modal-footer {
  454.   display: flex;
  455.   justify-content: flex-end;
  456.   gap: 0.5rem;
  457.   padding: 1rem;
  458.   border-top: 1px solid #eee;
  459. }
  460. .confirm-dialog {
  461.   max-width: 400px;
  462. }
  463. .confirm-dialog .modal-body {
  464.   padding: 1.5rem;
  465. }
  466. .confirm-dialog p {
  467.   margin: 0;
  468.   line-height: 1.5;
  469. }
  470. </style>
复制代码

总结与最佳实践回顾

在Vue3项目中连接MySQL数据库时,遵循以下最佳实践可以确保应用的安全性、性能和可维护性:

1. 架构设计

• 采用前后端分离架构,Vue3作为前端,后端服务作为中间层连接MySQL
• 使用RESTful API或GraphQL设计清晰的接口
• 考虑使用微服务架构处理复杂业务逻辑

2. 数据库连接

• 使用连接池管理数据库连接,避免频繁创建和销毁连接
• 确保正确释放数据库连接,防止连接泄露
• 监控连接池状态,及时调整连接池大小

3. 安全性

• 使用环境变量存储敏感信息,如数据库凭据
• 实现参数化查询或使用ORM工具防止SQL注入
• 实现认证和授权机制,保护敏感API
• 使用HTTPS加密传输数据
• 实现请求限流和防攻击措施

4. 性能优化

• 优化数据库查询,使用索引提高查询效率
• 避免N+1查询问题,使用JOIN或子查询一次性获取相关数据
• 实现分页查询,减少单次请求的数据量
• 使用缓存策略减少数据库访问
• 压缩响应数据,减少网络传输量

5. 错误处理

• 实现全局错误处理中间件,统一处理错误
• 提供有意义的错误信息,便于调试
• 记录错误日志,便于问题追踪
• 实现事务处理,确保数据一致性

6. 代码组织

• 分离业务逻辑和数据访问层
• 使用服务类封装API调用
• 实现可重用的组件和工具函数
• 编写单元测试和集成测试,确保代码质量

通过遵循这些最佳实践,你可以构建一个安全、高效、可维护的Vue3与MySQL集成应用。记住,技术选择应该根据项目需求和团队技能进行调整,没有一种方案适用于所有情况。
「七転び八起き(ななころびやおき)」
回复

使用道具 举报

您需要登录后才可以回帖 登录 | 立即注册

本版积分规则

加入Discord频道

加入Discord频道

加入QQ社群

加入QQ社群

联系我们|小黑屋|TG频道|RSS |网站地图

Powered by Pixtech

© 2025-2026 Pixtech Team.