node的项目实战相关

174 阅读35分钟

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,新建数据库

image.png

新建表

image.png

给表添加字段id title artile[注:id点上自动递增],保存表名为Articles

image.png

常用数据类型

image.png

image.png

image.png

image.png

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教程

clwy.cn/documents/m…

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

image.png

处理接口

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报错的问题

点击提取-提取到响应提示即可

流程

主窗口-新建团队-新建目录[后台]-新建目录[新闻通知]-添加接口

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中是需要修改的对象属性

image.png

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语法

1685956068955.png

1685956108964.png

1685956132305.png

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-DOMdocument 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知识相关

用于保护网络通信的加密协议
TLSSSL的后继

过程/工作原理:
对称加密+密钥加密
握手阶段:
    客户端给服务器发送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.大数相加

JS实现两个大数相加_js二进制相加代码-CSDN博客

2.插入排序

js 常用排序算法_js排序-CSDN博客

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.字符串全排列

js 字符串的全排序_js对字符串排序-CSDN博客

前端优化题

追加问题: 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里有NaNInfinity和-Infinity,则序列化的结果会变成null  
5JSON.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.AJAX

相关题:
是什么?原理?手写

2.本地存储,token

相关题:
三者的区别?

token登录流程

3.页面渲染

相关题:
渲染页面的过程
dom树和渲染树的区别
本笔记上方有浏览器渲染原理

4.精灵图和base64,svg

相关题:
精灵图和base64的区别
svg格式
精灵图多个小图拼接在一起
base64是字符串,缺点:低版本兼容性

3月9日

数据没有请求过来时怎么办
递归遇到过什么问题?