用React和Typescript编写纯净代码的十一种必备模式

译文
开发 前端
纯净代码是一种一致的编程风格,它使您的代码更易于编写、阅读和维护。本文带你学习 React 和 Typescript 的十种有用模式。

不知您是否知晓,由于JavaScript是一种松散类型的语言,而且需要缓存运行时(runtime),因此会造成错误滞后被发现,进而导致严重的后果。而作为一个知名的JavaScript库,React虽然已是当今最流行、且行业领先的前端开发库了。它也继承了此类问题。

对此,有人提出了纯净代码的概念。它是一种旨在提高软件代码的质量和可维护性,以读者为中心的一致性编程风格。我们常说,任何人都可以编写出能让计算机理解的代码,但只有优秀的开发人员才能够通过清晰简洁的设计模式,编写出易于人类阅读、理解、修改和维护的纯净代码,以降低软件的开发成本,并消除技术债务。

下面,我们将介绍十一种在使用React和TypeScript编写纯净代码时,必备且实用的模式。

1. 使用默认方式导入React

请导入如下代码:

import * as React from "react";

{具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/c95fa549af8a958fd074324fcfb6f73f }

上述代码既简单又粗暴。如果我们不想使用React的所有内容的话,就没有必要如此,而应当采用如下更好的默认导入模式:

import React, {useContext, useState} from "react";

{具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/c6de59538119ee33d9c2e71c64620a56 }

使用这种方法,我们可以从模块中按需解构出React内容,而不必导入所有的组件。当然,值得注意的是:在使用该导入方式之前,我们需要按照如下方式配置tsconfig.json文件:

{
"compilerOptions": {
"esModuleInterop": true
}
}

{具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/d704140bbe003cd05be31b6ae7120468 }

在上述配置代码段中,我们通过将esModuleInterop设置true,以启用​​[allowSyntheticDefaultImports]​​。它会让TypeScript能够支持我们的语法。

2. 在运行时的实现之前声明类型

请看如下的代码:

import React, {Component} from "react";
const initialState = { count: 1 }
const defaultProps = { name: "John Doe" }
type State = typeof initialState;
type Props = { count?: number } & typeof defaultProps
class Counter extends Component {
static defaultProps = defaultProps;
state = initialState;
// ...
}

{具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/f7b5daad358c0e9f3623baa83474ab01 }

由于我们将运行时与编译时(compile-time)的声明区分开来,因此上面的代码段看上去更加清晰、易读。此类声明类型被称为——编译类型的优先声明。

让我们再来看如下的代码:

import React, {Component} from "react";
type State = typeof initialState;
type Props = { count?: number } & typeof defaultProps
const initialState = { count: 1 }
const defaultProps = { name: "John Doe" }
class Counter extends Component {
static defaultProps = defaultProps;
state = initialState;
// ...
}

{具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/317170a0b580b92738a94fce7197be17 }

我们从代码的第一行就能够清晰地看到,开发人员已经知晓了对应的API。接着,我们就将编译时与运行时的声明区分开来了。

3. 始终为Children Prop(子属性)提供显式类型

在React.d.ts中,TypeScript需要将函数组件和类组件的Children Prop予以注解,以展示React是如何处理Children Prop的。对此,我们有必要为Children Prop显式地提供一个类型,以便将“children”用于内容映射的场景中。当然,如果我们的组件无需使用内容映射的话,则可以简单地用never类型予以注释。请参考如下代码段:

import React, {Component} from "react";
// Card.tsx
type Props = {
children: import('react').ReactNode
}
class Card extends Component<Props> {
render() {
const {children} = this.props;
return <div>{children}</div>;
}
}

{具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/45672ae71f63904fa7f6c88ec5a90e75 }

下面是一些用于注释Children Prop的其他有效类型:

  • ReactNode | ReactChild | ReactElement
  • 对于原语,我们可以使用string | number | boolean
  • 对象和数组也是有效的类型
  • never | null | undefined (注意:我们并不推荐使用null和undefined)

4. 使用类型推断来定义组件状态或DefaultProps

请参考如下代码段:

import React, {Component} from "react";
type State = { count: number };
type Props = {
someProps: string & DefaultProps;
}
type DefaultProps = {
name: string
}
class Counter extends Component<Props, State> {
static defaultProps: DefaultProps = {name: "John Doe"}
state = {count: 0}

// ...
}

{具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/05c4c6b8ff5930776a0febab60ce53f8 }

虽然上述代码可以被顺利执行,但我们有必要对其进行重构和改进,以便TypeScript的类型系统能够正确地推断出readonly类型(如DefaultProps和initialState),进而防止开发人员意外地设置状态:this.state = {},而引起错误。请参见如下代码:

import React, {Component} from "react";
const initialState = Object.freeze({ count: 0 })
const defaultProps = Object.freeze({name: "John Doe"})
type State = typeof initialState;
type Props = { someProps: string } & typeof defaultProps;
class Counter extends Component<Props, State> {
static readonly defaultProps = defaultProps;
readonly state = {count: 0}

// ...
}

{具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/6e4ea2442091e86b3903738d21f0b613 }

在上述代码中,通过冻结DefaultProps和initialState,TypeScript类型系统可以将它们推断为Readonly类型。可见,通过对静态DefaultProps和其类中的Readonly状态予以标记,我们消除了上文提到的,由设置状态可能引起的运行时错误。

5. 使用类型别名而不是接口(interface)来声明属性和状态

虽然我们可以使用interface,但是为了确保清晰的一致性,并应对无法使用interface的场景,我们应当使用类型别名。例如,在前面的示例中,我们通过重构代码,使得TypeScript的类型系统,能够从实现中定义状态类型,进而正确地推断出只读类型。而在如下代码中,我们就无法针对其模式使用interface:

// works
type State = typeof initialState;
type Props = { someProps: string } & typeof defaultProps;
// throws error
interface State = typeof initialState;
interface Props = { someProps: string } & typeof defaultProps;

{具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/74b89d92471cdadbba52065a87334549 }

此外,在不能使用由unions和intersection创建的types,去扩展interface时,我们也必须使用type的别名。

6. 不要在接口/类型别名中使用方法声明

通常,只有所有的类型和推理要素都以相同的方式被声明时,我们才能确保代码中的模式一致性。然而,--strictFunctionTypes参数只能有效地比较两个函数,而非方法。对此,你可以通过链接--https://github.com/Microsoft/TypeScript/issues/25296#issuecomment-401517062,来进一步了解有关Typescript相关问题的解释。当然,您也可以参考如下代码段:

// Don't do this
interface Counter {
start(count:number) : string
reset(): void
}

// Do
interface Counter {
start: (count:number) => string
reset: () => string
}

{具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/63cc5145646f5f02c4e28e36bd8af926 }

7. 不要使用FunctionComponent

请不要使用FunctionComponent (简写 FC ),来定义某个函数组件。通常,我们在将TypeScript与React一起使用时,对应的函数式组件可以被写成如下两种方式:

(1)常规性功能代码:

type Props = { message: string };
const Greeting = ({ message }: Props) => <div>{message}</div>;

{具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/276112f9e5ed21c69ba02ffec755a7e1 }

(2)使用React.FC或React.FunctionComponent的代码段(如下所示):

import React, {FC} from "react";
type Props = { message: string };
const Greeting: FC<Props> = (props) => <div>{props}</div>;

{具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/310dd40107547a3d3ed08ae782f767cf }

可见,使用FC的优点包括:针对displayName、propTypes和DefaultProps等静态属性,提供了类型检查和自动完成。不过,根据经验,它对propTypes、contextTypes、以及displayName的defaultProps,可能会造成问题。此外,FC为Children Prop提供的隐式类型,也存在着一些已知的问题。当然,如前所述,组件API本来就应该是显式的,因此我们没有必要将Children Prop设置为隐式类型。

8. 不要将构造函数用于类组件

请使用新的类字段建议(请参考--https://github.com/tc39/proposal-class-fields#consensus-in-tc39),而不必在JavaScript类中使用构造函数。毕竟,使用构造函数会涉及调用super()和传递props,这些都会引入不必要的板模式(plate)和复杂性。

我们可以使用如下代码段中的类字段,来编写更简洁、更易于维护的React类组件:

// Don't do
type State = {count: number}
type Props = {}
class Counter extends Component<Props, State> {
constructor(props:Props){
super(props);
this.state = {count: 0}
}
}
// Do
type State = {count: number}
type Props = {}
class Counter extends Component<Props, State> {
state = {count: 0}
}

{具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/ddd9bb947f736794db1d85d8b560f1f0 }

由上述代码可知,我们在使用类字段时,涉及到的boilerplate(锅炉板模式)越少,待处理的this变量也就越少。

9. 不要在类中使用公共访问器(Public Accessor)

让我们来看如下代码:

import { Component } from "react"
class Friends extends Component {
public fetchFriends () {}
public render () {
return // jsx blob
}
}

{具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/96e8c072ad43426b7306e77f1a462e4d }

在上述类中,由于public的所有元素的运行时都是默认的,因此我们无需通过显式使用public关键字,来添加额外的boilerplate文件,只需如下模式即可:

import { Component } from "react"
class Friends extends Component {
fetchFriends () {}
render () {
return // jsx blob
}
}

{具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/87da70d3cef765b652e9532b98b52921 }

10. 不要在组件类中使用私有访问器

让我们再来看如下代码:

import {Component} from "react"
class Friends extends Component {
private fetchProfileByID () {}

render () {
return // jsx blob
}
}

{具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/8e31307e752e5f6b93c901556bd1dfc1 }

在上面的代码中,私有访问器仅在编译时,将fetchProfileByID方法私有化(private)。而在运行时,fetchProfileByID方法仍然是公共的。

目前,我们有多种方法可以将JavaScript类中的属性和方法设定为私有。下面的代码段展示了其中的一种--使用下划线 (_) 的命名规则:

import {Component} from "react"
class Friends extends Component {
_fetchProfileByID () {}

render () {
return // jsx blob
}
}

{具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/d69d76d7c0c3c10c6e95af5354b0da2b }

虽然上述方法并没有真正使得fetchProfileByID成为私有方法,但它很好地向其他开发人员传达了我们的意图,即:任何指定的方法应该被视为私有方法。弱映射(weakmap)、符号和作用域变量(scoped variable)都是如此。正如下面的代码段所示,我们可以通过新的​​ECMAScript类字段​​​的“建议”,使用各种​​私有字段​​,来轻松地达到此目的:

import {Component} from "react"
class Friends extends Component {
#fetchProfileByID () {}
render () {
return // jsx blob
}
}

{具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/594c426e763afe26c12badf60432831e }

当然,值得注意的是,您需要使用TypeScript3.8及更高版本,才能支持私有字段的相关语法。

11. 不要使用枚举(enum)

虽然enum是JavaScript中的保留字,但是使用enum并非标准化的JavaScript惯用模式。鉴于枚举已在C#和Java之类的编程语言中被广泛使用,您可以在此按照如下代码方式,使用编译类型的表述方式:

// Don't do this
enum Response {
Successful,
Failed,
Pending
}
function fetchData (status: Response): void => {
// some code.
}

// Do this
type Response = Sucessful | Failed | Pending
function fetchData (status: Response): void => {
// some code.
}

{具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/2dd0ecbd8d54ceae715feedc81d445cb }

小结

毫无疑问,使用TypeScript会为您的代码添加许多额外的boilerplate,不过总体说来仍然是利大于弊的。希望上述介绍的十一种有关React和TypeScript应用的最佳实践和JavaScript惯用模式,能够让您的代码更加清晰、更加易被维护。

原文标题:10 Must-Know Patterns for Writing Clean Code With React and Typescript,作者: Alex Omeyer


责任编辑:华轩 来源: 51CTO
相关推荐

2020-06-03 16:50:24

TypeScriptReact前端

2020-06-01 09:40:06

开发ReactTypeScript

2022-03-09 08:36:12

ReactTypeScript干净代码

2022-08-19 09:01:59

ReactTS类型

2021-04-25 11:31:45

React代码整洁代码的实践

2022-02-24 09:00:38

React代码模式

2021-06-08 09:35:11

Cleaner ReaReact开发React代码

2021-10-27 09:00:00

后端开发工具

2020-05-08 19:52:31

Reactreact.js前端

2023-06-19 14:14:24

Rust程序Web

2023-07-30 17:10:32

TypeScript开发

2022-07-11 10:32:35

Vue3await响应式

2019-11-25 15:44:13

TS数据JavaScrip

2020-10-30 12:42:06

TypeScript编程开发

2023-05-29 13:56:00

JSReact

2017-02-28 21:57:05

React组件

2019-03-05 10:33:18

Linus Torv LinuxGit

2020-10-14 15:05:02

React应用程序

2011-03-04 16:29:40

宽带接入

2020-10-25 18:43:20

VueTypeScript前端
点赞
收藏

51CTO技术栈公众号