环境配置
- 确定安装了typescript、create-react-app、Node、npm
- 终端安装:
npx create-react-app 项目名 --typescript
如:npx create-react-app carad --typescript
。--typescript
是给react项目添加TS支持。 - npx可以避免安装全局模块,可以调用项目内部安装的模块
- create-react-app创建的项目中自带ESLint的检查。但是我们用TS编写的代码,ESLint默认不开启对TS的检查,我们需要手动添加配置文件,.vscode文件夹中的settings.json中的代码:
{
"eslint.validate": [
"javascript",
"javascriptreact",
{ "language": "typescript", "autoFix": true },
{ "language": "typescriptreact", "autoFix": true }
]
}
- create-react-app创建的项目不支持sass,安装一个:
npm install node-sass --save
色彩体系
- 可以在中国色找一些你喜欢的颜色作为本组件色彩体系
- 系统色板:基础色板+中性色板
- 产品色板:品牌色 + 功能色板
- 新建src/styles/_variables.scss,并配置颜色变量
- 不同浏览器元素不一致,比如每个浏览器的margin、padding等的默认值不统一,使用normalize.css来解决,他让元素在跨浏览器中保持一致。
normalize.css能做什么:保护有用的浏览器的默认样式,而不是完全去掉默认样式。为大多数元素提供一般化的样式。修复浏览器自身的bug并保证各种浏览器的一致性。优化CSS可用性。用详细的注释或文字来解释源码。
- 新建src/styles/_reboot.scss文件,直接将normalize.css的内容拷贝了过来,一些样式用我们定义的变量(在src/styles/_variables.scss中新增自定义的变量)替换了。
scss的@import与css的import不同,后者每次调用时都会创建一个额外的HTML请求,前者将文件包含在css中,不需要额外的HTML请求。
_reboot.scss、_variables.scss文件名都以_
开头,他告诉scss不要将这些文件编译到css文件中,这些文件只能当作模块导入,不能单独使用。导入时不用添加_
- 在index.tsx中引入:
import './styles/index.scss'
button
我们要给button添加不同的className,如btn-lg,btn-small等,使用classnames这个小工具:
- 安装:
npm install classnames --save
和npm install @types/classnames --save
- 导入:
import classNames from 'classnames'
- 使用:他接收多个参数,参数为true才会以空格隔开拼接到classname,若为对象,当对象的值为true时才将键作为classname。具体如下:
- 按钮分为普通按钮和link形式的a标签,针对不可点击的情况(disabled),button有disabled属性,可以直接使用,但是a标签没有,在本组件中,如果给a标签设置了disabled,那么我们在classname中加入disabled,后期通过样式来控制
import React, { FC, ButtonHTMLAttributes, AnchorHTMLAttributes } from 'react'
import classNames from 'classnames'
// 定义按钮的样式、尺寸、属性
export enum ButtonSize{
Large = 'lg',
Small = 'sm'
}
export enum ButtonType{
Primary = 'primary',
Default = 'default',
Danger = 'danger',
Link = 'link'
}
interface BaseButtonProps {
className?: string;
disabled?: boolean;/**设置 Button 的禁用 */
size?: ButtonSize;/**设置 Button 的尺寸 */
btnType?: ButtonType;/**设置 Button 的类型 */
children: React.ReactNode;
href?: string;
}
export const Button: FC<BaseButtonProps> = (props) => {
// 函数式组件中,props会作为参数传进来
const {
btnType,
disabled,
size,
children,
href
} = props// 从props中取出外界给我们传入的属性
// 根据属性添加classname: btn, btn-lg, btn-primary
const classes = classNames('btn', {
[`btn-${btnType}`]: btnType,
[`btn-${size}`]: size,
// 别忘了我们说过的特殊disabled属性,button天然支持这个属性,而a链接没有这么一个属性,
// 所以我们需要用样式来模拟这个属性,所以我们给a链接添加一个特殊的class,然后把它们填充到button和a元素上。
// a链接没有disabled属性,他的disabled直接添加到classname中:
'disabled': (btnType === 'link') && disabled// type为link(标识a标签)并提供了disabled属性
})
// 展示的内容
if (btnType === 'link' && href ) {
return (
<a
className={classes}
href={href}
{...restProps}
>
{children}
</a>
)
} else {
return (
<button
className={classes}
disabled={disabled}
{...restProps}
>
{children}
</button>
)
}
}
Button.defaultProps = {
disabled: false,
btnType: 'default',
}
export default Button;
使用:在app.tsc中导入:import Button, {ButtonType, ButtonSize} from ./components/Button/button.tsx
<Button disabled>hello</Button>
<Button btnType={ButtonType.Primary} size={ButtonSize.Large}>hello</Button>
<Button btnType={ButtonType.Link} href="http://www.baidu.com">baidu link</Button>
然后我们就能看到下面的按钮啦~
样式
基本样式
我们先来实现button的基本样式:
- 在src/styles/_variables.scss中为button添加一些变量
- 新建src/components/Button/_style.scss,里面的代码如下:
.btn {// button的基本样式
position: relative;
display: inline-block;
font-weight: $btn-font-weight;
line-height: $btn-line-height;
color: $body-color;
white-space: nowrap;// 不换行
text-align: center;// 水平居中
vertical-align: middle;//垂直居中
background-image: none;
border: $btn-border-width solid transparent;// transparent透明
padding: $btn-padding-y $btn-padding-x;
font-size: $btn-font-size;
border-radius: $border-radius;
box-shadow: $btn-box-shadow;
cursor: pointer;// 鼠标样式:手状
transition: $btn-transition;
// 禁用时的样式
&.disabled,// 类名中有disabled
&[disabled] {// 有disabled这个属性
cursor: not-allowed;// 鼠标变成不可点击状态
opacity: $btn-disabled-opacity;// 改变不透明度
box-shadow: none;
> * {// 给所有子元素设置
pointer-events: none;// 不接收鼠标事件
}
}
}
- 在src/styles/index.scss中导入上面那个_style.scss:
@import "../components/Button/style";
然后就得到了下面这些button,在禁用状态(disabled)下透明度变了
样式升级
思路:
(1)size不同时展示不同的大小,通过设置字体大小、padding、圆角尺寸来实现。
(2)type不同时展示不同的样式,通过设置背景颜色、边框、颜色、hover状态下的背景颜色、hover状态下的边框、hover状态下的颜色来实现。
(3)button的disabled状态和链接的disabled状态不同
当classname为btn-lg时要设置字体大小、padding、圆角尺寸,当classname为btn-sm时要设置字体大小、padding、圆角尺寸,那我们可以使用混合。
新建src/styles/_mixin.scss,并写以下代码,然后在src/styles/index.scss中引入mixin.scss
@mixin button-size($padding-y, $padding-x, $font-size, $border-raduis) {
padding: $padding-y $padding-x;
font-size: $font-size;
border-radius: $border-raduis;
}
@mixin button-style(
$background,
$border,
$color,
$hover-background: lighten($background, 7.5%),
$hover-border: lighten($border, 10%),
$hover-color: $color
) {
color: $color;
background: $background;
border-color: $border;
&:hover {// hover状态
color: $hover-color;
background: $hover-background;
border-color: $hover-border;
}
&:focus,// focus状态
&.focus {// focus类
color: $hover-color;
background: $hover-background;
border-color: $hover-border;
}
&:disabled,
&.disabled {// 在disabled状态下,hover和focus都不改变样式
color: $color;
background: $background;
border-color: $border;
}
}
src/components/Button/_style.scss中新增代码:
.btn-lg {
@include button-size($btn-padding-y-lg, $btn-padding-x-lg, $btn-font-size-lg, $btn-border-radius-lg);
}
.btn-sm {
@include button-size($btn-padding-y-sm, $btn-padding-x-sm, $btn-font-size-sm, $btn-border-radius-sm);
}
.btn-primary {
@include button-style($primary, $primary, $white)
}
.btn-danger {
@include button-style($danger, $danger, $white)
}
.btn-default {
@include button-style($white, $gray-400, $body-color, $white, $primary, $primary)
}
.btn-link {
font-weight: $font-weight-normal;
color: $btn-link-color;
text-decoration: $link-decoration;
box-shadow: none;
&:hover {
color: $btn-link-hover-color;
text-decoration: $link-hover-decoration;
}
&:focus,
&.focus {
text-decoration: $link-hover-decoration;
box-shadow: none;
}
&:disabled,
&.disabled {
color: $btn-link-disabled-color;
pointer-events: none;
}
}
然后我们就可以得到以下样式啦
完善
我们这里的按钮应该具备button和a标签的属性,TS中&
将多个类型合并为一个类型:
/*
ButtonHTMLAttributes<HTMLElement>: 获取所有的button属性
AnchorHTMLAttributes<HTMLElement>: 获取所有的a链接的属性
*/
type NativeButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLElement>
type AnchorButtonProps = BaseButtonProps & AnchorHTMLAttributes<HTMLElement>
有些属性在button上是必须的,但是在a链接上是可选的,所以我们将所有的属性设为可选的,TS的Partial<T>
会将所有属性设为可选的,T是泛型
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps>
我们在定义classname的时候,需要将用户自定义的classname也添加进来。还需要取出剩下的属性定义在我们的标签上。
export const Button: FC<ButtonProps> = (props) => {
const {
btnType,
className,
disabled,
size,
children,
href,
...restProps// 取出剩下的属性
} = props
// 添加classname: btn, btn-lg, btn-primary
// className是用户自定义的类名, 也需要添加进来
const classes = classNames('btn', className, {
[`btn-${btnType}`]: btnType,
[`btn-${size}`]: size,
'disabled': (btnType === 'link') && disabled
})
// 展示的内容
if (btnType === 'link' && href ) {
return (
<a
className={classes}
href={href}
{...restProps}
>
{children}
</a>
)
} else {
return (
<button
className={classes}
disabled={disabled}
{...restProps}
>
{children}
</button>
)
}
}
button.tsc代码如下:
import React, { FC, ButtonHTMLAttributes, AnchorHTMLAttributes } from 'react'
import classNames from 'classnames'
export type ButtonSize = 'lg' | 'sm'
export type ButtonType = 'primary' | 'default' | 'danger' | 'link'
interface BaseButtonProps {
className?: string;
disabled?: boolean;/**设置 Button 的禁用 */
size?: ButtonSize;/**设置 Button 的尺寸 */
btnType?: ButtonType;/**设置 Button 的类型 */
children: React.ReactNode;
href?: string;
}
type NativeButtonProps = BaseButtonProps & ButtonHTMLAttributes<HTMLElement>
type AnchorButtonProps = BaseButtonProps & AnchorHTMLAttributes<HTMLElement>
export type ButtonProps = Partial<NativeButtonProps & AnchorButtonProps>
export const Button: FC<ButtonProps> = (props) => {
const {
btnType,
className,
disabled,
size,
children,
href,
...restProps
} = props
const classes = classNames('btn', className, {
[`btn-${btnType}`]: btnType,
[`btn-${size}`]: size,
'disabled': (btnType === 'link') && disabled
})
if (btnType === 'link' && href ) {
return (
<a
className={classes}
href={href}
{...restProps}
>
{children}
</a>
)
} else {
return (
<button
className={classes}
disabled={disabled}
{...restProps}
>
{children}
</button>
)
}
}
Button.defaultProps = {
disabled: false,
btnType: 'default'
}
export default Button;
通用测试框架Jest
- 断言:判断测试出来的值是否是预期结果。
- Jest提供了各种API让我们来判断结果是否符合预期
- jest.test.js中测试代码如下:
//第一个参数是本次测试的名字
test('test common matcher', () => {
expect( 2 + 2 ).toBe(4)
expect(2 + 2).not.toBe(5)
})
test('test to be true or false', () => {
expect(1).toBeTruthy()
expect(0).toBeFalsy()
})
test('test number', () => {
expect(4).toBeGreaterThan(3)
expect(2).toBeLessThan(3)
})
test('test object', () => {
expect({name: 'viking'}).toEqual({name: 'viking'})
})
create-react-app已经内置了Jest:npx jest jest.test.js
直接运行jest.test.js文件。我们不希望每次运行时都写这个命令,那么我们可以使用npx jest jest.test.js --watch
让Jest一直运行
react测试工具——react-testing-library
- 查看项目的package.json:react-script的版本在3.3.0之上,说明自动搭载了test-library和jest-dom,就不用再安装test-library和jest-dom的依赖了,如果版本在3.3.0之下,手动安装test-library的依赖:
npm install --save-dev @testing-library/react
,手动安装和jest-dom的依赖:npm install --save-dev @testing-library/jest-dom
。 - 将上述安装好的库以自定义扩展的形式装上去,create-react-app支持Testsetup文件,用于存放每次测试前的全局通用配置,我们每次运行
jest
或npm run test
时,就可以先运行Testsetup文件。src/setupTests.ts代码如下:import '@testing-library/jest-dom/extend-expect';
- Jest会将以下三类文件看成测试文件:
(1)__tests__
文件夹下的.js
和.ts
结尾的文件
(2).test.js
和.test.ts
结尾的文件
(3).spec.js
和.spec.ts
结尾的文件 - button.test.tsx中的代码:
import React from 'react'
import { render, fireEvent } from '@testing-library/react'
import Button, { ButtonProps } from './button'
test('our first react test case', () => {
const wrapper = render(<Button>Nice</Button>)// 使用button组件
const element = wrapper.getByText('Nice')// 使用getByText找element
expect(element).toBeTruthy()// 输出应该为true
})
/*
正常情况下,点击按钮,调用函数
我们在测试时,如何追踪函数是否被调用呢,Jest给我们提供了Mock Functions,用于模拟代码中函数的真实实现,捕捉函数的调用,
调用了多少次,被谁调用等。比如代码中点击按钮后会发送异步请求,测试时不可能真的发一次异步请求,等着返回结果,
此时可以使用Mock Functions捕获函数的调用
jest.fn():创建出一个被监控的模拟函数
*/
const defaultProps = {
onClick: jest.fn()
}
const testProps: ButtonProps = {
btnType: 'primary',
size: 'lg',
className: 'klass'
}
const disabledProps: ButtonProps = {
disabled: true,
onClick: jest.fn(),
}
//describe对测试分类,第一个参数依旧是测试名称
describe('test Button component', () => {
//这里可以写it,也可以写test,都一样
it('should render the correct default button', () => {
//将defaultProps添加到button上
const wrapper = render(<Button {...defaultProps}>Nice</Button>)
//wrapper.getByText('Nice')是一个HTML,不是button,as HTMLButtonElement使用类型断言,让它变成button
const element = wrapper.getByText('Nice') as HTMLButtonElement
expect(element).toBeInTheDocument()// 判断组件是否在文档中
expect(element.tagName).toEqual('BUTTON')// tagName都是大写的
expect(element).toHaveClass('btn btn-default')// 测试是否有这些类名
expect(element.disabled).toBeFalsy()//
fireEvent.click(element)// 测试点击后能否触发。fireEvent用于触发用户事件。在element上面调用点击事件click方法
expect(defaultProps.onClick).toHaveBeenCalled()// 期望结果:defaultProps上的onClick被调用
})
it('should render the correct component based on different props', () => {
const wrapper = render(<Button {...testProps}>Nice</Button>)
const element = wrapper.getByText('Nice')
expect(element).toBeInTheDocument()
expect(element).toHaveClass('btn-primary btn-lg klass')
})
it('should render a link when btnType equals link and href is provided', () => {
const wrapper = render(<Button btnType='link' href="http://dummyurl">Link</Button>)
const element = wrapper.getByText('Link')
expect(element).toBeInTheDocument()
expect(element.tagName).toEqual('A')
expect(element).toHaveClass('btn btn-link')
})
it('should render disabled button when disabled set to true', () => {
const wrapper = render(<Button {...disabledProps}>Nice</Button>)
const element = wrapper.getByText('Nice') as HTMLButtonElement
expect(element).toBeInTheDocument()
expect(element.disabled).toBeTruthy()
fireEvent.click(element)
expect(disabledProps.onClick).not.toHaveBeenCalled()
})
})