白筱汐

想都是问题,做都是答案

0%

SQL 综合练习

– 创建 customers 表,用于存储客户信息
CREATE TABLE IF NOT EXISTS customers (
id int(11) NOT NULL AUTO_INCREMENT COMMENT ‘客户ID,自增长’,
name varchar(255) NOT NULL COMMENT ‘客户姓名,非空’,
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=’客户信息表’;

– 创建 orders 表,用于存储订单信息
CREATE TABLE IF NOT EXISTS orders (
id int(11) NOT NULL AUTO_INCREMENT COMMENT ‘订单ID,自增长’,
customer_id int(11) NOT NULL COMMENT ‘客户ID,非空’,
order_date date NOT NULL COMMENT ‘订单日期,非空’,
total_amount decimal(10,2) NOT NULL COMMENT ‘订单总金额,非空’,
PRIMARY KEY (id),
FOREIGN KEY (customer_id) REFERENCES customers (id) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=’订单信息表’;

– 创建 order_items 表,用于存储订单商品信息
CREATE TABLE IF NOT EXISTS order_items (
id int(11) NOT NULL AUTO_INCREMENT COMMENT ‘商品ID,自增长’,
order_id int(11) NOT NULL COMMENT ‘订单ID,非空’,
product_name varchar(255) NOT NULL COMMENT ‘商品名称,非空’,
quantity int(11) NOT NULL COMMENT ‘商品数量,非空’,
price decimal(10,2) NOT NULL COMMENT ‘商品单价,非空’,
PRIMARY KEY (id),
FOREIGN KEY (order_id) REFERENCES orders (id) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT=’订单商品信息表’;

– 向 customers 表插入数据
INSERT INTO customers (name)
VALUES
(‘张丽娜’),(‘李明’),(‘王磊’),(‘赵静’),(‘钱伟’),
(‘孙芳’),(‘周涛’),(‘吴洋’),(‘郑红’),(‘刘华’),
(‘陈明’),(‘杨丽’),(‘王磊’),(‘张伟’),(‘李娜’),
(‘刘洋’),(‘陈静’),(‘杨阳’),(‘王丽’),(‘张强’);

SELECT * FROM customers;

– 向 orders 表插入数据
INSERT INTO orders (customer_id, order_date, total_amount)
VALUES
(1, ‘2022-01-01’,100.00),(1, ‘2022-01-02’,200.00),
(2, ‘2022-01-03’,300.00),(2, ‘2022-01-04’,400.00),
(3, ‘2022-01-05’,500.00),(3, ‘2022-01-06’,600.00),
(4, ‘2022-01-07’,700.00),(4, ‘2022-01-08’,800.00),
(5, ‘2022-01-09’,900.00),(5, ‘2022-01-10’,1000.00);

SELECT * FROM orders;

– 向 order_items 表插入数据
INSERT INTO order_items (order_id, product_name, quantity, price)
VALUES
(1, ‘耐克篮球鞋’,1,100.00),
(1, ‘阿迪达斯跑步鞋’,2,50.00),
(2, ‘匡威帆布鞋’,3,100.00),
(2, ‘万斯板鞋’,4,50.00),
(3, ‘新百伦运动鞋’,5,100.00),
(3, ‘彪马休闲鞋’,6,50.00),
(4, ‘锐步经典鞋’,7,100.00),
(5, ‘亚瑟士运动鞋’,10,50.00),
(5, ‘帆布鞋’,1,100.00),
(1, ‘苹果手写笔’,2,50.00),
(2, ‘电脑包’,3,100.00),
(3, ‘苹果手机’,4,50.00),
(4, ‘苹果耳机’,5,100.00),
(5, ‘苹果平板’,7,100.00);

SELECT * FROM order_items;

– 需求1:查询每个客户的订单总金额
– 分析:客户的订单存在订单表里,可能有多个订单,这里需求 JOIN ON 关联2个表,然后用 GROUP BY 根据客户id 分组,再通过 SUM 函数计算价格总和。
SELECT
customers.NAME,
SUM(orders.total_amount) AS total_amount
FROM
customers
INNER JOIN orders ON customers.id = orders.customer_id
GROUP BY
customers.id
ORDER BY
total_amount DESC
LIMIT 0,
3;

– 需求2:查询每个客户的订单总金额,并计算其占比
– 分析:每个客户的总金额的需求上面已经实现了,这里需求算占比,就需要通过一个子查询来计算全部订单的总金额,然后相除
SELECT
customers.NAME,
SUM(orders.total_amount) AS total_amount,
SUM(orders.total_amount) / (SELECT SUM(total_amount) FROM orders) AS percentage
FROM
customers
INNER JOIN orders ON customers.id = orders.customer_id
GROUP BY
customers.id;

– 需求3:查询每个客户的订单总金额,并列出每个订单的商品清单
– 分析:这里在总金额的基础上,多了订单项的查询,需求多关联一个表
SELECT
customers.NAME,
orders.order_date,
orders.total_amount,
order_items.product_name,
order_items.quantity,
order_items.price
FROM
customers
JOIN orders ON customers.id = orders.customer_id
JOIN order_items ON orders.id = order_items.order_id
ORDER BY
customers.name,
orders.order_date;

– 需求4:查询每个客户的订单总金额,并列出每个订单的商品清单,同时只显示客户名字姓 “张” 的客户记录
– 分析:总金额和商品清单的需求前面实现过了,现在只需要加一个 WHERE 来过滤只查询姓张的客户
SELECT
customers.NAME,
orders.order_date,
orders.total_amount,
order_items.product_name,
order_items.quantity,
order_items.price
FROM
customers
JOIN orders ON customers.id = orders.customer_id
JOIN order_items ON orders.id = order_items.order_id
WHERE
name LIKE ‘张%’
ORDER BY
customers.NAME,
orders.order_date;
– 需求5:查询每个客户的订单总金额,并列出每个订单的商品清单,同时只显示订单日期在 2022-01-01 至 2022-01-03 之间的记录。
– 分析:通过 BETWEEN AND 来过滤时间范围。
SELECT
customers.NAME,
orders.order_date,
orders.total_amount,
order_items.product_name,
order_items.quantity,
order_items.price
FROM
customers
JOIN orders ON customers.id = orders.customer_id
JOIN order_items ON orders.id = order_items.order_id
WHERE
orders.order_date BETWEEN ‘2022-01-01’
AND ‘2022-01-03’
ORDER BY
customers.NAME,
orders.order_date;

– 需求6:查询每个客户的订单总金额,并计算商品数量,只包含商品名称包含 “鞋” 的商品,商品名用 - 连接,显示前3条记录
– 分析:查询订单总金额和商品总数量都需要用 GROUP BY 根据 customer.id 分组,过滤出只包含鞋的商品 %鞋%,把分组的多条商品名连接需要用 GROUP_CONCAT 函数,SEPARATOR 只能用于 GROUP_CONCAT函数,指定拼接字符串之间的分隔符
SELECT
c.NAME AS cutomer_name,
SUM(o.total_amount) total_amount,
COUNT(oi.id) total_quantity,
GROUP_CONCAT(oi.product_name SEPARATOR ‘-‘) product_names
FROM
customers c
JOIN orders o ON c.id = o.customer_id
JOIN order_items oi ON o.id = oi.order_id
WHERE
oi.product_name LIKE ‘%鞋%’
GROUP BY
c.id
ORDER BY
total_amount DESC
LIMIT 3;

– 查询存在订单的客户
– 分析:使用子查询和 EXISTS 实现
SELECT
*
FROM
customers
WHERE
EXISTS (SELECT 1 FROM orders WHERE orders.customer_id = customers.id);
– 查询没有下单过的客户
SELECT
*
FROM
customers c
WHERE
NOT EXISTS (SELECT 1 FROM orders o WHERE o.customer_id = c.id);

– 需求8:将王磊的订单总金额打九折
– 分析:update 更新订单金额,子查询查 王磊的订单

– 查询王磊的订单
SELECT * FROM orders
JOIN customers ON orders.customer_id = customers.id
WHERE customers.name = ‘王磊’;

– 订单可能有多条,使用 in 指定一个集合
UPDATE orders o SET o.total_amount = o.total_amount * 0.9
WHERE o.customer_id IN (SELECT id FROM customers WHERE name = ‘王磊’);

– 还原订单价格
UPDATE orders o
JOIN customers c ON o.customer_id = c.id
SET o.total_amount = o.total_amount / 0.9
WHERE c.name = ‘王磊’;

子查询和 EXISTS

SELECT * FROM student;

– 查询最高分
SELECT MAX(score) FROM student;

– 再查询这个分数位最分的学生
SELECT name, class FROM student WHERE score = 95;

– 合并 sql 成为子查询
SELECT name, class FROM student WHERE score = (SELECT MAX(score) FROM student);

– 查询成绩高于全校平均成绩的学生记录
SELECT * FROM student WHERE score > (SELECT AVG(score) FROM student);

– 查询部门表数据
SELECT * FROM department;

– 查询员工表的数据
SELECT * FROM employee;

– EXISTS 查询有员工的部门
SELECT name FROM department WHERE EXISTS (SELECT * FROM employee WHERE department.id = employee.department_id);

– NOT EXISTS 查询所有没有员工的部门
SELECT name FROM department WHERE NOT EXISTS (SELECT * FROM employee WHERE department.id = employee.department_id);

– 新建产品表
CREATE TABLE product (
id INT PRIMARY KEY,
name VARCHAR(50),
price DECIMAL(10,2),
category VARCHAR(50),
stock INT
);

– 插入数据
INSERT INTO product (id, NAME, price, category, stock)
VALUES
(1, ‘iPhone12’, 6999.00, ‘手机’, 100),
(2, ‘iPad Pro’, 7999.00, ‘平板电脑’, 50),
(3, ‘MacBook Pro’, 12999.00, ‘笔记本电脑’, 30),
(4, ‘AirPods Pro’, 1999.00, ‘耳机’, 200),
(5, ‘Apple Watch’, 3299.00, ‘智能手表’, 80);

– 查询价格最高的产品信息
– 通过一个子查询查最高的价格,然后外层查询价格为最高价格的产品
SELECT name, price FROM product WHERE price = (SELECT MAX(price) FROM product);

– 把每个产品分类的分类名、平均价格查出来放入另一个 avg_price_by_category 表
CREATE TABLE avg_price_by_category (
id INT AUTO_INCREMENT,
category VARCHAR(50) NOT NULL,
avg_price DECIMAL(10,2) NOT NULL,
PRIMARY KEY (id)
);

– 把产品表里的分类和平均价格查询出来插入这个表
INSERT INTO avg_price_by_category (category,avg_price)
SELECT category, AVG(price) FROM product GROUP BY category;

SELECT category, AVG(price) FROM product GROUP BY category

SELECT * FROM avg_price_by_category;

– 查询名字等于技术部的 department 的 id,然后更新 department_id 为这个 id 的所有 employee 的名字为 CONCAT(“技术-“, name)

UPDATE employee SET name = CONCAT(‘技术-‘,name) WHERE department_id = (SELECT id FROM department WHERE name = ‘技术部’);

SELECT * FROM employee;

– 删除所有技术部的员工
DELETE FROM employee WHERE department_id = (SELECT id FROM department WHERE name = ‘技术部’);

总结:
sql 和 sql 可以组合来完成更复杂的功能,这种语法叫做子查询。
它还有个特有的关键字 EXISTS(和 NOT EXISTS),当子查询有返回结果的时候成立,没有返回结果的时候不成立。
子查询不止 select 可用,在 update、insert、delete 里也可以用。
灵活运用子查询,能写出功能更强大的 sql.

mysql学习指南 - 一对多、多对多关系表设计

– 一对多关系,一个部门有多个员工
– 新建 department 表
CREATE TABLE department (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR (45) NOT NULL,
PRIMARY KEY (id)
);

– 新建 employee 表
CREATE TABLE employee (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(45) NOT NULL,
department_id INT NULL,
PRIMARY KEY(id),
INDEX department_id_idx (department_id),
CONSTRAINT department_id
FOREIGN KEY(department_id)
REFERENCES department(id)
ON DELETE SET NULL
ON UPDATE SET NULL
);

– 插入数据
INSERT INTO department (id, name)
VALUES
(1, ‘人事部’),
(2, ‘财务部’),
(3, ‘市场部’),
(4, ‘技术部’),
(5, ‘销售部’),
(6, ‘客服部’),
(7, ‘采购部’),
(8, ‘行政部’),
(9, ‘品控部’),
(10, ‘研发部’);

SELECT * FROM department;

INSERT INTO employee (id, name, department_id)
VALUES
(1, ‘张三’, 1),
(2, ‘李四’, 2),
(3, ‘王五’, 3),
(4, ‘赵六’, 4),
(5, ‘钱七’, 5),
(6, ‘孙八’, 5),
(7, ‘周九’, 5),
(8, ‘吴十’, 8),
(9, ‘郑十一’, 9),
(10, ‘王十二’, 10);

SELECT * FROM employee;

– 通过 join on 关联查询 id 为 5 的部门的所有的员工
SELECT * FROM department JOIN employee ON department.id = employee.department_id WHERE department.id = 5;

SELECT * FROM department LEFT JOIN employee ON department.id = employee.department_id;

– 所有员工都有部门,所以和 inner join 结果一样
select * from department RIGHT join employee on department.id = employee.department_id;

– 多对多关系,文章和标签
– 文章一个表、标签一个表、这两个表都不保存外键,然后添加一个中间表来保存双方的外键

– 创建 article 表
CREATE TABLE article (
id INT NOT NULL AUTO_INCREMENT,
title VARCHAR(50) NOT NULL,
content TEXT NOT NULL,
PRIMARY KEY (id)
);

INSERT INTO article (title, content)
VALUES
(‘文章1’, ‘这是文章1的内容。’),
(‘文章2’, ‘这是文章2的内容。’),
(‘文章3’, ‘这是文章3的内容。’),
(‘文章4’, ‘这是文章4的内容。’),
(‘文章5’, ‘这是文章5的内容。’);

– 创建 tag 表
CREATE TABLE tag (
id INT NOT NULL AUTO_INCREMENT,
name VARCHAR(50) NOT NULL,
PRIMARY KEY (id)
);

INSERT INTO tag (name)
VALUES
(‘标签1’),
(‘标签2’),
(‘标签3’),
(‘标签4’),
(‘标签5’);

– 创建中间表 article_tag
CREATE TABLE article_tag (
article_id INT NOT NULL,
tag_id INT NOT NULL,
PRIMARY KEY (article_id,tag_id),
INDEX tag_id_idx (tag_id),
CONSTRAINT article_id
FOREIGN KEY (article_id)
REFERENCES article (id)
ON DELETE CASCADE
on UPDATE CASCADE,
CONSTRAINT tag_id
FOREIGN KEY (tag_id)
REFERENCES tag (id)
ON DELETE CASCADE
on UPDATE CASCADE
);

– 插入数据
INSERT INTO article_tag (article_id, tag_id)
VALUES
(1, 1),
(1, 2),
(1, 3),
(2, 2),
(2, 3),
(2, 4),
(3, 3),
(3, 4),
(3, 5),
(4, 4),
(4, 5),
(4, 1),
(5, 5),
(5, 1),
(5, 2);

– as 作为表面或者列名可以 省略, 别名含特殊字符时建议加引号
SELECT * FROM article a JOIN article_tag at ON a.id = at.article_id
JOIN tag t ON t.id = at.tag_id
WHERE a.id = 1;

– 多对多 关联查询
SELECT
t.NAME AS 标签名,
a.title AS 文章标题
FROM
article a
JOIN article_tag at ON a.id = at.article_id
JOIN tag t ON t.id = at.tag_id
WHERE
a.id = 1;

一对一、join 查询、级联方式

– 新建 user 表
CREATE TABLE hello-mysql.user (
id INT NOT NULL AUTO_INCREMENT COMMENT ‘id’,
name VARCHAR(45) NOT NULL COMMENT ‘名字’,
PRIMARY KEY (id)
);

– 新建 id_card 表
CREATE TABLE id_card (
id int NOT NULL AUTO_INCREMENT COMMENT ‘id’,
card_name varchar(45) NOT NULL COMMENT ‘身份证号’,
user_id int DEFAULT NULL COMMENT ‘用户 id’,
PRIMARY KEY (id),
INDEX card_id_idx (user_id),
CONSTRAINT user_id FOREIGN KEY (user_id) REFERENCES user (id)
) CHARSET=utf8mb4;

– 插入数据
INSERT INTO user (name)
VALUES
(‘张三’),
(‘李四’),
(‘王五’),
(‘赵六’),
(‘孙七’),
(‘周八’),
(‘吴九’),
(‘郑十’),
(‘钱十一’),
(‘陈十二’);

– 查询
SELECT * FROM user;

– 插入 id_card 表数据
INSERT INTO id_card (card_name, user_id)
VALUES
(‘110101199001011234’,1),
(‘310101199002022345’,2),
(‘440101199003033456’,3),
(‘440301199004044567’,4),
(‘510101199005055678’,5),
(‘330101199006066789’,6),
(‘320101199007077890’,7),
(‘500101199008088901’,8),
(‘420101199009099012’,9),
(‘610101199010101023’,10);

SELECT * FROM id_card;

– 一对一查询 join on 其实默认是 inner join on,只返回2个表中能关联上的数据
SELECT * FROM user JOIN id_card ON user.id = id_card.user_id;

SELECT user.id,name,id_card.id as card_id, card_name FROM user INNER JOIN id_card ON user.id = id_card.user_id;

– left join 是额外返回左表中没有关联上的数据
– right join 是额外返回右表中没有关联上的数据
– 在 FROM 后的左表, JOIN 后表是右表。
SELECT user.id, name, id_card.id as card_id,card_name FROM user RIGHT JOIN id_card ON user.id = id_card.user_id;

SELECT user.id, name, id_card.id as card_id,card_name FROM user LEFT JOIN id_card ON user.id = id_card.user_id;

– MySQL 中的外键约束定义了当主表(被引用表)发生更新或删除操作时,从表(引用表)应该如何响应
– CASCADE: 主表主键更新,从表关联记录的外键跟着更新,主表记录删除,从表关联记录删除
– SET NULL:主表主键更新或者主表记录删除,从表关联记录的外键设置为 null
– RESTRICT:只有没有从表的关联记录时,才允许删除主表记录或者更新主表记录的主键 id
– NO ACTION: 同 RESTRICT,只是 sql 标准里分了 4 种,但 mysql 里 NO ACTION 等同于 RESTRICT。

SQL 查询语法以及函数

SELECT * FROM student;

SELECT name,score FROM student;

SELECT name as 姓名,score as 分数 FROM student;

SELECT name as 姓名,age as 年龄 FROM student WHERE age >= 18;

SELECT name as 姓名, class as 班级, score as 分数 FROM student WHERE gender=’男’ and score >= 90;

– 查询名字以“王”开头的学生
SELECT * from student WHERE name like ‘王%’;

SELECT * from student WHERE class IN (‘一班’,’二班’);

SELECT * from student WHERE class NOT IN (‘一班’,’二班’);

SELECT * from student WHERE age BETWEEN 18 AND 20;

SELECT * FROM student LIMIT 0,5;

SELECT * FROM student LIMIT 5;

– 跳过5条,限制展示 5 条
SELECT * FROM student LIMIT 5,5;

– order by 指定根据 score 升序排列,如果 score 相同再根据 age 降序排列
SELECT name,score,age FROM student ORDER BY score ASC, age DESC;

– 统计每个班级的平均成绩
SELECT class as 班级, AVG(score) AS 平均成绩 FROM student GROUP BY class ORDER BY 平均成绩 DESC;

– 查询每个班级的学生人数
SELECT class, COUNT(*) as count from student GROUP BY class;

SELECT class, AVG(score) AS avg_score FROM student GROUP BY class HAVING avg_score > 90;

– DISTINC 去重
SELECT DISTINCT class from student;

– 聚合函数:AVG,COUNT,SUM,MIN, MAX
SELECT class AS 班级, AVG(score) AS 平均成绩,COUNT(*) AS 人数,SUM(score) AS 总成绩,MIN(score) AS 最低分,MAX(score) AS 最高分 FROM student GROUP BY class;

– 字符串处理函数: CONCAT,SUBSTR,LENGTH,UPPER,LOWER
– substr 第二个参数表示开始的下标(mysql 下标从 1 开始)
SELECT CONCAT(‘xx’,name,’yy’), SUBSTR(name,2,3),LENGTH(name),UPPER(‘aa’),LOWER(‘TT’) FROM student;

– 数值函数:ROUND 四舍五入、CEIL 向上取整、FLOOR 向下取整、ABS 绝对值、MOD 取模
SELECT ROUND(1.234567,2),CEIL(1.234567),FLOOR(1.234567),ABS(-1.233),MOD(5,2);

– 日期函数:DATE,TIME,YEAR,MONTH,DAY
SELECT YEAR(‘2006-01-02 15:04:05’),MONTH(‘2006-01-02 15:04:05’),DAY(‘2006-01-02 15:04:05’),DATE(‘2006-01-02 15:04:05’),TIME(‘2006-01-02 15:04:05’);

– 条件函数:根据条件是否成立返回不同的值。比如 if case
SELECT name, IF(score >= 60,’及格’,’不及格’) from student;

SELECT name,
score,
CASE
WHEN score >= 90 THEN
‘优秀’
WHEN score >= 60 THEN
‘及格’
ELSE
‘差’
END AS ‘档次’
FROM
student;

– 系统函数:用于获取系统信息,比如 VERIOSN,DATABASE,USER
SELECT VERSION(),DATABASE(),USER();

– 其他函数:NULLIF,COALESCE,GREATEST,LEAST
– NULLIF,如果相等返回第一个值,不相等返回null
SELECT NULLIF(1,1),NULLIF(1,2);

– COALESCE,返回第一个非 null 的值
SELECT COALESCE(null,1), COALESCE(null,null,2);

– GREATEST、LEAST:返回几个值中最大 最小的。
SELECT GREATEST(1,2,3),LEAST(1,2,3,4);

– 类型转换函数:转换类型为另一种,比如 CAST、CONVERT、DATE_FORMAT、STR_TO_DAT

– 将 ‘123’ 转化为 整型3
SELECT GREATEST(1,CONVERT(‘123’,signed),3);

SELECT GREATEST(1,CAST(‘123’ AS signed),3);

SELECT DATE_FORMAT(‘2022-01-01’,’%y年%m月%d日’);

SELECT STR_TO_DATE(‘2025-06-06’,’%Y-%m-%d’);

介绍

因为最近项目中遇到合并表格行的需求,所以用这篇文章记录下实现思路。

看下实现效果:

合并单元格

实现逻辑

element plus 实现 table 的合并行或者列需要实现 span-method 方法。 官网地址element-plus-table

案例中的代码是实现行的合并,假设接口返回的数据是嵌套的模式,父级就是需要合并的列表,内层的数据长度就是需要合并的行数。当然如果嵌套的层次比较深,就需要遍历内层的数量统计最外层需要合并的数量。

案例代码是三层的结构,使用简单的嵌套循环,一般项目中也够用了,更多层级的数据结构也是一样的道理,多加n 次循环,此时建议使用递归来实现了。

代码实现

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
<script setup>
import { ref } from "vue";

// 模拟数据
const testData = [
{
fromMajorCity: 'BFN',
data: [
{
level: 1,
toMajorCity: 'BFN,CPT',
data: [
{
weightGt: 0,
weightLte: 15,
basic_weight: 5,
starting_price: 10,
thereafter_price: 4,
per_nkg: 1
},
{
weightGt: 15,
weightLte: null,
basic_weight: 5,
starting_price: 9.5,
thereafter_price: 4,
per_nkg: 1
}
]
},
{
level: 2,
toMajorCity: 'DUR,ELS,KIM,WLK.Other',
data: [
{
weightGt: 0,
weightLte: null,
basic_weight: 5,
starting_price: 10,
thereafter_price: 5,
per_nkg: 1
}
]
}
]
}
]

// 扁平化数据并且计算合并行的 rowspan
const flattenDataWithRowspan = (nestData) => {
const flatData = []

nestData.forEach((row) => {
let firstRowSpan = 0 // 最外层合并行数
row.data?.forEach((item) => {
const secondRowSpan = item.data.length // 第二层合并行数
item.secondRowSpan = secondRowSpan
firstRowSpan += secondRowSpan
})
row.firstRowSpan = firstRowSpan
})

nestData.forEach((row) => {
row.data?.forEach((item, i) => {
item.data?.forEach((val, index) => {
flatData.push({
fromMajorCity: row.fromMajorCity,
level: item.level,
toMajorCity: item.toMajorCity,
...val,
firstRowSpan: (index === 0 && i == 0) ? row.firstRowSpan : 0,
secondRowSpan: index === 0 ? item.secondRowSpan : 0,
})
})
})
})

return flatData
}

// 合并行的方法
function spanMethod({ row, columnIndex, column }) {
if (column.label === 'From Major City' || column.label === 'No' || column.label === 'Action') {
// 如果有值需要合并
if (row.firstRowSpan > 0) {
return {
rowspan: row.firstRowSpan,
colspan: 1,
}
} else {
return {
rowspan: 0,
colspan: 0,
}
}
}
if (column.label === 'Level' || column.label === 'To Major City') {
// 如果有值需要合并
if (row.secondRowSpan > 0) {
return {
rowspan: row.secondRowSpan,
colspan: 1,
}
} else {
return {
rowspan: 0,
colspan: 0,
}
}
}
}

const tableData = ref(flattenDataWithRowspan(testData))
</script>

<template>
<div>
<el-table :data="tableData" :span-method="spanMethod" border>
<el-table-column label="No" type="index" width="55" align="center"></el-table-column>
<el-table-column label="From Major City" prop="fromMajorCity" width="200" align="center"></el-table-column>
<el-table-column label="Level" prop="level" width="100" align="center"></el-table-column>
<el-table-column label="To Major City" prop="toMajorCity" width="200" align="center"></el-table-column>
<el-table-column label="Weight (>)" prop="weightGt" width="150" align="center"></el-table-column>
<el-table-column label="Weight (<=)" prop="weightLte" width="150" align="center"></el-table-column>
<el-table-column label="Basic Weight (kg)" prop="basic_weight" width="150" align="center"></el-table-column>
<el-table-column label="Starting Price" prop="starting_price" width="150" align="center"></el-table-column>
<el-table-column label="Thereafter Price" prop="thereafter_price" width="150" align="center"></el-table-column>
<el-table-column label="per N Kg" prop="perNkg" width="150" align="center"></el-table-column>
<el-table-column label="Action" width="200" align="center" fixed="right">
<template #default>
<el-button type="primary">Edit</el-button>
<el-button type="primary">Copy</el-button>
</template>
</el-table-column>
</el-table>
</div>
</template>

<style scoped></style>

react 状态管理库 Jotai

Jotai 是一个轻量级、原子化的状态管理库,专为 React 应用设计。与其他状态管理库(如 Redux、MobX 或 Recoil)相比,Jotai 提供了一种更简洁的方式来管理和共享状态,同时保持良好的性能和灵活性。

安装 Jotai

1
pnpm add jotai

基本用法

定义原子状态

1
2
// 定义一个原子
const counterAtom = atom(0)

使用原子状态

1
2
3
4
5
6
7
8
9
10
11
12
13
export default function Counter() {
// 使用原子
const [count, setCount] = useAtom(counterAtom)

return (
<div>
<h1>
{count}
<button onClick={() => setCount((c) => c + 1)}>add</button>
</h1>
</div>
)
}

派生原子

1
const doubleCounterAtom = atom((get) => get(counterAtom) * 2)

异步行为

1
2
3
4
5
6
7
8
9
10
const numAtom = atom(1)
const asyncReadCountAtom = atom(
// 异步读取 返回 promise
async (get) => {
await new Promise((reslove) => {
setTimeout(reslove, 1000)
});
return get(numAtom)
},
)
1
2
3
4
// 异步写
const asyncWriteCountAtom = atom(null, async (get, set) => {
set(numAtom, get(numAtom) + 1);
})

持久化存储

1
2
3
4
5
6
7
8
9
const theme = atomWithStorage('dark', false)

function ComponentUsingAsyncAtoms() {
const [asyncCount] = useAtom(asyncReadCountAtom)

return (
<p>async num: {asyncCount}</p>
)
}

react hooks 使用教程

本教程使用简单代码介绍了 react 的 useState,useEffect,useContext,useReducer, useRef,useMemo,useCallback 以及自定义hooks 的用法。

useState

向组件添加一个 状态变量,进行状态管理。

1
const [state, setState] = useState(initialState)
  • initialState: 你希望 state 初始化的值。它可以是任何类型的值,但对于函数有特殊的行为。在初始渲染后,此参数将被忽略。
    如果传递函数作为 initialState,则它将被视为 初始化函数。它应该是纯函数,不应该接受任何参数,并且应该返回一个任何类型的值。当初始化组件时,React 将调用你的初始化函数,并将其返回值存储为初始状态。
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
function App() {
const [count, setCount] = useState(0)

const handlePlus = () => {
setCount(count + 1)
}

// setCount 传入函数,第一个参数为 useState 的旧值
const handleMinus = () => {
setCount(count => count - 1)
}

return (
<>
<div>{count}</div>
<button onClick={handlePlus}>+</button>
<button onClick={handleMinus}>-</button>
</>

)
}

useEffect

useEffect(setup, dependencies?)

dependencies 传入空数组只会在组件更新时触发一次回调函数, setup 函数 在每次依赖项变更重新渲染后, setup 函数可以选择性返回一个
清理(cleanup) 函数,React 将首先使用旧值运行 cleanup 函数

1
2
3
useEffect(() => {
console.log('useEffect')
}, []);

数组中传入具体的状态,当状态变化时触发回调函数

1
2
3
useEffect(() => {
console.log('useEffect')
}, [count]);

setup 函数返回的函数称为 cleanup 清理函数

1
2
3
4
5
6
7
useEffect(() => {
console.log('useEffect')

return () => {
console.log('clear')
}
}, [count]);

useContext

通过父组件给后代组件传递数据时,如果嵌套的层级太深,可以通过在组件的最顶级调用 useContext 来读取和订阅 context。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
// 创建 context
const ThemeContext = createContext(null)

function App() {
return (
// 顶层组件传递 context 数据
<ThemeContext.Provider value={{color: 'green'}}>
<ChildComponent></ChildComponent>
</ThemeContext.Provider>
)
}

function ChildComponent() {
// 内部组件获取 context 数据
const theme = useContext(ThemeContext)
console.log(theme) // {color: 'green'}

return (
<div>Child Component</div>
)
}

以上代码演示了如何通过在顶层组件设置 context 并且传递数据,内部的组件如何获取数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
// 创建 context
const ThemeContext = createContext(null)

function App() {
const [theme, setTheme] = useState('green')

const switchTheme = () => {
setTheme(theme === 'green' ? 'pink' : 'green')
}

return (
// 传递 context 数据
<ThemeContext.Provider value={{color: theme}}>
<div>
<button onClick={ switchTheme }>switch theme</button>
</div>
<ChildComponent></ChildComponent>
</ThemeContext.Provider>
)
}

function ChildComponent() {
const theme = useContext(ThemeContext)
console.log(theme)

return (
<div style={theme}>Child Component</div>
)
}

通过结合 useState 可以更新 context 并且传递给后代,这里演示了如何 通过 context 全局更新主题颜色。

useReducer

对于复杂的状态设置和管理。

1
const [state, dispatch] = useReducer(reducer, initialArg, init?)
  • reducer: 用于更新 state 的纯函数。参数为 state 和 action,返回值是更新后的 state。state 与 action 可以是任意合法值。
  • initialArg: 用于初始化 state 的任意值。初始值的计算逻辑取决于接下来的 init 参数。
  • 可选参数 init: 用于计算初始值的函数。如果存在,使用 init(initialArg) 的执行结果作为初始值,否则使用 initialArg

通过不同的 reducer type 执行不同的操作

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const initialState = {
count: 0,
age: 18,
// 一些其他属性
}

const countReducer = (state, action) => {
switch (action.type) {
case 'increment':
return {
...state,
count: state.count + 1,
}
case 'decrement':
return {
...state,
count: state.count - 1,
}
default:
return state;
}
}

export function CountProvider() {

const [state, dispatch] = useReducer(countReducer, initialState)

const increment = () => {
dispatch({type: 'increment'})
}

const decrement = () => {
dispatch({type: 'decrement'})
}

return (
<div>
<h1>{state.count}</h1>
<button onClick={increment}>+</button>
<button onClick={decrement}>-</button>
</div>
)
}

useRef

用于在重新渲染中,去存储易变数据。数据存储在 current 属性上,常用于 DOM 访问。(修改 ref.current 不会触发重新渲染)

1
const ref = useRef(initialValue)
  • initialValue: ref 对象的 current 属性的初始值。可以是任意类型的值。这个参数在首次渲染后被忽略。

返回值: useRef 返回一个只有一个属性的对象:

  • current:初始值为传递的 initialValue。之后可以将其设置为其他值。如果将 ref 对象作为一个 JSX 节点的 ref 属性传递给 React,React 将为它设置 current 属性。

在后续的渲染中,useRef 将返回同一个对象。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
// useRef 操作 dom 案例
export function RefDemo() {
const inputRef = useRef(null)

const handleClick = () => {
inputRef.current.focus()
}

return (
<div>
<input ref={inputRef}/>
<button onClick={handleClick}>
聚焦输入框
</button>
</div>
)
}

useMemo

在每次重新渲染的时候能够缓存计算的结果

1
const cachedValue = useMemo(calculateValue, dependencies)
  • calculateValue:要缓存计算值的函数。它应该是一个没有任何参数的纯函数,并且可以返回任意类型。React 将会在首次渲染时调用该函数;在之后的渲染中,如果 dependencies 没有发生变化,React 将直接返回相同值。否则,将会再次调用 calculateValue 并返回最新结果,然后缓存该结果以便下次重复使用。
  • dependencies:所有在 calculateValue 函数中使用的响应式变量组成的数组。响应式变量包括 props、state 和所有你直接在组件中定义的变量和函数。如果你在代码检查工具中 配置了 React,它将会确保每一个响应式数据都被正确地定义为依赖项。依赖项数组的长度必须是固定的并且必须写成 [dep1, dep2, dep3] 这种形式。React 使用 Object.is 将每个依赖项与其之前的值进行比较。
1
2
3
4
5
6
7
// 省略部分代码。。。
const [count, setCount] = useState(0)

const doubleCount = useMemo(() => {
console.log('in use Memo')
return count * 2
}, [count])

useMemo 在多次重新渲染中缓存了 calculation 函数计算的结果直到依赖项的值发生变化。

另外你可以使用 memo 来优化你的代码,它允许你的组件在 props 没有改变的情况下跳过重新渲染。

1
const MemoizedComponent = memo(SomeComponent, arePropsEqual?)
1
2
3
4
5
const Greeting = memo(function Greeting({ name }) {
return <h1>Hello, {name}!</h1>;
});

export default Greeting;

useCallback

允许你在多次渲染中缓存函数。

1
const cachedFn = useCallback(fn, dependencies)
  • fn:想要缓存的函数。此函数可以接受任何参数并且返回任何值。在初次渲染时,React 将把函数返回给你(而不是调用它!)。当进行下一次渲染时,如果 dependencies 相比于上一次渲染时没有改变,那么 React 将会返回相同的函数。否则,React 将返回在最新一次渲染中传入的函数,并且将其缓存以便之后使用。React 不会调用此函数,而是返回此函数。你可以自己决定何时调用以及是否调用。

  • dependencies:有关是否更新 fn 的所有响应式值的一个列表。响应式值包括 props、state,和所有在你组件内部直接声明的变量和函数。如果你的代码检查工具 配置了 React,那么它将校验每一个正确指定为依赖的响应式值。依赖列表必须具有确切数量的项,并且必须像 [dep1, dep2, dep3] 这样编写。React 使用 Object.is 比较每一个依赖和它的之前的值。(简言之就是依赖项列表中的变量需要被想要缓存的函数内部使用)

  • 返回值:
    在初次渲染时,useCallback 返回你已经传入的 fn 函数
    在之后的渲染中, 如果依赖没有改变,useCallback 返回上一次渲染中缓存的 fn 函数;否则返回这一次渲染传入的 fn。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
export function UseCallbackDemo() {
const [state, setState] = useState('on')
const [count, setCount] = useState(0)

const handleAdd = () => {
setCount((count) => count + 1)
}

const cacheFn = useCallback(() => {
console.log(state)
}, [state]);

const handleChange = () => {
setState(state === 'on' ? 'off' : 'on')
}

return (
<div>
<button onClick={handleAdd}>add</button>
<h3>{count}</h3>
<button onClick={handleChange}>change</button>
<Child handleClick={cacheFn}></Child>
</div>
)
}

// eslint-disable-next-line react/prop-types
function Child({handleClick}) {
return (
<div>
<button onClick={() => handleClick()}>click</button>
</div>
)
}

点击 ‘add’ 会触发父组件的重新渲染,但点击子组件的 Child 的 ‘Click’ 发现打印的内容没有变化。点击 ‘Change’ 才会改变子组件的 handleClick 的打印结果。因为 useCallback 的依赖项里面包含 state 状态。

自定义 hook

自定义一个 useLocalStorage,该 Hook 用于将状态与 localStorage 同步,实现数据的持久化存储

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
export const useLocalStorage = (key, initialValue) => {
const [stateValue, setStateValue] = useState(() => {
try {
const item = localStorage.getItem(key);
// 如果存在则解析,否则判断 initialValue 是否为函数
return item ? JSON.parse(item) : (typeof initialValue === 'function' ? initialValue() : initialValue);
} catch (error) {
console.log(error)
return (typeof initialValue === 'function' ? initialValue() : initialValue);
}
})

const setStorage = (value) => {
let valueToStore;
if (typeof value === 'function') {
valueToStore = value(stateValue)
} else {
valueToStore = value
}
setStateValue(valueToStore)
localStorage.setItem(key, JSON.stringify(valueToStore))
}

return [stateValue, setStorage]
}
1
2
3
4
5
6
const useToggler = initialState => {
const [value, setValue] = React.useState(initialState);
const toggleValue = React.useCallback(() => setValue(prev => !prev), []);

return [value, toggleValue];
};

React Router v7 入门指南

本教程基于 react router v7 版本,旨在快速实现一个 react router 的案例功能。

详细内容请查看 react router官网

快速启动一个 react 项目

使用 vite 快速创建一个 react 项目

1
pnpm create vite my-vue-app --template react

安装依赖

1
pnpm i

启动项目

1
pnpm dev

实现基础路由功能

安装 react router 库

1
pnpm i react-router

新建路由页面

在 src 里新建 pages 目录,里面新建 about.jsx,home.jsx,list.jsx, 各 react 元素可以随意返回一些内容,以供路由匹配展示。

内容类似下面这样:

1
2
3
4
5
6
7
8
// about.jsx
export default function About() {
return (
<div>
About Page
</div>
)
}

配置路由

修改 main.jsx 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import { createRoot } from 'react-dom/client'
import { BrowserRouter, Routes, Route } from 'react-router'
import './index.css'
import App from './App.jsx'
import Home from './pages/home.jsx'
import List from './pages/list.jsx'
import About from './pages/about.jsx'

createRoot(document.getElementById('root')).render(
<BrowserRouter>
<Routes>
<Route path='/' element={<App />}>
<Route index element={<Home />}></Route>
<Route path='list' element={<List />}></Route>
<Route path='about' element={<About />}></Route>
</Route>
</Routes>
</BrowserRouter>,
)

react router 有2种路由模式, BrowserRouterHashRouter

  • BrowserRouter: 使用现代浏览器的 History API(pushState 和 replaceState)来同步 URL 和应用程序状态。
  • HashRouter:使用 URL 中的哈希片段(#)来保存路由状态。

使用 Routes 包括 Route 组件,可以创建我们的路由。Route 组件的 path 属性代表路由绑定的路径,element 代表路由要展示的元素内容。

有一个 Route 没有 path 属性,但是它出现 index 属性,代表它是父路由的默认子路由,也就是说当我们访问“根路径” 就能看到 Home 元素。

完成路由跳转和页面展示

修改 main,jsx 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
import { Link } from 'react-router'
import { Outlet } from 'react-router'
import './App.css'

function App() {

return (
<div>
<h1>react router demo v6</h1>
<nav>
<Link to="/" style={{ marginRight: '15px' }}>首页</Link>
<Link to="/list" style={{ marginRight: '15px' }}>列表</Link>
<Link to="/about" style={{ marginRight: '15px' }}>关于</Link>
</nav>

<Outlet />
</div>
)
}

export default App

使用 Link 组件的 to 属性可以配置路由的跳转地址,Outlet 组件用于展示当前路由匹配的页面(组件)。

重新启动项目,现在你可以看到如下图展示的内容,点击 home、about、list,可以看到路由的底部展示的渲染的内容发生了变化,并浏览器地址也对应更新。

页面效果

至此,我们已经完成了基础的路由配置和、跳转以及页面的展示。

匹配 404 页面

通常当我们访问一个不存在的页面的地址的时候,需要展示一个 404 页面。通常这个页面不属于根页面的子路由。

在 src/pages 目录里面新建一个 404.jsx 文件,内容如下:

1
2
3
4
5
export default function NotFound() {
return (
<div>Not Found 404</div>
)
}

修改 main.jsx 路由配置,就能完成我们需要的功能了,代码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
import {createRoot} from 'react-dom/client'
import {BrowserRouter, Routes, Route} from 'react-router'
import './index.css'
import App from './App.jsx'
import Home from './pages/home.jsx'
import List from './pages/list.jsx'
import About from './pages/about.jsx'
import NotFound from "./pages/404.jsx";

createRoot(document.getElementById('root')).render(
<BrowserRouter>
<Routes>
<Route path='/' element={<App/>}>
<Route index element={<Home/>}></Route>
<Route path='list' element={<List/>}></Route>
<Route path='about' element={<About/>}></Route>
</Route>
{/* 匹配 404 页面 */}
<Route path='*' element={<NotFound/>}></Route>
</Routes>
</BrowserRouter>,
)

现在在浏览器访问 http://localhost:5173/1 , 页面就会展示 “Not Found 404” 了。

获取路由参数 params 和 query

获取路由 params

配置 list 路由的子路由 list/id

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
// 省略相关重复代码
createRoot(document.getElementById('root')).render(
<BrowserRouter>
<Routes>
<Route path='/' element={<App/>}>
<Route index element={<Home/>}></Route>
<Route path='list' element={<List/>}>
{/*新增子路由,list/id*/}
<Route path=':id' element={<ListDetail/>}></Route>
</Route>
<Route path='about' element={<About/>}></Route>
</Route>
<Route path='*' element={<NotFound/>}></Route>
</Routes>
</BrowserRouter>,
)

在 src/pages 目录新建 list-detail.jsx 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import {useParams} from "react-router";

export default function ListDetail() {

// 获取 params 参数对象
const params = useParams()
console.log(params)

return (
<div>
List Detail Page id: {params.id}
</div>
)
}

修改 list.jsx 文件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
import {Link, Outlet} from "react-router";

export default function List() {
const data = [
{
id: 1,
name: '订单1'
},
{
id: 2,
name: '订单2'
}
];

return (
<div>
{ data.map( (item) => <Link style={{marginRight: '15px'}} to={ `/list/${item.id}`} key={item.id} >
detail {item.name}
</Link> ) }

{/* 展示子页面的内容 */}
<Outlet></Outlet>
</div>
)
}

现在重新启动项目,你会看到下面这样的界面:

页面效果

点击 列表 下面的 detail 订单 1 , 可以看到展示了 List Detail Page id: 1。

使用 useParams() 方法可以获取到 params 对象,里面包含了相关的参数信息。

获取路由 query

修改 list.jsx 文件,新增一个携带 query 参数的 Link 组件,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// 省略部分重复内容...
return (
<div>
{ data.map( (item) => <Link style={{marginRight: '15px'}} to={ `/list/${item.id}`} key={item.id} >
detail {item.name}
</Link> ) }

{/* 新增一个携带 query 参数的 Link 组件 */}
<Link to='/list/3?type=a' >detail type=a</Link>

{/* 展示子页面的内容 */}
<Outlet></Outlet>
</div>
)

修改 list-detail.jsx 文件,加入获取路由 query 参数的代码,内容如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
import {useParams, useSearchParams} from "react-router";

export default function ListDetail() {
const params = useParams()
console.log(params)

// searchParams 是一个 URLSearchParams 对象,具体请参考 MDN
let [searchParams] = useSearchParams();
console.log(searchParams)
console.log(searchParams.get('type')) // 获取具体 query 参数

return (
<div>
List Detail Page id: {params.id}
</div>
)
}

点击 新增的 Link 组件,跳转到 list-detail 页面,展示内容为 “List Detail Page id: 3”。

页面效果

查看控制台可以发现我们获取去到了路由的 query 参数 type=a; 这里说明一下 useSearchParams() 方法返回的数组中,第一个元素是 一个 URLSearchParams 实例; URLSearchParams 接口定义了一些实用的方法来处理 URL 的查询字符串。URLSearchParams 接口 MDN

如果需要获取更多路由信息,可以使用 useLocation 方法:

1
2
let location = useLocation();
console.log(location)

location 内容

编程式导航

有些时候我们需要使用 js 主动去触发页面的跳转,可以使用方法 useNavigate

params 和 query 参数你可以直接写在跳转链接里面, 例如 ‘/list/1?type=a’,不在演示。

1
2
3
4
5
// 编程式导航
const navigate = useNavigate();
const backToHome = () => {
navigate('/')
}

重定向

1
redirect("/login")

自定义导航守卫

在项目开发的过程中,一般我们会有这样的需求:有些页面可以随意访问,但是部分页面必须用户登录之后才能访问。我们可以给路由配置一个白名单,如果当前访问页面的路径在白名单里面,就能正常访问。反之,如果访问的页面需要权限,用户必须登录之后才能正常访问。不能访问的页面当用户触发跳转时,重定向到登录页面。在登录完成之后,在回到之前的页面。

封装一个校验路由权限并且重定向的组件。如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
// eslint-disable-next-line react/prop-types
export default function RequireAuth({children}) {
const whiteList = ["/", "/about"]

const { pathname } = useLocation();

// 不需要权限的页面
const notRequiredAuth = whiteList.includes(pathname);

// 访问的页面在白名单里,或者用户拥有权限 hasToken,直接跳转。否则重定向到到 login
return (hasToken || notRequiredAuth ? children : <Navigate to="/login" replace state={pathname} />)
}

接下来我们只要使用该组件包裹我们的路由组件就好了

1
2
3
<RequireAuth>
<Link to="/list" style={{marginRight: '15px'}}>列表</Link>
</RequireAuth>

Data Mode

react router v7 官网现在出现了3种模式,Framework Mode、Data Mode、Declarative Mode。

  • Framework Mode:
    • 强依赖 React Router Data APIs(loader、action、errorElement 等)
    • 文件即路由:如 app/routes/dashboard.tsx 就是 /dashboard
    • 默认 SSR 支持(如 Remix)
    • 路由定义抽象于文件系统,不用手动写 createBrowserRouter
  • Data Mode:
    • 使用 createBrowserRouter 配置路由并定义 loader、action 等函数。
    • 强调路由组件初始化前就完成数据加载
  • Declarative Mode:
    • 使用 “Routes” “Route” 等 JSX 方式声明路由,数据处理在组件内部完成。(传统模式)

Framework Mode 架构模式更适合于 SSR 应用,对于一般的管理后台,把路由根据文件来配置有点不太方便,管理后台通常有路由权限的问题,所以不建议使用此模式。

Declarative Mode 也就是传统的声明组件的方式配置路由,大部分使用过 react router 的用户都能接受,但是有点繁琐不够灵活。

Data Mode 支持配置路由的 loader、action 等,灵活性较好,可以学习这种模式,方便以后过度到架构模式

Data Mode 处理路由权限

1
2
3
4
5
6
7
8
9
10
11
12
13
14
// ProtectedLayout.tsx
import { Outlet } from 'react-router-dom';

export function ProtectedLayout() {
return <Outlet />;
}

export function authLoader() {
const token = localStorage.getItem('token');
if (!token) {
throw redirect('/login');
}
return null;
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
{
path: '/',
element: <RootLayout />,
children: [
{
element: <ProtectedLayout />,
loader: authLoader,
children: [
{
path: 'dashboard',
element: <DashboardPage />,
},
{
path: 'settings',
element: <SettingsPage />,
},
],
},
]
}

该文章内容在文章 Nestjs 和 Prisma 实现 Restful Api的基础上实现,是该系列最后一篇文章,如有需要,请先搜索查看前面的文章。

在REST API中实现身份验证

这一节我们将学习给用户相关的 REST 接口添加权限认证。

  • GET /users
  • GET /users/:id
  • PATCH /users/:id
  • DELETE /users/:id

权限认证的方案主要有2种,一种是基于 session 的方案,一种是基于 token 的方案。接下来我们将学习如何在 Nestjs 中使用 Json Web Tokens

在开始之前,我们先要生成 auth 模块的资源文件

1
npx nest generate resource

根据终端的提示,选择相应的回答。

  1. What name would you like to use for this resource (plural, e.g., “users”)? auth
  2. What transport layer do you use? REST API
  3. Would you like to generate CRUD entry points? No

现在,您应该在src/auth目录中找到一个新的 auth 模块。

安装和配置 passport

Passport 是一个流行的 Node.js 认证库,功能强大,支持多种认证策略,并具有很高的可配置性。它通常与 Express 框架一起使用,而 NestJS 本身也是基于 Express 构建的。NestJS 提供了一个官方的 Passport 集成库,称为 @nestjs/passport,使其在 NestJS 应用中更加容易使用和集成。

通过安装下面的这些库开始我们的工作

1
2
npm install --save @nestjs/passport passport @nestjs/jwt passport-jwt
npm install --save-dev @types/passport-jwt

你已经安装了所需要的包,现在可以在应用里配置 passport 了,打开 src/auth.module.ts 文件并添加以下代码

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
//src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { PrismaModule } from 'src/prisma/prisma.module';

export const jwtSecret = 'zjP9h6ZI5LoSKCRj';

@Module({
imports: [
PrismaModule,
PassportModule,
JwtModule.register({
secret: jwtSecret,
signOptions: { expiresIn: '5m' }, // e.g. 30s, 7d, 24h
}),
],
controllers: [AuthController],
providers: [AuthService],
})
export class AuthModule {}

@nestjs/passport 模块提供了一个可以导入到应用程序中的 PassportModule。PassportModule 是 passport 库的包装器,该库提供了特定于 NestJS 的实用程序。您可以在官方文档中阅读更多关于 PassportModule 的信息。

您还配置了一个 JwtModule,您将使用它来生成和验证jwt。JwtModule 是 jsonwebtoken 库的包装器。secret 提供了一个用于对 jwt 签名的密钥。expiresIn 对象定义jwt的过期时间。当前设置为1分钟。

注意:在实际的应用程序中,永远不应该将密钥直接存储在代码库中。NestJS提供了@nestjs/config包,用于从环境变量中加载秘密。您可以在官方文档中阅读更多相关内容。

实现 POST /auth/login 接口

POST /login 将用于验证用户。它将接受用户名和密码,如果认证通过,则返回JWT。首先创建一个LoginDto类,它将定义请求体 Body 的结构。

用 email 和 password 字段来定义 LoginDto 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//src/auth/dto/login.dto.ts
import { ApiProperty } from '@nestjs/swagger';
import { IsEmail, IsNotEmpty, IsString, MinLength } from 'class-validator';

export class LoginDto {
@IsEmail()
@IsNotEmpty()
@ApiProperty()
email: string;

@IsString()
@IsNotEmpty()
@MinLength(6)
@ApiProperty()
password: string;
}

您还需要定义一个新的 AuthEntity 来描述JWT有效负载的形状。在src/auth/entity目录下创建一个新文件auth.entity.ts:

1
2
mkdir src/auth/entity
touch src/auth/entity/auth.entity.ts

按照下面的内容定义 AuthEntity 类。

1
2
3
4
5
6
7
//src/auth/entity/auth.entity.ts
import { ApiProperty } from '@nestjs/swagger';

export class AuthEntity {
@ApiProperty()
accessToken: string;
}

AuthEntity只有一个名为accessToken的字符串字段,它将包含JWT。

在 AuthService 新增一个 login 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
//src/auth/auth.service.ts
import {
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { PrismaService } from './../prisma/prisma.service';
import { JwtService } from '@nestjs/jwt';
import { AuthEntity } from './entity/auth.entity';

@Injectable()
export class AuthService {
constructor(private prisma: PrismaService, private jwtService: JwtService) {}

async login(email: string, password: string): Promise<AuthEntity> {
// Step 1: Fetch a user with the given email
const user = await this.prisma.user.findUnique({ where: { email: email } });

// If no user is found, throw an error
if (!user) {
throw new NotFoundException(`No user found for email: ${email}`);
}

// Step 2: Check if the password is correct
const isPasswordValid = user.password === password;

// If password does not match, throw an error
if (!isPasswordValid) {
throw new UnauthorizedException('Invalid password');
}

// Step 3: Generate a JWT containing the user's ID and return it
return {
accessToken: this.jwtService.sign({ userId: user.id }),
};
}
}

login方法首先获取具有给定电子邮件的用户。如果没有找到用户,则抛出NotFoundException异常。如果找到用户,则检查密码是否正确。如果密码不正确,则会抛出UnauthorizedException异常。如果密码正确,则生成包含用户ID的JWT并返回。

现在我们在 AuthController 创建 POST /auth/login 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
//src/auth/auth.controller.ts

import { Body, Controller, Post } from '@nestjs/common';
import { AuthService } from './auth.service';
import { ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { AuthEntity } from './entity/auth.entity';
import { LoginDto } from './dto/login.dto';

@Controller('auth')
@ApiTags('auth')
export class AuthController {
constructor(private readonly authService: AuthService) {}

@Post('login')
@ApiOkResponse({ type: AuthEntity })
login(@Body() { email, password }: LoginDto) {
return this.authService.login(email, password);
}
}

启动项目(包括docker服务),访问本地 http://localhost:3000/api 调用 POST /auth/login, 传入一个正确的邮箱和密码。

1
2
3
4
{
"email": "alex@ruheni.com",
"password": "password-alex"
}

你可以看到接口成功的返回了 accessToken。

实现JWT身份验证策略

在 Passport 中,策略负责对请求进行身份验证,这是通过实现身份验证机制来完成的。在本节中,您将实现用于对用户进行身份验证的JWT身份验证策略。

您将不会直接使用 passport 包,而是与包装器包@nestjs/passport交互,它将在底层调用 passport 包。要用@nestjs/passport配置策略,需要创建一个类来扩展PassportStrategy类。在这门课上,你需要做两件主要的事情:

  1. 您将把JWT策略特定的选项和配置传递给构造函数中的super()方法。
  2. validate()回调方法,它将与数据库交互,根据JWT有效负载获取用户。如果找到用户,validate()方法将返回用户对象。

首先在 src/auth/strategy 目录下新建一个 jwt.strategy.ts 文件。

1
touch src/auth/jwt.strategy.ts

现在实现一下 JwtStrategy 类。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//src/auth/jwt.strategy.ts
import { Injectable, UnauthorizedException } from '@nestjs/common';
import { PassportStrategy } from '@nestjs/passport';
import { ExtractJwt, Strategy } from 'passport-jwt';
import { jwtSecret } from './auth.module';
import { UsersService } from 'src/users/users.service';

@Injectable()
export class JwtStrategy extends PassportStrategy(Strategy, 'jwt') {
constructor(private usersService: UsersService) {
super({
jwtFromRequest: ExtractJwt.fromAuthHeaderAsBearerToken(),
secretOrKey: jwtSecret,
});
}

async validate(payload: { userId: number }) {
const user = await this.usersService.findOne(payload.userId);

if (!user) {
throw new UnauthorizedException();
}

return user;
}
}

您已经创建了一个JwtStrategy类,它扩展了PassportStrategy类。PassportStrategy类接受两个参数:策略实现和策略名称。这里使用的是passport-jwt库中的预定义策略。

您正在向构造函数中的super()方法传递一些选项。jwtFromRequest选项需要一个可用于从请求中提取JWT的方法。在这种情况下,您将使用在API请求的Authorization头中提供承载令牌的标准方法 (BearerToken)。secretOrKey选项告诉策略使用什么密钥来验证JWT。还有更多的选项,您可以在 passport-jwt github 查看。

对于 passport-jwt, Passport首先验证JWT的签名并解码JSON。然后将解码后的JSON传递给validate()方法。基于JWT签名的工作方式,你可以保证收到一个有效的令牌,这个令牌是之前由你的应用程序签名和发出的。validate()方法预计会返回一个用户对象。如果没有找到用户,validate()方法会抛出一个错误。

ps: passport 还可以生成基于 session 的权限认证方案,具体可以查看 NestJs 官网 Passport 部分。

在 authModule 中将新增的 JwtStrategy 放入 providers 中。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
//src/auth/auth.module.ts
import { Module } from '@nestjs/common';
import { AuthService } from './auth.service';
import { AuthController } from './auth.controller';
import { PassportModule } from '@nestjs/passport';
import { JwtModule } from '@nestjs/jwt';
import { PrismaModule } from 'src/prisma/prisma.module';
import { UsersModule } from 'src/users/users.module';
import { JwtStrategy } from './jwt.strategy';

export const jwtSecret = 'zjP9h6ZI5LoSKCRj';

@Module({
imports: [
PrismaModule,
PassportModule,
JwtModule.register({
secret: jwtSecret,
signOptions: { expiresIn: '5m' }, // e.g. 7d, 24h
}),
UsersModule,
],
controllers: [AuthController],
providers: [AuthService, JwtStrategy],
})
export class AuthModule {}

现在,JwtStrategy 可以被其他模块使用。您还在导入中添加了UsersModule,因为在JwtStrategy类中使用了UsersService

为了在JwtStrategy类中访问UsersService,你还需要在UsersModule的导出中添加它。

1
2
3
4
5
6
7
8
9
10
11
12
13
// src/users/users.module.ts
import { Module } from '@nestjs/common';
import { UsersService } from './users.service';
import { UsersController } from './users.controller';
import { PrismaModule } from 'src/prisma/prisma.module';

@Module({
controllers: [UsersController],
providers: [UsersService],
imports: [PrismaModule],
exports: [UsersService],
})
export class UsersModule {}

实现JWT授权守卫

Guards (守卫)是 NestJS 的一个结构,它可以觉得请求是否可以继续下去。在这一部分,你将会实现一个自定义的 JwtAuthGuard,它将保护那些需要认证的路由。

在 src/auth 目录下新建一个 jwt-auth.guard.ts 文件。

1
touch src/auth/jwt-auth.guard.ts

现在我们来实现一下, JwtAuthGuard 类。

1
2
3
4
5
6
//src/auth/jwt-auth.guard.ts
import { Injectable } from '@nestjs/common';
import { AuthGuard } from '@nestjs/passport';

@Injectable()
export class JwtAuthGuard extends AuthGuard('jwt') {}

AuthGuard类需要策略的名称。在本例中,您使用的是在前一节中实现的JwtStrategy,它被命名为jwt。

现在我们可以使用这个 guard 作为一个装饰器来保护我们的路由接口了。给 UsersController 的路由添加 JwtAuthGuard 。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
// src/users/users.controller.ts
import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
ParseIntPipe,
UseGuards,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { UserEntity } from './entities/user.entity';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';

@Controller('users')
@ApiTags('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}

@Post()
@ApiCreatedResponse({ type: UserEntity })
async create(@Body() createUserDto: CreateUserDto) {
return new UserEntity(await this.usersService.create(createUserDto));
}

@Get()
@UseGuards(JwtAuthGuard)
@ApiOkResponse({ type: UserEntity, isArray: true })
async findAll() {
const users = await this.usersService.findAll();
return users.map((user) => new UserEntity(user));
}

@Get(':id')
@UseGuards(JwtAuthGuard)
@ApiOkResponse({ type: UserEntity })
async findOne(@Param('id', ParseIntPipe) id: number) {
return new UserEntity(await this.usersService.findOne(id));
}

@Patch(':id')
@UseGuards(JwtAuthGuard)
@ApiCreatedResponse({ type: UserEntity })
async update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto,
) {
return new UserEntity(await this.usersService.update(id, updateUserDto));
}

@Delete(':id')
@UseGuards(JwtAuthGuard)
@ApiOkResponse({ type: UserEntity })
async remove(@Param('id', ParseIntPipe) id: number) {
return new UserEntity(await this.usersService.remove(id));
}
}

现在你调用这些接口中的任意一个,如果没有授权,都会返回 401,结果如下:

1
2
3
4
{
"message": "Unauthorized",
"statusCode": 401
}

在Swagger中集成身份验证

目前在 Swagger 上还没有迹象表明这些接口需要权限。你可以在控制器中添加一个 @ApiBearerAuth() 装饰器来指示需要进行身份验证。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
// src/users/users.controller.ts

import {
Controller,
Get,
Post,
Body,
Patch,
Param,
Delete,
ParseIntPipe,
UseGuards,
} from '@nestjs/common';
import { UsersService } from './users.service';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { ApiBearerAuth, ApiCreatedResponse, ApiOkResponse, ApiTags } from '@nestjs/swagger';
import { UserEntity } from './entities/user.entity';
import { JwtAuthGuard } from 'src/auth/jwt-auth.guard';

@Controller('users')
@ApiTags('users')
export class UsersController {
constructor(private readonly usersService: UsersService) {}

@Post()
@ApiCreatedResponse({ type: UserEntity })
async create(@Body() createUserDto: CreateUserDto) {
return new UserEntity(await this.usersService.create(createUserDto));
}

@Get()
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ type: UserEntity, isArray: true })
async findAll() {
const users = await this.usersService.findAll();
return users.map((user) => new UserEntity(user));
}

@Get(':id')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ type: UserEntity })
async findOne(@Param('id', ParseIntPipe) id: number) {
return new UserEntity(await this.usersService.findOne(id));
}

@Patch(':id')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiCreatedResponse({ type: UserEntity })
async update(
@Param('id', ParseIntPipe) id: number,
@Body() updateUserDto: UpdateUserDto,
) {
return new UserEntity(await this.usersService.update(id, updateUserDto));
}

@Delete(':id')
@UseGuards(JwtAuthGuard)
@ApiBearerAuth()
@ApiOkResponse({ type: UserEntity })
async remove(@Param('id', ParseIntPipe) id: number) {
return new UserEntity(await this.usersService.remove(id));
}
}

现在刷新 Swagger 接口文档,你会发现在代码中加上装饰器的接口后面出现了一个 “锁” 的标记。这表明那个接口需要用户权限。

目前还不可能在Swagger中直接对自己进行“身份验证”,这样您就可以测试这些端点。要做到这一点,你可以在main.ts中的SwaggerModule设置中添加.addBearerAuth()方法调用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
// src/main.ts

import { NestFactory, Reflector } from '@nestjs/core';
import { AppModule } from './app.module';
import { SwaggerModule, DocumentBuilder } from '@nestjs/swagger';
import { ClassSerializerInterceptor, ValidationPipe } from '@nestjs/common';

async function bootstrap() {
const app = await NestFactory.create(AppModule);

app.useGlobalPipes(new ValidationPipe({ whitelist: true }));
app.useGlobalInterceptors(new ClassSerializerInterceptor(app.get(Reflector)));

const config = new DocumentBuilder()
.setTitle('Median')
.setDescription('The Median API description')
.setVersion('0.1')
.addBearerAuth()
.build();
const document = SwaggerModule.createDocument(app, config);
SwaggerModule.setup('api', app, document);

await app.listen(3000);
}
bootstrap();

现在,您可以通过单击Swagger中的Authorize按钮来添加令牌。Swagger会将令牌添加到您的请求中,以便您可以查询受保护的接口。

首先通过调用 /auth/login 进行授权登录,然后拿到接口返回的 accessToken 添加到 Swagger 接口文档弹出的令牌认证窗口中。

哈希密码

目前,密码以明文的形式存储到数据库中。这是有安全风险的,如果数据库被泄漏,那么所有密码都会被泄漏。要解决这个问题,我们需要先对密码进行哈希处理,然后再存储到数据库中。

我们需要安装 bcrypt 这个库

1
2
npm install bcrypt
npm install --save-dev @types/bcrypt

crate 和 update 这2个接口涉及到存储密码到数据库,所以我们需要修改这个它们对应的逻辑,在 UsersService 中找到这对应的方法,在调用 prisma 操作数据库之前,我们先将密码哈希。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
// src/users/users.service.ts
import { Injectable } from '@nestjs/common';
import { CreateUserDto } from './dto/create-user.dto';
import { UpdateUserDto } from './dto/update-user.dto';
import { PrismaService } from 'src/prisma/prisma.service';
import * as bcrypt from 'bcrypt';

export const roundsOfHashing = 10;

@Injectable()
export class UsersService {
constructor(private prisma: PrismaService) {}

async create(createUserDto: CreateUserDto) {
const hashedPassword = await bcrypt.hash(
createUserDto.password,
roundsOfHashing,
);

createUserDto.password = hashedPassword;

return this.prisma.user.create({
data: createUserDto,
});
}

findAll() {
return this.prisma.user.findMany();
}

findOne(id: number) {
return this.prisma.user.findUnique({ where: { id } });
}

async update(id: number, updateUserDto: UpdateUserDto) {
if (updateUserDto.password) {
updateUserDto.password = await bcrypt.hash(
updateUserDto.password,
roundsOfHashing,
);
}

return this.prisma.user.update({
where: { id },
data: updateUserDto,
});
}

remove(id: number) {
return this.prisma.user.delete({ where: { id } });
}
}

bcrypt.hash 哈希函数接受两个参数:哈希函数的输入字符串和哈希的轮数(也称为成本因子)。增加哈希的轮数会增加计算哈希所需的时间。这里需要在安全性和性能之间进行权衡。随着哈希次数的增加,计算哈希值需要更多的时间,这有助于防止暴力攻击。然而,当用户登录时,更多的哈希轮也意味着更多的时间来计算哈希

bcrypt 还自动使用另一种称为salt的技术来增加暴力破解哈希的难度。Salting是一种在散列之前将随机字符串添加到输入字符串中的技术。这样,攻击者就不能使用预先计算的哈希表来破解密码,因为每个密码都有不同的盐值。

您还需要更新数据库种子脚本,以便在将密码插入数据库之前对密码进行散列处理:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
// prisma/seed.ts
import { PrismaClient } from '@prisma/client';
import * as bcrypt from 'bcrypt';

// initialize the Prisma Client
const prisma = new PrismaClient();

const roundsOfHashing = 10;

async function main() {
// create two dummy users
const passwordSabin = await bcrypt.hash('password-sabin', roundsOfHashing);
const passwordAlex = await bcrypt.hash('password-alex', roundsOfHashing);

const user1 = await prisma.user.upsert({
where: { email: 'sabin@adams.com' },
update: {
password: passwordSabin,
},
create: {
email: 'sabin@adams.com',
name: 'Sabin Adams',
password: passwordSabin,
},
});

const user2 = await prisma.user.upsert({
where: { email: 'alex@ruheni.com' },
update: {
password: passwordAlex,
},
create: {
email: 'alex@ruheni.com',
name: 'Alex Ruheni',
password: passwordAlex,
},
});

// create three dummy posts
// ...
}

// execute the main function
// ...

运行 npx prisma db seed, 查看终端或者数据库发现用户密码已经被哈希处理了。

现在,如果您尝试使用正确的密码登录,您将面临HTTP 401错误。这是因为登录方法试图将来自用户请求的明文密码与数据库中的散列密码进行比较。更新登录方法以使用散列密码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
//src/auth/auth.service.ts
import { AuthEntity } from './entity/auth.entity';
import { PrismaService } from './../prisma/prisma.service';
import {
Injectable,
NotFoundException,
UnauthorizedException,
} from '@nestjs/common';
import { JwtService } from '@nestjs/jwt';
import * as bcrypt from 'bcrypt';

@Injectable()
export class AuthService {
constructor(private prisma: PrismaService, private jwtService: JwtService) {}

async login(email: string, password: string): Promise<AuthEntity> {
const user = await this.prisma.user.findUnique({ where: { email } });

if (!user) {
throw new NotFoundException(`No user found for email: ${email}`);
}

const isPasswordValid = await bcrypt.compare(password, user.password);

if (!isPasswordValid) {
throw new UnauthorizedException('Invalid password');
}

return {
accessToken: this.jwtService.sign({ userId: user.id }),
};
}
}

现在我们可以使用正确的密码来调用 /auth/login 接口来拿到 jwt token了。

总结

在本章中,您学习了如何在您的NestJS REST API中实现JWT身份验证。您还了解了如何设置密码和将身份验证与Swagger集成。

完整代码以及说明文档请查看 Nestjs 和 Prisma 实现 Restful Api