Typescript在react项目中的实践

一、理解 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): void 
};

const Button: SFC = ({ 
  onClick: handleClick, 
  children 
}) => (
  
);

2. 有状态组件

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

this.state.clicksCount = 2;

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

const initialState = { clicksCount: 0 }

/*使用TypeScript来从我们的实现中推断出State的类型。
好处是:这样我们不需要分开维护我们的类型定义和实现*/
type State = Readonly

class ButtonCounter extends Component {
  /*至此我们定义了类上的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): void;
  color?: string;
};

const Button: SFC = (
{ 
  onClick: handleClick, 
  color, 
  children 
}) => (
  
);
Button.defaultProps = {…}

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

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

export const withDefaultProps = 
< P extends object, DP extends Partial

= Partial

> ( defaultProps: DP, Cmp: ComponentType

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

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

const defaultProps = {
  color: 'red',
};

type DefaultProps = typeof defaultProps;
type Props = { onClick(e: MouseEvent): void } & DefaultProps;

const Button: SFC = ({ onClick: handleClick, color, children }) => (
  
);

const ButtonWithDefaultProps = withDefaultProps(defaultProps, Button);

组件使用如下

render() {
    return (
        
            Increment
        
    )
}

4. 范型组件

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

interface Props {
  content: T;
}

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

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

泛型函数:

function Foo(props: Props) {
  console.log(props);
}

/** 此时 Foo 的完整签名为: function Foo(props: Props): void */
Foo({ content: 42 });

/** 此时 Foo 的完整签名为: function Foo(props: Props): 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 {

}

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

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

interface IYarn {
  [prop: string]: any;
}

interface IProps {
  total: number;
  list: IYarn[];
  title: string;
}

const YarnList: SFC = ({ 
  list,total,title
}) => (
   
{list.map( .... )}
);

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

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

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

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

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

具体业务调用示例如下:

interface User {
  id: number;
  name: string;
}
const data: User[] = [
  {
    id: 1,
    name: "xsq"
  },
  {
    id: 2,
    name: "tracy"
  }
];

const App = () => {
  return (
    
); };

5.在数据请求中的应用

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

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

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

export enum StateCode {
  error = 400,
  ok = 200,
  timeout = 408,
  serviceError = 500
}

export interface IResponse {
  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 {
   path: string;
   data: T;
}

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

在接口业务调用时:

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

    });
  }
})

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


Typescript在react项目中的实践_第1张图片
ts1.jpg

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

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

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


Typescript在react项目中的实践_第2张图片
ts2.jpg

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

你可能感兴趣的:(Typescript在react项目中的实践)