【NodeJS】Express写接口的整体流程

前提条件

开发 Node.js,首先就必须要安装 Node.js。推荐使用 nvm,它可以随意切换 node 版本。下载 nvm,具体可以看本人另一篇文章:nvm的作用、下载、使用、以及Mac使用时遇到commond not found:nvm如何解决

nvm官方:https://github.com/coreybutler/nvm-windows/

1. express-generator快速生成项目目录

下载

bash 复制代码
npm i -g express-generator@4

快速生成项目目录,--no-view 表示不需要视图。

bash 复制代码
express --no-view benchu-api 

进入项目目录,下载依赖包

bash 复制代码
npm i

运行命令,启动服务

bash 复制代码
npm start

访问 http://localhost:3000 看到如下页面,即为成功

2. nodemon

本文使用 node 专门搭建接口,所以在 routes/index.js 中将返回格式修改为 json 并删掉 public/index.html 文件。

刷新发现页面没有变化,需要重新 npm start,发现正常返回了 json。

我这里下载了浏览器插件,视觉上更加美观。

频繁执行 npm start 过于繁琐,更推荐安装 nodemon,它可以监听改动并自动重启。

下载 nodemon

bash 复制代码
npm i nodemon

修改 package.json,将 start 命令由 node 改为 nodemon

后面再执行 npm start 时就是通过 nodemon 启动服务了,修改后自动重启服务,刷新页面即可。

3. 使用Docker运行数据库

在 Windows 与 macOS 下,整个安装和使用的流程都非常的不同,而且将来部署到服务器后,也会有一些默认配置不同。例如以下这个:

javascript 复制代码
low_case_table_names=0

它是让MySQL区分数据表名是否大小写的配置。默认在 Windows、macOS 和 Linux 下全都不同。而且当数据库初始化之后,也就说成功的启动过一次后,就不能修改了。

3.1 下载 Docker 并配置中国镜像

使用 Docker 来运行 MySQL 就不会有如上问题的困惑。这样在 Windows 与 macOS 上,甚至在 Linux 服务器上,它们的运行环境都是一致的。

官网下载:https://www.docker.com/get-started/

下载完成后,设置 => Docker Emgine中添加中国镜像的配置。

bash 复制代码
"registry-mirrors": [
  "https://docker.rainbond.cc"
]

保存配置后,切回项目目录,在 项目根目录下 新建一个 docker-compose.yml 文件。一定要在项目根目录中,放在其他地方会找不到的。然后将下面 MySQL 的配置复制进去。

javascript 复制代码
services:
  mysql:
    image: mysql:8.3.0
    command:
      --default-authentication-plugin=mysql_native_password
      --character-set-server=utf8mb4
      --collation-server=utf8mb4_general_ci
    environment:
      - MYSQL_ROOT_PASSWORD=tianbenchu
      - MYSQL_LOWER_CASE_TABLE_NAMES=0
    ports:
      - "3306:3306"
    volumes:
      - ./data/mysql:/var/lib/mysql

执行如下命令,下载并启动 MySQL

bash 复制代码
docker-compose up -d

如下提示标识下载启动完毕

再次切回 Docker,发现出现项目,以后 启动/关闭 MySQL 可以通过按钮快速实现。

3.2 下载客户端连接数据库

想要方便的操作数据库,需要下载客户端,我这里以 MacOS 举例,到 AppStore 中下载 sequel ace 即可,Windows 可以使用 Navicat。

连接数据库

密码就是配置文件中的 MYSQL_ROOT_PASSWORD

4. 创建数据库与表

4.1 创建数据库与表

继上一步连接无误后,创建新数据库。

创建表,这里以 Articles 命名,注意首字母一定要大写 ,不大写的话将来部署到 Linux 服务器会报错;且一定要为复数形式,即后缀s,否则 nodejs 将无法查询到这个表。

4.2 主键

表创建完毕后,继续观察,有个 key,里面写的 PRI,它是主键 的意思。一般,每个表都会设置一个 id,并把它设置成 primary key 表示它是唯一标识

主键,还会搭配自增 auto_increment 来使用,后续追加数据时 主键 自增。

4.3 字段类型

4.3.1 数字类型

类型 字节大小 有符号范围 (Signed) 无符号范围 (Unsigned)
TINYINT 1 -128 ~ 127 0 ~ 255
SMALLINT 2 -32768 ~ 32767 0 ~ 65535
MEDIUMINT 3 -8388608 ~ 8388607 0 ~ 16777215
INT/INTEGER 4 -2147483648 ~2147483647 0 ~ 4294967295
BIGINT 8 -9223372036854775808 ~ 9223372036854775807 0 ~ 18446744073709551615

4.3.2 字符串类型:

类型 说明 使用场景
CHAR 固定长度,小型固定长度的数据 身份证号、手机号、电话、密码
VARCHAR 可变长度,小型数据 姓名、地址、品牌、型号、用户的评论、文章的标题
TEXT 可变长度,字符个数大于 4000 存储文章正文
LONGTEXT 可变长度,超大型文本数据 存储超大型文本数据

4.3.3 时间类型

类型 字节大小 示例
DATE 4 '2020-01-01'
TIME 3 '12:29:59'
DATETIME 8 '2020-01-01 12:29:59'
YEAR 1 '2017'
TIMESTAMP 4 '1970-01-01 00:00:01' UTC ~ '2038-01-01 00:00:01' UTC

4.3.4 总结 - 常用数据类型

类型 含义 说明
int 整数 需要设定长度
decimal 小数 金额常用,需要设定长度。如 decimal(10, 2) 表示共存 10 位数,其中小数占 2 位
char、varchar 字符串 文字类的常用,需要设定长度。例如身份证号、文章的标题使用。
text 文本 存储大文本,无需设定长度。一般会用文字很多的时候,例如文章的正文部分。
date、time、datetime 日期 记录时间

4.4 新增字段

了解了数据类型后,添加 titlecontent

5. 基础 sql 语句

5.1 增

javascript 复制代码
INSERT INTO 表名 (列1, ...) VALUES (值1, ...)
// 多行插入
INSERT INTO 表名 (列1, ...) VALUES (值1, ...),(值1, ...)...;

// 例如:
INSERT INTO `Articles` (`title`, `content`) VALUES ('行路难·其一', '长风破浪会有时,直挂云帆济沧海。');

可以看到数据已成功加入

5.2 删

javascript 复制代码
DELETE FROM 表名 WHERE 条件

// 例如:
DELETE FROM `ARTICLES` WHERE `id`=5;

5.3 改

javascript 复制代码
UPDATE 表名 SET 列1=值1, 列2=值2, ... WHERE 条件

// 例如:
UPDATE `Articles` SET `title`='黄鹤楼送孟浩然之广陵', `content`='故人西辞黄鹤楼,烟花三月下扬州。' WHERE `id`=2;

5.4 查

javascript 复制代码
SELECT * FROM 表名;

// 例如:
SELECT * FROM `Articles`;

* 表示所有的字段,如果只需要查询某些字段,指定即可。

javascript 复制代码
SELECT `id`, `title` FROM `Articles`;

条件查询

javascript 复制代码
SELECT * FROM 表名 WHERE 条件;

// 想查询id大于2的文章:
SELECT * FROM `Articles` WHERE `id`>2;

排序,不添加声明,默认为 ASC 升序。

javascript 复制代码
-- 查询id大于2的文章,按 id 从大到小排序,即降序
SELECT * FROM `Articles` WHERE `id`>2 ORDER BY `id` DESC;

-- 查询id大于2的文章,按 id 从小到大排列,即升序
SELECT * FROM `Articles` WHERE `id`>2 ORDER BY `id` ASC;

6. 使用 Sequelize ORM 简化书写 sql 语句

手写 sql 语句过于繁琐且容易出错,可以使用一些工具简化书写逻辑,本文使用 Sequelize ORM

6.1 Sequelize 常用命令

先体验一下 Sequelize 简洁的语法:

如全量查询

javascript 复制代码
SELECT * FROM `Articles`;

// Sequelize 可以这么写
Article.findAll()

条件查询,findByPk 参数就是主键 primary key,说白了就是通过id来查找数据。

javascript 复制代码
SELECT * FROM `Articles` WHERE `id`=2;

// Sequelize 可以这么写
Article.findByPk(2)

6.2 安装 Sequelize

先安装sequelize的命令行工具

bash 复制代码
npm i -g sequelize-cli

然后在项目目录中,安装当前项目所依赖的 sequelize 包和对数据库支持依赖的mysql2

bash 复制代码
npm i sequelize mysql2

初始化项目

bash 复制代码
sequelize init

初始化完毕后,项目目录下生成了几个文件:

6.3 新增文件的作用

初始化 sequelize 完毕后,项目目录生成了如下几个文件:

  • config/config.json:该文件是 sequelize 需要的连接到数据库的配置文件。
  • migrations :迁移,用于处理 新增表修改字段删除表 等操作,而不用直接在客户端中点点点直接操作数据库。
  • models/index.js :模型文件,使用 sequelize 增删改查时,每个文件对应数据库中的一张表。
  • seeders:存放需要添加到数据表的测试数据。

6.3.1 配置 config/config.json

配置 config/config.json,Node项目就会自动的,连接到数据库上了。有三组配置,分别是:development、test 和 production。分别对应开发环境、测试环境、生产环境的配置。

本文只配置 development,其他的配置也是一样的。

  • password:保持与 docker-compose.yml 中一致
  • database:修改数据库名,保持与 Sequel Ace 客户端中一致
  • timezone:时区设置为东八区

6.3.2 migrations迁移和models模型

删除上文中通过客户端手动创建的表 Articles,通过命令新增模型

bash 复制代码
sequelize model:generate --name Article --attributes title:string,content:text

项目 migrations 迁移目录 和 models 模型目录下多了文件,稍作修改,将 content 字段设置为不可为 null。

  • up:通过 createTabel,创建了一个叫做Articles的表。表名为复数,但是 models/article.js 模型为单数。

  • down:新建表的反向操作,为 dropTable 也就是删除当前的表。这样当我们创建表,建完后,突然又发现有错误,也可以通过相关命令来删除当前表。

运行迁移命令

javascript 复制代码
sequelize db:migrate

再次刷新数据库客户端,除了我们创建的 Articles 表,还自动创建了 SequelizeMeta 表,这张表里记录了当前已经跑过了哪些迁移,当再次运行 sequelize db:migrate 时,已经运行过的迁移文件,就不会重复再次执行了。

6.3.3 种子文件

上一步运行迁移后已经创建成功了 Articles 表,下一步就是要填充一些在开发中用来测试的数据了。通过如下命令添加种子文件。

bash 复制代码
sequelize seed:generate --name article 

完成后,在 seeds 目录,就看到刚才命令新建的种子文件了。同样也是分为两个部分,up部分用来填充数据,down部分是反向操作,用来删除数据的。

up 和 down 中默认给了案例,仿照他的写法生成多条测试数据:

javascript 复制代码
"use strict"

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

    for (let i = 1; i <= 100; 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) {
    await queryInterface.bulkDelete("Articles", null, {})
  },
}

运行种子文件,生成大量数据,注意:命令后缀一定为该种子文件名

javascript 复制代码
sequelize db:seed --seed xxx-article

如我这里的种子文件为:20241126100352-article.js

就运行下面这行命令

javascript 复制代码
sequelize db:seed --seed 20241126100352-article

刷新数据库,发现数据添加成功。

6.4 Sequelize的基本使用

日常开发项目,都是采用固定的步骤:

步骤 命令 说明
第一步 sequelize model:generate --name Article --attributes ... 建模型和迁移文件
第二步 人工处理 根据需求调整迁移文件
第三步 sequelize db:migrate 运行迁移,生成数据表
第四步 sequelize seed:generate --name article 新建种子文件
第五步 人工处理 将种子文件修改为自己想填充的数据
第六步 sequelize db:seed --seed xxx-article 运行种子文件,将数据填充到数据表中

种子 文件并不是必须的,但对于一些需要一下子插入大量数据的情况来说,更推荐使用种子。

7. 使用 Sequelize 写几个接口

routes/admin/articles.js 新增路由

javascript 复制代码
var express = require("express")
var router = express.Router()

router.get("/", function (req, res, next) {
  res.json({ message: "articles api" })
})

module.exports = router

app.js 中引用并use路由

npm start 启动服务,访问 http://localhost:3000/admin/articles,看到正常返回。

7.1 写接口 - 获取文章列表

如果想读取数据库数据,就需要使用 模型routes/admin/articles.js 中引入模型,并通过模型读取数据库数据。

Article.findAll() 读取全部数据,读取数据是异步操作,使用 async await 等待。

javascript 复制代码
var express = require("express")
var router = express.Router()
const { Article } = require("../../models")

router.get("/", async function (req, res, next) {
  try {
    // 查询数据
    const articles = await Article.findAll()

    // 返回查询结果
    res.json({
      status: true,
      message: "查询文章列表成功",
      data: { articles },
    })
  } catch (err) {
    res.status(500).json({
      status: false,
      message: "查询文章列表失败",
      errors: [err.message],
    })
  }
})

module.exports = router

刷新页面,数据已出现

通过控制台可以看到,Article.findAll() 最终被转为 sql 语句执行。

实际场景中,数据通常是倒序,即最后添加的数据最先展示,定义查询条件即可,Article.findAll() 时传入查询条件。

javascript 复制代码
var express = require("express")
var 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)

    // 返回查询结果,成功状态码默认为200
    res.json({
      status: true,
      message: "查询文章列表成功",
      data: { articles },
    })
  } catch (err) {
    res.status(500).json({
      status: false,
      message: "查询文章列表失败",
      errors: [err.message],
    })
  }
})

module.exports = router

7.2 写接口 - 获取文章详情

routes/admin/articles.js 中添加获取详情接口,Article.findByPk(id) 通过 id 查询。

javascript 复制代码
// 查询文章详情
router.get("/:id", async function (req, res, next) {
  try {
    // 获取参数 - 文章ID
    const { id } = req.params

    // 查询数据
    const article = await Article.findByPk(id)

    // 返回查询结果
    if (article) {
      res.json({
        status: true,
        message: "查询文章详情成功",
        data: { article },
      })
    } else {
      return res.status(404).json({
        status: false,
        message: "文章不存在",
      })
    }
  } catch (err) {
    res.status(500).json({
      status: false,
      message: "查询文章详情失败",
      errors: [err.message],
    })
  }
})

访问 http://localhost:3000/admin/articles/66

访问 http://localhost:3000/admin/articles/666,并不存在改文章

控制台对应的 sql 语句

7.3 使用Apifox

通过浏览器页面查看返回不方便,更推荐使用apifox,将开发环境默认前缀设置为:http://localhost:3000,创建接口并测试。

7.4 写接口 - 创建文章

首先需要验证表单数据,在 models/article.js 中添加相应配置,当前案例只对 title 参数进行校验。validate 为验证规则,其中:

  • notNull: 没有传 title,就会提示:标题必须存在。
  • notEmpty:用户传了 title 过来,但是却没有值,就会提示:标题不能为空。
  • len:限制长度。
javascript 复制代码
title: {
  type: DataTypes.STRING,
  allowNull: false,
  validate: {
    notNull: {
      msg: '标题必须存在。'
    },
    notEmpty: {
      msg: '标题不能为空。'
    },
    len: {
      args: [2, 45],
      msg: '标题长度需要在2 ~ 45个字符之间。'
    }
  }
},

Article.create() 等同于 INSERT INTO 表名 (列1, ...) VALUES (值1, ...)

javascript 复制代码
// 白名单过滤 - 获取请求参数
function getBody(req){
  return {
    title: req.body.title,
    content: req.body.content,
  }
}

router.post("/", async function (req, res, next) {
  const body = getBody(req)
  try {
    // 创建数据
    const article = await Article.create(body)

    // 返回创建结果
    res.status(201).json({
      status: true,
      message: "创建文章成功",
      data: article,
    })
  } catch (err) {
    res.json(err)
  }
})

apifox 测试,故意不传 title 查看报错信息,errors 是个列表,需要遍历所有错误信息。

修改 catch 捕获错误的相关逻辑

javascript 复制代码
catch (err) {
  if (err.name === "SequelizeValidationError") {
    const errors = err.errors.map((error) => error.message)
    res.status(400).json({
      status: false,
      message: "创建文章失败",
      errors,
    })
  } else {
    res.status(500).json({
      status: false,
      message: "创建文章失败",
      errors: [err.message],
    })
  }
}

7.5 写接口 - 删除文章

Article.destroy({ where: { id } }) 等同于如下 sql 语句。

javascript 复制代码
DELETE FROM \`Articles\` WHERE \`id\` = '102'
javascript 复制代码
// 删除文章
router.delete("/:id", async function (req, res, next) {
  try {
    // 获取参数 - 文章ID
    const { id } = req.params

    // 查询数据
    const article = await Article.findByPk(id)

    if (article) {
      // 删除数据
      await Article.destroy({ where: { id } })
      // 返回删除结果
      res.json({
        status: true,
        message: "删除文章成功",
      })
    } else {
      return res.status(404).json({
        status: false,
        message: "文章不存在",
      })
    }
  } catch (err) {
    res.status(500).json({
      status: false,
      message: "删除文章失败",
      errors: [err.message],
    })
  }
})

7.6 写接口 - 更新文章

javascript 复制代码
// 白名单过滤 - 获取请求参数
function getBody(req){
  return {
    title: req.body.title,
    content: req.body.content,
  }
}

router.put("/:id", async function (req, res, next) {
  const body = getBody(req)

  try {
    // 获取参数 - 文章ID
    const { id } = req.params

    // 查询数据
    const article = await Article.findByPk(id)

    if (article) {
      // 更新数据
      await article.update(body)
      // 返回更新结果
      res.json({
        status: true,
        message: "更新文章成功",
      })
    } else {
      return res.status(404).json({
        status: false,
        message: "文章不存在",
      })
    }
  } catch (err) {
    res.status(500).json({
      status: false,
      message: "更新文章失败",
      errors: [err.message],
    })
  }
})

7.7 写接口 - 模糊查询

做这种复杂的查询,要引入 Op。然后修改 findAll 的查询条件,title: { [Op.like]: %${query.title}% },这段代码等同于如下 sql 语句。

javascript 复制代码
select * from Articles where title like '%标题 10%'`
javascript 复制代码
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 (err) {
    res.status(500).json({
      status: false,
      message: "查询文章列表失败",
      errors: [err.message],
    })
  }
})

7.8 写接口 - 分页

分页的 sql 语句如下,LIMIT 后第一个参数为从哪个索引 开始查找,第二个参数为查找多少数据。

javascript 复制代码
SELECT * FROM `Articles` LIMIT 0, 10;

// 假如一页10条,第二页参数为 10 10,而不是 10 20
SELECT * FROM `Articles` LIMIT 10, 10;

需要注意 sequelize 中,查询条件的 offset 对应 sql 语句中第一个参数,limit 字段也就是 pageSize 对应 sql 语句中第二个参数。

findAll 改为 findAndCountAll,该方法返回对象,其中 count 为数据总数,rows 为当前查询到的数据。

javascript 复制代码
// 查询文章列表
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
    // 计算offset
    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)
    res.json({
      status: true,
      message: "查询文章列表成功",
      data: {
        articles: rows,
        pagination: { total: count, currentPage, pageSize },
      },
    })
  } catch (err) {
    res.status(500).json({
      status: false,
      message: "查询文章列表失败",
      errors: [err.message],
    })
  }
})

8. 封装响应 优化代码结构

上面的接口代码有大量重复性代码,适当封装实现复用。思路如下:

  • 如果没找到数据导致报错,通过抛出异常的方式,让catch来捕获异常,而不是直接在 try 中写 if。
  • 成功和失败状态响应的封装。
  • 查询、更新、删除都需要先查询当前文章,定义一个公共方法,可以直接调用这个方法。

项目根目录中新建一个 utils 目录,其中建一个 response.js,通过继承的方式,自定义一个 404 错误类,以及成功和失败状态的响应。

utils/response.js

javascript 复制代码
/**
 * 自定义 404 错误类
 */
class NotFoundError extends Error {
  constructor(message) {
    super(message)
    this.name = "NotFoundError"
  }
}

/**
 * 请求成功
 * @param res
 * @param message
 * @param data
 * @param code
 */
function success(res, message, data = {}, code = 200) {
  res.status(code).json({
    status: true,
    message,
    data,
  })
}

/**
 * 请求失败
 * @param res
 * @param error
 */
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,
}

现在就可以通过如下方式抛出异常

javascript 复制代码
throw new NotFoundError('错误信息')

成功的响应

javascript 复制代码
// 查询文章详情
success(res, '查询文章成功。', { article });

// 创建文章,状态码 201
success(res, '创建文章成功。', { article }, 201);

// 更新文章
success(res, '更新文章成功。', { article });

// 删除文章 文章已经被删掉了,所以不需要传 data
success(res, '删除文章成功。');

失败的响应

javascript 复制代码
catch (error) {
    failure(res, error);
}

封装查询文章的方法,查询、更新、删除中均可使用

javascript 复制代码
/**
 * 公共方法:查询当前文章
 */
async function getArticle(req) {
  // 获取文章 ID
  const { id } = req.params
  // 查询当前文章
  const article = await Article.findByPk(id)
  // 没找到 - 抛出异常
  if (!article) {
    throw new NotFoundError(`ID: ${id}的文章未找到。`)
  }

  return article
}

优化后的整体代码

javascript 复制代码
const express = require("express")
const router = express.Router()
const { Article } = require("../../models")
const { Op } = require("sequelize")
const { NotFoundError, success, failure } = require("../../utils/response")

/**
 * 查询文章列表
 * GET /admin/articles
 */
router.get("/", async function (req, res) {
  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: 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)
  }
})

/**
 * 查询文章详情
 * GET /admin/articles/:id
 */
router.get("/:id", async function (req, res) {
  try {
    const article = await getArticle(req)
    success(res, "查询文章成功。", { article })
  } catch (error) {
    failure(res, error)
  }
})

/**
 * 创建文章
 * POST /admin/articles
 */
router.post("/", async function (req, res) {
  try {
    const body = filterBody(req)

    const article = await Article.create(body)
    success(res, "创建文章成功。", { article }, 201)
  } catch (error) {
    failure(res, error)
  }
})

/**
 * 更新文章
 * PUT /admin/articles/:id
 */
router.put("/:id", async function (req, res) {
  try {
    const article = await getArticle(req)
    const body = filterBody(req)

    await article.update(body)
    success(res, "更新文章成功。", { article })
  } catch (error) {
    failure(res, error)
  }
})

/**
 * 删除文章
 * DELETE /admin/articles/:id
 */
router.delete("/:id", async function (req, res) {
  try {
    const article = await getArticle(req)

    await article.destroy()
    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
}

/**
 * 公共方法:白名单过滤
 * @param req
 * @returns {{title, content: (string|string|DocumentFragment|*)}}
 */
function filterBody(req) {
  return {
    title: req.body.title,
    content: req.body.content,
  }
}

module.exports = router

9. 总结

9.1 创建流程

  1. 安装 docker 和 数据库图形化工具
  2. npm i -g express-generator@4:安装 express 脚手架快速搭建项目
  3. npm i -g sequelize-cli: 安装 sequelize 命令行工具,可以执行模型、迁移、种子相关的命令
  4. express --no-view clwy-api: 创建项目
  5. npm i:下载项目依赖
  6. 删除 public/index.html,根目录下创建 docker-compose.yml 并配置数据库
  7. npm i nodemon :安装 nodemon,将 package.json 中的启动方式修改为 nodemon
  8. npm i sequelize mysql2:安装 sequelize 与 mysql2 依赖包,然后就可以使用 sequelize 操作 mysql
  9. sequelize init :初始化 sequelize,修改 config/config.json 中数据库相关配置
  10. npm start:启动服务

9.2 开发流程

  1. 创建数据库
  2. sequelize model:generate --name Article --attributes title:string,content:text :创建模型,注意: 模型名为单数形式,表名为复数形式,并指定字段与类型
  3. 上面一步会创建迁移文件,修改迁移文件中字段的要求,如本文将 title 字段设置为不可为null
  4. sequelize db:migrate:运行迁移文件,创建表
  5. sequelize seed:generate --name article:创建种子文件
  6. sequelize db:seed --seed xxx-article:运行指定种子文件,--seed后为完整文件名
  7. sequelize db:seed:all:运行所有种子文件
  8. 运行完种子文件,往表中添加测试数据

9.3 req说明

  • req.params:获取路由里的参数 /admin/articles/:id
  • req.query:获取 URL 地址里的查询参数 /admin/articles ?title=hello&currentPage=2&pageSize=20
  • req.body:获取通过 POST、PUT 发送的数据

9.4 Sequelize ORM 操作数据库常用方法

  • findAll:查询所有记录
  • findAndCountAll:查询所有记录,并统计数据总数
  • findByPk:通过主键查询单条数据
  • create:创建新数据
  • update:更新数据
  • destroy:删除数据
相关推荐
GISer_Jing12 分钟前
Vue前端进阶面试题(六)
前端·javascript·vue.js
YueiL16 分钟前
数据结构每日一题|判断链表是否有环型结构
javascript·数据结构·链表
之诚21 分钟前
使用Gradle编译前端的项目
前端·vue·gradle
FenceRain30 分钟前
uniapp 扩展picker-view实现条件查询
前端·javascript·uni-app
喵喵酱仔__32 分钟前
uniapp echarts tooltip formation 不识别html
前端·javascript·echarts
new出一个对象33 分钟前
uniapp在App端定义全局弹窗,当打开关闭弹窗会触发onShow、onHide生命周期怎么解决?
前端·uni-app
EterNity_TiMe_1 小时前
【论文复现】上下位关系自动检测方法
前端·javascript·网络·人工智能·前端框架·easyui
天农学子1 小时前
Easyui 实现订单拆分开票功能
前端·javascript·easyui
Swift社区1 小时前
工具与技术在 Debug 中的应用
前端·typescript·debug
阿珊和她的猫1 小时前
React中事件处理和合成事件:理解与使用
前端·react.js·前端框架