最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • React+Typescript最佳实践

    正文概述 掘金(前端梦想家)   2021-02-21   552

    React+Typescript最佳实践

    前言

    随便叨叨一句,TypeScript是一个JavaScript的类型化超集,可以编译成纯JavaScript,比如随便新建一个index.ts文件,随便来个函数,然后cd到该文件,执行tsc index.ts 就可以编译成js文件了,当然前提是全局安装了typescript。 关于typescript的好处,网上一大把原因,这里不赘述,从我的开发经验看:

    • Typescript是一门静态类型语言,可以在开发的过程中暴露出很多不经意的问题,从而减少bug,测试也更少骂娘了;

    • 同时,因为在开发过程中涉及到很多类型的定义,因此Typescript项目具有很高的可维护性;

    • 当然最重要的一点,Typescript具有很强的类型推断的能力,只要需求开发前把类型,接口定义好,那么你就可以一路回车,特别爽。当然我这里声明的是开发的过程同时写好Typescript,因为我见过很多为了写Typescript而写Typescript的,他们是先将功能实现,然后再来补TS的。

    那下面我们就从实际项目中来看看怎么写好Typescript

    启动

    • 如果你的项目原来是JS+React的,打算迁移到Typescript,那么我强烈建议你先移步,看我的另外一篇文章AnDesignPro项目迁移TS并配合Eslint+Prettier代码格式化落地

    • 如果你是从零开始搭建项目,那么执行以下命令就可以生成一份React+Typescript的项目了。然后再配置eslint+prettier代码格式化工具,配置过程,也可以参考上面的文章。

    最佳实践

    组件定义

    组件定义可以分为函数组件和类组件。

    • 函数组件,是目前最流行的组件定义方式,有两种书写方式。
    import React from 'react';
    
    // 函数声明式写法
    function Heading({}:IProps): React.ReactNode {
      return <h1>My Website Heading</h1>;
    }
    
    // 函数扩展式写法
    const OtherHeading: React.FC<IProps> = () => <h1>My Website Heading</h1>;
    

    第一种写法:使用了函数声明式写法,注明了这个函数的返回值是 React.ReactNode 类型;

    第二种写法:使用了函数表达式写法,返回一个函数而不是值或者表达式,所以注明这个函数的返回值是 React.FC 类型。

    • 类组件
    interface StateProps { }
    
    interface IProps { }
    
    class Page extends React.Component<IProps, StateProps> {
        constructor(pops) {
            
        }
        
        componentDidMount(){
            
        }
    
        render() {
            return (
                <div>
                    
                </div>
            );
        }
    }
    

    Props

    TypeScript 中,我们可以使用 interface 或者 type 关键字来将 props 声明成泛型。

    • 首先介绍 函数组件 的写法
    import React from 'react';
    import { connect } from "dva";
    import { ThunkDispatch } from "redux-thunk";
    import { AnyAction } from "redux";
    // 项目的全局State类型
    import { State } from '@/types';
    
    // 从mapStateToProps中推导出从redux取出的类型,前提是state中已经声明了类型
    type MapStateToProps = Readonly<ReturnType<typeof mapStateToProps>>
    
    // 从mapDispatchToProps推导出异步方法的类型
    type MapDispatchToProps = Readonly<ReturnType<typeof mapDispatchToProps>>
    
    // 从默认值中推导出父组件传递给子组件的类型
    type DefaultProps = Readonly<typeof defaultProps>
    
    // 拼装成IProps
    type IProps =DefaultProps & MapStateToProps & MapDispatchToProps
    
    const defaultProps = {
        name: 'FrontDream',
        age: 18
    }
    
    const Content = ({
        // 解构你的props
        name,
        age
    }: IProps) => {
    
        return (
            <div>
    
            </div>
        )
    }
    const mapStateToProps = (state: State) => {
        return {
            items: state.Info.items,
        }
    }
    
    const mapDispatchToProps = (dispatch: ThunkDispatch<any, any, AnyAction>) => ({
        fetchData: () => {
            dispatch({
                type: "receiptInfo/fetch",
            })
        },
    })
    
    // 设置组件默认值
    Content.defaultProps = defaultProps
    
    export default connect(
        mapStateToProps,
        mapDispatchToProps
    )(Content)
    
    
    • 然后介绍 类组件IProps
    import React from 'react';
    import { connect } from "dva";
    import { ThunkDispatch } from "redux-thunk";
    import { AnyAction } from "redux";
    // 项目的全局State类型
    import { State } from '@/types';
    
    // 父组件传递到该组件的默认值
    const defaultProps = {
        name: 'FrontDream',
        age: 18
    }
    
    const initState = {
        content: ''
    }
    
    // 从mapStateToProps中推导出从redux取出的类型,前提是state中已经声明了类型
    type MapStateToProps = Readonly<ReturnType<typeof mapStateToProps>>
    
    // 从mapDispatchToProps推导出异步方法的类型
    type MapDispatchToProps = Readonly<ReturnType<typeof mapDispatchToProps>>
    
    // 从默认值中推导出父组件传递给子组件的类型
    type DefaultProps = Readonly<typeof defaultProps>
    
    // 拼装成IProps
    type IProps = DefaultProps & MapStateToProps & MapDispatchToProps
    
    // 从initState推导出组件的state类型
    type StateProps = Readonly<typeof initState>
    
    class Content extends React.Component<IProps, StateProps> {
        static readonly defaultProps: DefaultProps = defaultProps
        
        readonly state: StateProps = initState
    
        componentDidMount(){
    
        }
    
        render() {
            return (
                <div>
    
                </div>
            );
        }
    }
    
    const mapStateToProps = (state: State) => {
        return {
            items: state.Info.items,
        }
    }
    
    const mapDispatchToProps = (dispatch: ThunkDispatch<any, any, AnyAction>) => ({
        fetchData: () => {
            dispatch({
                type: "receiptInfo/fetch",
            })
        },
    })
    
    export default connect(
        mapStateToProps,
        mapDispatchToProps
    )(Content)
    
    

    其中 IProps 可以分成几个子集,根据实际使用场景,一般可以分为以下四类:

    • DefaultProps,直接从父组件传递给当前组件的属性;
    type DefaultProps = Readonly<typeof defaultProps> // 从默认值中推导出父组件传递给子组件的类型
    
    • MapStateToProps,通过 connectredux 中的 store 获取的属性;
    type MapStateToProps = Readonly<ReturnType<typeof mapStateToProps>>  // 从mapStateToProps中推导出从redux取出的类型,前提是state中已经声明了类型
    
    • MapDispatchToProps,通过 connectmapDispatchToProps获取到异步函数;
    type MapDispatchToProps = Readonly<ReturnType<typeof mapDispatchToProps>>  // 从mapDispatchToProps推导出异步方法的类型
    
    • RouteProps,通过 react-router-dom 中的 route 路由传递的属性

    多个 props 可以使用 & 符号连接起来,如上面的例子中。

    import React from 'react';
    import { connect } from "dva";
    import { ThunkDispatch } from "redux-thunk";
    import { AnyAction, compose } from "redux";
    import {withRouter, WithRouterProps} from 'react-router'
    // 项目的全局State类型
    import { State } from '@/types';
    
    // 父组件传递到该组件的默认值
    const defaultProps = {
        name: 'FrontDream',
        age: 18
    }
    
    const initState = {
        content: ''
    }
    
    // 从mapStateToProps中推导出从redux取出的类型,前提是state中已经声明了类型
    type MapStateToProps = Readonly<ReturnType<typeof mapStateToProps>>
    
    // 从mapDispatchToProps推导出异步方法的类型
    type MapDispatchToProps = Readonly<ReturnType<typeof mapDispatchToProps>>
    
    // 从默认值中推导出父组件传递给子组件的类型
    type DefaultProps = Readonly<typeof defaultProps>
    
    // 拼装成IProps
    type IProps = DefaultProps & MapStateToProps & MapDispatchToProps & WithRouterProps
    
    // 从initState推导出组件的state类型
    type StateProps = Readonly<typeof initState>
    
    class Content extends React.Component<IProps, StateProps> {
        static readonly defaultProps: DefaultProps = defaultProps
    
        readonly state: StateProps = initState
    
        componentDidMount(){
    
        }
    
        render() {
            return (
                <div>
    
                </div>
            );
        }
    }
    
    const mapStateToProps = (state: State) => {
        return {
            items: state.Info.items,
        }
    }
    
    const mapDispatchToProps = (dispatch: ThunkDispatch<any, any, AnyAction>) => ({
        fetchData: () => {
            dispatch({
                type: "receiptInfo/fetch",
            })
        },
    })
    
    export default compose<React.ComponentClass<DefaultProps>>(
        withRouter,
        connect(
            mapStateToProps,
            mapDispatchToProps
        )(Content)
    )
    

    仔细对比一下哦。

    可能很多人现在就很好奇了,这怎么跟我平时写的不一样啊,我平时写props都是像下面这么写的,:

    // 从别人的最佳实践抄来的例子,我都好奇,这样的也能叫最佳实践?
    type Props = {
      color?: string;
      name?: string
      children: React.ReactNode;
      onClick: ()  => void;
    }
    

    我敢说,这种写法叫傻瓜式Typescript,而不是聪明的Typescript!。 为啥这么说尼,让我一一道来,这样的问题所在。

    1. 这样写没有为组件设置defaultProps,也没有设置初始initstate。程序最重要的就是容错性,当发生错误时,系统能不崩溃,有足够的健壮性,这才是好的系统,给组件的propsstate设置默认值和初始值,提高了系统的健壮性和容错性。
    2. 按照上面的傻瓜式写法,当系统需要增加新功能,或者新需求,添加了新的props或者state,需要修改两处地方,一处是stateprops,一处是stateprops所对应的interface。也就是说,我就想改个功能,却需要修改两处地方。反观,上上面推导式的TS写法,通过ReturnTypetypeof结合推导出类型,则可以只写一处地方,而且,类型也更加精准!

    你觉得尼?动手去试试?

    props搞定了,平时的开发也就搞定一大半了。

    Hooks中的使用

    1. useState
    • 字符串类型

      使用 useState 时,TypeScript 可以自动推断类型,因此直接给个空字符串就行了

        const [name, setName] = useState('');
    
    • 对象

      需要初始化带有空值的 state,可以使用泛型来传递。

        type User = {
            name: string;
            age: number;
        };
        const [user, setUser] = useState<User | null>(null);
        // 或者
        const [user, setUser] = useState({} as User);
    
    • 数组
        type User = {
            name: string;
            age: number;
        };
        const [user, setUser] = useState<Array<User>>([]);
        // 或者
        const [user, setUser] = useState([] as User[]);
    

    2.useEffect

    没有返回值,无需传递类型。

    3.useLayoutEffect

    没有返回值,无需传递类型。

    4.useRef

    useRef 传递非空初始值的时候可以推断类型,传入第一个泛型参数来定义ref.current 的类型。如下示例:

    // 存储div dom
    const divRef = React.useRef<HTMLDivElement | null>(null);
    
    // 存储button dom
    const buttonRef = React.useRef<HTMLButtonElement | null>(null);
    
    // 存储br dom
    const brRef = React.useRef<HTMLBRElement | null>(null);
    
    // 存储a dom
    const linkRef = React.useRef<HTMLLinkElement | null>(null);
    
    

    其他标签类似

    1. useCallback

      无需传递类型,但是注意函数的入参需要定义类型,不然就推断为 any 了。

        const part = 2;
        
        const res = useCallback((value: number) => value * part, [part]);
    

    6.useMemo

    useMemo 无需传递类型,根据函数的返回值就能推断出类型。

    const res = React.useMemo(() => {
      complexComputed(a, b)
    }, [a, b])
    

    7.useContext

    首先在createContext的时候通过泛型定义需要向下传递的类型,以阿里的antdesignform为例:

    import React from 'react';
    import { WrappedFormUtils } from 'antd/es/form/Form';
    
    export interface Cubicle extends Partial<any> { }
    
    export interface IFormContext {
        form?: WrappedFormUtils;
        type?: string;
        changeCubicles?(data: Cubicle[]): void;
    }
    
    const FormContext = React.createContext<IFormContext>({});
    

    在使用的时候就可以直接:

        const { form, type } = useContext(FormContext);
    

    8.userReducer

    useReducer 接受两个参数:reducerinitialState,只需要对传入 useReducerreducer 函数的入参 stateaction 进行类型约束,就能够推断出类型。在使用 useReducer 或者 redux 的时候,需要配合使用联合类型。

    以如下例子为例 参考

    import React, { useReducer } from 'react';
    
    const initialState = { count: 0 };
    
    /** 联合类型,使用 | 连接 */
    type ACTIONTYPE =
    | { type: 'increment'; payload: number }
    | { type: 'decrement'; payload: string }
    | { type: 'reset'; payload: number };
    
    function reducer(state: typeof initialState, action: ACTIONTYPE) {
        switch (action.type) {
            case 'increment':
                return { count: state.count + action.payload };
            case 'decrement':
                return { count: state.count - Number(action.payload) };
            case 'reset':
                return { count: action.payload };
            default:
                throw new Error();
        }
    }
    
    const Counter: React.FC = () => {
        const [state, dispatch] = useReducer(reducer, initialState);
        return (
            <>
                Count: {state.count}
                <button onClick={() => dispatch({ type: 'decrement', payload: '5' })}>-</button>
                <button onClick={() => dispatch({ type: 'increment', payload: 5 })}>+</button>
                <button onClick={() => dispatch({ type: 'reset', payload: 0 })}>reset</button>
            </>
        );
    };
    
    export default Counter;
    

    事件处理

    1. 表单事件onChange事件
    import React from 'react';
    
    const Input: React.FC = () => {
        const [value, setValue] = React.useState('');
        
        const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
            setValue(e.target.value);
        };
    
        return <input value={value} onChange={onChange} />;
    };
    
    export default Input;
    
    

    表单通常用于收集内部状态的信息。 它们主要用于登录和注册页面,因此提交的信息可以通过表单收集并发送到服务器进行处理。

    React.FormEvent 通常用于元素的事件类型:

    class App extends React.Component<> {
        clickHandler = (e: React.FormEvent<HTMLButtonElement>) => {
            // ...
        }
    
        changeHandler = (e: React.FormEvent<HTMLInputElement>) => {
            // ...
        }
    
        render() {
            return (
                <div>
                    <button onClick={this.clickHandler}>Click</button>
                    <input type="text" onChange={this.changeHandler} />
                </div>
            )
        }
    }
    
    

    clickHandlerchangeHandler 的事件参数 e都有React.FormEvent 类型。

    我们可以省略使用React.FormEvent输入处理程序的参数,而改为输入处理程序的返回值。 这是通过使用:React.ChangeEventHandler

    class App extends React.Component<> {
        clickHandler: React.FormEvent<HTMLButtonElement> = (e) => {
            // ...
        }
    
        changeHandler: React.FormEvent<HTMLInputElement> = (e) => {
            // ...
        }
    
        render() {
            return (
                <div>
                    <button onClick={this.clickHandler}>Click</button>
                    <input type="text" onChange={this.changeHandler} />
                </div>
            )
        }
    }
    
    1. abutton的点击事件
    import React from 'react';
    
    const Button: React.FC = () => {
      const handleClick = (event: React.MouseEvent<HTMLAnchorElement | HTMLButtonElement>) => {
        
      };
    
      return (
        <div>
          <button onClick={handleClick}>click</button>
          <a href="" onClick={handleClick}>
            link
          </a>
        </div>
      );
    };
    
    export default Button;
    
    

    3.TextArea

    这是指文本区域元素的React.ChangeEvent,并且文本区域元素是HTMLTextAreaElement的实例。

    泛型T 就是 HTMLTextAreaElement。事件即React.ChangeEvent.

    4.Select

    selecttextarea,但是元素类型为HTMLSelectElement,事件为React.ChangeEvent.

    1. Form

    同理,类型为HTMLFormElement,事件为React.ChangeEvent.

    1. Video, Audio

    同理,类型为HTMLVideoElementHTMLAudioElement,事件为React.ChangeEvent.

    7.React.SyntheticEvent

    这个类型是当你不关心类型时:

    class App extends React.Component<> {
        submitHandler = (e: React.SyntheticEvent) => {
            // ...
        }
    
        render() {
            return (
                <form onSubmit={this.submitHandler}>
                    ...
                </form>
            )
        }
    }
    
    1. 其他事件
    ClipboardEvent<T = Element> 剪贴板事件对象
    DragEvent<T = Element> 拖拽事件对象
    ChangeEvent<T = Element>  Change 事件对象
    KeyboardEvent<T = Element> 键盘事件对象
    MouseEvent<T = Element> 鼠标事件对象
    TouchEvent<T = Element>  触摸事件对象
    WheelEvent<T = Element> 滚轮事件对象
    AnimationEvent<T = Element> 动画事件对象
    TransitionEvent<T = Element> 过渡事件对象
    

    Axios请求

    Axios请求主要是通过定义一个全局的响应类型,并留出泛型,让用户自定义各个接口自己的类型,看看下面的例子,你就一目了然了:

    // 全局的后台返回的类型
    interface ResponseData<T = any> {
      code: number
      result: T
      message: string
    }
    
    // 各个接口自己的返回类型
    interface User {
      name: string
      age: number
    }
    
    function getUser<T>() {
      return axios<ResponseData<T>>('/extend/user')
        .then(res => res.data)
        .catch(err => console.error(err))
    }
    
    
    async function test() {
      const user = await getUser<User>()
      if (user) {
        console.log(user.result.name)
      }
    }
    
    test()
    

    当然,如果你的异步请求工具,不是Axios那么也可以照猫画虎,也是定义全局的响应类型和业务接口的响应类型,并通过泛型传入。

    ❤️ 爱心三连击

    1.看到这里了就点个在看支持下吧,你的「在看」是我创作的动力。

    2.关注公众号前端梦想家,「一起学前端」!

    3.添加微信【qdw1370336125】,拉你进技术交流群一起学习。


    起源地下载网 » React+Typescript最佳实践

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元