如果有任何错误,请务必指出。
参考链接:https://github.com/typescript…
(一)构建基础的组件文件
- 首先我们需要导入
react
:import * as React from 'react'
为每个文件创建一个类组件。在一个文件内请尽量只写一个类组件,其他的请使用函数组件
// ReactTs.tsx class TsExample extends React.Component<any, any> {}
使用
extends
来继承React.component
接口- 我们注意到
React.Component
后面带了<any, any>
这个奇怪的字符串。这是React.Component
接口的类型参数。第一个是props
的类型,第二个是state
的类型。 为了给
props
和state
定义类型,我们可以使用interface
关键字。我们可以将类型定义写在当前的组件文件,但是由于在当前文件中具有了import
关键字,当前文件会成为一个模块,即无法被全局引用。所以建议新建一个文件index.d.ts
来储存interface\type
等// index.d.ts interface ITsExampleProps { name: string }
注:
export
和import
会将一个文件变成模块,即里面的变量不会被暴露到全局接下来我们为这个组件初始化
state
// ReactTs.tsx class TsExample extends React.Component<ITsExampleProps, ITsExampleState> { public state = { count: 0 } }
此处建议不要
public state = {}
和constructor
两种方法混用为组件添加 UI:每个类组件都应该有一个
render
方法来进行 UI 渲染。render
方法中必须返回一个ReactNode
,ReactNode
可以是一个HTMLElement
或者一个字符串,数字// ReactTs.tsx class TsExample extends React.Component<ITsExampleProps, ITsExampleState> { public state = { count: 0 } public render() { let { count } = this.state return <div styleName="wrap">这里是 ui</div> } }
为组件添加并调用方法,使用
state
// ReactTs.tsx class TsExample extends React.Component<IExampleProps, IExampleState> { public state = { count: 0, currentCount: 0 } public plusCount = () => { this.setState({ count: ++this.state.count }) } public render() { let { count } = this.state return ( <div styleName=""> {/* 调用state */} <div>{count}</div> {/* 调用方法 */} <Button onClick={this.plusCount}>增加</Button> <div> <Button onClick={() => { this.showCurrentCount(this.state.count) }} > 当前数量 </Button> </div> </div> ) } }
8) 接下来我们将该组件作为一个子组件放到另一个组件中
// example.tsx
class Example extends React.Component<IExampleProps, IExampleState> {
public render() {
return (
<div styleName="example">
<ReactTs />
</div>
)
}
}
为子组件传入一个参数
// example.tsx class Example extends React.Component<IExampleProps, IExampleState> { public render() { return ( <div styleName='example'> <ReactTs name="React-Ts" /> </div> ) } }
10) 在子组件中使用参数:this.props.name
// ReactTs.tsx
public render() {
let {count} = this.state
return (
<div styleName='React-Ts'>
<div>名字:{this.props.name}</div>
<div>增加数量:{count}</div>
<div>当前数量:{this.state.currentCount}</div>
<div><Button onClick={this.plusCount}>增加</Button></div>
<Button
onClick={() => { this.showCurrentCount(this.state.count) }}>
当前数量
</Button>
</div>
)
}
(二)使用函数式组件
- 一般,在一个文件内,我们应该只写一个类组件,其他的子组件应该是函数式组件。
函数式组件类型可以使用
React.FC
,使用这个类型有个好处就是,提醒你必须返回一个ReactNode
const FuncExample: React.FC<IFunExampleProps> = (props: IFunExampleProps) => { return <div></div> }
3) 在函数式组件中,我们无法像在类组件中一样使用state
和生命钩子函数,但React
提供了HOOK
4) 使用 useState
来存储函数组件的状态。在下面的例子中,我们使用了React.useState
,传入color
的初始值,返回了一个对象,我们使用解构,获得了color, setColor
。其中 color
相当于 this.state.color
,setColor('green')
相当于 this.setState({color: 'green'})
。
const FuncExample: React.FC<IFunExampleProps> = (props: IFunExampleProps) => {
const [color, setColor] = React.useState('blue')
const changeColor = () => {
setColor('green')
}
return (
<div>
<div style={{ color }}>函数式组件</div>
<Button type="primary" className="example-button" onClick={changeColor}>
点击换色{' '}
</Button>
</div>
)
}
假如你想像在类组件中使用
componentWillUnmount
,componentDidMount
,componentDidUpdate
,你可以使用useEffect
。useEffect
接受一个回调函数,这个回调函数内代码会在componentDidMount
,componentDidUpdate
时执行。回调函数的返回值应该是一个函数,这个函数会在componentWillUnmount
被执行。const FuncExample: React.FC<IFunExampleProps> = (props: IFunExampleProps) => { const [color, setColor] = React.useState('blue') const [fontSize, setFontsize] = React.useState('16px') const changeColor = () => { setColor('green') } React.useEffect(() => { let timer = setInterval(() => { setFontsize('100px') }, 10000) return () => { clearInterval(timer) } return ( <div> <div style={{color,fontSize}}>函数式组件</div> <Button type='primary' className='example-button' onClick={changeColor}>点击换色 </Button> </div> ) }
假如我们需要操作
DOM
,我们可以通过useRef
来获取。text.current
就是被我们绑定的元素。注意:不允许直接操作 DOM,除非迫不得已const FuncExample: React.FC<IFunExampleProps> = (props: IFunExampleProps) => { const [color, setColor] = React.useState('blue') const [fontSize, setFontsize] = React.useState('16px') const text = React.useRef<HTMLDivElement>(null) const changeColor = () => { setColor('green') } const changeBgC = () => { (text.current as HTMLDivElement).style.backgroundColor = '#e9e9e9' } React.useEffect(() => { let timer = setInterval(() => { setFontsize('100px') }, 10000) return () => { clearInterval(timer) } return ( <div> <div style={{color,fontSize}}>函数式组件</div> <Button type='primary' className='example-button' onClick={changeColor}>点击换色 </Button> <Button type='primary' className='example-button' onClick={changeBgC}>点击换背景色</Button> </div> ) }
(三) 其他
- 有时候我们需要使用到默认参数
defaultProps
,这个默认参数是从多个组件的props
中提取出来的。这时可以使用交叉类型 &
,注意,请不要在交叉类型中使用相同的属性,就算使用了,也请不要为两个同名属性定义不同的基础类型,这样将会造成这个属性需要同时满足两种基础类型。
type propsType = typeof defaultProps & {
count: number
}
const defaultProps = {
name: 'world'
}
const DefaultPrppsExample = (props: propsType) => {
return (
<div>
{props.name}
{props.count}
</div>
)
}
// 在另一个组件中使用
;<DefaultPrppsExample count={1} name={'默认参数示例'} />
createRef
和forwardRef
在函数式组件中,我们可以使用
useRef
来获取到React
元素。在类组件中,我们可以使用createRef
来获取React
元素。当我们需要获取某个组件中的元素时,使用forwardRef
来获取这个组件内部的元素。还是不建议直接操纵DOM
元素const RefExample = React.forwardRef((props: React.CSSProperties, ref: React.Ref<HTMLInputElement>) => { return <input style={{ ...props}} ref={ref} /> }) class TsExample extends React.Component<ITsExampleProps, ITsExampleState> { ... public state = { inputRef: React.createRef() } public getFocus = () => { if (this.state.inputRef) { // 注意:这里断言的类型应该是使用 RefObject,因为其里面的具有 current: T | null, // 如果使用的是 ReactRef ,这个类型是一个联合类型,里面有可能是 null ((this.state.inputRef as React.RefObject<HTMLInputElement>).current as HTMLInputElement).focus() } } ... <div styleName="example-item"> <h3>使用 createRef 和 forwardRef</h3> <RefExample color='red' ref={this.state.inputRef} /> <Button onClick={this.getFocus}>获取焦点</Button> </div> ... }
3) React
默认会把我们的子组件挂载到父组件上。当我们想自己指定挂载的元素时,就需要用到 createPortal
了。
这里写了一个简陋的蒙层组件(样式未完全实现),即使我们是TsExample
中使用了这个组件,但这个组件还是会被挂载到一个插入 body
的元素上
注意:这里我们在父组件上绑定了一个点击事件,PortalExample
被点击时依然会冒泡进而触发父组件的点击事件
const PortalExample = (props: IPortarExampleProps) => {
const [modelDOM, setModelDOM] = React.useState(document.createElement('div'))
modelDOM.classList.add('modal')
if (props.visible) {
document.body.append(modelDOM)
return ReactDOM.createPortal(
<div styleName='modal-inner'>
蒙层
</div>,
modelDOM)
}else {
if (modelDOM.parentNode) {
modelDOM.parentNode.removeChild(modelDOM)
}
return null
}
}
class TsExample extends React.Component<ITsExampleProps, ITsExampleState> {
...
<div styleName="example-item" onClick={this.handleModalClick}>
<h1>使用 createPortal 将子组件挂载到父组件以外的元素内</h1>
<PortalExample visible={this.state.modalVisible} />
<Button onClick={this.toggleModal}>切换modal</Button>
</div>
...
}
(四) TS
类型基本使用
我们在使用类型注解的时候往往会使用以下类型
- 基本类型包括了
js
的七种基本类型string, number, boolean, undefined, null, symbol
,还有void, never
- 可以使用
type, interface
来声明变量的形状
1. 联合类型
使用 |
来创造一个联合类型。例如:type pet = 'cat' | 'dog'
当我们需要规定一个函数参数的类型时,又恰巧有一个已经有一个接口已经满足了,我们可以使用 keyof
来快速生成一个联合类型
2. 类型守护
当我们在使用联合类型时,假如两个子类型 A 和 B 都是接口或类等时,我们预想的是要么是 A,要么是 B。但实际上,A&B
也是符合 A|B
的。
这个时候我们需要先用 in
关键字来判断是符合哪个类型再进行操作
interface IMan {
handsNum: number
}
interface IDog {
tailNum: number
}
public typeGuarding = (obj: IMan | IDog) => {
if ('handsNum' in obj) {
...
} else if ('tailNum' in obj) {
...
}
}
3. 交叉类型
使用 &
来创造一个联合类型,之前已经有示例了
4. 可选类型
可选类型其实不是一个类型,但在接口和函数参数中都有可选这个概念。
interface IPet {
name: string
age?: number
}
interface IMan {
...
}
const feedPet (pet: IPet, man?: IMan) => {
...
}
注意:函数的可选参数必须写在最后面
5. 默认参数
在函数声明中,直接为参数赋予一个值,可以起到默认参数的作用
interface IPetDefault {
name: string
age = 1
}
const feedPet (pet: IPet, man={name: '我'}) => {
...
}
6.enum
当我们需要使用枚举类型时,就需要用到enum
关键字。默认,第一个的值为 0,然后递增,可以手动指定每一个的值,之后的值也会递增。我们可以通过值查key
,也可以通过key
查值
enum animal {
乌龟,
鳄鱼,
麻雀,
河马
}
animal['乌龟'] // 0
animal[0] // 乌龟
7. 类型断言
当我们给一个函数传入参数时,我们知道这个参数时符合被规定的参数类型的,但编译器是不知道的。为了让编译器能知道类型符合,我们就需要使用类型断言,关键字:as
type animalName = '乌龟' | '鳄鱼' | '麻雀' | '河马'
animal[this.state.animalName as animalName] // 0
类型断言里还有另一个类型:在值的后面使用 !
,表示这个值不为 null
type animalNameWithNull = '乌龟' | '鳄鱼' | '麻雀' | '河马' | null
animal[(this.state.animalName as animalName)!] // 0
8. 使用类型扩展(type branding)
注:类型扩展是直译的结果,如果你知道这种用法的名字,请修正它
TS
在检查一个类型是否符合是,并不是查找类型的名字,而是比较类型的结构。也就是说,当你为两种类型userId
和 orderId
都定义了一样的结构:{name: string}
。这时候,TS
会判定它们是同一种类型。请看下面的例子:
interface IOrderId {
name: string
}
interface IUserId {
name: string
}
let userId: IUserId = {
name: 'userId'
}
let orderId: IOrderId = userId // ok
这里你是否会疑惑呢?两种不同的类型却可以相互赋值。这就是 结构类型检查 了。假如名字不同,类型就不同的就是 名称类型检查
那我们如何来避免呢?第一种方法是多加一个字段
interface MyUserId {
name: string
type: 'user'
}
interface MyOrderId {
name: string
type: 'order'
}
let myUserId: MyUserId = {
name: 'user',
type: 'user'
}
let myOrderId: MyOrderId = myUserId // error
还有另一种方法,那就是利用 unique symbol
和 交叉类型
type UserIdString = string & { readonly brand: unique symbol }
type OrderIdString = string & { readonly brand: unique symbol }
const getUniqueUserId = (id: string) => {
return id as UserIdString
}
const getUniqueOrderId = (id: string) => {
return id as OrderIdString
}
let uniqueUserId: UserIdString = getUniqueUserId('1')
let uniqueOrderId: OrderIdString = uniqueUserId // error
unique symbol
是 symbol
的子类型。我们在上面用交叉类型将 {readonly brand: unique symbol}
也加入到类型中去,这样,两种类型就算结构一样,它们也是不相同的
9. 函数重载
函数重载用于我们的函数拥有不同的输入或输出时
第一个函数声明应该是最精确的,因为编译器是从前往后开始匹配的。函数实现应该是兼容各个函数声明的。
这里函数重载的意义在于,如果们直接使用第三种声明,那么传入 3 个参数也是可以接受的,但是据我们所知, padding 是不接受 3 个参数的
// 重载
function padding(all: number)
function padding(topAndBottom: number, leftAndRight: number)
function padding(top: number, right: number, bottom: number, left: number)
// Actual implementation that is a true representation of all the cases the function body needs to handle
function padding(a: number, b?: number, c?: number, d?: number) {
if (b === undefined && c === undefined && d === undefined) {
b = c = d = a
} else if (c === undefined && d === undefined) {
c = a
d = b
}
return {
top: a,
right: b,
bottom: c,
left: d
}
}
10. 使用 typeof
关键字快速定义一个类型
有时候,我们在前面定义了一个变量,并使用类型推导(也就是说,这时候我们并没有明确地给它声明类型)。然后我们想要使用这个变量的类型了,怎么办?回去定义一下类型吗?使用 typeof 变量名
就可以获取到这个变量的类型了
11. 工具泛型的使用
TS
中内置了一些语法糖给我们使用,例如 Partial、Omit、Exclude
等
这里举例 partial
的用法:Patial
关键字可以让将一个接口内部的属性变成可选
interface dog {
name: string
bark: boolean
}
type littleDog = Partial<dog>
let dog1: littleDog = {
name: 'dog1'
}
let dog2: littleDog = {
bark: false
}
12. 模块声明
当我们需要引入某些模块时,这些模块又没有被声明过,这个时候就会报错了。我们只需要使用
decleare module 模块名
就可解决
decleare module "*.png"
import * as logo from 'logo.png'