美文网首页
Typescript在react项目中的实践

Typescript在react项目中的实践

作者: tracyXia | 来源:发表于2020-04-09 14:59 被阅读0次

    一、理解 Typescript 配置文件

    熟悉 Typescript 配置文件是 TS 项目开发的最基本要求。TS 使用 tsconfig.json 作为其配置文件,它主要包含两块内容:

    1.指定待编译的文件
    2.定义编译选项

    我们都知到TS项目的编译命令为tsc,该命令就是使用项目根路径下的tsconfig.json文件,对项目进行编译。

    简单的配置示例如下:

    {
      "compilerOptions": {
        "module": "commonjs",
        "noImplicitAny": true,
        "removeComments": true,
        "preserveConstEnums": true,
        "sourceMap": true
      },
      "files": [
        "app.ts",
        "foo.ts",
      ]
    }
    

    其中,compilerOptions 用来配置编译选项,files 用来指定待编译文件。这里的待编译文件是指入口文件,任何被入口文件依赖的文件都将包括在内。

    也可以使用 include 和 exclude 来指定和排除待编译文件:

    {
      "compilerOptions": {
        "module": "commonjs",
        "noImplicitAny": true,
        "removeComments": true,
        "preserveConstEnums": true,
        "sourceMap": true
      },
      "include": [
        "src/**/*"
      ],
      "exclude": [
        "node_modules",
        "**/*.spec.ts"
      ]
    }
    /*************************************
                exclude中的通配符
    * :匹配 0 或多个字符(注意:不含路径分隔符)
    ? :匹配任意单个字符(注意:不含路径分隔符)
    **/ :递归匹配任何子路径
    **************************************/
    

    即指定待编译文件有两种方式:

    • 使用 files 属性
    • 使用 include 和 exclude 属性

    这里进行编译的文件都是TS文件(拓展名为 .ts、.tsx 或 .d.ts 的文件)

    • 如果 files 和 include 都未设置,那么除了 exclude 排除的文件,编译器会默认包含路径下的所有 TS 文件。
    • 如果同时设置 files 和 include ,那么编译器会把两者指定的文件都引入。
    • exclude 只对 include 有效,对 files 无效。即 files 指定的文件如果同时被 exclude 排除,那么该文件仍然会被编译器引入。

    常用的编译配置如下:

    配置项字段名 默认值 说明
    target es3 生成目标语言的版本
    allowJs false 允许编译 JS 文件
    noImplicitAny false 存在隐式 any 时抛错
    jsx Preserve 在 .tsx 中支持 JSX :React 或 Preserve
    noUnusedLocals false 检查只声明、未使用的局部变量(只提示不报错)
    noImplicitThis false this 可能为 any 时抛错
    noImplicitReturns false 不存在 return 时抛错
    types 默认的,所有位于 node_modules/@types 路径下的模块都会引入到编译器 如果指定了types,只有被列出来的包才会被包含进来。

    对于types 选项,有一个普遍的误解,以为这个选项适用于所有的类型声明文件,包括用户自定义的声明文件,其实不然。这个选项只对通过 npm 安装的声明模块有效,用户自定义的类型声明文件与它没有任何关系。默认的,所有位于 node_modules/@types 路径下的模块都会引入到编译器。如果不希望自动引入node_modules/@types路径下的所有声明模块,那可以使用 types 指定自动引入哪些模块。比如:

    {
      "compilerOptions": {
        "types" : ["node", "lodash", "express"]
      }
    }
    //此时只会引入 node 、 lodash 和 express 三个声明模块,其它的声明模块则不会被自动引入。
    

    配置复用

    //建立一个基础的配置文件 configs/base.json 
    {
      "compilerOptions": {
        "noImplicitAny": true,
        "strictNullChecks": true
      }
    }
    
    //tsconfig.json 就可以引用这个文件的配置了:
    {
      "extends": "./configs/base",
      "files": [
        "main.ts",
        "supplemental.ts"
      ]
    }
    

    二、Typescript在React中的应用

    1. 无状态组件

    无状态组件也被称为展示型组件。在部分时候,它们也是纯函数组件

    在@types/react中已经预定义一个类型type SFC,它也是类型interface StatelessComponent的一个别名,此外,它已经有预定义的children和其他(defaultProps、displayName等等…),所以在写无状态组件时我们可以直接使用SFC

    import React, { MouseEvent, SFC } from 'react';
    
    type Props = { 
      onClick(e: MouseEvent<HTMLElement>): void 
    };
    
    const Button: SFC<Props> = ({ 
      onClick: handleClick, 
      children 
    }) => (
      <button onClick={handleClick}>{children}</button>
    );
    

    2. 有状态组件

    我们知道我们在React中不能像下面这样直接更新state:

    this.state.clicksCount = 2;
    

    我们应当通过setState来维护状态机,但上述写法,在ts编译时并不会报错。此时我们可以作如下限制:

    const initialState = { clicksCount: 0 }
    
    /*使用TypeScript来从我们的实现中推断出State的类型。
    好处是:这样我们不需要分开维护我们的类型定义和实现*/
    type State = Readonly<typeof initialState>
    
    class ButtonCounter extends Component<object, State> {
      /*至此我们定义了类上的state属性,及state其中的各属性均为只读*/
      readonly state: State = initialState;
    
      doBadthing(){
        this.state.clicksCount = 2; //设置后,该写法编译报错
        this.state = { clicksCount: 2 } //设置后,该写法编译报错
      }
    }
    

    3.处理组件的默认属性

    如果使用的typescript是3.x的版本的话,就不用担心这个问题,就直接在jsx中使用defaultProps就可以了。如果使用的是2.x的版本就要关注下述问题了

    如果我们想定义默认属性,我们可以在我们的组件中通过以下代码定义

    type Props = {
      onClick(e: MouseEvent<HTMLElement>): void;
      color?: string;
    };
    
    const Button: SFC<Props> = (
    { 
      onClick: handleClick, 
      color, 
      children 
    }) => (
      <button style={{ color }} onClick={handleClick}>
        {children}
      </button>
    );
    Button.defaultProps = {…}
    

    在strict mode模式下,会有这有一个问题,可选的属性color的类型是一个联合类型undefined | string。因此,在对color属性做一些操作时,TS会报错。因为它并不知道它在React创建中通过Component.defaultProps中已经定义了默认属性

    在这里我采取的方案是,构建可复用的高阶函数withDefaultProps,统一由他来更新props类型定义和设置默认属性。

    export const withDefaultProps = 
    < P extends object, DP extends Partial<P> = Partial<P> >
    (
      defaultProps: DP,
      Cmp: ComponentType<P>,
    ) => {
      // 提取出必须的属性
      type RequiredProps = Omit<P, keyof DP>;
      // 重新创建我们的属性定义,通过一个相交类型,将所有的原始属性标记成可选的,必选的属性标记成可选的
      type Props = Partial<DP> & Required<RequiredProps>;
    
      Cmp.defaultProps = defaultProps;
    
      // 返回重新的定义的属性类型组件,通过将原始组件的类型检查关闭,然后再设置正确的属性类型
      return (Cmp as ComponentType<any>) as ComponentType<Props>;
    };
    
    

    此时,可以使用withDefaultProps高阶函数来定义我们的默认属性

    const defaultProps = {
      color: 'red',
    };
    
    type DefaultProps = typeof defaultProps;
    type Props = { onClick(e: MouseEvent<HTMLElement>): void } & DefaultProps;
    
    const Button: SFC<Props> = ({ onClick: handleClick, color, children }) => (
      <button style={{ color }} onClick={handleClick}>
        {children}
      </button>
    );
    
    const ButtonWithDefaultProps = withDefaultProps(defaultProps, Button);
    

    组件使用如下

    render() {
        return (
            <ButtonWithDefaultProps
                onClick={this.handleIncrement}
            >
                Increment
            </ButtonWithDefaultProps>
        )
    }
    

    4. 范型组件

    范型是指在定义函数、接口或类的时候,不预先指定具体的类型,而在使用的时候再指定类型的一种特性。

    interface Props<T> {
      content: T;
    }
    

    上述代码表明 Props 接口定义了这么一种类型:

    • 它是包含一个 content 字段的对象
    • 该 content 字段的类型由使用时的泛型 T 决定

    泛型函数:

    function Foo<T>(props: Props<T>) {
      console.log(props);
    }
    
    /** 此时 Foo 的完整签名为: function Foo<number>(props: Props<number>): void */
    Foo({ content: 42 });
    
    /** 此时 Foo 的完整签名为: function Foo<string>(props: Props<string>): void */
    Foo({ content: "hello" });
    

    泛型组件:
    将上面的 Foo 函数返回 JSX 元素,就成了一个 React 组件。因为它是泛型函数,它所形成的组件也就成了 泛型组件。当然你很可能会思考泛型组件的用途。

    思考下面的实践:

    import React, { Fragment, PureComponent } from 'react';
    
    interface IYarn {
      ...
    }
    
    export interface IProps {
      total: number;
      list: IYarn[];
      title: string;
    }
    
    class YarnList  extends PureComponent<IProps> {
    
    }
    

    上述组件就是用于展示一个列表,其实列表中的分页加载、滚动刷新逻辑等对于所有列表而言都是通用的,当我们想重复利用该容器组件时,你很可能会发现,不同的业务中列表中的属性字段并不通用。

    此时,你为了尽可能满足大部分数据类型,你很可能将列表的元素类型做如下定义:

    interface IYarn {
      [prop: string]: any;
    }
    
    interface IProps {
      total: number;
      list: IYarn[];
      title: string;
    }
    
    const YarnList: SFC<IProps> = ({ 
      list,total,title
    }) => (
       <div>
            {list.map(
                 ....
            )}
       </div>
    );
    

    在这里已经可以看到类型的丢失了,因为出现了 any,而我们使用 TypeScript 的首要准则是尽量避免 any

    对于复杂类型,类型的丢失就完全享受不到 TypeScript 所带来的类型便利了。

    此时,我们就可以使用泛型,把类型传递进来。实现如下:

    interface IProps<T> {
      total: number;
      list: T[];
      title: string;
    }
    const YarnList: SFC<IProps> = ({ 
      list,total,title
    }) => (
       <div>
            <div>title</div>
            <div>total</div>
            {list.map(
                 ....
            )}
       </div>
    );
    

    改造后,列表元素的类型完全由使用的地方决定,作为列表组件,内部它无须关心,同时对于外部传递的入参,类型也没有丢失。

    具体业务调用示例如下:

    interface User {
      id: number;
      name: string;
    }
    const data: User[] = [
      {
        id: 1,
        name: "xsq"
      },
      {
        id: 2,
        name: "tracy"
      }
    ];
    
    const App = () => {
      return (
        <div className="App">
          <YarnList list={data} title="xsq_test" total=2/>
        </div>
      );
    };
    

    5.在数据请求中的应用

    假设我们对接口的约定如下:

    {
      code: 200,
      message: "",
      data: {}
    }
    
    • code代表接口的成功与失败
    • message代表接口失败之后的服务端消息输出
    • data代表接口成功之后真正的逻辑

    因此,我们可以对response定义的类型如下:

    export enum StateCode {
      error = 400,
      ok = 200,
      timeout = 408,
      serviceError = 500
    }
    
    export interface IResponse<T> {
      code: StateCode;
      message: string;
      data: T;
    }
    

    接下来我们可以定义具体的一个数据接口类型如下:

    export interface ICommodity {
      id: string;
      img: string;
      name: string;
      price: number;
      unit: string;
    }
    
    export interface IFavorites {
      id: string;
      img: string;
      name: string;
      url: string;
    }
    
    /*列表接口返回的数据格式*/
    export interface IList {
      commodity: ICommodity[];
      groups: IFavorites[];
    }
    
    /*登录接口返回的数据格式*/
    export interface ISignIn{
      Id: string;
      name: string;
      avatar: string;
      permissions: number[];
    }
    

    通过开源请求库 axios在项目中编写可复用的请求方法如下:

    const ROOT = "https://tracy.me"
    
    interface IRequest<T> {
       path: string;
       data: T;
    }
    
    export function service<T>({ path, data}: IRequest<T>): Promise<IResponse>{
      return new Promise((resolve) => {
        const request: AxiosRequestConfig = {
          url: `${ROOT}/${path}`,
          method: "POST",
          data: data
        }
        axios(request).then((response: AxiosResponse<IResponse>) => {
          resolve(response.data);
        })
      });
    }
    

    在接口业务调用时:

    service({
      path: "/list",
      data: {
        id: "xxx"
      }
    }).then((response: IResponse<IList>) => {
      const { code, data } = response;
      if (code === StateCode.ok) {
        data.commodity.map((v: ICommodity) => {
    
        });
      }
    })
    

    此时,我们每一个接口的实现,都可以从约定的类型中得到 TypeScript 工具的支持


    ts1.jpg

    假设哪一天,后端同学突然要变更之前约定的接口字段,以往我们往往采取全局替换,但是当项目过于庞大时,个别字段的变更也是很棘手的,要准确干净的替换往往不是易事

    但此时,由于我们使用的是TypeScript。例如,我们配合后端同学,将前面ISignin接口中的name改成了nickname。

    此时,在接口调用的位置,TS编译器将给我们提供准确的定位与提示


    ts2.jpg

    随着代码量的增加,我们会从Typescript中获取更多的收益,只是往往开始的时候会有些许苦涩,但与你的收益相比,还是值得的

    相关文章

      网友评论

          本文标题:Typescript在react项目中的实践

          本文链接:https://www.haomeiwen.com/subject/clscmhtx.html