不知您是编备模否知晓,由于JavaScript是写纯一种松散类型的语言,而且需要缓存运行时(runtime),净代因此会造成错误滞后被发现,种必进而导致严重的编备模后果。而作为一个知名的写纯JavaScript库,React虽然已是净代当今最流行、且行业领先的种必前端开发库了。它也继承了此类问题。编备模 对此,写纯有人提出了纯净代码的净代概念。它是种必一种旨在提高软件代码的质量和可维护性,以读者为中心的编备模一致性编程风格。我们常说,写纯任何人都可以编写出能让计算机理解的净代代码,但只有优秀的开发人员才能够通过清晰简洁的设计模式,编写出易于人类阅读、理解、修改和维护的纯净代码,亿华云计算以降低软件的开发成本,并消除技术债务。 下面,我们将介绍十一种在使用React和TypeScript编写纯净代码时,必备且实用的模式。 请导入如下代码: { 具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/c95fa549af8a958fd074324fcfb6f73f } 上述代码既简单又粗暴。如果我们不想使用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能够支持我们的语法。 请看如下的代码: 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。接着,我们就将编译时与运行时的声明区分开来了。 在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 render() { const { children} = this.props; return } { 具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/45672ae71f63904fa7f6c88ec5a90e75 } 下面是一些用于注释Children Prop的云服务器其他有效类型: 请参考如下代码段: import React, { Component} from "react"; type State = { count: number }; type Props = { someProps: string & DefaultProps; } type DefaultProps = { name: string } class Counter extends Component 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 static readonly defaultProps = defaultProps; readonly state = { count: 0} // ... { 具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/6e4ea2442091e86b3903738d21f0b613 } 在上述代码中,通过冻结DefaultProps和initialState,TypeScript类型系统可以将它们推断为Readonly类型。可见,通过对静态DefaultProps和其类中的Readonly状态予以标记,我们消除了上文提到的,由设置状态可能引起的运行时错误。 虽然我们可以使用interface,但是为了确保清晰的一致性,并应对无法使用interface的场景,我们应当使用类型别名。例如,在前面的示例中,我们通过重构代码,使得TypeScript的类型系统,能够从实现中定义状态类型,进而正确地推断出只读类型。而在如下代码中,我们就无法针对其模式使用interface: // works type State = typeof initialState; type Props = { someProps: string } & typeof defaultProps; // throws error interface State = typeof initialState; { 具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/74b89d92471cdadbba52065a87334549 } 此外,在不能使用由unions和intersection创建的types,去扩展interface时,我们也必须使用type的别名。 通常,只有所有的类型和推理要素都以相同的方式被声明时,我们才能确保代码中的模式一致性。然而,--strictFunctionTypes参数只能有效地比较两个函数,而非方法。对此,你可以通过链接--https://github.com/Microsoft/TypeScript/issues/25296#issuecomment-401517062,来进一步了解有关Typescript相关问题的解释。当然,您也可以参考如下代码段: // Dont 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 } 请不要使用FunctionComponent (简写 FC ),来定义某个函数组件。通常,我们在将TypeScript与React一起使用时,对应的函数式组件可以被写成如下两种方式: (1)常规性功能代码: type Props = { message: string }; { 具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/276112f9e5ed21c69ba02ffec755a7e1 } (2)使用React.FC或React.FunctionComponent的代码段(如下所示): import React, { FC} from "react"; type Props = { message: string }; { 具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/310dd40107547a3d3ed08ae782f767cf } 可见,使用FC的优点包括:针对displayName、propTypes和DefaultProps等静态属性,提供了类型检查和自动完成。不过,根据经验,它对propTypes、contextTypes、以及displayName的defaultProps,可能会造成问题。此外,FC为Children Prop提供的隐式类型,也存在着一些已知的问题。当然,如前所述,组件API本来就应该是显式的,因此我们没有必要将Children Prop设置为隐式类型。 请使用新的类字段建议(请参考--https://github.com/tc39/proposal-class-fields#consensus-in-tc39),而不必在JavaScript类中使用构造函数。毕竟,使用构造函数会涉及调用super()和传递props,这些都会引入不必要的板模式(plate)和复杂性。 我们可以使用如下代码段中的类字段,来编写更简洁、更易于维护的React类组件: // Dont do type State = { count: number} type Props = { } class Counter extends Component constructor(props:Props){ super(props); this.state = { count: 0} } } // Do type State = { count: number} type Props = { } class Counter extends Component state = { count: 0} { 具体请参见GitHub的Gist链接--https://gist.github.com/lawrenceagles/ddd9bb947f736794db1d85d8b560f1f0 } 由上述代码可知,我们在使用类字段时,涉及到的boilerplate(锅炉板模式)越少,待处理的this变量也就越少。 让我们来看如下代码: 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 } 让我们再来看如下代码: 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及更高版本,才能支持私有字段的相关语法。 虽然enum是JavaScript中的保留字,但是使用enum并非标准化的JavaScript惯用模式。鉴于枚举已在C#和Java之类的编程语言中被广泛使用,您可以在此按照如下代码方式,使用编译类型的表述方式: // Dont 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