用React+Express.js+Mysql做一个全栈demo(CRUD)

前后端调用逻辑

本文所涉及的源代码以及数据库文件、思维导图文件在我的GitHub地址
视频教程来自油管LamaDev

准备工具

react+express.js+axios+mysql

配置express.js

mkdir backend
cd backend

通过 npm init 命令为你的应用创建一个 package.json 文件。

$ npm init

创建index.js

$ npm install express --save

更改package.json如下,主要更改module以及使用nodeman

{
  "name": "backend",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "type": "module",
  "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "start": "nodemon index.js "
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "body-parser": "^1.20.2",
    "cors": "^2.8.5",
    "express": "^4.18.2",
    "mysql": "^2.18.1",
    "nodemon": "^2.0.22"
  }
}
npm start //开启nodejs后端

react

正常下载就好了

npx create-react-app client

在index.js下配置express.js

import express from 'express';
import mysql from 'mysql';  //引入mysql包
import cors from 'cors'  //解决前后端跨域问题的包
const app = express();  //创建一个服务器实例

//简历数据库连接
const db = mysql.createConnection({
  host:"localhost",
  user:"root",
  password:"123456",
  database:"express",  //数据库名字 不是连接名 
})

app.use(express.json());  //接受前端传来的json  //?类似一个自动转换??
app.use(cors());


app.listen(8800,()=>{
  console.log("Connected to backend!");
})  //监听8800端口 理解为后端的端口号

axios设置

npm install axios

前端利用axios连接nodejs最好封装一下

新建request/index.js,配置封装axios,导出service

//在index.js中引入axios
import axios from 'axios';
//引入qs模块,用来序列化post类型的数据
import QS from 'qs';
//antd的message提示组件,大家可根据自己的ui组件更改。
import { message } from 'antd'

//保存环境变量
const isPrd = process.env.NODE_ENV == 'production';

//区分开发环境还是生产环境基础URL
export const basicUrl = isPrd ? 'http://localhost:8800' : 'http://localhost:8800'

//设置axios基础路径
const service = axios.create({
  baseURL: basicUrl
})

// 请求拦截器
service.interceptors.request.use(config => { 
  // 每次发送请求之前本地存储中是否存在token,也可以通过Redux这里只演示通过本地拿到token
  // 如果存在,则统一在http请求的header都加上token,这样后台根据token判断你的登录情况
  // 即使本地存在token,也有可能token是过期的,所以在响应拦截器中要对返回状态进行判断 
  const token = window.localStorage.getItem('userToken') || window.sessionStorage.getItem('userToken');
  //在每次的请求中添加token
  config.data = Object.assign({}, config.data, {
    token: token,
  })
  //设置请求头
  config.headers = {
    'Content-Type':'application/json'  //发送json数据 
  }
  //序列化请求参数,不然post请求参数后台接收不正常
  // config.data = QS.stringify(config.data)
  return config
}, error => { 
    return error;
})

// 响应拦截器
service.interceptors.response.use(response => {
  //根据返回不同的状态码做不同的事情
  // 这里一定要和后台开发人员协商好统一的错误状态码
  if (response.code) {
    switch (response.code) {
      case 200:
        return response.data;
      case 401:
        //未登录处理方法
        break;
      case 403:
        //token过期处理方法
        break;
      default:
        message.error(response.data.msg)
    }
  } else { 
    return response;
  }
})
//最后把封装好的axios导出
export default service

mysql数据库建立

数据库下建立连接express->books表

/*
 Navicat MySQL Data Transfer

 Source Server         : localhost_3306
 Source Server Type    : MySQL
 Source Server Version : 80028
 Source Host           : localhost:3306
 Source Schema         : express

 Target Server Type    : MySQL
 Target Server Version : 80028
 File Encoding         : 65001

 Date: 06/04/2023 19:55:09
*/

SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;

-- ----------------------------
-- Table structure for books
-- ----------------------------
DROP TABLE IF EXISTS `books`;
CREATE TABLE `books`  (
  `id` int NOT NULL AUTO_INCREMENT,
  `title` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
  `description` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NOT NULL,
  `cover` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_bin NULL DEFAULT NULL,
  `price` int NULL DEFAULT NULL,
  PRIMARY KEY (`id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 48 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_bin ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

前后端路由

前后端调用不可避开的一个知识点就是路由。路由又分成前端路由和后端路由。根据我个人的理解,前端路由负责页面的跳转,后端路由负责api接口的路径。

前端路由

在react中我们设置三个界面,分别是主页,添加图书页以及修改图书页。

那么在业务中我们通常用Router->index.js这种文件结构设置前端的路由,方便集成化管理。同时要使用react-router的hooks降低代码量、lazy和suspense用于懒加载以及路由跳转的加载渲染。

import React ,{lazy,Suspense}from 'react'
import './style.scss'
import { AnimatePresence } from "framer-motion";
import { useRoutes  } from 'react-router-dom';

const Books  = lazy(
  async()=>await import('../Component/Books')
);

const Add  = lazy(
  async()=>await import('../Component/Add')
);

const Update  = lazy(
  async()=>await import('../Component/Update')
);


function Index() {
  const element = useRoutes([
    {
      path:'/',
      element:<Books></Books>
    },
    {
      path:'/add',
      element:<Add></Add>
    },
    {
      path:'/update/:id',
      element:<Update></Update>
    },
  ])
  return (
    <div className="router flex-center">
      <Suspense fallback={<p>loading</p>}>
          {element}
      </Suspense>
      
    </div>
     
  )
}

export default Index

后端路由

后端路由就是要对应前端在不同业务需求下的不同操作,比如我们这个图书管理的demo,后端的起码要声明crud的路由。

例如如下的针对get请求的路由设置。

app.get("/",(req,res)=>{
  res.json("hello this is backend");
})

app.get("/books",(req,res)=>{
  const q = "select * from books";
  db.query(q,(err,data)=>{
    if(err) return res.json(err);
    return res.json(data);
  });
})

app.post("/books",(req,res)=>{
  console.log(req.body);
  try {
    const q = "insert into books (`title`,`description`,`price`,`cover`) values(?)";
    const values = [
    req.body.title,  //前端传来的数据
    req.body.description, 
    req.body.price,
    req.body.cover  
    ]
    db.query(q,[values],(err,data)=>{
      if(err) return res.json(err);
      return res.json("book has been created successfully");
    })
  } catch (error) {
    console.log(error);
  } 
  
})



app.delete("/books/:id",(req,res)=>{
  console.log(req.params.id);
  try {
    const bookId = req.params.id;
    const q = "delete from books where id = ?";
    db.query(q,[bookId],(err,data)=>{
    if(err) return res.json(err);
    return res.json("book has been deleted successfully");
  })
  } catch (error) {
    console.log(error);
  }
})

app.put("/books/:id",(req,res)=>{
  console.log(req.params.id);
  try {
    const bookId = req.params.id;
    const q = "update  books set `title` = ?,`description` = ?,`price` = ?,`cover` = ? where  id = ?";
    
    const values = [
      req.body.title,  //前端传来的数据
      req.body.description, 
      req.body.price,
      req.body.cover  
      ]
    //结构传入变量
    db.query(q,[...values,bookId],(err,data)=>{
    if(err) return res.json(err);
    return res.json("book has been updated successfully");
  })
  } catch (error) {
    console.log(error);
  }
})

后端index.js的完整代码如下:

import express from 'express';
import mysql from 'mysql';
import cors from 'cors'
const app = express();  //创建一个服务器实例



//简历数据库连接
const db = mysql.createConnection({
  host:"localhost",
  user:"root",
  password:"123456",
  database:"express",  //数据库名字 不是连接名 
})

app.use(express.json());  //接受前端传来的json  //?类似一个自动转换??
app.use(cors());


app.get("/",(req,res)=>{
  res.json("hello this is backend");
})

app.get("/books",(req,res)=>{
  const q = "select * from books";
  db.query(q,(err,data)=>{
    if(err) return res.json(err);
    return res.json(data);
  });
})

app.post("/books",(req,res)=>{
  console.log(req.body);
  try {
    const q = "insert into books (`title`,`description`,`price`,`cover`) values(?)";
    const values = [
    req.body.title,  //前端传来的数据
    req.body.description, 
    req.body.price,
    req.body.cover  
    ]
    db.query(q,[values],(err,data)=>{
      if(err) return res.json(err);
      return res.json("book has been created successfully");
    })
  } catch (error) {
    console.log(error);
  } 
  
})



app.delete("/books/:id",(req,res)=>{
  console.log(req.params.id);
  try {
    const bookId = req.params.id;
    const q = "delete from books where id = ?";
    db.query(q,[bookId],(err,data)=>{
    if(err) return res.json(err);
    return res.json("book has been deleted successfully");
  })
  } catch (error) {
    console.log(error);
  }
})

app.put("/books/:id",(req,res)=>{
  console.log(req.params.id);
  try {
    const bookId = req.params.id;
    const q = "update  books set `title` = ?,`description` = ?,`price` = ?,`cover` = ? where  id = ?";
    
    const values = [
      req.body.title,  //前端传来的数据
      req.body.description, 
      req.body.price,
      req.body.cover  
      ]
    //结构传入变量
    db.query(q,[...values,bookId],(err,data)=>{
    if(err) return res.json(err);
    return res.json("book has been updated successfully");
  })
  } catch (error) {
    console.log(error);
  }
})


app.listen(8800,()=>{
  console.log("Connected to backend!");
})

封装hook

我们从最底层开始封装

在react下新建request/api.js用来存放前端封装的axios的方法。

以下是封装的图书增删改查方法,我建议最好封装,尽管这个例子很简单代码复用很少。

import service from "./index";


//获取图书
export const fetchAllBooks = async()=>{
  try {
    // const res = await axios.get("http://localhost:8800/books")
    let data;
    await service.get("/books").then((res)=>{
      data = res.data
    })
    return data;
  } catch (error) {
    console.log(error);
  }
  
}

//添加图书
export const addBooks = async(book)=>{
  try {
    await service.post("/books",book)
  } catch (error) {
    console.log(error);
  }
  
}

//删除图书
export const deleteBook = async(id)=>{
  try {
    console.log(id);
    console.log("books/"+id);
    await service.delete("/books/"+id)
  } catch (error) {
    console.log(error);
  }
}

//更新图书
export const updateBook = async(id,book)=>{
  try {
    console.log(id);
    console.log("books/"+id);
    await service.put("/books/"+id,book)
  } catch (error) {
    console.log(error);
  }
}

接着封装useBookHook.JS文件,useBookHook封装books状态量,以及和books相关的方法,这里封装了CRUD方法。

如果不这样封装,那么用到图书的地方就要调用一次,代码重写率很高。同时我也建议以后都将数据和方法封装到一起。如果数据交叉那么可以用redux来存储数据。(这是我跟公司大神学习的)

import { useMemo, useEffect, useState,useCallback } from 'react';
import { fetchAllBooks ,deleteBook,addBooks,updateBook } from '../request/api';

export const useFetchBooks = ()=>{
  const [books,setBooks] = useState();
  //获取图书
  const getBooks = useCallback(async()=>{
    await  fetchAllBooks().then((res)=>{
      setBooks(res);
    });
  },[books])
  //删除图书
  const handleDelete = useCallback(async(id)=>{
    await deleteBook(id)
    await getBooks();
  },[books])
  //添加图书
  const handleAdd  = useCallback(async(book)=>{
    await addBooks(book);
    await getBooks();
  },[books])
  //修改图书
  const handleUpdate = useCallback(async(id,book)=>{
    await updateBook(id,book);
    await getBooks();
  },[books])
  useEffect(()=>{
    getBooks()
  },[])
  return {
    books,
    async Delete(id){
      return handleDelete(id);
    },
    async Add(book){
      return handleAdd(book);
    },
    async Update(id,book){
      return handleUpdate(id,book);
    }
  }
}

调用逻辑

books变量,直接从hooks中就能读取,hooks调用axios.get获取。

const {books,Delete}=  useFetchBooks();

这里我们拿delete举例。

首先组件有个删除按钮,点击删除图书。

组件中的调用如下(代码片段)

const {books,Delete}=  useFetchBooks();
  const navigate = useNavigate();
  //删除图书方法
  const handleDelete = async(id) =>{
    await Delete(id);
  }
  <Button size = "large" type = "default" danger onClick = {()=>handleDelete(item.id)}>Delete</Button>

组件调用useBookHooks的方法

const handleDelete = useCallback(async(id)=>{
    await deleteBook(id)
    await getBooks();//删除后更新状态量
  },[books])

request/api.js的封装方法

//删除图书
export const deleteBook = async(id)=>{
  try {
    console.log(id);
    console.log("books/"+id);
    await service.delete("/books/"+id)
  } catch (error) {
    console.log(error);
  }
}

后端的对应处理

app.delete("/books/:id",(req,res)=>{
  console.log(req.params.id);
  try {
    const bookId = req.params.id;
    const q = "delete from books where id = ?";
    db.query(q,[bookId],(err,data)=>{
    if(err) return res.json(err);
    return res.json("book has been deleted successfully");
  })
  } catch (error) {
    console.log(error);
  }
})
  • 3
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值