Node.js零基础到项目实战 Express+MySQL+Sequelize (一)

一、创建Express项目

bash 复制代码
// 使用express-generator脚手架
npm install -g express-generator@4

// 创建项目
express --no-view clwy-api

// 进入 clwy-api
npm install

// 安装 nodemon
npm intall nodemon
  1. 将项目中 routes 文件夹中文件的 res.render 改为 res.json
  2. 删除 public 文件夹下的 index.html
  3. package.json 中项目的 script.start"node ./bin/www" 改为 "nodemon ./bin/www", 让项目可以实时更新

二、使用Docker安装Mysql

  1. 从官网下载 Docker
  2. 打开Docker的设置-> Docker Engine,添加国内的镜像 "registry-mirrors": [ "https://docker.1ms.run" ]
  3. 进入项目的根目录建立 docker-compose.yml 文件 和 config/my.cnf 文件
yml 复制代码
version: "3.8"
services:
  mysql:
    image: mysql:8.0
    container_name: mysql_dev
    environment:
      MYSQL_ROOT_PASSWORD: rootpassword  # 必填
      MYSQL_DATABASE: app_db              # 自动创建数据库
      MYSQL_USER: dev_user                # 自动创建普通用户
      MYSQL_PASSWORD: userpass
      TZ: Asia/Shanghai                   # 设置时区[5,6](@ref)
    ports:
      - "3306:3306"                      # 主机端口:容器端口
    volumes:
      - ./mysql_data:/var/lib/mysql      # 数据持久化[2,4](@ref)
      - ./config/my.cnf:/etc/mysql/conf.d/my.cnf  # 自定义配置[3](@ref)
    restart: unless-stopped              # 异常退出自动重启[3](@ref)
cnf 复制代码
[mysqld]
character-set-server = utf8mb4     # 支持中文及Emoji
collation-server = utf8mb4_unicode_ci
max_connections = 200              # 最大连接数
default_authentication_plugin=mysql_native_password  # 兼容旧客户端[6](@ref)
  1. 运行 docker-compose up -d MySQL服务端 就会自动下载,同时在Docker中也能看到 clwy-api 项目
  2. 下载MySQL客户端,macOS 直接在苹果商店搜索 Sequel Ace 进行下载即可。
txt 复制代码
TCP/IP

Name: 本机
Host: 127.0.0.1
UserName: root
Password: rootpassword // 在docker-compose.yml中写有

点击 Test connection
成功后 点击 Add to Favorites

三、操作数据库

  1. 创建数据库

  1. 创建数据表
  1. 常用SQL语句
sql 复制代码
-- 插入
insert into 表名 (列1,...) values (值1, ...);

-- 多行插入
insert into 表名 (列1,...) values (值1, ...),(值1, ...);

-- 更新
update 表名 set 列1=值1, 列2=值2, ... where 条件

-- 删除
delete from 表名 where 条件

-- 查询
select * from 表名

四、使用Sequelize ORM

开发过程中一般是不会通过 SQL语句 直接操作数据库的,会使用 ORM 来操作数据库,ORM 是数据库和编程语言之间建立的一种 映射关系,可以通过十分简单的代码来进行数据库的各种操作

  1. 安装Sequelize
  • npm i -g sequelize-cli
  • npm i sequelize mysql2
  • sequelize init
  1. 文件说明
  • config.json 存放的是 sequelize 所需要连接到数据库的配置文件
  • migrations 迁移,对数据表做新增表、修改字段、删除表等等操作
  • models 存放的是模型文件,使用sequelize 执行增删改查操作,每个模型文件都对应数据库的一张表
  • seeders 存放的是种子文件,需要添加到数据表的测试数据
  1. 模型、迁移和种子
  • 修改 config/config.json 文件
json 复制代码
{
  "development": {
    "username": "root",
    "password": "rootpassword",
    "database": "clwy_api_development",
    "host": "127.0.0.1",
    "dialect": "mysql",
    "timezone": "+08:00"
  },
  "test": {
    "username": "root",
    "password": null,
    "database": "clwy_api_test",
    "host": "127.0.0.1",
    "dialect": "mysql"
  },
  "production": {
    "username": "root",
    "password": null,
    "database": "clwy_api_production",
    "host": "127.0.0.1",
    "dialect": "mysql"
  }
}
  • sequelize model:generate --name Article --attributes title:string,content:text 新建模型文件命令
txt 复制代码
models/article.js
migrations/20250804092221-create-article.js
  • sequelize db:migrate 迁移创建数据库表
  • 修改 seeders/20250804092904-article.js 文件
js 复制代码
'use strict';

/** @type {import('sequelize-cli').Migration} */
module.exports = {
  async up (queryInterface, Sequelize) {
   const articles = []
   const counts = 100

   for(let i = 1; i < counts; i++) {
    const article = {
      title: `文章的标题${i}`,
      content: `文章的标题${i}`,
      createdAt: new Date(),
      updatedAt: new Date()
    }

    articles.push(article)
   }

   await queryInterface.bulkInsert('Articles', articles, {})
  },

  async down (queryInterface, Sequelize) {
  }
};
  • sequelize db:seed --seed 20250804092904-article 插入数据

五、Apifox的使用

Apifox 用来调试API的工具

六、接口

1. 查询文章列表 GET /admin/articles

js 复制代码
// routes/admin/articles.js
const express = require('express');
const router = express.Router();
const {Article} = require('../../models')

router.get('/', async function(req, res, next) {
    try {
        const condition = {
            order: [['id', 'DESC']]
        }

        const articles = await Article.findAll(condition)

        res.json({
            status:true,
            message: '查询文章列表成功',
            data: {
                articles
            }
        })
    }catch(error) {
        res.status(500).json({
            status:false,
            message: '查询文章列表失败',
            errors: [error.message]
        })
    }
});

module.exports = router;
js 复制代码
// app.js
const adminArticlesRouter = require('./routes/admin/articles');
app.use('/admin/articles', adminArticlesRouter);

2. 查询单条文章 GET /admin/artilces/:id

js 复制代码
// routes/admin/articles.js
router.get('/:id', async function(req, res, next) {
    try {
        const {id} = req.params
        const article = await Article.findByPk(id)

        if(article) {
            res.json({
                status:true,
                message: '查询文章成功',
                data: article
            })
        } else {
            res.status(404).json({
                status:false,
                message: '文章未找到',
            })
        }
    } catch(error) {
        res.status(500).json({
            status:false,
            message: '查询文章失败',
            errors: [error.message]
        })
    }
})

3. 创建文章 POST /admin/articles

js 复制代码
router.post('/', async function(req, res, next) {
    try {
        const articles = await Article.create(req.body)

        res.status(201).json({
            status:true,
            message: '创建文章成功',
            data: {
                articles
            }
        })
    }catch(error) {
        res.status(500).json({
            status:false,
            message: '创建文章失败',
            errors: [error.message]
        })
    }
});

4. 删除文章 DELETE /admin/artilces/:id

js 复制代码
router.delete('/:id', async function(req, res, next) {
    try {
        const {id} = req.params
        const article = await Article.findByPk(id)

        if(article) {
            await article.destroy()

            res.json({
                status:true,
                message: '删除文章成功',
            })
        } else {
            res.status(404).json({
                status:false,
                message: '文章未找到',
            })
        }
    } catch(error) {
        res.status(500).json({
            status:false,
            message: '删除文章失败',
            errors: [error.message]
        })
    }
})

5. 更新文章 PUT /admin/artilces/:id

js 复制代码
router.put('/:id', async function(req, res, next) {
    try {
        const {id} = req.params
        const article = await Article.findByPk(id)

        if(article) {
            await article.update(req.body)

            res.json({
                status:true,
                message: '更新文章成功',
            })
        } else {
            res.status(404).json({
                status:false,
                message: '文章未找到',
            })
        }
    } catch(error) {
        res.status(500).json({
            status:false,
            message: '更新文章失败',
            errors: [error.message]
        })
    }
})

6. 模糊查询 GET /admin/articles?title=

js 复制代码
const {Op} = require('sequelize')

router.get('/', async function(req, res, next) {
    try {

        const query = req.query
        const condition = {
            order: [['id', 'DESC']]
        }
        if(query.title) {
            condition.where = {
                title: {
                    [Op.like]: `%${query.title}%`
                }
            }
        }
        const articles = await Article.findAll(condition)
        res.json({
            status:true,
            message: '查询文章列表成功',
            data: {
                articles
            }
        })
    }catch(error) {
        res.status(500).json({
            status:false,
            message: '查询文章列表失败',
            errors: [error.message]
        })
    }
});

7. 数据分页 GET /admin/articles?currentPage=&pageSize=

js 复制代码
router.get('/', async function(req, res, next) {
    try {

        const query = req.query
        const currentPage = Math.abs(Number(query.currentPage)) || 1
        const pageSize = Math.abs(Number(query.pageSize)) || 10
        const offset = (currentPage - 1) * pageSize

        const condition = {
            limit: pageSize,
            offset
        }
        const {count, rows} = await Article.findAndCountAll(condition)
        res.json({
            status:true,
            message: '查询文章列表成功',
            data: {
                articles: rows,
                pagination: {
                    total: count,
                    currentPage,
                    pageSize
                }
            }
        })
    }catch(error) {
        res.status(500).json({
            status:false,
            message: '查询文章列表失败',
            errors: [error.message]
        })
    }
});

七、问题

1. 白名单过滤表单数据

js 复制代码
// routes/admin/articles.js
router.post('/', async function(req, res, next) {
    try {
        const body = filterBody(req)
        const articles = await Article.create(body)

        res.status(201).json({
            status:true,
            message: '创建文章成功',
            data: {
                articles
            }
        })
    }catch(error) {
        res.status(500).json({
            status:false,
            message: '创建文章失败',
            errors: [error.message]
        })
    }
});

function filterBody(req) {
    return {
        title: req.body.title,
        content: req.body.content
    }
}

2. 验证表单数据

js 复制代码
// models/article.js
Article.init({
    title: {
      type: DataTypes.STRING,
      allowNull: false,
      validate: {
        notNull: {
          msg: '标题必须存在'
        },
        notEmpty: {
          msg: '标题不能为空'
        },
        len: {
          args:[2,45],
          msg: '标题长度需要在2~45个字符之间'
        }
      }
    },
    content: DataTypes.TEXT
  }, {
    sequelize,
    modelName: 'Article',
  });
js 复制代码
// routes/admin/articles.js
router.post('/', async function(req, res, next) {
    try {
        const body = filterBody(req)
        const articles = await Article.create(body)

        res.status(201).json({
            status:true,
            message: '创建文章成功',
            data: {
                articles
            }
        })
    }catch(error) {
        if(error.name === 'SequelizeValidationError') {
            const errors = error.errors.map(e => e.message)
            res.status(400).json({
                status:false,
                message: '请求参数错误',
                errors
            })
        } else {
            res.status(500).json({
                status:false,
                message: '创建文章失败',
                errors: [error.message]
            })
        }
    }
});

八、对以上增删查改 封装响应优化代码

js 复制代码
// utils/response.js
class NotFoundError extends Error {
    constructor(message) {
        super(message)
        this.name = 'NotFoundError'
    }
}

function success(res, message, data = {}, code = 200) {
    res.status(code).json({
        status:true,
        message,
        data
    })
}

function failure(res, error) {
    if(error.name == 'SequelizeValidationError') {
        const errors = error.errors.map(e => e.message)
        return  res.status(400).json({
            status:false,
            message: '请求参数错误',
            errors
        })
    }

    if(error.name === 'NotFoundError') {
        return res.status(404).json({
            status:false,
            message: '资源不存在',
            errors: [error.message]
        })
    }

    res.status(500).json({
        status:false,
        message: '服务器错误',
        errors: [error.message]
    })
}

module.exports = {
    NotFoundError,
    success,
    failure
}
js 复制代码
// routes/admin/articles.js
const express = require('express');
const router = express.Router();
const {Article} = require('../../models')
const {Op} = require('sequelize')
const {NotFoundError, success, failure} = require('../../utils/response')

router.get('/', async function(req, res, next) {
    try {

        const query = req.query
        const currentPage = Math.abs(Number(query.currentPage)) || 1
        const pageSize = Math.abs(Number(query.pageSize)) || 10
        const offset = (currentPage - 1) * pageSize

        const condition = {
            order: [['id', 'DESC']],
            limit: pageSize,
            offset
        }

        if(query.title) {
            condition.where = {
                title: {
                    [Op.like]: `%${query.title}%`
                }
            }
        }

        const {count, rows} = await Article.findAndCountAll(condition)
        success(res, '查询文章列表成功', {
            articles: rows,
            pagination: {
                total: count,
                currentPage,
                pageSize
            }
        })
    }catch(error) {
        failure(res, error)
    }
});

router.get('/:id', async function(req, res, next) {
    try {
        const article = await getArticle(req)
        success(res, '查询文章成功', {article})
    } catch(error) {
        failure(res, error)
    }
})

router.post('/', async function(req, res, next) {
    try {
        const body = filterBody(req)
        const article = await Article.create(body)
        success(res, '创建文章成功', {article}, 201)
    }catch(error) {
        failure(res, error)
    }
});

router.delete('/:id', async function(req, res, next) {
    try {
        const article = await getArticle(req)
        await article.destroy()
        success(res, '删除文章成功')
    } catch(error) {
        failure(res, error)
    }
})

router.put('/:id', async function(req, res, next) {
    try {

        const body = filterBody(req)
        const article = await getArticle(req)
        await article.update(body)
        success(res, '更新文章成功')
    } catch(error) {
        failure(res, error)
    }
})

async function getArticle(req) {
    const {id} = req.params
    const article = await Article.findByPk(id)
    if(!article) {
        throw new NotFoundError(`ID: ${id}的文章未找到。`)
    }

    return article
} 

function filterBody(req) {
    return {
        title: req.body.title,
        content: req.body.content
    }
}
module.exports = router;

学习视频地址:bilibili

相关推荐
OpenTiny社区11 分钟前
一文解读“Performance面板”前端性能优化工具基础用法!
前端·性能优化·opentiny
拾光拾趣录32 分钟前
🔥FormData+Ajax组合拳,居然现在还用这种原始方式?💥
前端·面试
不会笑的卡哇伊42 分钟前
新手必看!帮你踩坑h5的微信生态~
前端·javascript
bysking43 分钟前
【28 - 记住上一个页面tab】实现一个记住用户上次点击的tab,上次搜索过的数据 bysking
前端·javascript
Dream耀1 小时前
跨域问题解析:从同源策略到JSONP与CORS
前端·javascript
前端布鲁伊1 小时前
【前端高频面试题】面试官: localhost 和 127.0.0.1有什么区别
前端
HANK1 小时前
Electron + Vue3 桌面应用开发实战指南
前端·vue.js
極光未晚1 小时前
Vue 前端高效分包指南:从 “卡成 PPT” 到 “丝滑如德芙” 的蜕变
前端·vue.js·性能优化
郝亚军1 小时前
炫酷圆形按钮调色器
前端·javascript·css
Spider_Man1 小时前
别再用Express了!用Node.js原生HTTP模块装逼的正确姿势
前端·http·node.js