0%

// 这里插入之前的文章目录

这一篇我们继续介绍如何使用Ant Design的Table组件,关于前面的系列介绍,可以参考上面的目录。

为Table组件添加Loading效果

在实际开发中,表格数据通常是通过异步请求获取的,异步请求通常是耗时的,我们不能让用户干等,需要加上Loading以提高用户体验。Ant Design为Table组件提供了loading属性来控制数据的加载状态,我们就利用这个属性来实现Loading效果。

首先定义一个loading状态变量,用于控制表格的Loading状态。然后在获取用户数据时,将loading设置为true,请求完成后再设置为false

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
export default function Users() {
const [loading, setLoading] = useState(true);
const [tableData, setTableData] = useState<User[]>([]);

useEffect(() => {
setLoading(true);
fetchUsers().then((res) => {
setTableData(res.data);
setLoading(false);
});
}, []);

return (
<Table loading={loading} dataSource={tableData} columns={columns}></Table>
);
}

此时运行程序,表格数据加载时页面会显示Loading效果,等数据加载完成后,Loading会自动消失。

但是这段代码有一个非常隐蔽的问题,你看出来了吗?如果fetchUsers产生了异常,也就是当.then没有被调用时,loading状态将一直保持为true,导致表格一直处于Loading状态。为了避免这种情况,我们需要在.catch中也将loading设置为false

1
2
3
4
5
6
7
8
9
10
11
12
useEffect(() => {
setLoading(true);
fetchUsers()
.then((res) => {
setTableData(res.data);
setLoading(false);
})
.catch((error) => {
console.error('获取用户数据失败:', error);
setLoading(false);
});
}, []);

这时候,无论用户数据获取成功还是失败,表格的Loading状态都会正确更新。但是上面的代码还是不太优雅,因为我们在每个分支中都重复了setLoading(false),这不是一个好的实践。我们可以使用finally来简化代码。

1
2
3
4
5
6
7
8
9
10
11
12
13
useEffect(() => {
setLoading(true);
fetchUsers()
.then((res) => {
setTableData(res.data);
})
.catch((error) => {
console.error('获取用户数据失败:', error);
})
.finally(() => {
setLoading(false);
});
}, []);

现在已经消除了重复代码,但是Promise的方式产生了很多的缩进,这就是所谓的“回调地狱”。我们再使用async/await来重构一下代码。因为要使用async/await,我们需要将useEffect中的逻辑封装成一个新的async函数 - getUsers,然后在useEffect中调用它。注意,我们不能直接在useEffect中使用await操作,如果非要那么做的话,只能将await代码封装成一个IIFE放到useEffect中,但是那样可读性非常差。

使用async/await重构后的代码如下, 是不是更清晰了呢?

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
const getUsers = async () => {
try {
setLoading(true);
const res = await fetchUsers();
setTableData(res.data);
} catch (error) {
console.error('Error fetching users:', error);
} finally {
setLoading(false);
}
};

useEffect(() => {
getUsers();
}, []);

更友好的提示

回顾上面的代码,似乎一切都很完美了,但是如果用户获取数据失败,也就是走到catch分支时,我们只是在控制台打印了错误信息,这个错误信息对我们分析程序错误很有帮助,但是对于用户来说,却没有得到任何提示,用户并不知道发生了什么,所以我们可以添加一个更友好的提示,比如使用Ant Design的message组件来告知用户发生了错误。

1
2
3
4
5
6
7
8
9
10
11
12
const getUsers = async () => {
try {
setLoading(true);
const res = await fetchUsers();
setTableData(res.data);
} catch (error) {
console.error('Error fetching users:', error);
message.error('获取用户数据失败。');
} finally {
setLoading(false);
}
};

好了,今天就到这里了,我们明天继续。祝大家编程愉快!

介绍

Ant Design是一个非常出色的React UI组件库,今天来介绍一下如何使用Ant DesignTable组件。Table组件在数据可视化方面有非常广泛的应用,对于展示列表数据非常有用。在实际开发工作中,也是必须要掌握的一个组件。

创建React项目和安装Ant Design的过程我就省略了,大家直接参照官网即可,下面直接开始介绍Table组件的使用。我们要开发一个用户管理页面,用于展示用户列表数据。

定义用户数据结构

首先定义一下用户类型信息,这个类型后续要被很多地方使用。

1
2
3
4
5
6
export interface User {
id: number; // 用户ID
name: string; // 姓名
email: string; // 邮箱
phone: string; // 手机号
}

创建获取用户数据的API

兵马未动,粮草先行,为了获取用户数据,我们先创建一个API,可以在src/api目录下新建一个UserAPI.ts文件,模拟获取用户数据的API - fetchUsers。这个API使用Promise + setTimeout模拟异步请求,1秒后返回一个用户列表数据。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { User } from "./User";

export async function fetchUsers() {
const users: User[] = [
{ id: 1, name: 'Philip', email: 'philip@mail.google.com', phone: '13012345678' },
{ id: 2, name: 'Richard', email: 'richard@mail.google.com', phone: '15012345678' },
];

return new Promise<{ data: User[] }>((resolve) => {
setTimeout(() => {
resolve({ data: users });
}, 1000); // 1秒后返回数据
});
}

创建用户页面

在项目的src/pages目录下新建一个users目录,然后在该目录下新建一个index.tsx文件,作为用户页面的入口。初始代码很简单,就是引入Ant Design的Table组件并渲染一个空表格。

1
2
3
4
5
6
7
import Table from 'antd/es/table';

export default function Users() {
return (
<Table></Table>
)
}

为Table组件添加数据源

接下来,我们需要在Users组件中使用useEffect钩子来获取用户数据,并将其传递给Table组件。我们还需要定义一个状态来存储用户数据。

1
2
3
4
5
6
7
8
9
const [users, setUsers] = useState<User[]>([]);

useEffect(() => {
fetchUsers().then(response => {
setUsers(response.data);
});
}, []);

return <Table dataSource={tableData}></Table>;

此时运行App,你会看到一个空的表格,因为表格的Columns还没有定义。接下来我们定义Columns。Columns决定了表格的每一列都显示什么。

定义表格的Columns

Columns是一个数组,数组中每一个元素对应表格中的一列,每个列需要如下属性:

  • title:列的标题 - 这个用于显示在表头
  • dataIndex:列对应的数据字段 - 这个用于从数据中获取对应的值并显示到表格中
  • key:列的唯一标识符
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
},
{
title: '姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '邮箱',
dataIndex: 'email',
key: 'email',
},
{
title: '手机号',
dataIndex: 'phone',
key: 'phone',
}
];

此时再运行npm run start,你就可以看到表格中的数据了。
用户列表表格
完整代码如下:

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
import Table from 'antd/es/table';
import { useEffect, useState } from 'react';
import { User } from './User';
import { fetchUsers } from './UserAPI';

export default function Users() {
const [tableData, setTableData] = useState<User[]>([]);

const columns = [
{
title: 'ID',
dataIndex: 'id',
key: 'id',
},
{
title: '姓名',
dataIndex: 'name',
key: 'name',
},
{
title: '邮箱',
dataIndex: 'email',
key: 'email',
},
{
title: '手机号',
dataIndex: 'phone',
key: 'phone',
},
];

useEffect(() => {
fetchUsers().then((res) => {
setTableData(res.data);
});
}, []);

return <Table dataSource={tableData} columns={columns}></Table>;
}

好了,今天就到这里,在后续的文章中,我们会逐步完善这个管理页面,添加更多的功能,随着功能的增加,我们会逐步介绍Ant Design的其他组件和用法。

JavaScript中最狠的角色:NaN,为什么说它是最狠的角色呢?因为它狠起来,连自己都不放过!

如果要在纯数学中找到一个变量x,且满足 x != x,那么很遗憾,数学中不存在这样的变量。但是在JS的世界中却存在这样一个变量,它就是 NaN,它自己和自己不相等,够不够狠?

1
2
NaN == NaN // false
NaN === NaN // false

NaN的全称是“Not a Number”,它表示一个非数字的值。通常情况下,NaN出现在以下几种情况:

什么情况会产生NaN?

以下情况都会产生NaN

失败的数字类型转换

如果尝试将一个非数字类型的变量转换为数字类型,但转换失败,则会产生NaN

1
2
Number(undefined); // NaN
parseInt('abc'); // NaN

无效的数学运算

在数学运算中,如果运算无法产生一个数字,则会产生NaN。例如:

1
2
3
Math.sqrt(-1); // NaN,负数的平方根是虚数,不是数字 
Math.log(-1); // NaN,负数的对数是未定义的
Math.pow('abc', 2); // NaN,'abc'不能转换为数字

无穷值

当一个数学运算的结果是无穷大或无穷小时,JS会将其转换为NaN。例如:

1
2
3
1 / 0; // Infinity
0 * Infinity; // NaN,0乘以无穷大是未定义的
Infinity - Infinity; // NaN,无穷大减去无穷大是未定义的

非数字类型的运算

在JS中,如果对非数字类型的值进行数学运算,通常会产生NaN。例如:

1
2
3
'abc' * 2; // NaN,字符串不能参与数学运算
undefined + 1; // NaN,undefined转换为数字时是NaN
2 ** NaN; // NaN不能参加数学运算

其他情况

这个情况和第一种类似,就是将一个非数字类型表示为数字类型时,如果转换失败,也会产生NaN。例如:

1
2
new Date("blabla").getTime(); // NaN,无法将无效的日期字符串转换为时间戳
"".charCodeAt(1); // NaN,空字符串的第二个字符不存在

唯一的例外

在JS所支持的所有数学运算中,只有一个运算不会产生NaN,那就是0次幂运算。

1
NaN ** 0 // 1

如何判断NaN?

给定一个变量x,如何判断它是否是NaN呢?由开头的介绍可知,我们无法使用==或者===来判断NaN,所以JS提供了专门用于判断NaN的函数:

  • isNaN(x):这个函数主要用来判断x是否能转换为数字,如果不能转换为数字,则返回true,否则返回false。注意,这个函数会对非数字类型的值进行隐式转换。
  • Number.isNaN(x):这个函数是ES6引入的,它只会判断x是否是NaN,不会进行隐式转换。如果xNaN,则返回true,否则返回false
  • Object.is(x, NaN):这个函数也是ES6引入的,它可以用来判断x是否是NaN

看几个列子,理解一下。

1
2
3
4
5
6
7
8
9
isNaN(NaN); // true
isNaN('abc'); // true,因为'abc'不能转换为数字
isNaN(`123`); // false,因为123可以转换为数字

Number.isNaN(NaN); // true
Number.isNaN('abc'); // false,因为'abc'不是NaN

Object.is(NaN, NaN); // true
Object.is(NaN, 123); // false

面试题

如果让你写一个函数判断一个变量是否是NaN,你会怎么写?我们可以根据NaN的特性:NaN与自身不相等来实现这个函数。

1
2
3
4
5
6
function isReallyNaN(x) {
return x !== x; // NaN是唯一一个不等于自己的值
}

console.log(isReallyNaN(NaN)); // true
console.log(isReallyNaN(123)); // false

今天上午,我兴致勃勃的提交了一个PR,结果跑Pipeline的时候出了问题,我们用的是ADO(Azure DevOps),失败的原因是Unit Test覆盖率不够,覆盖率不够算是常见的问题了,但是这个问题比较特殊,SonarQube报告显示Unit Test覆盖率为0%,这就奇怪了。

我们的项目已经开始三个月了,PR也提交了很多,还没有遇到覆盖率是0%的情况。于是开始排查SonarQube的配置,是不是无法拉取覆盖率报告导致覆盖率为了,看了很多log,发现没有问题。

又回过头来排查项目代码,检查pipeline的配置,也没有发现问题。

百思不得解,这时候,有一位同事反应,他在本地跑测试的时候(npx jest --coverage),也无法生成覆盖率报告了。正常情况下,jest会生成一个lcov格式的覆盖率报告,位于项目根目录的coverage目录下。于是尝试删除coverage目录,重新运行测试,还是没有生成覆盖率报告。

这基本上可以断定是本地配置的问题,与SonarQube或者Pipeline配置都无关。于是开始排查jest的配置文件,发现了一个问题:

1
2
3
4
module.exports = {
collectCoverage: false, // 这里设置为false,导致不收集覆盖率
// 其他配置...
};

这个配置项collectCoverage被设置为false,这就导致了jest不会收集覆盖率信息。于是将其改为true,重新运行测试,一切正常!

原来,随着项目的不断推进,单元测试用例越来越多,导致跑一次测试要好几分钟,于是前端组的小张为了加快测试速度,临时将collectCoverage设置为false,以便快速跑通测试用例。没想到这个配置被提交到了代码库中,导致了覆盖率报告无法生成。

概述

TypeScript Utility Types(实用工具类)包含一系列预定义的类型,用于简化类型操作,善用这些类型可以让我们的代码更加简洁优雅,今天来学习一下Omit类型。Omit类型可以优雅的解决类型重复问题,避免冗余代码。

Omit类型的作用是什么?与以往不同,我们先不介绍晦涩难懂的概念,而是从一个实际的例子出发,进而引出这个类型。

从类型中排除属性

考虑下面这个常见的场景,你正在开发一个用户管理系统,这个系统的一个重要功能就是管理用户,比如创建用户,更新用户信息,删除用户等。

为了支持以上操作,我们首先要定义一个用户类型User,下面就是这个User类型的定义:其中包括用户id,姓名,手机号和邮箱。

1
2
3
4
5
6
interface User {
id: number; // 用户ID
name: string; // 用户名
phone: string; // 手机号
email: string; // 邮箱
}

首先:我们编写创建用户的函数,这个函数需要接收一个User类型的参数,并调用后端API来创建用户。

1
2
3
4
5
6
7
8
function createUser(user: User) {
// 调用后端API来创建用户
api.createUser(user).then(response => {
console.log('User created:', response.data);
}).catch(error => {
console.error('Error creating user:', error);
});
}

接下来,我们编写更新用户信息的函数,这个函数需要接收一个User类型的参数,并调用后端API来更新用户信息。

1
2
3
4
5
6
7
8
function updateUser(user: User) {
// 调用后端API来更新用户信息
api.updateUser(user).then(response => {
console.log('User updated:', response.data);
}).catch(error => {
console.error('Error updating user:', error);
});
}

删除用户的函数我们就不写了,因为它不影响我们今天的主题。

到现在为止,似乎一切都很顺利,创建用户和更新用户的函数使用同一个User类型,代码也很简洁。
但是,这里有一个小问题,那就是在创建用户的时候,其实我们并不需要提供用户的id,因为只有用户创建成功之后才会生成这个id。虽然说多传一个id属性不会报错,但是从逻辑上来说,这个id属性是多余的。作为一个有追求的程序员,不能容忍这种冗余代码的存在。

于是有的同学说:这还不简单吗?再定一个类型用于创建用户,把id属性去掉不就行了?于是就有了下面的NewUser类型。

1
2
3
4
5
interface NewUser {
name: string;
phone: string;
email: string;
}

但是,这样做有一个问题, NewUser类型和User类型几乎一模一样,除了id属性外,其他字段都是重复的,这就是典型的冗余代码,对于日后的维护十分不便。

设想一下,假如某一天需要为用户添加一个新的属性,比如address,那么我们就需要在UserNewUser两个类型中都添加这个属性,维护起来非常麻烦,删除某个属性也面临同样的问题。

有没有更好的办法呢?这时候,Omit类型就派上用场了,Omit类型允许我们从一个类型中排除某些属性,从而创建一个新的类型。

下面这段代码表示:创建一个新类型NewUser,它是从User类型中排除掉id属性后的结果。

1
type NewUser = Omit<User, 'id'>;

现在,我们可以使用NewUser类型来创建用户了。

1
2
3
4
5
6
7
8
function createUser(user: NewUser) {
// 调用后端API来创建用户
api.createUser(user).then(response => {
console.log('User created:', response.data);
}).catch(error => {
console.error('Error creating user:', error);
});
}

如果某一天需要为用户添加一个新的属性,比如address,我们只需要在User类型中添加这个属性,而不需要修改NewUser类型,这样就避免了冗余代码的出现。删除某个属性也同样方便。

更方便的是,Omit一次可以排除多个属性,比如我们还想排除email属性,可以这样写:

1
type NewUser = Omit<User, 'id' | 'email'>;

这样,NewUser类型就会同时排除idemail属性。

写到这里,似乎该结束了,但是作为一个有追求的程序员,你以为这就完了吗?当然不行,我们要举一反三,如果反过来该怎么办呢?

向类型中添加属性

假设你维护的是一个老系统,原来的代码先定义了NewUser类型(只有创建用户的需求),现在添加了一个新需求:要求添加一个函数用于更新用户的信息,更新用户信息就需要提供用户ID,这时候你需要一个新的类型,也就是在NewUser的基础上添加id属性。通俗点说,前面的例子是在一个类型中排除某些属性,而现在我们需要在一个类型中添加某些属性。这相当于Omit的反向操作。

那TypeScript中有没有这样的实用类型呢?非常遗憾,TypeScript的标准库中并没有提供这样的类型,但是我们可以自己实现一个。

下面的代码使用&操作符来创建一个新的类型UpdateUser,它包含了NewUser的所有属性,并添加了一个id属性。

1
type UpdateUser = NewUser & { id: number };

当然,还可以使用类型继承的方式来实现同样的效果:

1
2
3
interface UpdateUser extends NewUser {
id: number;
}

这样,我们就可以使用UpdateUser类型来更新用户信息了。

1
2
3
4
5
6
7
8
function updateUser(user: UpdateUser) {
// 调用后端API来更新用户信息
api.updateUser(user).then(response => {
console.log('User updated:', response.data);
}).catch(error => {
console.error('Error updating user:', error);
});
}

是不是很优雅呢?你学会了吗?学会了就点个关注吧,后续会有更多有趣的TypeScript知识分享。

参考

  1. https://www.typescriptlang.org/docs/handbook/utility-types.html#omittype-keys

慎用sessionStorage.clear():一个由全局清理引发的Token丢失问题

今天下午,我正在全神贯注地编写代码,测试组的一位同事突然找到我,说我的页面在发送请求的时候没有携带token,导致api调用出错。

我:不可能,token都是前端统一携带的,我的页面并无特殊处理,怎么可能没有携带token?

妹子:可是我在测试的时候发现确实没有携带token。

我:你确定是我的页面吗?

妹子:是的,你可以现在就看一下。

于是我F12打开浏览器的开发者工具,来到我的页面查看网络请求,果然发现请求头中没有携带token。气氛一度十分尴尬…

我:稍等,容我检查一下。

妹子没说话,可能觉得我刚才不够信任她…

经过一番调试,发现的确是我的问题,我这个页面比较特殊,是类似一个用户向导的功能,共分为三个步骤,每个步骤对应独立的页面,而且每个页面都需要用户填写一些信息,除了第一步之外,其他的步骤都支持后退修改。所有的步骤都支持取消操作,表示用户放弃了本次操作。

为了支持在最后一个页面提交,我使用了sessionStorage来存储用户填写的信息,如果用户在某个页面点击了取消按钮,那么我会清空sessionStorage中的数据。问题就出在这个清空操作上,我用的是sessionStorage.clear()方法,这个方法会清空整个sessionStorage,而不仅仅是我这个页面存储的数据。

1
2
3
const onCancelButtonClick = () => {
sessionStorage.clear();
};

巧合的是,前几天开发登录功能的小伙伴刚刚把存储token的代码从localStorage改成了sessionStorage,导致我在清空sessionStorage的时候也把token给清空了。于是就产生了妹子所说的没有携带token的问题。

解决的办法也很简单,就是把清空sessionStorage的代码改成只删除当前页面存储的数据,而不是清空整个sessionStorage。可以使用sessionStorage.removeItem('key')方法来删除指定的键值对。

1
2
// 清空指定数据
sessionStorage.removeItem('userData');

这样就可以避免清空其他页面存储的数据,也不会影响到token的存储。

总结

慎用sessionStorage.clear()方法,它会清空整个sessionStorage,可能会导致其他页面存储的数据丢失。这个方法应用在关闭整个应用的时候做总的清理工作。

如果只是某个页面的清理工作,建议使用sessionStorage.removeItem('key')方法来删除指定的键值对,这样可以避免不必要的数据丢失。

最近使用React+Ant Design进行项目开发,单元测试的过程及其痛苦,尤其对于单元测试的覆盖率问题,一直无法达标,经常导致pipeline失败。

其实我们项目的单元测试覆盖率要求并不高,只有60%,之前的项目要求都是90%的。之所以覆盖率。

最近的项目使用阿里umijs框架,在做Unit Test时遇到了一些问题,在此记录一下,以供日后查阅。

创建Umi项目

使用以下命令创建一个新的Umi项目,项目模版选择ant-design-pro,npm client选择npm,npm registry选择npm

1
npx create-umi@latest

生成jest配置

Umi的方便之处是可以一键生成jest配置,只需运行如下命令即可:

1
npx umi g jest

运行测试

使用以下命令运行测试

Read more »

今天在玩耍typescript的时候,发现了一个有趣的问题,tsconfig.json中的outDir配置导致tsc命令无法编译,tsconfig.json的配置是这样的:

Read more »