本文章为观看哔站视频 Express+MySQL+Sequelize实作API所作随笔,边敲代码便跟学。现发布只作为参考,有空再整理😀,同时也会补充在学习时自己的一些疑问及心得。
本人自学前端后初次涉猎nodejs,个人觉得此教程完备,适合初学者,推荐大家学习。
创建项目
-
创建项目
# 安装 express-generator yarn global add express-generator # 创建名为demo的项目 express --no-view demo # 进入项目目录 cd demo # 安装依赖包 npm i
-
安装 nodemon,监听代码变动
npm i nodemon
修改
package.json
"scripts": { "start": "nodemon ./bin/www" },
删除public/index.html后可以看见route/index.js下的JSON响应文段
安装Docker
-
安装
Windows安装使用Docker http://t.csdnimg.cn/eNFqZ
使用WSL安装Ubuntu Linux系统教程http://t.csdnimg.cn/OIBsE
-
配置
根目录创建docker-compose.yml文件,内容:
version: '3.8' services: mysql: image: mysql:8.3.0 command: mysqld --character-set-server=utf8mb4 --collation-server=utf8mb4_general_ci environment: - MYSQL_ROOT_PASSWORD=xx(输入实际密码) - MYSQL_LOWER_CASE_TABLE_NAMES=0 ports: - "3307:3306" volumes: - ./data/mysql:/var/lib/mysql
运行
docker-compose up -d
命令,下载mysql并连接
安装好后,如图:![外链图片转存失败,源站可能有防盗链由于我安装了MySql占用了3306端口,所以上方ports改为"3307:3306"
-
客户端管理
安装好后只是安装了MySql服务端,若想要更好管理客户端需要安装Navicat Premium 17破解版(http://t.csdnimg.cn/cN8Pu)
(这样之后就还可以通过使用Navicat来操作数据库 )
创建数据库和数据表
1.如何创建数据库与数据表?
2.MySQL中常见的数据类型
3.主键是什么?
4.给数据表,增加标题和内容
- 连接docker以及navicat连接mysql
- 创建数据库、表、基础查询语句
使用Sequelize ORM
- 安装
npm i -g sequelize-cli npm i sequelize mysql2 sequelize init
自动生成文件
配置环境(config.json),着重修改开发环境
"development": { "username": "root", "password": "xx(输入实际密码)", "database": "clwy_api_development", "host": "127.0.0.1", "dialect": "mysql", "timezone": "+08:00", "port": 3307 },
模型用于操作数据库
删除原先Articles表后,执行下方命令,创建模型:
sequelize model:generate --name Article --attributes title:string,content:text
迁移文件用来创建、修改表
模型名字是单数:
Article
;表的名字一定是复数形式,也就是Articles
对应数据库用单数,表用复数
执行下方命令,可在数据库客户端看到表
sequelize db:migrate
注意需要config上添加port:“3307”;以及Navicat上需要已经创建好同名数据库
之后就可在Navicat上看到Articles表了(另一个SequelizeMeta表是记录已经迁移的文件,下次迁移时就不会重复执行)
种子文件用于填充数据
sequelize seed:generate --name article
在seeders目录可见新建的种子文件
up
部分用来填充数据,down
部分用来删除数据的参照默认案例代码,得到up部分
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, {}); },
得到down部分
async down (queryInterface, Sequelize) { await queryInterface.bulkDelete('Articles', null, {}); }
运行种子
sequelize db:seed --seed xxx-article
(xxx-article这个是自己具体种子名)我的命令如下:
sequelize db:seed --seed 20240823095240-article
总结开发步骤:
步骤 命令 说明 第一步 sequelize model:generate --name Article --attributes … 建模型和迁移文件 第二步 人工处理 根据需求调整迁移文件 第三步 sequelize db:migrate 运行迁移,生成数据表 第四步 sequelize seed:generate --name article 新建种子文件 第五步 人工处理 将种子文件修改为自己想填充的数据 第六步 sequelize db:seed --seed xxx-article 运行种子文件,将数据填充到数据表中 这里的
种子
部分并不是必须的,但对于一些需要一下子插入大量数据的情况来说,使用种子还是非常方便的。我们可以根据实际的需求来灵活运用。
接口1:查询文章列表(GET)
routes文件夹下是我们的核心部分
-
编写路由,admin/articles,在app.js中配置好。接下来要访问数据库,使用模型:
注意:await Article.findAll();返回的是promise,所以需要使用async…await
下方代码除标准外 还加上了排序(逆序)、try catch
const express = require('express'); const router = express.Router(); const { Article } = require('../../models'); /* GET home page. */ 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: '查询文章列表失败。', error: [error.message] }); } }); module.exports = router;
接口2:查询文章详情(GET)
- 在路由中添加id参数
- 如何查询文章详情?
- 查询不存在的数据,应该怎么办?
router.get('/:id', async function (req, res, next) {
try {
const article = await Article.findByPk(req.params.id);
if (article) {
res.json({
status: true,
message: '查询文章详情成功。',
data: { article }
})
}else{
res.status(404).json({
status: false,
message: '查询文章详情失败。',
error: [`文章不存在。`]
})
}
}
catch (error) {
res.status(500).json({
status: false,
message: '查询文章详情失败。',
error: [error.message]
})
}
})
使用Apifox
①创建目录
②写入地址http://localhost:3000/admin/articles
③点击发送查看结果并保存
④前缀优化:配置开发环境前缀http://localhost:3000
;api地址就为/admin/articles
⑤分享
接口3:创建文章(POST)
- POST请求是怎么回事?
- 如何使用Apifox发送POST请求?
- 如何获取到POST请求的数据,并存入数据库中?
- 200与201状态码的区别
注意点:
- 接口地址和查询地址相同,但是是用POST
- 获取表单数据,使用req.body
- 通过Apifox提交表单,使用Body ->x-www-form-unlencoded
- 创建完成后使用状态码 201 返回
router.post('/', async function (req, res, next) {
try {
const article = await Article.create(req.body);
res.status(201).json({
status: true,
message: '创建文章成功。',
data: { article }
})
}catch(error){
res.status(500).json({
status: false,
message: '创建文章失败。',
error: [error.message]
})
}
})
接口4:删除文章
- 什么是DELETE请求?
- 删除文章如何实现?
router.delete('/:id', async function (req, res, next) {
try {
const article = await Article.findByPk(req.params.id);
if (article) {
await article.destroy();
res.json({
status: true,
message: '删除文章成功。'
// data: { article }
})
}else{
res.status(404).json({
status: false,
message: '删除文章失败。',
error: [`文章不存在。`]
})
}
}catch(error){
res.status(500).json({
status: false,
message: '删除文章失败。',
error: [error.message]
})
}
})
接口5:更新文章
- 什么是PUT请求
- 更新文章如何实现
注意:更新文章params传入id,body传入title和content
router.put('/:id', async function (req, res, next) {
try {
const article = await Article.findByPk(req.params.id);
if (article) {
await article.update(req.body);
res.json({
status: true,
message: '更新文章成功。',
data: { article }
})
}else{
res.status(404).json({
status: false,
message: '更新文章失败。',
error: [`文章不存在。`]
})
}
}catch(error){
res.status(500).json({
status: false,
message: '更新文章失败。',
error: [error.message]
})
}
})
接口6:模糊搜索
1.如何搜索文章?
2.SQL语句中的like与%
3.req.query获取搜索参数值
4.Node.js中如何实现模糊搜索?
注意:
- 此搜索近搜索标题单个参数,若多个参数则:
/articles?title=标题 10&content=xxx
- SQL代码:
SELECT * FROM articles WHERE title LIKE '%特定关键字%';
- 在接口1上更新,添加上查询内容
/*
* 接口1 - 查询文章列表(允许模糊查询)
* GET /admin/articles?title=xxx
* */
const { Op } = require('sequelize');//注意引入Op
router.get('/', async function (req, res, next) {
try {
const { title } = req.query;
const condition = { order: [['id', 'DESC']] };
if(title){
condition.where = { title: { [Op.like]: `%${title}%` } };
}
const articles = await Article.findAll(condition);
// 返回成功响应,包含文章列表
res.json({
status: true,
message: '查询文章列表成功。',
data: { articles }
});
} catch (error) {
// 发生错误时返回错误响应
res.status(500).json({
status: false,
message: '查询文章列表失败。',
error: [error.message]
});
}
});
接口7:数据分页
1.分页是什么?
2.分页实现的原理与计算公式
3.在Node.js中如何实现分页?
- SQL代码:
SELECT* FROM Articles LIMIT offset,pageSize
- api查询:
/articles?pageSize=15¤tPage=2
- 计算:offset = (currentPage - 1) * pageSize
- 在接口1上更新
- Math.abs是绝对值,Number是将字符串转换为数字
当前页数(current Page) | 从哪开始(offset) | 每页显示多少条(pageSize) |
---|---|---|
第1页 | 0 | 10 |
/*
* 接口1 - 查询文章列表(允许模糊查询,分页查询若不传参则默认显示第一页10条)
* GET /admin/articles?title=xxx
* */
router.get('/', async function (req, res, next) {
try {
const query = req.query;
// 当前是第几页,如果不传,那就是第一页(Math.abs是绝对值,Number是将字符串转换为数字)
const currentPage = Math.abs(Number(query.currentPage)) || 1;
// 每页显示多少条数据,如果不传,那就显示10条
const pageSize = Math.abs(Number(query.pageSize)) || 10;
const condition = {
order: [['id', 'DESC']],
limit: pageSize, //文档要求使用limit和offset
offset: (currentPage - 1) * pageSize //计算出当前页的偏移量
};
if (query.title) {
condition.where = { title: { [Op.like]: `%${query.title}%` } };
}
const { rows, count } = await Article.findAndCountAll(condition);//将findAll改为findAndCountAll,其返回的结果是一个对象,包含rows和count两个属性
// 返回成功响应,包含文章列表
res.json({
status: true,
message: '查询文章列表成功。',
data: {
articles: rows, //文章列表
pagination: {
currentPage, //当前页码
pageSize, //每页显示多少条数据
total: count //总条数
}
}
});
} catch (error) {
// 发生错误时返回错误响应
res.status(500).json({
status: false,
message: '查询文章列表失败。',
error: [error.message]
});
}
});
问题1:白名单过滤表单数据
-
存在问题:
- 例子:订单表有商品编号和订单状态两个字段。若用户找到api地址,同时传送stauts:true,导致此订单为已支付。
- 原因:使用的req.body会接收传送过来的所有数据。
-
解决办法:白名单过滤(强参数过滤)
使用过滤后的body,而不是req.body
// 使用白名单过滤 const body={ title:req.body.title, content:req.body.content } const article = await Article.create(body);
优化:将过滤封装成函数,更新文章时也可使用
function filterBody(req) { return { title: req.body.title, content: req.body.content } } /* * 接口5 - 更新文章 * PUT /admin/articles/:id * */ router.put('/:id', async function (req, res, next) { try { const article = await Article.findByPk(req.params.id); if (article) { const body = filterBody(req); await article.update(body); res.json({ status: true, message: '更新文章成功。', data: { article } }) } else { res.status(404).json({ status: false, message: '更新文章失败。', error: [`文章不存在。`] }) } } catch (error) { res.status(500).json({ status: false, message: '更新文章失败。', error: [error.message] }) } })
-
总结:
- 永远不要相信用户提交的数据
- 用户提交的数据一定要过滤
问题2:验证表单数据
-
问题:存在用户不填写数据或者填写数据不符合。
-
解决办法
-
解决办法1:在路由中编写if语句进行校验。
仍存在问题,导致路由代码繁多且不易复用
-
解决办法2:①模型中添加校验,②接口catch(error)处反馈校验错误提示
- 在模型中添加校验(找到Article.init中title)
title: { type: DataTypes.STRING, allowNull: false, validate: { notNull: { msg: '标题必须存在。' }, notEmpty: { msg: '标题不能为空。' }, len: { args: [2, 45], msg: '标题长度需要在2 ~ 45个字符之间。' } } },
-
在接口的catch(error)处反馈校验错误提示
catch (error) { if (error.name === 'SequelizeValidationError') //error.name为这个即为不符合模型中校验 { 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] }); } }
-
终极版:增删改查(封装响应,优化代码)
1.何为DRY原则?
2.如何自定义异常?
3.如何将重复的代码封装成函数?
- 编程届有句话叫做Done’t Repeat Yourself. 代码段中尽可能不要重复代码。
- 方法:
- 定义一个公共方法用于查询文章
- 封装查询:创建一个公共方法(如
getArticle
)来查询当前文章,减少代码重复,无论在哪个地方需要查询,都可以直接调用。- 异常处理:使用抛出异常的方式,如
NotFoundError
,在查询失败时,让调用者捕获异常,而不是在代码中直接处理。- 定义异常类:为特定错误创建自定义类,如
NotFoundError
,提供自定义错误信息。- 简化响应:定义函数如
success
和fail
,处理成功和失败的响应格式,统一状态码和提示信息。- 清理重复代码:删除重复的查询、更新和删除文章的代码段,替换为调用封装好的方法。
- 错误处理函数:创建一个函数来处理不同类型的错误,如表单验证错误和找不到资源(404)错误,统一使用标准响应格式。
- 优化路由:使用封装好的函数替换路由中的具体逻辑,保持代码简洁和清晰。
- 检查和测试:确保优化后的代码能正常工作,并在API测试工具中验证接口。
utils/response.js文件:
- 自定义404错误
- 查询文章方法
- 响应函数
success
这里是导入,是将模块化文件导入。所以单个文件需要依赖完整。所以在response.js文件中需要引用
const { Article } = require('../models');
// 引入Article模型
const { Article } = require('../models');
/**
* 自定义404错误
*/
class NotFoundError extends Error {
constructor(message) {
super(message);
this.name = 'NotFoundError';
}
}
/**
* 公共方法:查询当前文章
*/
async function getArticle(req) {
const article = await Article.findByPk(req.params.id)
if (!article) {
throw new NotFoundError(`ID:${req.params.id}文章不存在。`);
}
return article;
}
/**
* 请求成功success函数
* @param res
* @param message
* @param data
* @param code
*/
function success(res, message, data = {}, code = 200) {
res.status(code).json({
status: true,
message,
data
})
}
/**
* 请求失败failure函数
* @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
});
}
else if (error.name === 'NotFoundError') {
return res.status(404).json({
status: false,
message: '资源不存在',
errors: [error.message]
})
}
else {
return res.status(500).json({
status: false,
message: '服务器错误。',
errors: [error.message]
});
}
}
module.exports = {
NotFoundError,
getArticle,
success,
failure
}
route/admin/articles.js文件:
-
引入
const {NotFoundError,getArticle} = require('../../utils/response');
-
并将查询文章详情、更新、删除进行代码修改:
const article = await Article.findByPk(req.params.id);
改为const article = getArticle(req)
,将if else删去 -
将成功处(res.json代码段)改为使用
success
函数,将错误处catch (error) 内代码改为使用failure(res, error)