1.node的优势
并发能力特别强
Node.js使用了非阻塞的方式运行。同样的一台服务器,并发量是其他传统语言的数倍
第二:开发简单,无需学习新的语言
Node.js开发后端,使用的框架叫做Express,它是一个非常轻量的,学习起来非常简单的框架。
Node.js所使用的就是JS语言,并没有什么新的语法,更不用学习其他新的语言。
2.创建express项目
常用的node环境
express
koa
全局安装express
npm i -g express-generator@4
创建项目,进入项目,npm i安装依赖
express --no-view clwy-api
// --no-view参数,它的意思是不需要任何视图模板
进入项目
npm i
npm start
终端显示 node ./bin/www代表成功,此时访问localhost:3000,得到html格式,不是接口所需要的json格式
修改为JSON格式
修改routes/index.js
router.get("/", function (req, res, next) {
// res.render('index', { title: 'Express' });
res.json({ message: "hello nodejs" })
})
删除public下的index.html
!!!重启启动项目
页面json格式优化:JSON handle插件
nodemon安装
npm i nodemon
!!注意在项目路径下
"scripts": { "start": "nodemon ./bin/www" }
重新启动即可
3.项目结构解析
项目结构解析
bin/www:项目启动文件
node_modules,package-lock.json:依赖包和锁包版本号,npm i都会重新生成
public:静态文件存放
重点关注:app.js和routes即可
router解析
router.get("/hello", function (req, res, next) {
// res.render('index', { title: 'Express' });
res.json({ message: "hello nodejs2" })
})
// /hello表示接口的访问路径,现在要访问http://localhost:3000/hello才能访问了
// req参数
// res返回
// next回调多用于登录等场景
4.通过docker安装mysql
安装docker
// docker的安装地址及汉化包配置(需梯子)
https://github.com/asxez/DockerDesktop-CN/releases
// 配置中国镜像(docker引擎中)(无下载问题可不装)
{
"builder": {
"gc": {
"defaultKeepStorage": "20GB",
"enabled": true
}
},
"experimental": false,
"registry-mirrors": [
"https://xelrug2w.mirror.aliyuncs.com"
]
}
// 其他问题根据clwy文档更正
https://clwy.cn/chapters/fullstack-node-mysql#section8
安装mysql
项目app.js同级目录下,新建docker-compose.yml
// docker-compose.yml
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=clwy1234
- MYSQL_LOWER_CASE_TABLE_NAMES=0
ports:
- "3306:3306"
volumes:
- ./data/mysql:/var/lib/mysql
docker-compose up -d,此时docker中就显示有文件的mysql了
安装navicat
// navicat下载地址
https://www.navicat.com.cn/products/navicat-premium-lite
// 连接navicat
连接名:mysql/账号:root/密码:clwy1234
// 双击打开连接
5.创建数据库和数据表
创建数据库
右键mysql,新建数据库
新建表
给表添加字段id title artile[注:id点上自动递增],保存表名为Articles
常用数据类型
6.sql新增\编辑\删除
新增
// 第一种方法:手动输入,点下方的√或者ctrl+s保存
// 第二种方法:查询-新建查询-输入语句-运行-刷新Articles表
// 单行插入
INSERT INTO 表名 (列1, ...) VALUES (值1, ...)
// 例:
INSERT INTO `Articles` (`title`, `content`) VALUES ('行路难·其一', '长风破浪会有时,直挂云帆济沧海。');
// SQL语句规则说明
// **大小写**:INSERT INTO,WHERE都使用了大写字母,其实你改为小写也是一样可以正确运行的
// **反引号**:表的名字和字段的名字`title`\`content`使用了`号。这里不写这个符号也是可以运行的,但是有些情况下必须写,例如我们有一个字段刚好叫做`insert`
// 多行插入
// INSERT INTO 表名 (列1, ...) VALUES (值1, ...),(值1, ...)...;
// 例:
INSERT INTO `Articles` (`title`, `content`) VALUES ('将进酒', '天生我材必有用,千金散尽还复来。'), ('宣州谢朓楼饯别校书叔云', '抽刀断水水更流,举杯消愁愁更愁。'), ('梦游天姥吟留别', '安能摧眉折腰事权贵,使我不得开心颜!'), ('春夜宴从弟桃花园序', '天地者,万物之逆旅也;光阴者,百代之过客也。'), ('宣州谢朓楼饯别校书叔云', '弃我去者,昨日之日不可留;乱我心者,今日之日多烦忧。'), ('庐山谣寄卢侍御虚舟', '我本楚狂人,凤歌笑孔丘。手持绿玉杖,朝别黄鹤楼。'), ('行路难', '长风破浪会有时,直挂云帆济沧海'), ('将进酒', '人生得意须尽欢,莫使金樽空对月。天生我材必有用,千金散尽还复来。'), ('望庐山瀑布', '飞流直下三千尺,疑是银河落九天。'), ('访戴天山道士不遇', '树深时见鹿,溪午不闻钟。'), ('清平调', '云想衣裳花想容,春风拂槛露华浓。'), ('春夜洛城闻笛', '谁家玉笛暗飞声,散入春风满洛城。');
编辑
// UPDATE 表名 SET 列1=值1, 列2=值2, ... WHERE 条件
// 例:
UPDATE `Articles` SET `title`='黄鹤楼送孟浩然之广陵', `content`='故人西辞黄鹤楼,烟花三月下扬州。' WHERE `id`=2;
删除
// DELETE FROM 表名 WHERE 条件
// 例:
DELETE FROM `ARTICLES` WHERE `id`=5;
mysql教程
7.sql查询
查询
// 查询全部
SELECT * FROM `Articles`
// 查询id和title
SELECT `id`,`title` FROM `Articles`
条件查询
SELECT * FROM 表名 WHERE 条件;
-- 例如:
SELECT * FROM `Articles` WHERE `id`=2;
-- 或者,想查询id大于2的文章:
SELECT * FROM `Articles` WHERE `id`>2;
升序降序
// ASC升序
// DESC降序
SELECT `id`,`title` FROM `Articles` WHERE `id` > 2 ORDER BY `id` ASC
// 延伸:
// 其他查询:`聚合查询`、`分组查询`、`连接查询`
8.ORM
orm是什么?
ORM:在数据库和编程语言之间的一种映射关系
sequelize orm的使用
// 先安装`sequelize`的命令行工具,需要全局安装
npm i -g sequelize-cli
// 安装当前项目所依赖的`sequelize`包和对数据库支持依赖的`mysql2`
npm i sequelize mysql2
// 初始化项目
sequelize init
// 生成四个文件夹
**config**:`sequelize`连接数据库的配置文件。
**models**:模型文件,当我们使用`sequelize`来执行增删改查时,就需要用这里的模型文件了。
每个模型都对应数据库中的一张表。
**migrations**:迁移,如果需要对数据库做新增表、修改字段、删除表等等操作,就需要在这里添加迁移文件了。
而不是像以前那样,使用客户端软件来直接操作数据库。
**seeders**,存放的种子文件。一般会将一些需要添加到数据表的测试数据存在这里。
只需要运行一个命令,数据表中就会自动填充进一些用来测试内容的了。
├── config
│ └── config.json
├── migrations
├── models
│ └── index.js
└── seeders
9.模型、迁移与种子
配置config/config.json
{
"development": {
"username": "root", // 账号
"password": "clwy1234",
"database": "api_dev",
"host": "127.0.0.1",
"dialect": "mysql",// 密码
"timezone": "+08:00" // 时区
},
"test": {
"username": "root",
"password": null,
"database": "api_dev_test",
"host": "127.0.0.1",
"dialect": "mysql",
"timezone": "+08:00" // 时区
},
"production": {
"username": "root",
"password": null,
"database": "api_dev_production",
"host": "127.0.0.1",
"dialect": "mysql",
"timezone": "+08:00" // 时区
}
}
模型model和迁移文件migrations
先删除掉Articles表,重新生成
新建模型model和迁移文件migrations:
sequelize model:generate --name Article --attributes title:string,content:text
// models下生成了两个文件article.js和index.js
// migrations文件夹里生成了一个由当前时间+create-article命名的文件
注意:
-
models/article.js是单数,表名Articles是复数
-
20250609080105-create-article.js它的作用就是用来创建、修改表的
在`up`中,通过`createTabel`,创建了一个叫做`Articles`的表 在`down`中,删除表 额外生成了createdAt,updatedAt两个时间字段
运行迁移
sequelize db:migrate
// 刷新navicat,可以看到`Articles`表又神奇的出现了,而且还多了两个时间字段。这就是`迁移文件`的作用。
// 另外一张表`SequelizeMeta`是我们运行迁移命令时,自动生成的。
// 这张表里记录了当前已经跑过了哪些迁移,这样当你再次运行`sequelize db:migrate`时,
// 已经运行过的迁移文件,就不会重复再次执行了
种子文件seeders
生成seed
sequelize seed:generate --name article
生成100条数据
"use strict"
/** @type {import('sequelize-cli').Migration} */
module.exports = {
async up(queryInterface, Sequelize) {
const arts = []
const counts = 100
for (let i = 1; i <= counts; i++) {
const article = {
title: `文章的标题 ${i}`,
content: `文章的内容 ${i}`,
createdAt: new Date(),
updatedAt: new Date(),
}
arts.push(article)
}
await queryInterface.bulkInsert("Articles", arts, {})
},
async down(queryInterface, Sequelize) {
await queryInterface.bulkDelete("Articles", null, {})
},
}
执行
sequelize db:seed --seed 20250609091829-article
10.查询文章列表
建议使用cmd窗口起一下项目
新建routes/admin/articles.js,仿照其它路由引入express,在app.js中引入adminArticles路由
// 引入express
const express = require("express")
const router = express.Router()
// 引入Article表
const { Article } = require("../../models")
// async/await异步查询
// condition条件查询,按照id顺序倒序
// trycatch捕获错误
router.get("/", async function (req, res, next) {
try {
const condition = {
order: [["id", "DESC"]],
}
const ats = await Article.findAll(condition)
res.json({
status: 200,
data: ats,
message: "查询文章列表成功!",
})
} catch (error) {
console.error("Error fetching articles:", error)
res.status(500).json({
status: 500,
message: "查询文章列表失败!",
error: error.message,
})
}
})
module.exports = router
// app.js中
// 核心1
const adminArticlesRouter = require("./routes/admin/articles")
// 核心2
app.use("/admin/articles", adminArticlesRouter)
访问localhost:3000/admin/articles得到数据,在窗口能看到findAll等语句被转化成了SELECT语句
11.查询单个文章
核心:使用动态路由+findByPk
注意:对空值或不存在的值作处理
// articles.js中
router.get("/:id", async function (req, res, next) {
try {
const id = req.params.id
if (!id) {
return res.status(400).json({
status: 400,
message: "文章ID不能为空!",
})
}
const article = await Article.findByPk(id)
if (!article) {
return res.status(404).json({
status: 404,
message: "文章未找到!",
})
}
res.json({
status: 200,
data: article,
message: "查询文章成功!",
})
} catch (error) {
console.error("Error fetching articles:", error)
res.status(500).json({
status: 500,
message: "查询文章列表失败!",
error: error.message,
})
}
})
或者使用findOne方法(由ai生成)
// articles.js中
router.get("/:id", async function (req, res, next) {
try {
const ats = await Article.findOne({
where: {
id: req.params.id,
},
})
if (!ats) {
return res.status(404).json({
status: 404,
message: "文章未找到!",
})
}
res.json({
status: 200,
data: ats,
message: "查询文章详情成功!",
})
} catch (error) {
console.error("Error fetching article:", error)
res.status(500).json({
status: 500,
message: "查询文章详情失败!",
error: error.message,
})
}
})
12.apifox的使用
13.创建文章
apifox新建接口,使用查询文章列表的接口,改为post
处理接口
router.post("/", async function (req, res, next) {
try {
const { title, content } = req.body
if (!title || !content) {
return res.status(400).json({
status: 400,
message: "标题、内容和作者不能为空!",
})
}
const newArticle = await Article.create(req.body)
res.status(201).json({
status: 201,
data: newArticle,
message: "创建文章成功!",
})
} catch (error) {
console.error("Error creating article:", error)
res.status(500).json({
status: 500,
message: "创建文章失败!",
error: error.message,
})
}
})
校验响应结果201报错的问题
点击提取-提取到响应提示即可
流程
主窗口-新建团队-新建目录[后台]-新建目录[新闻通知]-添加接口
-
下方将文档模式改为调试模式,输入http://localhost:3000/admin/articles 访问成功!
-
右上方配置环境变量
开发环境,前置URL:http://localhost:3000 注意:http://localhost:3000/ 可能会错误访问到html
-
删除掉接口栏中的前缀,改为
/admin/articles,/admin/articles/{id}两个接口并保存 -
可以点击分享以分享接口
13.删除文章
删除
// 删除文章
router.delete("/:id", async function (req, res, next) {
try {
const article = await Article.findByPk(req.params.id)
if (!article) {
return res.status(404).json({
status: 404,
message: "文章未找到!",
})
}
await article.destroy()
res.json({
status: 200,
message: "删除文章成功!",
})
} catch (error) {
console.error("Error deleting article:", error)
res.status(500).json({
status: 500,
message: "删除文章失败!",
error: error.message,
})
}
})
批量删除
// 扩展:批量删除文章
router.delete("/", async function (req, res, next) {
try {
const { ids } = req.body
if (!ids || !Array.isArray(ids) || ids.length === 0) {
return res.status(400).json({
status: 400,
message: "请提供要删除的文章ID数组!",
})
}
const deletedCount = await Article.destroy({
where: {
id: ids,
},
})
res.json({
status: 200,
message: `成功删除 ${deletedCount} 篇文章`,
})
} catch (error) {
console.error("Error deleting articles:", error)
res.status(500).json({
status: 500,
message: "删除文章失败!",
error: error.message,
})
}
})
// apifox操作
body-json-{"ids":[]}
14.更新文章
接口处理
// 更新文章
router.put("/:id", async function (req, res, next) {
try {
const article = await Article.findByPk(req.params.id)
if (!article) {
return res.status(404).json({
status: 404,
message: "文章未找到!",
})
}
const updatedArticle = await article.update(req.body)
res.json({
status: 200,
data: updatedArticle,
message: "更新文章成功!",
})
} catch (error) {
console.error("Error updating article:", error)
res.status(500).json({
status: 500,
message: "更新文章失败!",
error: error.message,
})
}
})
apifox
注:body中是需要修改的对象属性
Executing (default): SELECT `id`, `title`, `content`, `createdAt`, `updatedAt`
FROM `Articles` AS `Article` WHERE `Article`.`id` = '2';
Executing (default): UPDATE `Articles` SET `title`=?,`content`=?,`updatedAt`=? WHERE `id` = ?
UPDATE `Articles` SET `title`=?,`content`=?,`updatedAt`=? WHERE `id` = ?
15.模糊查询
模糊查询的核心-mysql语法
目标:查询到数据库中标题中含有'标题 10'的数据(注意空格)
语法:
SELECT * FROM `Articles` WHERE `title` LIKE `%标题10%`
接口
基于查询的接口进行改造
// 模糊查询
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 ats = await Article.findAll(condition)
res.json({
status: 200,
data: ats,
message: "查询文章列表成功!",
})
} catch (error) {
console.error("Error fetching articles:", error)
res.status(500).json({
status: 500,
message: "查询文章列表失败!",
error: error.message,
})
}
})
// 对应的mysql
SELECT `id`, `title`, `content`, `createdAt`, `updatedAt`
FROM `Articles`
AS `Article` WHERE `Article`.`title` LIKE '%10%' ORDER BY `Article`.`id` DESC
// 注意
// Op需要引入;控制台上的报错信息
const { Op } = require("sequelize")
扩展:多个条件的查询
// 模糊查询
// 多个条件的,通过[Op.or]\[Op.and]控制
router.get("/", async function (req, res, next) {
try {
const { title, content } = req.query
const condition = {
order: [["id", "DESC"]],
}
if (title || content) {
condition.where = {
[Op.or]: [
{
title: {
[Op.like]: `%${title}%`,
},
},
{
content: {
[Op.like]: `%${content}%`,
},
},
],
}
}
const articles = await Article.findAll(condition)
res.json({
status: 200,
data: articles,
message: "查询文章成功!",
})
} catch (error) {
console.error("Error searching articles:", error)
res.status(500).json({
status: 500,
message: "查询文章失败!",
error: error.message,
})
}
})
16.分页
// 模糊查询
router.get("/", async function (req, res, next) {
try {
const { title = "", page = 1, pageSize = 10 } = req.query
const offset = (page - 1) * pageSize
const totalCount = await Article.count({
where: {
title: {
[Op.like]: `%${title}%`,
},
},
})
const condition = {
order: [["id", "DESC"]],
}
if (title) {
condition.where = {
title: {
[Op.like]: `%${title}%`,
},
}
}
const ats = await Article.findAll({
offset,
limit: parseInt(pageSize),
...condition,
})
res.json({
status: 200,
data: ats,
message: "查询文章列表成功!",
total: totalCount,
})
} catch (error) {
console.error("Error fetching articles:", error)
res.status(500).json({
status: 500,
message: "查询文章列表失败!",
error: error.message,
})
}
})
17.白名单
表单的新建\更新如果传入的id也一并处理的话,会有错误的,所有要把需要的属性筛出来
function filterBody(body) {
const allowedFields = ["title", "content"]
const filteredBody = {}
for (const field of allowedFields) {
if (body[field] !== undefined) {
filteredBody[field] = body[field]
}
}
return filteredBody
}
// 处理优化新建文章\更新文章接口
//#region 新增文章
router.post("/", async function (req, res, next) {
try {
const body = filterBody(req.body)
const { title, content } = body
if (!title || !content) {
return res.status(400).json({
status: 400,
message: "标题、内容和作者不能为空!",
})
}
const newArticle = await Article.create(body)
res.status(201).json({
status: 201,
data: newArticle,
message: "创建文章成功!",
})
} catch (error) {
console.error("Error creating article:", error)
res.status(500).json({
status: 500,
message: "创建文章失败!",
error: error.message,
})
}
})
// 更新文章
router.put("/:id", async function (req, res, next) {
try {
const article = await Article.findByPk(req.params.id)
if (!article) {
return res.status(404).json({
status: 404,
message: "文章未找到!",
})
}
const body = filterBody(req.body)
const { title, content } = body
if (!title || !content) {
return res.status(400).json({
status: 400,
message: "标题、内容不能为空!",
})
}
const updatedArticle = await article.update(body)
res.json({
status: 200,
data: updatedArticle,
message: "更新文章成功!",
})
} catch (error) {
console.error("Error updating article:", error)
res.status(500).json({
status: 500,
message: "更新文章失败!",
error: error.message,
})
}
})
18.表单验证
问题:怎么验证title字段同名问题?
修改models/articles.js
Article.init(
{
title: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notNull: {
msg: "标题必须存在",
},
notEmpty: {
msg: "标题不能为空",
},
len: {
args: [2, 50],
msg: "标题长度必须在2-50之间",
},
},
},
content: DataTypes.TEXT,
},
{
sequelize,
modelName: "Article",
}
)
优化routes/admin/articles.js中新增和更新接口
//#region 新增文章
router.post("/", async function (req, res, next) {
try {
const body = filterBody(req.body)
const newArticle = await Article.create(body)
res.status(201).json({
status: 201,
data: newArticle,
message: "创建文章成功!",
})
} catch (error) {
if (error.name === "SequelizeValidationError") {
return res.status(400).json({
status: 400,
message: "创建文章失败!",
error: error.errors.map((err) => err.message),
})
} else {
console.error("Error creating article:", error)
res.status(500).json({
status: 500,
message: "创建文章失败!",
error: [error.message],
})
}
}
})
//#region 更新文章
router.put("/:id", async function (req, res, next) {
try {
const article = await Article.findByPk(req.params.id)
if (!article) {
return res.status(404).json({
status: 404,
message: "文章未找到!",
})
}
const body = filterBody(req.body)
const updatedArticle = await article.update(body)
res.json({
status: 200,
data: updatedArticle,
message: "更新文章成功!",
})
} catch (error) {
if (error.name === "SequelizeValidationError") {
return res.status(400).json({
status: 400,
message: "更新文章失败!",
error: error.errors.map((err) => err.message),
})
} else {
console.error("Error updating article:", error)
res.status(500).json({
status: 500,
message: "更新文章失败!",
error: error.message,
})
}
}
})
// 注:
可以在catcherror中通过res.json(error)打印error查看错误信息
19.封装响应,优化代码
新建utils/response.js
/**
* 自定义错误类 NotFoundError
* 用于表示资源未找到的情况
*/
class NotFoundError extends Error {
constructor(message) {
super(message)
this.status = 404
this.message = message
this.error = "Not Found"
this.name = "NotFoundError"
}
}
/**
* 成功响应函数
* @param {Object} res - 响应对象
* @param {Object} data - 响应数据
* @param {string} message - 响应消息
* @param {number} status - 响应状态码
*/
function success(res, data, message, status) {
res.status(status || 200).json({
status: status || 200,
data,
message: message || "请求成功",
})
}
/**
* 错误响应函数
* @param {Object} res - 响应对象
* @param {Error} error - 错误对象
* @param {number} status - 响应状态码
*/
// 注:可以调整减少,视情况而定
function failure(res, error, message, status) {
if (error.name === "SequelizeValidationError") {
status = 400
message = error.errors.map((err) => err.message).join(";")
error = "Bad Request"
}
if (error instanceof NotFoundError) {
status = 404
error = "Not Found"
message = "请求资源不存在"
}
if (error instanceof InternalServerError) {
status = 500
error = "Internal Server Error"
message = "内部服务器错误"
}
if (error instanceof BadRequestError) {
status = 400
error = "Bad Request"
message = "请求错误"
}
if (error instanceof UnauthorizedError) {
status = 401
error = "Unauthorized"
message = "未授权"
}
if (error instanceof ForbiddenError) {
status = 403
error = "Forbidden"
message = "禁止访问"
}
if (error instanceof ConflictError) {
status = 409
error = "Conflict"
message = "资源冲突"
}
if (error instanceof UnprocessableEntityError) {
status = 422
error = "Unprocessable Entity"
message = "请求实体无法处理"
}
if (error instanceof InternalServerError) {
status = 500
error = "Internal Server Error"
message = "内部服务器错误"
}
if (error instanceof ServiceUnavailableError) {
status = 503
error = "Service Unavailable"
message = "服务不可用"
}
if (error instanceof GatewayTimeoutError) {
status = 504
error = "Gateway Timeout"
message = "网关超时"
}
if (error instanceof MethodNotAllowedError) {
status = 405
error = "Method Not Allowed"
message = "请求方法不允许"
}
res.status(status || 500).json({
status: status || 500,
error,
message: message || "服务器内部错误",
})
}
module.exports = {
NotFoundError,
success,
failure,
}
调整routes/admin/articles.js
const { NotFoundError, success, failure } = require("../../utils/response")
/**
* 获取文章
* @param {*} req
* @returns {Promise} - 文章对象
*/
async function getArticle(req) {
const article = await Article.findByPk(req.params.id)
if (!article) {
throw new NotFoundError("文章不存在")
}
return article
}
// 并处理成功/查询/失败的回调函数
20.复习
安装express
全局安装express脚手架
npm i -g express-generator@4
安装sequelize命令行工具
npm i -g sequelize-cli
创建项目,进入项目,npm i安装依赖(注:删掉index.html)
express --no-view clwy-api
npm i
// 同时配置package.json
npm i nodemon
// 安装sql的mysql的依赖
npm i sequelize mysql2
// sqlize安装好修改config配置链接数据库
模型\迁移\种子
// 创建模型
sequelize model:generate --name Article --attributes title:string,content:text
// 运行迁移文件
sequelize db:migrate
// 生成seed种子文件
sequelize seed:generate --name article
// 运行指定种子文件
sequelize db:seed --seed 20250609091829-article
// 运行全部种子文件
sequelize db:seed:all
RESTfulAPI
get:查询
post:创建
put:更新
delte:删除
// 额外的
patch:打补丁,修改部分字段.微信小程序的支持不够所以不用
获取数据
// admin/:id
req.params
// admin?id=''&name=''
req.query
// 请求体
req.body
操作数据库
// 查询所有记录
findAll
// 查询所有记录,并统计数据条数
findAndCountAll
// 通过主键查
findByPk
// 创建
create
// 更新
update
// 删除
delete
21.实战数据库设计
企业里项目开发的流程?
如何根据需求设计数据库?
关联的数据表?
数据的索引?
22.mysql workbench
数据库表的关联图谱
23.建表
回滚迁移
sequelize db:migrate:undo
migration里添加属性unsigned
id: { allowNull: false, autoIncrement: true, primaryKey: true, type: Sequelize.INTEGER.UNSIGNED, // 无符号 },
运行迁移命令
sequelize db:migrate
运行种子命令
sequelize db:seed --seed 20250609091829-article
创建category表
sequelize model:generate --name Category --attributes name:string,rank:integer
创建用户表
sequelize model:generate --name User --attributes email:string,username:string,password:string,nickname:string,sex:tinyint,company:string,introduce:text,role:tinyint
创建课程表
sequelize model:generate --name Course --attributes categoryId:integer,userId:integer,name:string,image:string,recommended:boolean,introductory:boolean,content:text,likesCount:integer,chaptersCount:integer
创建章节表
sequelize model:generate --name Chapter --attributes courseId:integer,title:string,content:text,video:string,rank:integer
创建设置表
sequelize model:generate --name Setting --attributes name:string,icp:string,copyright:string
运行迁移
sequelize db:migrate
运行种子命令
sequelize db:seed --seed 20250609091829-setting
24.分类接口
创建分类种子
sequelize seed:generate --name category
添加种子的内容
async up(queryInterface, Sequelize) { await queryInterface.bulkInsert( "Categories", [ { name: "前端开发", rank: 1, createdAt: new Date(), updatedAt: new Date() }, { name: "后端开发", rank: 2, createdAt: new Date(), updatedAt: new Date() }, { name: "移动端开发", rank: 3, createdAt: new Date(), updatedAt: new Date() }, { name: "数据库", rank: 4, createdAt: new Date(), updatedAt: new Date() }, { name: "服务器运维", rank: 5, createdAt: new Date(), updatedAt: new Date() }, { name: "公共", rank: 6, createdAt: new Date(), updatedAt: new Date() }, ], {} ) },
async down(queryInterface, Sequelize) { await queryInterface.bulkDelete("Categories", null, {}) },
运行种子
sequelize db:seed --seed 种子文件
修改模型,添加验证
Category.init(
{
name: {
type: DataTypes.STRING, // 使用字符串类型来存储名称
allowNull: false, // 不允许为空
unique: { msg: "名称已存在,请选择其他名称。" }, // 唯一性约束
validate: {
notNull: { msg: "名称必须填写。" },
notEmpty: { msg: "名称不能为空。" },
len: { args: [2, 45], msg: "长度必须是2 ~ 45之间。" },
},
},
rank: {
type: DataTypes.INTEGER,
allowNull: false,
validate: {
notNull: { msg: "排序必须填写。" },
notEmpty: { msg: "排序不能为空。" },
isInt: { msg: "排序必须为整数。" }, // 验证是否为整数
isPositive(value) {
// 自定义验证函数,确保排序为正整数
if (value <= 0) {
throw new Error("排序必须是正整数。")
}
},
// 要求不能小于1
min: {
args: 1,
msg: "排序必须大于或等于1。",
},
},
},
},
{
sequelize,
modelName: "Category",
}
)
复制修改路由文件,appJs添加路由,修改apifox
const adminCategoriesRouter = require("./routes/admin/categories") app.use("/admin/categories", adminCategoriesRouter)
25.系统设置接口
// 初始化数据库 sequelize db:migrate
// 创建种子文件setting sequelize seed:generate --name setting
// 设置种子文件 async up(queryInterface, Sequelize) { await queryInterface.bulkInsert( "Settings", [ { name: "长乐未央", icp: "鄂ICP备13016268号-11", copyright: "© 2013 Changle Weiyang Inc. All Rights Reserved.", createdAt: new Date(), updatedAt: new Date(), }, ], {} ) },
// 运行种子文件,表中添加了一条数据 sequelize db:seed --seed 20250630100059-setting
注:
-
路由文件中只需要2个接口,查询详情和修改系统设置,同时IP可以不传,使用findOne查找第一条数据即可, 同时注意修改字段数据const allowedFields = ["name", "icp", "copyright"]
-
app.js(注意大小写) const adminSettingsRouter = require("./routes/admin/settings") app.use("/admin/settings", adminSettingsRouter)
26.管理接口
后台接口开发定式
- 种子填充数据
- 修改模型(增加验证、增加关联)
- 复制其他路由文件,进行查找替换
- 修改白名单和搜索
- app.js中添加路由
- Apifox测试
user用户表中少了一个头像字段
sequelize migration:create --name add-avatar-to-user
async up(queryInterface, Sequelize) { await queryInterface.addColumn("Users", "avatar", { type: Sequelize.STRING, }) },
async down(queryInterface, Sequelize) { await queryInterface.removeColumn("Users", "avatar") },
sequelize db:migrate
User.init( { // ... avatar: { type: DataTypes.STRING, validate: { isUrl: { msg: "图片地址不正确。" }, }, }, }, { sequelize, modelName: "User", } )
初始化一个用户-管理员
sequelize seed:generate --name user
async up(queryInterface, Sequelize) { await queryInterface.bulkInsert('Users', [ { email: 'admin@clwy.cn', username: 'admin', password: '123123', nickname: '超厉害的管理员', sex: 2, role: 100, createdAt: new Date(), updatedAt: new Date() }, { email: 'user1@clwy.cn', username: 'user1', password: '123123', nickname: '普通用户1', sex: 0, role: 0, createdAt: new Date(), updatedAt: new Date() }, { email: 'user2@clwy.cn', username: 'user2', password: '123123', nickname: '普通用户2', sex: 0, role: 0, createdAt: new Date(), updatedAt: new Date() }, { email: 'user3@clwy.cn', username: 'user3', password: '123123', nickname: '普通用户3', sex: 1, role: 0, createdAt: new Date(), updatedAt: new Date() } ], {}); },
async down(queryInterface, Sequelize) { await queryInterface.bulkDelete('Users', null, {}); }
sequelize db:seed --seed 种子文件
修改模型(增加验证)
email: { type: DataTypes.STRING, allowNull: false, validate: { notNull: { msg: '邮箱必须填写。' }, notEmpty: { msg: '邮箱不能为空。' }, isEmail: { msg: '邮箱格式不正确。' }, async isUnique(value) { const user = await User.findOne({ where: { email: value } }) if (user) { throw new Error('邮箱已存在,请直接登录。'); } } } }, username: { type: DataTypes.STRING, allowNull: false, validate: { notNull: { msg: '用户名必须填写。' }, notEmpty: { msg: '用户名不能为空。' }, len: { args: [2, 45], msg: '用户名长度必须是2 ~ 45之间。' }, async isUnique(value) { const user = await User.findOne({ where: { username: value } }) if (user) { throw new Error('用户名已经存在。'); } } }, }, password: { type: DataTypes.STRING, allowNull: false, validate: { notNull: { msg: '密码必须填写。' }, notEmpty: { msg: '密码不能为空。' }, len: { args: [6, 45], msg: '密码长度必须是6 ~ 45之间。' } } }, nickname: { type: DataTypes.STRING, allowNull: false, validate: { notNull: { msg: '昵称必须填写。' }, notEmpty: { msg: '昵称不能为空。' }, len: { args: [2, 45], msg: '昵称长度必须是2 ~ 45之间。' } } }, sex: { type: DataTypes.TINYINT, allowNull: false, validate: { notNull: { msg: '性别必须填写。' }, notEmpty: { msg: '性别不能为空。' }, isIn: { args: [[0, 1, 2]], msg: '性别的值必须是,男性:0 女性:1 未选择:2。' } } }, company: DataTypes.STRING, introduce: DataTypes.TEXT, role: { type: DataTypes.TINYINT, allowNull: false, validate: { notNull: { msg: '用户组必须选择。' }, notEmpty: { msg: '用户组不能为空。' }, isIn: { args: [[0, 100]], msg: '用户组的值必须是,普通用户:0 管理员:100。' } } }, avatar: { type: DataTypes.STRING, validate: { isUrl: { msg: '图片地址不正确。' } } },
注意:
- 限定值在要求范围内用isIn
- unique验证有问题 使用unique验证,必须要给字段加唯一索引。 出错后,异常的名字是SequelizeUniqueConstraintError,而不是SequelizeValidationError。也就说要去resonses.js里增加另一个判断。 更糟糕的是,不能通过msg自定义提示信息。改用下面的方式:(有条件把之前的文件也调整一下) async isUnique(value) { const user = await User.findOne({ where: { username: value } }) if (user) { throw new Error('用户名已经存在。'); } }
白名单: const allowedFields = ["email", "username", "password", "nickname", "avatar", "sex", "role", "company", "introduce"]
模糊查询条件修改:(我暂时没有改)
app.js: const adminUsersRouter = require("./routes/admin/users") app.use("/admin/users", adminUsersRouter)
27.使用bcryptjs加密数据
<!-- 安装bcrypt.js -->
npm i bcryptjs
<!-- 使用bcrypt.js写在路由router中会很麻烦,而且会影响模型model验证的长度问题,∴写在模型验证里 -->
<!-- 注:记得引入bcrypt.js -->
password: {
type: DataTypes.STRING,
allowNull: false,
// validate: {
// notNull: { msg: "密码必须填写。" },
// notEmpty: { msg: "密码不能为空。" },
// len: { args: [6, 45], msg: "密码长度必须是6 ~ 45之间。" },
// },
set(value) {
// 检查是否为空
if (!value) {
throw new Error("密码必须填写。")
}
if (!value || value.length < 6) {
throw new Error("密码长度必须是6 ~ 45之间。")
}
// 使用 bcrypt 加密密码
// 这里的 10 是 bcrypt 的 salt rounds,表示加密强度
// 可以根据需要调整这个值,值越大加密越强,但也会增加加密时间
this.setDataValue("password", bcrypt.hashSync(value, 10))
},
},
<!-- 修改种子文件 -->
{
email: "admin@clwy.cn",
username: "admin",
// password: "123123",
password: bcrypt.hashSync("123123", 10),
nickname: "超厉害的管理员",
sex: 2,
role: 100,
createdAt: new Date(),
updatedAt: new Date(),
},
<!-- 清空用户表,执行种子 -->
sequelize db:seed --seed 种子文件
<!-- 对比密码 -->
<!-- 后续登陆会用到 -->
const isPasswordValid = bcrypt.compareSync("123123", "加密后的密码");
28.课程course接口,关联模型
添加初始课程数据
sequelize seed:generate --name course
async up(queryInterface, Sequelize) { await queryInterface.bulkInsert('Courses', [ { categoryId: 1, userId: 1, name: 'CSS 入门', recommended: true, introductory: true, createdAt: new Date(), updatedAt: new Date() }, { categoryId: 2, userId: 1, name: 'Node.js 项目实践(2024 版)', recommended: true, introductory: false, createdAt: new Date(), updatedAt: new Date() }, ], {}); },
async down(queryInterface, Sequelize) { await queryInterface.bulkDelete('Courses', null, {}); }
sequelize db:seed --seed 种子
修改模型(增加验证)
categoryId: {
type: DataTypes.INTEGER,
allowNull: false,
validate: {
notNull: { msg: '分类ID必须填写。' },
notEmpty: { msg: '分类ID不能为空。' },
async isPresent(value) {
const category = await sequelize.models.Category.findByPk(value)
if (!category) {
throw new Error(ID为:${value} 的分类不存在。);
}
}
}
},
userId: {
type: DataTypes.INTEGER,
allowNull: false,
validate: {
notNull: { msg: '用户ID必须填写。' },
notEmpty: { msg: '用户ID不能为空。' },
async isPresent(value) {
const user = await sequelize.models.User.findByPk(value)
if (!user) {
throw new Error(ID为:${value} 的用户不存在。);
}
}
}
},
name: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notNull: { msg: '名称必须填写。' },
notEmpty: { msg: '名称不能为空。' },
len: { args: [2, 45], msg: '名称长度必须是2 ~ 45之间。' }
}
},
image: {
type: DataTypes.STRING,
validate: {
isUrl: { msg: '图片地址不正确。' }
}
},
recommended: {
type: DataTypes.BOOLEAN,
validate: {
isIn: { args: [[true, false]], msg: '是否推荐的值必须是,推荐:true 不推荐:false。' }
}
},
introductory: {
type: DataTypes.BOOLEAN,
validate: {
isIn: { args: [[true, false]], msg: '是否入门课程的值必须是,推荐:true 不推荐:false。' }
}
},
content: DataTypes.TEXT,
likesCount: DataTypes.INTEGER,
chaptersCount: DataTypes.INTEGER
-
每个课程都是是属于一个分类的,每个课程也是属于某一个用户的。所以这里的categoryId和userId在分类表和用户表中,必须有对应的ID值 自定义验证isPresent,通过用户过传递过来的categoryId和userId去分类表和用户表里查了一下,确保提交的数据有对应的分类和用户
-
要用到其他模型,前面要加上sequelize.models
修改路由文件
修改白名单和搜索
- 布尔值在query里可能是字符串
添加路由
管理模型
static associate(models) { models.Course.belongsTo(models.Category, { as: "category" }) models.Course.belongsTo(models.User, { as: "user" }) }
const { Course, Category, User } = require("../../models")
const condition = { attributes: { exclude: ["CategoryId", "UserId"] }, include: [ { model: Category, as: "category", attributes: ["id", "name"], }, { model: User, as: "user", attributes: ["id", "username", "avatar"], }, ], order: [["id", "DESC"]], }
对应表的关联关系
每个分类,都有很多课程。每个用户,也可以发布很多课程,用到的方法叫做hasMany。 如:
static associate(models) { models.Category.hasMany(models.Course, { as: 'courses' }); }
static associate(models) { models.User.hasMany(models.Course, { as: 'courses' }); }
孤儿记录
- 方案一:可以在数据库里,设置外键约束,确保数据完整性,这样删除的时候,就会提示错误。但要注意啊,一般在企业里,是不让用外键约束。因为使用外键约束后,数据库会产生额外的性能开销。在高并发、数据量大的情况,可能造成性能瓶颈。
- 方案二:常规做法就是在代码层面来处理了;删除分类的同事删除子项
- 方案三:在删除分类的时候,查询一下,有没有关联的课程。只要有对应的课程,就提示用户,不能删除。
const count = await Course.count({ where: { categoryId: category.id, }, }) if (count > 0) { throw new Error("该分类下有课程,不能删除!") }
29.章节chapter接口
添加数据
sequelize seed:generate --name chapter
async up(queryInterface, Sequelize) { await queryInterface.bulkInsert('Chapters', [ { courseId: 1, title: 'CSS 课程介绍', content: 'CSS的全名是层叠样式表。官方的解释,我就不细说了,因为就算细说了,对新手朋友们来说,听得还是一脸懵逼。那我们就用最通俗的说法来讲,到底啥是CSS?', video: '', rank: 1, createdAt: new Date(), updatedAt: new Date() }, { courseId: 2, title: 'Node.js 课程介绍', content: '这套课程,定位是使用 JS 来全栈开发项目。让我们一起从零基础开始,学习接口开发。先从最基础的项目搭建、数据库的入门,再到完整的真实项目开发,一步步的和大家一起完成一个真实的项目。', video: '', rank: 1, createdAt: new Date(), updatedAt: new Date() }, { courseId: 2, title: '安装 Node.js', content: '安装Node.js,最简单办法,就是直接在官网下载了安装。但这种方法,却不是最好的办法。因为如果需要更新Node.js的版本,那就需要把之前的卸载了,再去下载安装其他版本,这样就非常的麻烦了。', video: '', rank: 2, createdAt: new Date(), updatedAt: new Date() }, ], {}); },
async down(queryInterface, Sequelize) { await queryInterface.bulkDelete('Chapters', null, {}); }
sequelize db:seed --seed xxx-chapter
修改模型(增加验证)
courseId: {
type: DataTypes.INTEGER,
allowNull: false,
validate: {
notNull: { msg: '课程ID必须填写。' },
notEmpty: { msg: '课程ID不能为空。' },
async isPresent(value) {
const course = await sequelize.models.Course.findByPk(value)
if (!course) {
throw new Error(ID为:${ value } 的课程不存在。);
}
}
}
},
title: {
type: DataTypes.STRING,
allowNull: false,
validate: {
notNull: { msg: '标题必须填写。' },
notEmpty: { msg: '标题不能为空。' },
len: { args: [2, 45], msg: '标题长度必须是2 ~ 45之间。' }
}
},
content: DataTypes.TEXT,
video: {
type: DataTypes.STRING,
validate: {
isUrl: { msg: '视频地址不正确。' }
}
},
rank: {
type: DataTypes.INTEGER,
allowNull: false,
validate: {
notNull: { msg: '排序必须填写。' },
notEmpty: { msg: '排序不能为空。' },
isInt: { msg: '排序必须为整数。' },
isPositive(value) {
if (value <= 0) {
throw new Error('排序必须是正整数。');
}
}
}
},
关联模型
每个章节都属于一个课程,所以是belongsTo课程
static associate(models) { models.Chapter.belongsTo(models.Course, { as: 'course' }); }
static associate(models) { // ... models.Course.hasMany(models.Chapter, { as: 'chapters' }); }
配置路由
- 修改路由文件
- 白名单
- getCondition关联模型的查询
- 修改查询单条的代码
- 修改搜索
- 删除时防止孤儿记录
- 添加路由
- 测试接口
注意:
- 排序 order: [ ["rank", "ASC"], ["id", "ASC"], ],
30.echarts数据统计接口
统计用户性别
//#region 统计用户性别 router.get("/sex", async function (req, res, next) { try { // 查询用户性别统计 const maleCount = await User.count({ where: { sex: 0, }, }) const femaleCount = await User.count({ where: { sex: 1, }, }) const unknownCount = await User.count({ where: { sex: 2, }, }) const list = [ { name: "男", value: maleCount }, { name: "女", value: femaleCount }, { name: "未知", value: unknownCount }, ] success(res, { list }, "查询用户性别成功!") } catch (error) { failure(res, error, "查询用户性别失败!") } })
每个月的用户数量
- 考察到mysql的分组查询
//#region 统计每个月的用户数量
router.get("/user", async function (req, res, next) {
try {
// 第一种方法:使用原始查询按月统计用户数量
// const result = await User.findAll({
// attributes: [
// [sequelize.literal("DATE_FORMAT(createdAt, '%Y-%m')"), "month"],
// [sequelize.fn("COUNT", sequelize.col("id")), "count"],
// ],
// group: [sequelize.literal("month")],
// order: [[sequelize.literal("month"), "ASC"]],
// })
// // 处理查询结果
// const data = {
// month: [],
// values: [],
// }
// result.forEach((item) => {
// const raw = item.get({ plain: true })
// data.month.push(raw.month)
// data.values.push(raw.count)
// })
// 第二种方法:使用原始 SQL 查询按月统计用户数量
// const [results] = await sequelize.query("SELECT DATE_FORMAT(createdAt, '%Y-%m') AS month, COUNT(*) AS value FROM Users GROUP BY month ORDER BY month ASC")
// const data = {
// months: [],
// values: [],
// }
// results.forEach((item) => {
// data.months.push(item.month)
// data.values.push(item.value)
// })
success(res, { results }, "查询每月用户数量成功!")
} catch (error) { failure(res, error, "查询每月用户数量失败!") } })
31.jwt实现管理员登录
优化error.js/responses.js
// #region 自定义400错误类 class BadRequestError extends Error { constructor(message) { super(message) this.name = "BadRequestError" } }
// #region 自定义401错误类 class UnauthorizedError extends Error { constructor(message) { super(message) this.name = "UnauthorizedError" } } // #region 自定义404错误类 class NotFoundError extends Error { constructor(message) { super(message) this.name = "NotFoundError" // this.message = message || "资源未找到" // this.statusCode = 404 } }
// #endregion module.exports = { BadRequestError, UnauthorizedError, NotFoundError, }
// 400 错误 if (error.name === "BadRequestError") { status = 400 message = error.message || "请求错误" } // 401 错误 if (error.name === "UnauthorizedError") { status = 401 message = error.message || "未授权" } // 404 错误 if (error.name === "NotFoundError") { status = 404 message = error.message || "资源未找到" }
登录思路
- 第一步:验证login和password,因为这里不是往数据库里存储数据,所以模型中的验证是用不了的。
- 第二步:判断不为空-查询数据库判断当前用户是否存在-比对密码-验证是否为管理员-jwt生成令牌
const bcrypt = require("bcryptjs")
npm i jsonwebtoken const jwt = require("jsonwebtoken")
npm i dotenv 创建.env文件,SECRET='hello' app.js中 const dotenv = require("dotenv") dotenv.config()
注意:修改了环境变量,项目要重启
const crypto = require('crypto'); // node自带的 console.log(crypto.randomBytes(32).toString('hex')); // 复制粘贴给SECRET使用
router.post("/login", async (req, res) => { try { const { login, password } = req.body
if (!login || !password) {
throw new BadRequestError("用户名和密码不能为空")
}
const condition = {
where: {
[Op.or]: [{ email: login }, { username: login }],
},
}
const user = await User.findOne(condition)
if (!user) {
throw new NotFoundError("用户不存在,无法登录。")
}
const isPasswordValid = await bcrypt.compare(password, user.password)
if (!isPasswordValid) {
throw new UnauthorizedError("密码错误,登录失败。")
}
if (user.role !== 100) {
throw new UnauthorizedError("您不是管理员,无法登录。")
}
<!-- jwt.sign生成令牌 -->
const token = jwt.sign(
{
id: user.id,
},
process.env.SECRET,
{
expiresIn: "30d",
}
)
success(res, { token }, "登录成功")
} catch (error) { return failure(res, error, "登录失败") } })
32.中间件,认证接口
中间件是什么?
-
给所有接口加一个验证
-
中间件,就是在运行某一个方法之前,要先去运行的方法
-
如何验证 token 是否正确? jwt.verify(token,密钥)
中间件
const jwt = require("jsonwebtoken") const { User } = require("../models") const { UnauthorizedError } = require("../utils/error") const { success, failure } = require("../utils/responses")
module.exports = async (req, res, next) => { try { // 判断 Token 是否存在 const { token } = req.headers if (!token) { throw new UnauthorizedError("当前接口需要认证才能访问。") } // 验证 token 是否正确 const decoded = jwt.verify(token, process.env.SECRET) // 从 jwt 中,解析出之前存入的 userId const { userId } = decoded // 查询一下,当前用户 const user = await User.findByPk(userId) if (!user) { throw new UnauthorizedError("用户不存在。") } // 验证当前用户是否是管理员 if (user.role !== 100) { throw new UnauthorizedError("您没有权限使用当前接口。") } // 如果通过验证,将 user 对象挂载到 req 上,方便后续中间件或路由使用 req.user = user // 一定要加 next(),才能继续进入到后续中间件或路由 next() } catch (error) { failure(res, error) } }
/**
- 请求失败
- @param res
- @param error */ function failure(res, error) { // ...
if (error.name === 'JsonWebTokenError') { return res.status(401).json({ status: false, message: '认证失败', errors: ['您提交的 token 错误。'] }); }
if (error.name === 'TokenExpiredError') { return res.status(401).json({ status: false, message: '认证失败', errors: ['您的 token 已过期。'] }); } // ... }
// app.js中 const adminAuth = require("./middlewares/admin-auth") // 所有的 admin 路由,都需要进行认证 app.use("/admin/articles", adminAuth, adminArticlesRouter) app.use("/admin/categories", adminAuth, adminCategoriesRouter) app.use("/admin/settings", adminAuth, adminSettingsRouter) app.use("/admin/users", adminAuth, adminUsersRouter) app.use("/admin/courses", adminAuth, adminCoursesRouter) app.use("/admin/chapters", adminAuth, adminChaptersRouter) app.use("/admin/chart", adminAuth, adminChartRouter) app.use("/admin/auth", adminAuthRouter) // 登录接口不用
测试接口,保存后置操作
apifox-登录接口-后置操作-提取变量
登录后,如何获取到当前用户的信息?
成功登录后,user信息挂在req.user上
以课程接口-创建文章为例,我们接受了一个userId 这个值,是用户通过表单传递过来的,这其实是不对的。 应该是谁登录了,那就是谁发布的课程 所以白名单filterBody里修改一下不要接受userId值了,从req.user中取
33.首页、分类、课程接口
首页接口
// 使用router/index.js
// 首页的课程焦点图/人气课程/入门课程
// 写完记得app.js中引入
const express = require("express")
const router = express.Router()
const { Course, Category, User } = require("../models")
const { success, failure } = require("../utils/responses")
const { where } = require("sequelize")
router.get("/", async (req, res) => {
try {
// 课程表-焦点图
const recommendedCourses = await Course.findAll({
attributes: { exclude: ["CategoryId", "UserId", "content"] },
include: [
{
model: Category,
as: "category",
attributes: ["id", "name"],
},
{
model: User,
as: "user",
attributes: ["id", "username", "nickname", "avatar", "company"],
},
],
order: [["id", "DESC"]],
where: { recommended: true }, // 字段recommended进行筛选,仅返回该字段值为 true 的课程记录
limit: 10, // 限制查询结果最多返回10条记录
})
// 人气课程
const likeCourses = await Course.findAll({
attributes: { exclude: ["CategoryId", "UserId", "content"] },
order: [
["likesCount", "DESC"],
["id", "DESC"],
],
limit: 10,
})
// 入门课程
const introductoryCourses = await Course.findAll({
attributes: { exclude: ["CategoryId", "UserId", "content"] },
where: { introductory: true },
order: [["id", "DESC"]],
limit: 10,
})
success(res, { recommendedCourses, likeCourses, introductoryCourses })
} catch (error) {
failure(res, error)
}
})
module.exports = router
分类接口
// 新建router/categories.js
// 课程页面
// 写完记得app.js中引入
const express = require("express")
const router = express.Router()
const { Category } = require("../models")
const { success, failure } = require("../utils/responses")
router.get("/", async (req, res) => {
try {
const categories = await Category.findAll({
order: [
["rank", "DESC"],
["id", "DESC"],
],
})
success(res, categories)
} catch (error) {
failure(res, error)
}
})
module.exports = router
课程、课程详情接口
// 使用router/index.js
// 首页的课程焦点图/人气课程/入门课程
// 写完记得app.js中引入
const express = require("express")
const router = express.Router()
const { Course, Category, Chapter, User } = require("../models")
const { success, failure } = require("../utils/responses")
router.get("/", async (req, res) => {
try {
const query = req.query
const currentPage = Math.abs(query.page) || 1
const pageSize = Math.abs(query.pageSize) || 10
const offset = (currentPage - 1) * pageSize
if (!query.categoryId) {
throw new Error("categoryId is required")
}
const condition = {
attributes: {
exclude: ["CategoryId", "UserId", "content"],
},
where: {
categoryId: query.categoryId,
},
limit: pageSize,
offset,
order: [["id", "DESC"]],
}
const { count, rows } = await Course.findAndCountAll(condition)
success(res, {
courses: rows,
pagination: {
total: count,
currentPage,
pageSize,
},
})
} catch (error) {
failure(res, error)
}
})
// 课程详情接口
// 传入id
router.get("/:id", async (req, res) => {
try {
const id = req.params.id
const condition = {
attributes: { exclude: ["CategoryId", "UserId"] },
include: [
{
model: Category,
as: "category",
attributes: ["id", "name"],
},
{
model: Chapter,
as: "chapters",
attributes: ["id", "title", "rank", "createdAt"],
order: [
["rank", "ASC"],
["id", "DESC"],
],
},
{
model: User,
as: "user",
attributes: ["id", "username", "nickname", "avatar", "company"],
},
],
}
const course = await Course.findByPk(id, condition)
if (!course) {
throw new Error("course not found,课程未找到")
}
success(res, { course })
} catch (error) {
failure(res, error)
}
})
module.exports = router
app.js中引入
// app.js
// 前台路由文件
const indexRouter = require("./routes/index")
const categoriesRouter = require("./routes/categories")
const coursesRouter = require("./routes/courses")
// 前台路由配置
app.use("/", indexRouter)
app.use("/categories", categoriesRouter)
app.use("/courses", coursesRouter)
// apifox测试
/
/categories
34.章节接口
Express简介
路由(Route):Route是什么、Route的定义、Route的实现、Route的实例、Route的运行流程、Request对象、Response对象
中间件:中间件简介、中间件功能、中间件的分类、中间件实例
中间件实例:Router是什么、为什么使用Router、Router的使用
EJS模板:EJS是什么、EJS的使用、EJS语法



7,node.js怎么分辨登录状态
12/21/23更新
sass颜色函数:lighten darken
问题:在项目中一个btn的hover/active/disabled状态往往是难以维护的
原先代码:
<div>
<button class="btn type1">按钮</button>
</div>
.btn {
padding: 20px;
}
.btn.type1 {
background: #409eff;
color: #fff;
}
.btn.type1:hover {
background: #73b8ff;
}
.btn.type1:active {
background: #0d84ff;
}
.btn.type1:disabled {
background: #a6d2ff;
}
解决方法:使用sass颜色函数
.btn {
padding: 40px;
}
.btn.type1 {
$color: #409eff;
background: $color;
color: #fff;
&:hover {
background: lighten($color, 10%); // 变淡
}
&:active {
background: darken($color, 10%); // 变深
}
&:disabled {
background: lighten($color, 30%); // 变淡
}
}
注意:
1,scss文件不能直接link引入html文件,需要通过vscode插件编译成css文件才可以
扩展:循环得到多个不同颜色的btn
<div>
<button class="btn type1">按钮1</button>
<button class="btn type2">按钮2</button>
<button class="btn type3">按钮3</button>
</div>
index.scss中:
$btnColor: #409eff,
#67c23a,
#f54343;
@for $i from 1 through length($btnColor) {
.btn {
padding: 40px;
}
.btn.type#{$i} {
$color: nth($btnColor, $i);
background: $color;
color: #fff;
&:hover {
background: lighten($color, 10%); // 变淡
}
&:active {
background: darken($color, 10%); // 变深
}
&:disabled {
background: lighten($color, 30%); // 变淡
}
}
}
24年,1月2日更新
eventloop相关
相关知识点: 1,进程线程 2,异步 3,eventloop
1,进程线程
什么是异步?
重点:JS是单线程语言
JS阻碍渲染?
任务优先级?
重点1:任务没有优先级,任务队列有
重点2:根据w3c的说法,随着浏览器的复杂度越来越高,每个任务都会有一个任务类型,但是不管怎样,微队列永远是优先级最高的
重点3:微队列》交互队列》延时队列
以上可以总结为:JS的事件循环
JS中的计时器能做到精确计时吗?
重点1:不能
重点2:多层循环有延迟,操作系统函数有误差,事件循环队列交互>延时,没有原子钟等等
1月3日更新
浏览器渲染原理
渲染的本质:
html字符串==>像素信息
面试题:
渲染
1,解析html/parseHTML
解析html-DOM树
document object model
解析css-CSSOM树
css object model
样式表:
<style>
<link ...>
<div style=""> // 内联
浏览器默认样式表
注:除了浏览器默认样式表都是可以改变的,内联用dom.style改变,其它的可以用document.styleSheets,选择适当的元素addRule('div','border:1px solid red !important')改变
HTML 解析过程中遇到 CSS 代码怎么办?
重点:预解析线程,下载解析css
重点:css不会阻塞解析html
HTML 解析过程中遇到 JS 代码怎么办?
重点:遇到js代码必须暂停
重点:JS执行可能回修改当前的DOM树,∴会阻塞解析html的根本原因
2,样式计算
让DOM树的每一个节点计算出最终样式
css属性值的计算过程:
层叠
继承
视觉格式化模型:
盒模型
包含块
3,布局Layout
DOM 树 和 Layout 树不⼀定是⼀⼀对应的
原因1:display:none的节点没有几何信息
原因2:伪元素选择器::before在dom树中不存在伪元素节点,但是拥有集合信息,∴会生成到布局树中
原因3:匿名行盒、匿名块盒(内容必须在行盒中,行盒和块盒不能相邻)
4,分层
重点1:好处提高效率
重点2: 滚动条、堆叠上下文、opacity、transform都会影响,或者用will-change:transform希望分层
5,绘制paint
6,分块tiling
在合成线程分块+合成
7,光栅化Raster
GPU加速
优先处理靠近视口的块
8,画draw
画完交给gpu最终呈现
问:为什么要交给gpu?
答:gpu是浏览器端的,渲染流程在沙盒里的涉及到安全问题
总结:浏览器的渲染
渲染主线程中:
解析html+样式计算+布局layout+分层layer+绘制painter
合成线程中:
分块tiling+光栅化raster+画draw
2.reflow回流/repaint重绘
例:改变宽度width:300px
重新计算样式、布局等等
影响效率
∴在代码运行过程中,尽量少的影响几何信息,如margin、padding、宽高、字体大小
重点:是异步完成的
∵可能会有多个连续的操作,浏览器会合并这些操作,统一计算
随之而来的问题:如果在这些操作后,获取最新的布局信息如clientWidth可能会有问题,∴遇到这种问题浏览器会立即reflow获取最新数据
例:改变字体颜色等
重新分层+绘制,没有样式计算
∴reflow一定会引起repaint
3.transform为什么效率高?
∵transform既不影响布局也不影响绘制指令,只影响draw画的操作
1月13日更新
面试题相关
1,移动端1px问题
问题产生:由于移动设备的dpr问题导致,css里的1px在物理像素里的显示不同
解决方法:(常见)
1,transform:scale,可以用在伪元素,媒体查询中
2,viewport视口,也是flexible.js库的原理,改变initial-scale的值
2.渐进增强、优雅降级
两种开发策略,一种是向前兼容,一种是向后兼容
渐进增强:先构建最基本的功能,然后根据浏览器情况追加功能、样式
向后兼容:构建后完整功能,再根据低版本兼容
4.进程、线程的概念
----->大师课第一节
6.title和h1、b和strong、i em
title:html标题
h1:一级标题
b是加粗文本
strong是h5标签,强调
i是斜体
em是h5标签,强调
7.TLS/SSL的工作原理---->HTTP知识相关
用于保护网络通信的加密协议
TLS是SSL的后继
过程/工作原理:
对称加密+密钥加密
握手阶段:
客户端给服务器发送hello,告诉服务器TLS版本,随机数,加密套件,服务器给客户端加密套件、随机数、公钥,双方根据预主密钥生成会话密钥,进行对称加密
HTTPS是什么?加密原理和证书。SSL/TLS握手过程_哔哩哔哩_bilibili
7-2.防御XSS攻击---->HTTP知识相关,网络安全
后端中间件、组件转译
HTTPOnly防止恶意截取cookie
HTTPencode转译,有一个xssFilter库
css中用encodeForCss方法函数
8.&& ||的返回值
&&与 ||或都可以进行短路操作
||或有一个真则为真,∴
第一个数求值为false则返回第二个数
第一个数为对象则返回第一个数
都为undefined返回undefined
都为null返回null
都为NaN返回NaN
&&全为真才为真,第一个操作数为false第二个操作数就不执行 ,∴
第一个数求值为false则返回第一个数
第一个数为对象则返回第一个数
都为undefined返回undefined
都为null返回null
都为NaN返回NaN
11.margin\padding的使用场景?
margin是容器之间的距离,边框外;padding是容器内元素和元素的距离,边框内
margin可能会有重叠问题,此时可以用padding结局--->跳转问题3
3.from memory cache \ from dist cache 的区别,性能对比,怎么选择
3.service worker
看视频
3.webpack优化性能
3.node相关
eventloop和浏览器的区别
process.nextTick执行顺序
算法题
1.大数相加
2.插入排序
const arr = [5, 2, 4, 6, 1, 3]
const insertFn = (arr) => {
for (let i = 1; i < arr.length; i++) {
const cur = arr[i]
let j = i - 1
while (j >= 0 && arr[j] > cur) {
arr[j + 1] = arr[j]
j--
}
arr[j + 1] = cur
}
return arr
}
insertFn(arr)
3.字符串全排列
前端优化题
追加问题: grid布局 工程化 前端监控?
手写题
1月14日,周日更新
12.console.log()
是同步的,但是打印引用类型比如说对象的属性的时候,打印的是快照
但是查看对象的时候,看的是地址里的值
解决方法:JSON序列化
13.数据类型题相关
基础类型:
num
string
boo
undefined
null
bigint
symbol
引用类型:(即object)
[]
{}
延伸:
typeof null ====>object
13-2. 1.0.1+0.2为什么不=0.3,双精度浮点数,解决方法
回答核心:
∵计算机是二进制的,0.1+0.2会转换成二进制科学计数法计算再转回十进制,0.1转换二进制会是一个无限循环,导致产生误差结果为0.3000000000004
JS的存储方式:双精度浮点数
解决方法:
1.小数转换成整数计算
2.Number.EPSILON代表极小数来比较
15.深拷贝/浅拷贝
15-1.浅拷贝
// 浅拷贝:(精确拷贝)
// 基本数据类型直接拷贝值,引用数据类型只能拷贝引用地址
let a = 1;
let b = a
a = 2
console.log(b); // 1
let a = { name: "qc" };
let b = a
a.name = 'zch'
console.log(b); // {name:'zch'}
// 实现
function shallowClone(obj) {
const newObj = {}
for (const key in object) {
if (Object.hasOwnProperty(key)) {
newObj[key] = obj[key]
}
}
return newObj
}
// 存在浅拷贝现象的
// 1.Object.assign
// 第一层深拷贝了,但是第二层的引用类型还是浅拷贝
const obA = {
name: 'a',
nature: [1, 2, 3],
age: {
a1: 1,
a2: 2
},
fn: function () {
console.log('obA');
}
}
const obB = Object.assign({}, obA)
obA.age.a1 = 3;
console.log(obB); // 跟着obA一起改变
// 2.Array.prototype.slice
const arr1 = [1, 2, 3]
const arr2 = arr1.slice(0)
arr1[0] = 2
console.log(arr1) // [2,2,3]
console.log(arr2) // [2,2,3]
// 3.Array.prototype.concat
// 第一层深拷贝了,但是第二层的引用类型还是浅拷贝
const arr1 = [1, 2, 3, [4, 5]]
const arr2 = arr1.concat()
arr1[3][0] = 5
console.log(arr1) // 相同,发生了浅拷贝
console.log(arr2)
// 4,...拓展运算符
// 第一层深拷贝了,但是第二层的引用类型还是浅拷贝
const arr1 = [1, 2, 3, [4, 5]]
const arr2 = arr1.concat()
arr1[3][0] = 5
console.log(arr1) // 相同,发生了浅拷贝
console.log(arr2)
15-2.深拷贝
// 1.lodash库的_cloneDeep
// 2.JSON序列化
// 有弊端,见15-3
// 函数、undefined、symbol都会丢失等
// 3.循环递归
见例1
// 例1
function cloneDeep(params, hash = new WeakMap()) {
if (params === null) return params
if (params instanceof RegExp) return new RegExp(params)
if (params instanceof Date) return new Date(params)
if (typeof params != 'object') return params
if (hash.get(params)) return hash.get(params)
let cloneObj = new params.constructor()
hash.set(params, cloneObj)
for (const key in params) {
if (params.hasOwnProperty(key)) {
cloneObj[key] = cloneDeep(params[key], hash)
}
}
return cloneObj
}
15-3.JSON序列化的弊端
1.如果obj里面有时间对象,则JSON.stringify后再JSON.parse的结果,时间将只是字符串的形式,而不是对象的形式
2.如果obj里有RegExp(正则表达式的缩写)、Error对象,则序列化的结果将只得到空对象;
3、如果obj里有函数,undefined,则序列化的结果会把函数或 undefined丢失;
4、如果obj里有NaN、Infinity和-Infinity,则序列化的结果会变成null
5、JSON.stringify()只能序列化对象的可枚举的自有属性,例如 如果obj中的对象是有构造函数生成的, 则使用JSON.parse(JSON.stringify(obj))深拷贝后,会丢弃对象的constructor;
6、如果对象中存在循环引用的情况也无法正确实现深拷贝;
15.严格模式,有什么好处
15.函数作用域,词法作用域,匿名函数,函数表达式和函数声明
15。实现一个源生ajax,fetch
15.前端解析二进制,流媒体、图片二进制怎么渲染到页面
15.实现一个简单的模版引擎
15.函数记忆?什么情况下用?实现一个具有记忆功能的函数
15.lazyMan
15。parseQuery实现
15。乱序算法
15。数组去重,考虑到NAN object
1月15日笔记 1、JSON序列化的弊端需要加深
2月29日更新 10,反转链表,
未知的 2,千分位----还没掌握
3月7日更新
参考链接:
- 1.JS面试题
- 2.【JS春招面试题】浏览器的存储方式有什么?_哔哩哔哩_bilibili
1.AJAX
相关题:
是什么?原理?手写
2.本地存储,token
相关题:
三者的区别?
token登录流程
3.页面渲染
相关题:
渲染页面的过程
dom树和渲染树的区别
本笔记上方有浏览器渲染原理
4.精灵图和base64,svg
相关题:
精灵图和base64的区别
svg格式
精灵图多个小图拼接在一起
base64是字符串,缺点:低版本兼容性
3月9日
数据没有请求过来时怎么办
递归遇到过什么问题?