rowKey的作用是什么?
rowKey
是Ant Design的Table组件的一个重要属性,用于指定每一行数据的唯一标识符。对于Table组件来说,dataSource
和columns
这两个属性中都要包含key
属性,否则就需要在
1 | index.js:1 Warning: Each child in a list should have a unique "key" prop. |
这个警告信息非常之多,这里有一个窍门可以快速定位到问题所在,那就是找到at Table
这一行,然后再下面一行就是是错误发生的组件,比如下面这个警告信息,它表示错误发生在User
组件中:
如何指定rowKey?
为dataSource指定唯一标识符
找到错误之后,修复就简单了,由前面的介绍可知,修复这个警告有两种方式:
- 在
dataSource
中的每一行数据中添加一个唯一的key
属性。 - 在
<Table>
组件中指定rowKey
属性。
这里我们采用第二种,因为这样可以避免在dataSource
每一行数据中都添加key
属性,代码更简洁。我们可以将Table组件的rowKey
设置为用户ID即可。
1 | <Table |
也可以简写成下面的形式,只要确保你的dataSource
中的每一行数据都有一个唯一的id
属性即可。Ant Design会自动使用这个id
作为每一行的唯一标识符。上面的函数形式主要是为了兼容数据源中没有id
属性的情况。可以通过函数生成一个唯一标识供Table使用。
1 | <Table |
需要注意的是:不要使用数组下标作rowKey
,因为在数据发生变化时,数组下标会发生变化,导致React无法正确识别哪些数据发生了变化,从而引发渲染问题。
1 | rowKey={(record, index) => index} // 不要使用数组元素下标做rowKey |
为columns指定唯一标识符
columns
只能在定义的时候指定key
属性。
1 | const columns = [ |
总结
React要求数组中的每个元素都要有一个唯标识(默认是key
属性),其底层原因是为了Diff算法能够高效地比较和更新组件。Ant Design的Table组件也遵循了这个原则,我们可以通过如下几种方式来指定每一行数据的唯一标识符:
为dataSource指定唯一标识符
- 在
dataSource
中的每一行数据中添加一个唯一的key
属性。 - 在Table组件中指定
rowKey
属性,可以是一个字符串(表示数据源中每一行数据的唯一标识符)或者一个函数(返回每一行数据的唯一标识符)。
为columns指定唯一标识符
对于columns
来说,必须在定义时指定一个唯一的key
属性,因为Table组件的rowKey
只作用于dataSource
,而不作用于columns
。
react-ant-design-component-table-2
// 这里插入之前的文章目录
这一篇我们继续介绍如何使用Ant Design的Table组件,关于前面的系列介绍,可以参考上面的目录。
为Table组件添加Loading效果
在实际开发中,表格数据通常是通过异步请求获取的,异步请求通常是耗时的,我们不能让用户干等,需要加上Loading以提高用户体验。Ant Design为Table组件提供了loading
属性来控制数据的加载状态,我们就利用这个属性来实现Loading效果。
首先定义一个loading
状态变量,用于控制表格的Loading状态。然后在获取用户数据时,将loading
设置为true
,请求完成后再设置为false
。
1 | export default function Users() { |
此时运行程序,表格数据加载时页面会显示Loading效果,等数据加载完成后,Loading会自动消失。
但是这段代码有一个非常隐蔽的问题,你看出来了吗?如果fetchUsers
产生了异常,也就是当.then
没有被调用时,loading
状态将一直保持为true
,导致表格一直处于Loading状态。为了避免这种情况,我们需要在.catch
中也将loading
设置为false
。
1 | useEffect(() => { |
这时候,无论用户数据获取成功还是失败,表格的Loading状态都会正确更新。但是上面的代码还是不太优雅,因为我们在每个分支中都重复了setLoading(false)
,这不是一个好的实践。我们可以使用finally
来简化代码。
1 | useEffect(() => { |
现在已经消除了重复代码,但是Promise的方式产生了很多的缩进,这就是所谓的“回调地狱”。我们再使用async/await
来重构一下代码。因为要使用async/await
,我们需要将useEffect
中的逻辑封装成一个新的async
函数 - getUsers
,然后在useEffect
中调用它。注意,我们不能直接在useEffect
中使用await
操作,如果非要那么做的话,只能将await
代码封装成一个IIFE
放到useEffect
中,但是那样可读性非常差。
使用async/await
重构后的代码如下, 是不是更清晰了呢?
1 | const getUsers = async () => { |
更友好的提示
回顾上面的代码,似乎一切都很完美了,但是如果用户获取数据失败,也就是走到catch
分支时,我们只是在控制台打印了错误信息,这个错误信息对我们分析程序错误很有帮助,但是对于用户来说,却没有得到任何提示,用户并不知道发生了什么,所以我们可以添加一个更友好的提示,比如使用Ant Design的message
组件来告知用户发生了错误。
1 | const getUsers = async () => { |
好了,今天就到这里了,我们明天继续。祝大家编程愉快!
react-ant-design-component-table
介绍
Ant Design
是一个非常出色的React
UI组件库,今天来介绍一下如何使用Ant Design
的Table
组件。Table
组件在数据可视化方面有非常广泛的应用,对于展示列表数据非常有用。在实际开发工作中,也是必须要掌握的一个组件。
创建React
项目和安装Ant Design
的过程我就省略了,大家直接参照官网即可,下面直接开始介绍Table
组件的使用。我们要开发一个用户管理页面,用于展示用户列表数据。
定义用户数据结构
首先定义一下用户类型信息,这个类型后续要被很多地方使用。
1 | export interface User { |
创建获取用户数据的API
兵马未动,粮草先行,为了获取用户数据,我们先创建一个API,可以在src/api
目录下新建一个UserAPI.ts
文件,模拟获取用户数据的API - fetchUsers
。这个API使用Promise
+ setTimeout
模拟异步请求,1秒后返回一个用户列表数据。
1 | import { User } from "./User"; |
创建用户页面
在项目的src/pages
目录下新建一个users
目录,然后在该目录下新建一个index.tsx
文件,作为用户页面的入口。初始代码很简单,就是引入Ant Design的Table组件并渲染一个空表格。
1 | import Table from 'antd/es/table'; |
为Table组件添加数据源
接下来,我们需要在Users
组件中使用useEffect
钩子来获取用户数据,并将其传递给Table
组件。我们还需要定义一个状态来存储用户数据。
1 | const [users, setUsers] = useState<User[]>([]); |
此时运行App,你会看到一个空的表格,因为表格的Columns还没有定义。接下来我们定义Columns。Columns决定了表格的每一列都显示什么。
定义表格的Columns
Columns是一个数组,数组中每一个元素对应表格中的一列,每个列需要如下属性:
title
:列的标题 - 这个用于显示在表头dataIndex
:列对应的数据字段 - 这个用于从数据中获取对应的值并显示到表格中key
:列的唯一标识符
1 | const columns = [ |
此时再运行npm run start
,你就可以看到表格中的数据了。
完整代码如下:
1 | import Table from 'antd/es/table'; |
好了,今天就到这里,在后续的文章中,我们会逐步完善这个管理页面,添加更多的功能,随着功能的增加,我们会逐步介绍Ant Design的其他组件和用法。
JavaScript中最狠的角色:NaN
JavaScript中最狠的角色:NaN
,为什么说它是最狠的角色呢?因为它狠起来,连自己都不放过!
如果要在纯数学中找到一个变量x,且满足 x != x
,那么很遗憾,数学中不存在这样的变量。但是在JS的世界中却存在这样一个变量,它就是 NaN
,它自己和自己不相等,够不够狠?
1 | NaN == NaN // false |
NaN
的全称是“Not a Number”,它表示一个非数字的值。通常情况下,NaN
出现在以下几种情况:
什么情况会产生NaN?
以下情况都会产生NaN
:
失败的数字类型转换
如果尝试将一个非数字类型的变量转换为数字类型,但转换失败,则会产生NaN
。
1 | Number(undefined); // NaN |
无效的数学运算
在数学运算中,如果运算无法产生一个数字,则会产生NaN
。例如:
1 | Math.sqrt(-1); // NaN,负数的平方根是虚数,不是数字 |
无穷值
当一个数学运算的结果是无穷大或无穷小时,JS会将其转换为NaN
。例如:
1 | 1 / 0; // Infinity |
非数字类型的运算
在JS中,如果对非数字类型的值进行数学运算,通常会产生NaN
。例如:
1 | 'abc' * 2; // NaN,字符串不能参与数学运算 |
其他情况
这个情况和第一种类似,就是将一个非数字类型表示为数字类型时,如果转换失败,也会产生NaN
。例如:
1 | new Date("blabla").getTime(); // 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
,不会进行隐式转换。如果x
是NaN
,则返回true
,否则返回false
。Object.is(x, NaN)
:这个函数也是ES6引入的,它可以用来判断x
是否是NaN
。
看几个列子,理解一下。
1 | isNaN(NaN); // true |
面试题
如果让你写一个函数判断一个变量是否是NaN
,你会怎么写?我们可以根据NaN
的特性:NaN与自身不相等来实现这个函数。
1 | function isReallyNaN(x) { |
jest-config-do-not-collect-coverage
今天上午,我兴致勃勃的提交了一个PR,结果跑Pipeline
的时候出了问题,我们用的是ADO
(Azure DevOps),失败的原因是Unit Tes
t覆盖率不够,覆盖率不够算是常见的问题了,但是这个问题比较特殊,SonarQube
报告显示Unit Test覆盖率为0%,这就奇怪了。
我们的项目已经开始三个月了,PR也提交了很多,还没有遇到覆盖率是0%的情况。于是开始排查SonarQube的配置,是不是无法拉取覆盖率报告导致覆盖率为了,看了很多log,发现没有问题。
又回过头来排查项目代码,检查pipeline
的配置,也没有发现问题。
百思不得解,这时候,有一位同事反应,他在本地跑测试的时候(npx jest --coverage
),也无法生成覆盖率报告了。正常情况下,jest
会生成一个lcov
格式的覆盖率报告,位于项目根目录的coverage
目录下。于是尝试删除coverage
目录,重新运行测试,还是没有生成覆盖率报告。
这基本上可以断定是本地配置的问题,与SonarQube
或者Pipeline
配置都无关。于是开始排查jest
的配置文件,发现了一个问题:
1 | module.exports = { |
这个配置项collectCoverage
被设置为false
,这就导致了jest
不会收集覆盖率信息。于是将其改为true
,重新运行测试,一切正常!
原来,随着项目的不断推进,单元测试用例越来越多,导致跑一次测试要好几分钟,于是前端组的小张为了加快测试速度,临时将collectCoverage
设置为false
,以便快速跑通测试用例。没想到这个配置被提交到了代码库中,导致了覆盖率报告无法生成。
typescript-utility-types-omit
概述
TypeScript Utility Types
(实用工具类)包含一系列预定义的类型,用于简化类型操作,善用这些类型可以让我们的代码更加简洁优雅,今天来学习一下Omit
类型。Omit
类型可以优雅的解决类型重复问题,避免冗余代码。
Omit
类型的作用是什么?与以往不同,我们先不介绍晦涩难懂的概念,而是从一个实际的例子出发,进而引出这个类型。
从类型中排除属性
考虑下面这个常见的场景,你正在开发一个用户管理系统,这个系统的一个重要功能就是管理用户,比如创建用户,更新用户信息,删除用户等。
为了支持以上操作,我们首先要定义一个用户类型User
,下面就是这个User
类型的定义:其中包括用户id,姓名,手机号和邮箱。
1 | interface User { |
首先:我们编写创建用户的函数,这个函数需要接收一个User
类型的参数,并调用后端API来创建用户。
1 | function createUser(user: User) { |
接下来,我们编写更新用户信息的函数,这个函数需要接收一个User
类型的参数,并调用后端API来更新用户信息。
1 | function updateUser(user: User) { |
删除用户的函数我们就不写了,因为它不影响我们今天的主题。
到现在为止,似乎一切都很顺利,创建用户和更新用户的函数使用同一个User
类型,代码也很简洁。
但是,这里有一个小问题,那就是在创建用户的时候,其实我们并不需要提供用户的id
,因为只有用户创建成功之后才会生成这个id。虽然说多传一个id
属性不会报错,但是从逻辑上来说,这个id
属性是多余的。作为一个有追求的程序员,不能容忍这种冗余代码的存在。
于是有的同学说:这还不简单吗?再定一个类型用于创建用户,把id
属性去掉不就行了?于是就有了下面的NewUser
类型。
1 | interface NewUser { |
但是,这样做有一个问题, NewUser
类型和User
类型几乎一模一样,除了id
属性外,其他字段都是重复的,这就是典型的冗余代码,对于日后的维护十分不便。
设想一下,假如某一天需要为用户添加一个新的属性,比如address
,那么我们就需要在User
和NewUser
两个类型中都添加这个属性,维护起来非常麻烦,删除某个属性也面临同样的问题。
有没有更好的办法呢?这时候,Omit
类型就派上用场了,Omit
类型允许我们从一个类型中排除某些属性,从而创建一个新的类型。
下面这段代码表示:创建一个新类型NewUser
,它是从User
类型中排除掉id
属性后的结果。
1 | type NewUser = Omit<User, 'id'>; |
现在,我们可以使用NewUser
类型来创建用户了。
1 | function createUser(user: NewUser) { |
如果某一天需要为用户添加一个新的属性,比如address
,我们只需要在User
类型中添加这个属性,而不需要修改NewUser
类型,这样就避免了冗余代码的出现。删除某个属性也同样方便。
更方便的是,Omit
一次可以排除多个属性,比如我们还想排除email
属性,可以这样写:
1 | type NewUser = Omit<User, 'id' | 'email'>; |
这样,NewUser
类型就会同时排除id
和email
属性。
写到这里,似乎该结束了,但是作为一个有追求的程序员,你以为这就完了吗?当然不行,我们要举一反三,如果反过来该怎么办呢?
向类型中添加属性
假设你维护的是一个老系统,原来的代码先定义了NewUser
类型(只有创建用户的需求),现在添加了一个新需求:要求添加一个函数用于更新用户的信息,更新用户信息就需要提供用户ID,这时候你需要一个新的类型,也就是在NewUser
的基础上添加id
属性。通俗点说,前面的例子是在一个类型中排除某些属性,而现在我们需要在一个类型中添加某些属性。这相当于Omit
的反向操作。
那TypeScript中有没有这样的实用类型呢?非常遗憾,TypeScript的标准库中并没有提供这样的类型,但是我们可以自己实现一个。
下面的代码使用&
操作符来创建一个新的类型UpdateUser
,它包含了NewUser
的所有属性,并添加了一个id
属性。
1 | type UpdateUser = NewUser & { id: number }; |
当然,还可以使用类型继承的方式来实现同样的效果:
1 | interface UpdateUser extends NewUser { |
这样,我们就可以使用UpdateUser
类型来更新用户信息了。
1 | function updateUser(user: UpdateUser) { |
是不是很优雅呢?你学会了吗?学会了就点个关注吧,后续会有更多有趣的TypeScript
知识分享。
参考
javascript-sessionStorage-clear-case-1
慎用sessionStorage.clear():一个由全局清理引发的Token丢失问题
今天下午,我正在全神贯注地编写代码,测试组的一位同事突然找到我,说我的页面在发送请求的时候没有携带token,导致api调用出错。
我:不可能,token都是前端统一携带的,我的页面并无特殊处理,怎么可能没有携带token?
妹子:可是我在测试的时候发现确实没有携带token。
我:你确定是我的页面吗?
妹子:是的,你可以现在就看一下。
于是我F12打开浏览器的开发者工具,来到我的页面查看网络请求,果然发现请求头中没有携带token。气氛一度十分尴尬…
我:稍等,容我检查一下。
妹子没说话,可能觉得我刚才不够信任她…
经过一番调试,发现的确是我的问题,我这个页面比较特殊,是类似一个用户向导的功能,共分为三个步骤,每个步骤对应独立的页面,而且每个页面都需要用户填写一些信息,除了第一步之外,其他的步骤都支持后退修改。所有的步骤都支持取消操作,表示用户放弃了本次操作。
为了支持在最后一个页面提交,我使用了sessionStorage来存储用户填写的信息,如果用户在某个页面点击了取消按钮,那么我会清空sessionStorage中的数据。问题就出在这个清空操作上,我用的是sessionStorage.clear()
方法,这个方法会清空整个sessionStorage,而不仅仅是我这个页面存储的数据。
1 | const onCancelButtonClick = () => { |
巧合的是,前几天开发登录功能的小伙伴刚刚把存储token的代码从localStorage改成了sessionStorage,导致我在清空sessionStorage的时候也把token给清空了。于是就产生了妹子所说的没有携带token的问题。
解决的办法也很简单,就是把清空sessionStorage的代码改成只删除当前页面存储的数据,而不是清空整个sessionStorage。可以使用sessionStorage.removeItem('key')
方法来删除指定的键值对。
1 | // 清空指定数据 |
这样就可以避免清空其他页面存储的数据,也不会影响到token的存储。
总结
慎用sessionStorage.clear()
方法,它会清空整个sessionStorage,可能会导致其他页面存储的数据丢失。这个方法应用在关闭整个应用的时候做总的清理工作。
如果只是某个页面的清理工作,建议使用sessionStorage.removeItem('key')
方法来删除指定的键值对,这样可以避免不必要的数据丢失。
Ant design单元测试踩坑记
最近使用React+Ant Design进行项目开发,单元测试的过程及其痛苦,尤其对于单元测试的覆盖率问题,一直无法达标,经常导致pipeline失败。
其实我们项目的单元测试覆盖率要求并不高,只有60%,之前的项目要求都是90%的。之所以覆盖率。