博客
关于我
强烈建议你试试无所不能的chatGPT,快点击我
React接入Typescript
阅读量:4085 次
发布时间:2019-05-25

本文共 13533 字,大约阅读时间需要 45 分钟。

系列引言

最近准备培训新人, 为了方便新人较快入手 React 开发并编写高质量的组件代码, 我根据自己的实践经验对React 组件设计的相关实践和规范整理了一些文档, 将部分章节分享了出来. 由于经验有限, 文章可能会有某些错误, 希望大家指出, 互相交流.

由于篇幅太长, 所以拆分为几篇文章. 主要有以下几个主题:

类型检查

静态类型检查对于当今的前端项目越来越不可或缺, 尤其是大型项目. 它可以在开发时就避免许多类型问题, 减少低级错误的; 另外通过类型智能提示, 可以提高编码的效率; 有利于书写自描述的代码(类型即文档); 方便代码重构(配合 IDE 可以自动重构). 对于静态类型检查的好处这里就不予赘述, 读者可以查看这个回答.

Javascript 的类型检查器主要有和, 笔者两者都用过, Typescript 更强大一些, 可以避免很多坑, 有更好的生态(例如第三方库类型声明), 而且 VSCode 内置支持. 而对于 Flow, 连 Facebook 自己的开源项目(如 Yarn, Jest)都抛弃了它, 所以不建议入坑. 所以本篇文章使用 Typescript(v3.3) 对 React 组件进行类型检查声明

建议通过官方文档来. 笔者此前也整理了 Typescript 相关的

当然 Flow 也有某些 Typescript 没有的特性:

React 组件类型检查依赖于@types/react@types/react-dom

目录

 


 

1. 函数组件

React Hooks 出现后, 函数组件有了更多出镜率. 由于函数组件只是普通函数, 它非常容易进行类型声明

 

1️⃣ 使用ComponentNameProps 形式命名 Props 类型, 并导出

 

2️⃣ 优先使用FC类型来声明函数组件

FCFunctionComponent的简写, 这个类型定义了默认的 props(如 children)以及一些静态属性(如 defaultProps)

import React, { FC } from 'react';/** * 声明Props类型 */export interface MyComponentProps {  className?: string;  style?: React.CSSProperties;}export const MyComponent: FC
= props => { return
hello react
;};复制代码

你也可以直接使用普通函数来进行组件声明, 下文会看到这种形式更加灵活:

export interface MyComponentProps {  className?: string;  style?: React.CSSProperties;  // 手动声明children  children?: React.ReactNode;}export function MyComponent(props: MyComponentProps) {  return 
hello react
;}复制代码

 

3️⃣ 不要直接使用export default导出组件.

这种方式导出的组件在React Inspector查看时会显示为Unknown

export default (props: {}) => {  return 
hello react
;};复制代码

如果非得这么做, 请使用命名 function 定义:

export default function Foo(props: {}) {  return 
xxx
;}复制代码

 

4️⃣ 默认 props 声明

实际上截止目前对于上面的使用FC类型声明的函数组件并:

import React, { FC } from 'react';export interface HelloProps {  name: string;}export const Hello: FC
= ({ name }) =>
Hello {name}!
;Hello.defaultProps = { name: 'TJ' };// ❌! missing name
;复制代码

笔者一般喜欢这样子声明默认 props:

export interface HelloProps {  name?: string; // 声明为可选属性}// 利用对象默认属性值语法export const Hello: FC
= ({ name = 'TJ' }) =>
Hello {name}!
;复制代码

如果非得使用 defaultProps, 可以这样子声明 👇. Typescript 可以推断和在函数上定义的属性, 这个特性在 Typescript 开始支持.

import React, { PropsWithChildren } from 'react';export interface HelloProps {  name: string;}// 直接使用函数参数声明// PropsWithChildren只是扩展了children, 完全可以自己声明// type PropsWithChildren

= P & {// children?: ReactNode;// }const Hello = ({ name }: PropsWithChildren

) =>

Hello {name}!
;Hello.defaultProps = { name: 'TJ' };// ✅ ok!
;复制代码

这种方式也非常简洁, 只不过 defaultProps 的类型和组件本身的 props 没有关联性, 这会使得 defaultProps 无法得到类型约束, 所以必要时进一步显式声明 defaultProps 的类型:

Hello.defaultProps = { name: 'TJ' } as Partial
;复制代码

 

5️⃣ 泛型函数组件

泛型在一下列表型或容器型的组件中比较常用, 直接使用FC无法满足需求:

import React from 'react';export interface ListProps
{ visible: boolean; list: T[]; renderItem: (item: T, index: number) => React.ReactNode;}export function List
(props: ListProps
) { return
;}// Testfunction Test() { return (
{ /*自动推断i为number类型*/ }} /> );}复制代码

 

6️⃣ 子组件声明

使用Parent.Child形式的 JSX 可以让节点父子关系更加直观, 它类似于一种命名空间的机制, 可以避免命名冲突. 相比ParentChild这种命名方式, Parent.Child更为优雅些. 当然也有可能让代码变得啰嗦.

import React, { PropsWithChildren } from 'react';export interface LayoutProps {}export interface LayoutHeaderProps {} // 采用ParentChildProps形式命名export interface LayoutFooterProps {}export function Layout(props: PropsWithChildren
) { return
{props.children}
;}// 作为父组件的属性Layout.Header = (props: PropsWithChildren
) => { return
{props.children}
;};Layout.Footer = (props: PropsWithChildren
) => { return
{props.children}
;};// Test
header
footer
;复制代码

 

7️⃣ Forwarding Refs

React.forwardRef 在 16.3 新增, 可以用于转发 ref, 适用于 HOC 和函数组件.

函数组件在 16.8.4 之前是不支持 ref 的, 配合 forwardRef 和 useImperativeHandle 可以让函数组件向外暴露方法

/***************************** * MyModal.tsx ****************************/import React, { useState, useImperativeHandle, FC, useRef, useCallback } from 'react';export interface MyModalProps {  title?: React.ReactNode;  onOk?: () => void;  onCancel?: () => void;}/** * 暴露的方法, 适用`{ComponentName}Methods`形式命名 */export interface MyModalMethods {  show(): void;}export const MyModal = React.forwardRef
((props, ref) => { const [visible, setVisible] = useState(); // 初始化ref暴露的方法 useImperativeHandle(ref, () => ({ show: () => setVisible(true), })); return
...
;});/******************* * Test.tsx *******************/const Test: FC<{}> = props => { // 引用 const modal = useRef
(null); const confirm = useCallback(() => { if (modal.current) { modal.current.show(); } }, []); const handleOk = useCallback(() => {}, []); return (
);};复制代码

 


 

2. 类组件

相比函数, 基于类的类型检查可能会更好理解(例如那些熟悉传统面向对象编程语言的开发者).

1️⃣ 继承 Component 或 PureComponent

import React from 'react';/** * 首先导出Props声明, 同样是{ComponentName}Props形式命名 */export interface CounterProps {  defaultCount: number; // 可选props, 不需要?修饰}/** * 组件状态, 不需要暴露 */interface State {  count: number;}/** * 类注释 * 继承React.Component, 并声明Props和State类型 */export class Counter extends React.Component
{ /** * 默认参数 */ public static defaultProps = { defaultCount: 0, }; /** * 初始化State */ public state = { count: this.props.defaultCount, }; /** * 声明周期方法 */ public componentDidMount() {} /** * 建议靠近componentDidMount, 资源消费和资源释放靠近在一起, 方便review */ public componentWillUnmount() {} public componentDidCatch() {} public componentDidUpdate(prevProps: CounterProps, prevState: State) {} /** * 渲染函数 */ public render() { return (
{this.state.count}
); } /** * ① 组件私有方法, 不暴露 * ② 使用类实例属性+箭头函数形式绑定this */ private increment = () => { this.setState(({ count }) => ({ count: count + 1 })); }; private decrement = () => { this.setState(({ count }) => ({ count: count - 1 })); };}复制代码

 

2️⃣ 使用static defaultProps定义默认 props

Typescript 开始支持对使用 defaultProps 对 JSX props 进行推断, 在 defaultProps 中定义的 props 可以不需要'?'可选操作符修饰. 代码如上 👆

 

3️⃣ 子组件声明

类组件可以使用静态属性形式声明子组件

export class Layout extends React.Component
{ public static Header = Header; public static Footer = Footer; public render() { return
{this.props.children}
; }}复制代码

 

4️⃣ 泛型

export class List
extends React.Component
> { public render() {}}复制代码

 


 

3. 高阶组件

在 React Hooks 出来之前, 高阶组件是 React 的一个重要逻辑复用方式. 相比较而言高阶组件比较重, 且难以理解, 容易造成嵌套地狱(wrapper). 另外对 Typescript 类型化也不友好(以前会使用来计算导出的 props). 所以新项目还是建议使用 React Hooks.

一个简单的高阶组件:

import React, { FC } from 'react';/** * 声明注入的Props */export interface ThemeProps {  primary: string;  secondary: string;}/** * 给指定组件注入'主题' */export function withTheme

(Component: React.ComponentType

) { /** * WithTheme 自己暴露的Props */ interface OwnProps {} /** * 高阶组件的props, 忽略ThemeProps, 外部不需要传递这些属性 */ type WithThemeProps = P & OwnProps; /** * 高阶组件 */ const WithTheme = (props: WithThemeProps) => { // 假设theme从context中获取 const fakeTheme: ThemeProps = { primary: 'red', secondary: 'blue', }; return

; }; WithTheme.displayName = `withTheme${Component.displayName}`; return WithTheme;}// Testconst Foo: FC<{ a: number } & ThemeProps> = props =>

;const FooWithTheme = withTheme(Foo);() => {
;};复制代码

再重构一下:

/** * 抽取出通用的高阶组件类型 */type HOC
=

( Component: React.ComponentType

,) => React.ComponentType

;/** * 声明注入的Props */export interface ThemeProps { primary: string; secondary: string;}export const withTheme: HOC

= Component => props => { // 假设theme从context中获取 const fakeTheme: ThemeProps = { primary: 'red', secondary: 'blue', }; return
;};复制代码

使用高阶组件还有一些痛点:

  • 无法完美地使用 ref(这已不算什么痛点)
    • 在 React.forwardRef 发布之前, 有一些库会使用 innerRef 或者 wrapperRef, 转发给封装的组件的 ref.
    • 无法推断 ref 引用组件的类型, 需要显式声明.
  • 高阶组件类型报错很难理解

 


 

4. Render Props

React 的 props(包括 children)并没有限定类型, 它可以是一个函数. 于是就有了 render props, 这是和高阶组件一样常见的模式:

import React from 'react';export interface ThemeConsumerProps {  children: (theme: Theme) => React.ReactNode;}export const ThemeConsumer = (props: ThemeConsumerProps) => {  const fakeTheme = { primary: 'red', secondary: 'blue' };  return props.children(fakeTheme);};// Test
{({ primary }) => { return
; }}
;复制代码

 


 

5. Context

Context 提供了一种跨组件间状态共享机制

import React, { FC, useContext } from 'react';export interface Theme {  primary: string;  secondary: string;}/** * 声明Context的类型, 以{Name}ContextValue命名 */export interface ThemeContextValue {  theme: Theme;  onThemeChange: (theme: Theme) => void;}/** * 创建Context, 并设置默认值, 以{Name}Context命名 */export const ThemeContext = React.createContext
({ theme: { primary: 'red', secondary: 'blue', }, onThemeChange: noop,});/** * Provider, 以{Name}Provider命名 */export const ThemeProvider: FC<{ theme: Theme; onThemeChange: (theme: Theme) => void }> = props => { return (
{props.children}
);};/** * 暴露hooks, 以use{Name}命名 */export function useTheme() { return useContext(ThemeContext);}复制代码

 


 

6. 杂项

1️⃣ 使用handleEvent命名事件处理器.

如果存在多个相同事件处理器, 则按照handle{Type}{Event}命名, 例如 handleNameChange.

export const EventDemo: FC<{}> = props => {  const handleClick = useCallback
(evt => { evt.preventDefault(); // ... }, []); return
;};复制代码

 

2️⃣ 内置事件处理器的类型

@types/react内置了以下事件处理器的类型 👇

type EventHandler
> = { bivarianceHack(event: E): void }['bivarianceHack'];type ReactEventHandler
= EventHandler
>;type ClipboardEventHandler
= EventHandler
>;type CompositionEventHandler
= EventHandler
>;type DragEventHandler
= EventHandler
>;type FocusEventHandler
= EventHandler
>;type FormEventHandler
= EventHandler
>;type ChangeEventHandler
= EventHandler
>;type KeyboardEventHandler
= EventHandler
>;type MouseEventHandler
= EventHandler
>;type TouchEventHandler
= EventHandler
>;type PointerEventHandler
= EventHandler
>;type UIEventHandler
= EventHandler
>;type WheelEventHandler
= EventHandler
>;type AnimationEventHandler
= EventHandler
>;type TransitionEventHandler
= EventHandler
>;复制代码

可以简洁地声明事件处理器类型:

import { ChangeEventHandler } from 'react';export const EventDemo: FC<{}> = props => {  /**   * 可以限定具体Target的类型   */  const handleChange = useCallback
>(evt => { console.log(evt.target.value); }, []); return
;};复制代码

 

3️⃣ 自定义组件暴露事件处理器类型

和原生 html 元素一样, 自定义组件应该暴露自己的事件处理器类型, 尤其是较为复杂的事件处理器, 这样可以避免开发者手动为每个事件处理器的参数声明类型

自定义事件处理器类型以{ComponentName}{Event}Handler命名. 为了和原生事件处理器类型区分, 不使用EventHandler形式的后缀

import React, { FC, useState } from 'react';export interface UploadValue {  url: string;  name: string;  size: number;}/** * 暴露事件处理器类型 */export type UploadChangeHandler = (value?: UploadValue, file?: File) => void;export interface UploadProps {  value?: UploadValue;  onChange?: UploadChangeHandler;}export const Upload: FC
= props => { return
...
;};复制代码

 

4️⃣ 获取原生元素 props 定义

有些场景我们希望原生元素扩展一下一些 props. 所有原生元素 props 都继承了React.HTMLAttributes, 某些特殊元素也会扩展了自己的属性, 例如InputHTMLAttributes. 具体可以参考方法的实现

import React, { FC } from 'react';export function fixClass<  T extends Element = HTMLDivElement,  Attribute extends React.HTMLAttributes
= React.HTMLAttributes
>(cls: string, type: keyof React.ReactHTML = 'div') { const FixedClassName: FC
= props => { return React.createElement(type, { ...props, className: `${cls} ${props.className}` }); }; return FixedClassName;}/** * Test */const Container = fixClass('card');const Header = fixClass('card__header', 'header');const Body = fixClass('card__body', 'main');const Footer = fixClass('card__body', 'footer');const Test = () => { return (
header
header
footer
);};复制代码

 

5️⃣ 不要使用 PropTypes

有了 Typescript 之后可以安全地约束 Props 和 State, 没有必要引入 React.PropTypes, 而且它的表达能力比较弱

 

6️⃣ styled-components

styled-components 是目前最流行的CSS-in-js库, Typescript 在 2.9 支持泛型. 这意味着可以简单地对 styled-components 创建的组件进行类型约束

// 依赖于@types/styled-componentsimport styled from 'styled-components/macro';const Title = styled.h1<{ active?: boolean }>`  color: ${props => (props.active ? 'red' : 'gray')};`;// 扩展已有组件const NewHeader = styled(Header)<{ customColor: string }>`  color: ${props => props.customColor};`;复制代码

了解更多

 

7️⃣ 为没有提供 Typescript 声明文件的第三方库自定义模块声明

笔者一般习惯在项目根目录下(和 tsconfig.json 同在一个目录下)放置一个global.d.ts. 放置项目的全局声明文件

// /global.d.ts// 自定义模块声明declare module 'awesome-react-component' {  // 依赖其他模块的声明文件  import * as React from 'react';  export const Foo: React.FC<{ a: number; b: string }>;}复制代码

了解更多

 

8️⃣ 为文档生成做好准备

目前社区有多种 react 组件文档生成方案, 例如, 还有. 它们底层都使用对 Typescript 进行解析. 就目前而言, 它还有些坑, 而且解析比较慢. 不管不妨碍我们使用它的风格对代码进行注释:

import * as React from 'react';import { Component } from 'react';/** * Props注释 */export interface ColumnProps extends React.HTMLAttributes
{ /** prop1 description */ prop1?: string; /** prop2 description */ prop2: number; /** * prop3 description */ prop3: () => void; /** prop4 description */ prop4: 'option1' | 'option2' | 'option3';}/** * 对组件进行注释 */export class Column extends Component
{ render() { return
Column
; }}复制代码

9️⃣ 开启 strict 模式

为了真正把 Typescript 用起来, 应该始终开启 strict 模式, 避免使用 any 类型声明.

 


 

扩展资料

作者:荒山
链接:https://juejin.im/post/5cd7f2c4e51d453a7d63b715
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。

你可能感兴趣的文章
一定记得每飞几次或者隔一天要把螺丝和浆帽拧一次,确实会松的
查看>>
《多旋翼无人飞行器嵌入式飞控开发指南》里基于FreeRTOS的无人机软件框架
查看>>
思岚A1的SDK其实很好读懂,每个函数清晰明了,可以直接调用
查看>>
pixhawk(PX4)的一些论坛网站(包括中文版的PX4用户手册和PX4开发手册)
查看>>
串级 PID 为什么外环输出是内环的期望?(和我之前对串级PID的总结一样)
查看>>
我刚刚才完全清楚GPS模块的那根杆子是怎么固定安装好的
查看>>
去github里面找找也没有别人无人机+SLAM的工程
查看>>
PX4与ROS关系以及仿真控制(键盘控制无人机)
查看>>
我对无人机重心高度的理解
查看>>
现在明白为什么无名博客里好几篇文章在讲传感器的滞后
查看>>
实际我看Pixhawk定高模式其实也是飞得很稳,飘得也不厉害
查看>>
Pixhawk解锁常见错误
查看>>
C++的模板化等等的确实比C用起来方便多了
查看>>
ROS是不是可以理解成一个虚拟机,就是操作系统之上的操作系统
查看>>
用STL algorithm轻松解决几道算法面试题
查看>>
ACfly之所以不怕炸机因为它觉得某个传感器数据不安全就立马不用了
查看>>
我发觉,不管是弄ROS OPENCV T265二次开发 SDK开发 caffe PX4 都是用的C++
查看>>
ROS的安装(包含文字和视频教程,我的ROS安装教程以这篇为准)
查看>>
国内有个码云,gitee
查看>>
原来我之前一直用的APM固件....现在很多东西明白了。
查看>>