从零开始构建一个完整的全栈CRUD应用,vue+express+mysql

28 阅读6分钟

从零到一构建Vue+Node.js+MySQL全栈CRUD应用

本文档详细介绍了如何从零开始构建一个基于Vue.js前端、Node.js+Express后端和MySQL数据库的完整CRUD(创建、读取、更新、删除)应用程序。

目录

项目概述

本项目是一个完整的Web应用,允许用户对数据库中的用户信息进行CRUD操作。项目采用前后端分离架构,前端使用Vue.js框架,后端使用Node.js和Express框架,数据存储在MySQL数据库中。

主要功能包括:

  • 查看所有用户列表
  • 添加新用户
  • 编辑现有用户信息
  • 删除用户
  • 搜索用户

技术栈

前端

  • Vue.js 2.x
  • Vue Router
  • Axios
  • Bootstrap 4

后端

  • Node.js
  • Express
  • CORS
  • body-parser
  • MySQL

项目结构

nyap/
├── client/                   # 前端Vue.js项目
│   ├── public/
│   ├── src/
│   │   ├── components/       # Vue组件
│   │   │   ├── AddUser.vue   # 添加用户组件
│   │   │   ├── User.vue      # 用户详情/编辑组件
│   │   │   └── UsersList.vue # 用户列表组件
│   │   ├── services/         # API服务
│   │   │   └── UserService.js # 用户相关API调用
│   │   ├── router/           # 路由配置
│   │   │   └── index.js
│   │   ├── App.vue           # 根组件
│   │   └── main.js           # 入口文件
│   ├── package.json
│   └── vue.config.js         # Vue配置
│
├── server/                   # 后端Node.js项目
│   ├── index.js              # 服务器入口
│   ├── database.sql          # 数据库初始化脚本
│   └── package.json
│
└── README.md                 # 项目说明

开发环境配置

前置条件

  • Node.js (v14+)
  • npm 或 yarn
  • MySQL 数据库

后端开发

创建项目

  1. 创建项目目录:
mkdir nyap
cd nyap
mkdir server
cd server
  1. 初始化Node.js项目:
npm init -y

安装依赖

npm install express cors body-parser mysql --save
npm install nodemon --save-dev
  1. 更新package.json中的scripts:
"scripts": {
  "start": "node index.js",
  "dev": "nodemon index.js",
  "test": "echo \"Error: no test specified\" && exit 1"
}

配置数据库

  1. 创建database.sql文件来初始化数据库:
-- 创建数据库(如果不存在)
CREATE DATABASE IF NOT EXISTS nyap;

-- 使用数据库
USE nyap;

-- 创建用户表
CREATE TABLE IF NOT EXISTS users (
  id INT AUTO_INCREMENT PRIMARY KEY,
  name VARCHAR(100) NOT NULL,
  email VARCHAR(100) NOT NULL UNIQUE,
  phone VARCHAR(20),
  created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);

-- 插入一些示例数据
INSERT INTO users (name, email, phone) VALUES
('张三', 'zhangsan@example.com', '13800138001'),
('李四', 'lisi@example.com', '13800138002'),
('王五', 'wangwu@example.com', '13800138003');
  1. 执行SQL脚本:
mysql -u root -p < database.sql

创建服务器

创建index.js文件,配置Express服务器:

const express = require('express');
const cors = require('cors');
const bodyParser = require('body-parser');
const mysql = require('mysql');

const app = express();
const PORT = process.env.PORT || 8080;

// 中间件
app.use(cors());
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: true }));

// 创建MySQL连接
const connection = mysql.createConnection({
  host: 'localhost',
  user: 'root',     // 替换为您的MySQL用户名
  password: 'root', // 替换为您的MySQL密码
  database: 'nyap'  // 替换为您的数据库名
});

// 连接到MySQL
connection.connect(err => {
  if (err) {
    console.error('连接到MySQL失败:', err);
    return;
  }
  console.log('成功连接到MySQL数据库');
});

// API路由定义...(见下一节)

// 启动服务器
app.listen(PORT, () => {
  console.log(`服务器运行在端口 ${PORT} 上`);
});

实现API

index.js中添加API路由:

// 获取所有用户
app.get('/api/users', (req, res) => {
  connection.query('SELECT * FROM users', (err, results) => {
    if (err) {
      console.error('获取用户失败:', err);
      return res.status(500).json({ message: '获取用户失败', error: err });
    }
    res.json(results);
  });
});

// 获取单个用户
app.get('/api/users/:id', (req, res) => {
  connection.query('SELECT * FROM users WHERE id = ?', [req.params.id], (err, results) => {
    if (err) {
      console.error('获取用户失败:', err);
      return res.status(500).json({ message: '获取用户失败', error: err });
    }
    if (results.length === 0) {
      return res.status(404).json({ message: '未找到用户' });
    }
    res.json(results[0]);
  });
});

// 创建用户
app.post('/api/users', (req, res) => {
  const { name, email, phone } = req.body;
  
  if (!name || !email) {
    return res.status(400).json({ message: '名称和邮箱是必需的' });
  }
  
  connection.query(
    'INSERT INTO users (name, email, phone) VALUES (?, ?, ?)',
    [name, email, phone || null],
    (err, results) => {
      if (err) {
        console.error('创建用户失败:', err);
        return res.status(500).json({ message: '创建用户失败', error: err });
      }
      res.status(201).json({
        id: results.insertId,
        name,
        email,
        phone
      });
    }
  );
});

// 更新用户
app.put('/api/users/:id', (req, res) => {
  const { name, email, phone } = req.body;
  
  if (!name || !email) {
    return res.status(400).json({ message: '名称和邮箱是必需的' });
  }
  
  connection.query(
    'UPDATE users SET name = ?, email = ?, phone = ? WHERE id = ?',
    [name, email, phone || null, req.params.id],
    (err, results) => {
      if (err) {
        console.error('更新用户失败:', err);
        return res.status(500).json({ message: '更新用户失败', error: err });
      }
      if (results.affectedRows === 0) {
        return res.status(404).json({ message: '未找到用户' });
      }
      res.json({ id: req.params.id, name, email, phone });
    }
  );
});

// 删除用户
app.delete('/api/users/:id', (req, res) => {
  connection.query('DELETE FROM users WHERE id = ?', [req.params.id], (err, results) => {
    if (err) {
      console.error('删除用户失败:', err);
      return res.status(500).json({ message: '删除用户失败', error: err });
    }
    if (results.affectedRows === 0) {
      return res.status(404).json({ message: '未找到用户' });
    }
    res.json({ message: '用户已成功删除' });
  });
});

启动后端服务

npm run dev

前端开发

创建项目

  1. 创建Vue项目:
cd ..
mkdir client
cd client
  1. 初始化Node.js项目:
npm init -y

安装依赖

npm install vue vue-router axios bootstrap
npm install @vue/cli-service @vue/cli-plugin-babel --save-dev
  1. 更新package.json中的scripts:
"scripts": {
  "serve": "vue-cli-service serve",
  "build": "vue-cli-service build",
  "test": "echo \"Error: no test specified\" && exit 1"
}
  1. 创建vue.config.js配置文件:
module.exports = {
  devServer: {
    port: 8081
  }
}

配置路由

  1. 创建路由目录和文件:
mkdir -p src/router
  1. 创建src/router/index.js:
import Vue from 'vue';
import VueRouter from 'vue-router';

Vue.use(VueRouter);

const routes = [
  {
    path: '/',
    alias: '/users',
    name: 'users',
    component: () => import('../components/UsersList.vue')
  },
  {
    path: '/users/:id',
    name: 'user-details',
    component: () => import('../components/User.vue')
  },
  {
    path: '/add',
    name: 'add',
    component: () => import('../components/AddUser.vue')
  }
];

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
});

export default router;

创建服务

  1. 创建服务目录和文件:
mkdir -p src/services
  1. 创建src/services/UserService.js:
import axios from 'axios';

const API_URL = 'http://localhost:8080/api';

class UserService {
  getAll() {
    return axios.get(`${API_URL}/users`);
  }

  get(id) {
    return axios.get(`${API_URL}/users/${id}`);
  }

  create(data) {
    return axios.post(`${API_URL}/users`, data);
  }

  update(id, data) {
    return axios.put(`${API_URL}/users/${id}`, data);
  }

  delete(id) {
    return axios.delete(`${API_URL}/users/${id}`);
  }
}

export default new UserService();

创建组件

  1. 创建组件目录:
mkdir -p src/components
  1. 创建src/App.vue根组件:
<template>
  <div id="app">
    <nav class="navbar navbar-expand navbar-dark bg-dark">
      <router-link to="/" class="navbar-brand">Vue MySQL CRUD</router-link>
      <div class="navbar-nav mr-auto">
        <li class="nav-item">
          <router-link to="/users" class="nav-link">用户列表</router-link>
        </li>
        <li class="nav-item">
          <router-link to="/add" class="nav-link">添加用户</router-link>
        </li>
      </div>
    </nav>

    <div class="container mt-3">
      <router-view />
    </div>
  </div>
</template>

<script>
export default {
  name: 'App'
};
</script>

<style>
.list {
  text-align: left;
  max-width: 750px;
  margin: auto;
}
</style>
  1. 创建src/main.js入口文件:
import Vue from 'vue';
import App from './App.vue';
import router from './router';
import 'bootstrap/dist/css/bootstrap.min.css';

Vue.config.productionTip = false;

new Vue({
  router,
  render: h => h(App)
}).$mount('#app');
  1. 创建src/components/UsersList.vue组件:
<template>
  <div class="list row">
    <div class="col-md-8">
      <div class="input-group mb-3">
        <input
          type="text"
          class="form-control"
          placeholder="搜索用户名"
          v-model="searchName"
          @keyup.enter="searchByName"
        />
        <div class="input-group-append">
          <button
            class="btn btn-outline-secondary"
            type="button"
            @click="searchByName"
          >
            搜索
          </button>
        </div>
      </div>
    </div>
    <div class="col-md-6">
      <h4>用户列表</h4>
      <ul class="list-group">
        <li
          class="list-group-item"
          :class="{ active: index === currentIndex }"
          v-for="(user, index) in users"
          :key="index"
          @click="setActiveUser(user, index)"
        >
          {{ user.name }}
        </li>
      </ul>
    </div>
    <div class="col-md-6">
      <div v-if="currentUser">
        <h4>用户信息</h4>
        <div>
          <label><strong>姓名:</strong></label> {{ currentUser.name }}
        </div>
        <div>
          <label><strong>邮箱:</strong></label> {{ currentUser.email }}
        </div>
        <div>
          <label><strong>电话:</strong></label> {{ currentUser.phone }}
        </div>

        <router-link
          :to="'/users/' + currentUser.id"
          class="badge badge-warning"
        >
          编辑
        </router-link>
      </div>
      <div v-else>
        <br />
        <p>请点击用户...</p>
      </div>
    </div>
  </div>
</template>

<script>
import UserService from "../services/UserService";

export default {
  name: "users-list",
  data() {
    return {
      users: [],
      currentUser: null,
      currentIndex: -1,
      searchName: ""
    };
  },
  methods: {
    retrieveUsers() {
      UserService.getAll()
        .then(response => {
          this.users = response.data;
          console.log(response.data);
        })
        .catch(e => {
          console.log(e);
        });
    },

    refreshList() {
      this.retrieveUsers();
      this.currentUser = null;
      this.currentIndex = -1;
    },

    setActiveUser(user, index) {
      this.currentUser = user;
      this.currentIndex = index;
    },

    searchByName() {
      // 在实际应用中,这里可能需要调用后端API进行搜索
      // 在本例中,我们在前端进行筛选
      UserService.getAll()
        .then(response => {
          this.users = response.data.filter(user => 
            user.name.toLowerCase().includes(this.searchName.toLowerCase())
          );
        })
        .catch(e => {
          console.log(e);
        });
    }
  },
  mounted() {
    this.retrieveUsers();
  }
};
</script>

<style>
.list {
  text-align: left;
  max-width: 750px;
  margin: auto;
}
</style>
  1. 创建src/components/AddUser.vue组件:
<template>
  <div class="submit-form">
    <div v-if="!submitted">
      <div class="form-group">
        <label for="name">姓名</label>
        <input
          type="text"
          class="form-control"
          id="name"
          required
          v-model="user.name"
          name="name"
        />
      </div>

      <div class="form-group">
        <label for="email">邮箱</label>
        <input
          type="email"
          class="form-control"
          id="email"
          required
          v-model="user.email"
          name="email"
        />
      </div>

      <div class="form-group">
        <label for="phone">电话</label>
        <input
          type="text"
          class="form-control"
          id="phone"
          v-model="user.phone"
          name="phone"
        />
      </div>

      <button @click="saveUser" class="btn btn-success">提交</button>
    </div>

    <div v-else>
      <h4>用户添加成功!</h4>
      <button class="btn btn-success" @click="newUser">添加另一个</button>
    </div>
  </div>
</template>

<script>
import UserService from "../services/UserService";

export default {
  name: "add-user",
  data() {
    return {
      user: {
        id: null,
        name: "",
        email: "",
        phone: ""
      },
      submitted: false
    };
  },
  methods: {
    saveUser() {
      const data = {
        name: this.user.name,
        email: this.user.email,
        phone: this.user.phone
      };

      UserService.create(data)
        .then(response => {
          this.user.id = response.data.id;
          console.log(response.data);
          this.submitted = true;
        })
        .catch(e => {
          console.log(e);
        });
    },
    
    newUser() {
      this.submitted = false;
      this.user = {
        id: null,
        name: "",
        email: "",
        phone: ""
      };
    }
  }
};
</script>

<style>
.submit-form {
  max-width: 300px;
  margin: auto;
}
</style>
  1. 创建src/components/User.vue组件:
<template>
  <div v-if="currentUser" class="edit-form">
    <h4>用户信息</h4>
    <form>
      <div class="form-group">
        <label for="name">姓名</label>
        <input
          type="text"
          class="form-control"
          id="name"
          v-model="currentUser.name"
        />
      </div>
      <div class="form-group">
        <label for="email">邮箱</label>
        <input
          type="email"
          class="form-control"
          id="email"
          v-model="currentUser.email"
        />
      </div>
      <div class="form-group">
        <label for="phone">电话</label>
        <input
          type="text"
          class="form-control"
          id="phone"
          v-model="currentUser.phone"
        />
      </div>
    </form>

    <button
      type="submit"
      class="badge badge-success"
      @click="updateUser"
    >
      更新
    </button>

    <button class="badge badge-danger mr-2" @click="deleteUser">
      删除
    </button>

    <p>{{ message }}</p>
  </div>

  <div v-else>
    <br />
    <p>请点击一个用户...</p>
  </div>
</template>

<script>
import UserService from "../services/UserService";

export default {
  name: "user",
  data() {
    return {
      currentUser: null,
      message: ""
    };
  },
  methods: {
    getUser(id) {
      UserService.get(id)
        .then(response => {
          this.currentUser = response.data;
          console.log(response.data);
        })
        .catch(e => {
          console.log(e);
        });
    },

    updateUser() {
      UserService.update(this.currentUser.id, this.currentUser)
        .then(response => {
          console.log(response.data);
          this.message = "用户更新成功!";
        })
        .catch(e => {
          console.log(e);
        });
    },

    deleteUser() {
      UserService.delete(this.currentUser.id)
        .then(response => {
          console.log(response.data);
          this.$router.push({ name: "users" });
        })
        .catch(e => {
          console.log(e);
        });
    }
  },
  mounted() {
    this.message = "";
    this.getUser(this.$route.params.id);
  }
};
</script>

<style>
.edit-form {
  max-width: 300px;
  margin: auto;
}
</style>

启动前端服务

npm run serve

整合和测试

  1. 确保后端服务器运行在端口8080:
cd ../server
npm run dev
  1. 确保前端服务器运行在端口8081:
cd ../client
npm run serve
  1. 打开浏览器,访问 http://localhost:8081

API参考

方法URL说明请求体响应
GET/api/users获取所有用户用户对象数组
GET/api/users/:id获取单个用户单个用户对象
POST/api/users创建新用户{name, email, phone}创建的用户对象,带ID
PUT/api/users/:id更新用户{name, email, phone}更新后的用户对象
DELETE/api/users/:id删除用户{message: '用户已成功删除'}

常见问题和解决方案

1. 连接被拒绝错误 (ECONNREFUSED)

问题: 前端报错:POST http://localhost:8080/api/users net::ERR_CONNECTION_REFUSED

解决方案:

  • 确保后端服务器正在运行 (npm run dev)
  • 确认后端服务器监听的端口是否正确 (8080)
  • 前端服务中配置的API地址是否正确

2. 404错误

问题: 前端报错:POST http://localhost:8081/api/users 404 (Not Found)

解决方案:

  • 检查API URL是否正确。前端应该连接到后端服务器 (8080),而不是前端服务器 (8081)
  • UserService.js中修改API_URL:
const API_URL = 'http://localhost:8080/api';

3. 数据库连接错误

问题: 后端服务器报错:连接到MySQL失败

解决方案:

  • 确保MySQL服务正在运行
  • 检查数据库连接配置是否正确 (用户名、密码、数据库名)
  • 确保数据库nyap已创建

优化和改进

  1. 添加认证机制:

    • 实现JWT认证
    • 添加登录/注册功能
  2. 优化前端:

    • 添加表单验证
    • 实现分页功能
    • 添加更高级的搜索和筛选
  3. 增强后端:

    • 使用环境变量管理配置
    • 实现更好的错误处理
    • 添加日志记录
    • 设计更RESTful的API
  4. 测试:

    • 添加单元测试
    • 添加集成测试
  5. 部署:

    • 添加部署脚本
    • 配置持续集成/持续部署(CI/CD)
    • 容器化(Docker)