Node 小册 | 实现一个简单的CRUD网站

474 阅读4分钟

本文主要实现的是CRUD请求 实现用户表的增删改查

Screen Shot 2022-04-01 at 22.17.01.png

仓库 github.com/LuckyChou71…

前端

  • React17
  • Vite
  • Tailwind
  • Antd ProComponent

后端

  • Express
  • Joi
  • Sequelize
  • Log4js
  • mysql2
  1. Express的基础部分可以参考作者的 juejin.cn/post/694249…

  2. 本文只记录一些后端的实现 前端代码参考 github.com/LuckyChou71… 这个仓库

  3. 此外 你还需要有mysql的环境

搭建项目

  1. 安装依赖
// 初始化项目
yarn init -y

// 服务框架
yarn add express

// 参数校验
yarn add joi

// 数据库
yarn add mysql2
yarn add sequelize

// 日志
yarn add log4js
  1. 生成项目目录
├─ src
│  ├─ app.ts // 入口文件
│  ├─ dao // 数据库相关操作
│  │  ├─ config.ts
│  │  ├─ connect.ts
│  │  └─ user.ts
│  ├─ log // 日志相关操作
│  │  └─ index.ts
│  ├─ routes // 路由
│  │  └─ user.ts
│  ├─ utils // 工具方法
│  │  └─ index.ts
│  └─ validator // 参数验证
│     ├─ index.ts
│     └─ user.ts

ok 接下来我们就来完善上面的每一个文件

Log

用 log4js 可以很方便的记录log 看一下项目运行时记录的一些log

数据库相关

Screen Shot 2022-04-01 at 22.20.00.png

api相关

Screen Shot 2022-04-01 at 22.20.23.png

log4js 配置如下

import log4js from 'log4js';

type LogCate = 'user' | 'error';

log4js.configure({
  appenders: {
    cheese: { type: 'dateFile', filename: 'log/cheese.log' },
    userLog: { type: 'dateFile', filename: 'log/user.log' },
    errLog: { type: 'dateFile', filename: 'log/error.log' },
  },
  categories: {
    user: { appenders: ['userLog'], level: 'debug' },
    error: { appenders: ['errLog'], level: 'debug' },
    default: { appenders: ['cheese'], level: 'debug' },
  },
});

export const logger = (category: LogCate) => log4js.getLogger(category);

说明一下

appenders 可以理解为配置每一个规则项

categories 为具体的每一个配置 其中的 appenders 为一个数组 它可以启用多条规则

然后在需要使用的地方 logger([categories中的配置项]).debug([debug信息])即可

数据库

数据库的相关操作放在 dao/ 目录下 表结构如下

+-----------+--------------+------+-----+---------+----------------+
| Field     | Type         | Null | Key | Default | Extra          |
+-----------+--------------+------+-----+---------+----------------+
| id        | int          | NO   | PRI | NULL    | auto_increment |
| nickname  | varchar(255) | NO   |     | NULL    |                |
| password  | varchar(255) | NO   |     | NULL    |                |
| status    | tinyint(1)   | YES  |     | 0       |                |
| address   | varchar(255) | YES  |     | NULL    |                |
| tel       | varchar(255) | YES  |     | NULL    |                |
| createdAt | datetime     | NO   |     | NULL    |                |
| updatedAt | datetime     | NO   |     | NULL    |                |
+-----------+--------------+------+-----+---------+----------------+

这边我们使用 orm 框架 sequelize 来操作数据库 相关介绍作者写在另一篇文章里

juejin.cn/post/707998…

  1. 新建一个 user.ts 文件 在里面创建我们的 user modal
import { DataTypes } from 'sequelize';
import sequelize from './connect';

const UserInfo = sequelize.define('user_info', {
  // 在这里定义模型属性
  id: {
    type: DataTypes.INTEGER,
    autoIncrement: true,
    primaryKey: true,
  },
  nickname: {
    type: DataTypes.STRING,
    allowNull: false,
  },
  password: {
    type: DataTypes.STRING,
    allowNull: false,
  },
  status: {
    type: DataTypes.BOOLEAN,
    defaultValue: false,
  },
  address: {
    type: DataTypes.STRING,
  },
  tel: {
    type: DataTypes.STRING,
  },
});

UserInfo.sync();

export default UserInfo;
  1. connect

新建一个 connect.ts 文件 用作数据库的连接

这里需要说明的是

  1. 在创建 Sequelize 实例的时候 要指定 timezone: '+08:00' 这样记录的时间才是北京时间
  2. 指定 logQueryParameters 为 true 不然记录的sql语句都是 INSERT INTO ... VALUES (DEFAULT,?,?,?,?,?)的形式
  3. sequelize.authenticate 用来测试是否建立连接
import { Sequelize } from 'sequelize';
import figlet from 'figlet';
import chalk from 'chalk';

import { logger } from '../log';
import Config from './config';

const sequelize = new Sequelize('demo', Config.username, Config.password, {
  host: 'localhost',
  dialect: 'mysql',
  timezone: '+08:00',
  logging: (msg) => logger('user').debug(msg),
  logQueryParameters: true,
});

(async () => {
  try {
    await sequelize.authenticate();

    figlet.text(
      'Connect',
      {
        font: 'Larry 3D',
        horizontalLayout: 'default',
        verticalLayout: 'default',
        width: 80,
        whitespaceBreak: true,
      },
      (err, data) => {
        if (!err) console.log(chalk.yellow(data));
      }
    );
  } catch (error) {
    console.error(chalk.red('Unable to connect to the database:', error));
  }
})();

export default sequelize;

参数校验

对于每一个request的参数 其实都有属于它的数据结构 借助 joi 这个库 我们可以快速的进行参数的校验

下面以 update 这个接口需要的参数举例

const update = Joi.object({
  // id是number类型 必传
  id: Joi.number().required(),
  // nickname是string 可选
  nickname: Joi.string(),
  password: Joi.string(),
  status: Joi.boolean(),
  // address可以是string或者null 可选
  address: Joi.allow(string, null),
  tel: Joi.allow(string, null),
});

// 需要验证的地方
update.validateAsync(params)

当参数验证失败时 就会返回一个error 例如 id is required

路由

提前说明一下 util 文件

主要用于 请求参数的校验 / 错误请求的log记录 / 成功状态的返回

import { logger } from '../log';

const validateParams = async (req: any, res: any, statement: Promise<any>) => {
  try {
    await statement;
  } catch (error: any) {
    // 记录错误的api请求
    logger('error').error({
      api: req.originalUrl,
      param: req.body,
      error: error.message,
    });
    res.status(400);
    res.json({
      message: error.message,
      status: false,
    });
  }
};

const success = (res: any) =>
  res.json({
    message: 'success',
    status: true,
  });

export const ReqUtils = {
  validateParams,
};

export const ResUtils = {
  success,
};

下面就是 增删改查的实现

主要流程就是 request --> 校验参数 --> 操作数据库 --> response

功能methodpath参数位置
post/addreq.body
delete/delete/:idreq.params
put/updatereq.body
get/readreq.query
import express from 'express';

import UserInfo from '../dao/user';
import Validator from '../validator/user';
import { ReqUtils, ResUtils } from '../utils';

const { add, read, del, update } = Validator;

const { validateParams } = ReqUtils;

const router = express.Router();

router.post('/add', async (req, res) => {
  await validateParams(req, res, add.validateAsync(req.body));
  await UserInfo.create(req.body);
  ResUtils.success(res);
});

router.delete('/delete/:id', async (req, res) => {
  await validateParams(req, res, del.validateAsync(req.params));
  await UserInfo.destroy({ where: req.params });
  ResUtils.success(res);
});

router.put('/update', async (req, res) => {
  await validateParams(req, res, update.validateAsync(req.body));
  await UserInfo.update(req.body, { where: { id: req.body?.id } });
  ResUtils.success(res);
});

router.get('/read', async (req, res) => {
  await validateParams(req, res, read.validateAsync(req.query));
  const result = await UserInfo.findAll({ where: req.query as any });
  res.json(result);
});

export default router;

app.ts

import express from 'express';
import userRouter from './routes/user';

const app = express();

// 解析post请求的body
app.use(express.urlencoded({ extended: false }));
app.use(express.json());

// 配置路由
app.use('/user', userRouter);

app.listen('8080');

🎉 🎉 🎉