Skip to content

Latest commit

 

History

History
962 lines (713 loc) · 35.1 KB

File metadata and controls

962 lines (713 loc) · 35.1 KB

五、使用类型安全组件简化开发和重构

本章重点介绍的工具是 Flow,它是 JavaScript 应用的静态类型检查器。流的范围和您可以使用它做的事情是巨大的,所以我将在一个用于使 React 组件更好的工具的上下文中介绍流。在本章中,您将学习以下内容:

  • 在 React 应用中引入类型安全性可以解决的问题
  • 在您的项目中启用流程
  • 使用流验证您的组件
  • 使用类型安全增强 React 开发的其他方法

类型安全解决什么问题?

类型安全不是万能的。例如,我完全有能力编写一个充满 bug 的类型安全应用。有趣的是,在引入了类型检查器之后,这种 bug 就不再发生了。那么,在引入类似于流的工具之后,您可以期待什么类型的事情呢?我将分享我在学习流程时经历的三个因素。流程文档中的类型系统部分对此主题进行了更详细的介绍,可在中找到 https://flow.org/en/docs/lang/

以保证取代猜测

像 JavaScript 这样的动态类型语言的一个很好的特性是,您可以编写代码而不必考虑类型。类型是好的,它们确实解决了很多问题,我想说的是,信不信由你,但有时你需要能够只编写代码,而不必正式验证代码的正确性。换句话说,有时候猜测正是你所需要的。

如果我正在编写一个函数,我知道它以一个对象作为参数,那么我可以假设传递给我的函数的任何对象都将具有预期的属性。这允许我实现所需的功能,而无需确保将正确的类型作为参数传递。不过,这只会持续很长时间。因为你的代码总是会得到一些意想不到的东西作为输入传递给它。一旦您有了一个包含许多活动部件的复杂应用,类型安全性就可以消除猜测。

Flow 采用了一种有趣的方法。它不是基于类型编译新的 JavaScript 代码,而是基于类型注释检查源代码是否正确。然后从源中删除这些注释,使其可以运行。通过使用类似于类型检查器的流,可以明确说明每个组件愿意接受哪些输入,以及它如何通过使用类型注释与应用的其余部分进行迭代。

删除运行时检查

在动态语言(如 JavaScript)中处理未知类型数据的解决方案是在运行时检查值。根据值的类型,您可能必须执行一些替代操作才能获得代码所期望的值。例如,JavaScript 中的一个常见习惯用法是确保值既不是未定义的,也不是 null。如果是,那么我们要么抛出一个错误,要么提供一个默认值。

当您执行运行时检查时,它会改变您思考代码的方式。一旦您开始执行这些检查,它们不可避免地会演变为更复杂的检查和更多的检查。这种心态实际上相当于不相信自己或他人使用正确的数据调用代码。您认为,因为您的函数很可能会被垃圾参数调用,所以您需要准备好处理抛出到函数中的任何内容。

另一方面,采用类型安全性意味着您不必依靠实现自定义解决方案来防范坏数据。让类型系统替您处理这个问题。您只需要考虑您的代码需要使用哪些类型,然后从那里开始。思考我的代码需要什么,而不是如何获得代码需要的东西。

明显的低严重性错误

如果您可以使用诸如 Flow 之类的类型检查器来删除由于坏类型而悄悄出现的潜在错误,那么您将留下高级应用错误。这些错误在发生时是显而易见的,因为应用完全是错误的。它产生错误的输出,计算错误的数字,一个屏幕没有加载,等等。您可以更容易地看到这些类型的 bug 并与之交互。这使它们变得显而易见,当 bug 显而易见时,它们更容易跟踪和修复。

另一方面,您有一些错误的 bug。这些可能是由错误类型造成的。让这些类型的 bug 特别可怕的是,你甚至不知道有什么地方出了问题。您的应用中的某些内容可能有点不正确。或者它可能会被彻底破坏,因为您的部分代码需要一个数组,但它某种程度上是有效的,因为它得到了另一种 iterable,它在一个地方有效,但在其他地方无效。

如果您刚刚使用了类型注释并使用流检查了源代码,它会告诉您传递的不是数组的内容。静态检查类型时,这些类型的错误没有空间。事实证明,这些通常是更难找出的 bug。

安装和初始化流

在开始实现类型安全组件之前,需要安装并初始化流。我将向您展示如何在create-react-app环境中实现这一点,但对于几乎任何 React 环境都可以遵循相同的步骤。

您可以在全局范围内安装 Flow,但我建议您在本地安装它,以及您的项目所依赖的所有其他软件包。除非有充分的理由在全球范围内安装,否则请在本地安装。这样,任何安装您的应用的人都可以通过运行npm install获得所有依赖项。

要在本地安装 Flow,请运行以下命令:

npm install flow-bin --save-dev

这将在您的项目本地安装流可执行文件,并更新您的package.json,以便流作为项目的依赖项安装。现在,让我们在package.json中添加一个新命令,以便您可以对源代码运行流类型检查器。使scripts部分如下所示:

"scripts": { 
  "start": "react-scripts start",
  "build": "react-scripts build", 
  "test": "react-scripts test --env=jsdom", 
  "eject": "react-scripts eject", 
  "flow": "flow" 
}, 

现在,您可以通过在终端中执行以下命令来运行 Flow:

npm run flow

这将按预期运行flow脚本,但 Flow 将抱怨无法找到流配置文件:

Could not find a .flowconfig in . or any of its parent directories. 

解决此问题的最简单方法是使用flow init命令:

npm run flow init 

这将在项目目录中创建一个.flowconfig文件。您现在不必担心更改此文件中的任何内容;只是 Flow 希望它出现。现在,当您运行npm run flow时,您应该会收到一条消息,指示没有错误:

Launching Flow server for 05/installing-and-initializing-flow
Spawned flow server (pid=46516)
No errors!  

事实证明,您的所有源文件实际上都没有被检查。这是因为默认情况下,Flow 只检查以// @flow指令作为第一行的文件。让我们继续在App.js顶部添加这一行:

// @flow 
import React, { Component } from 'react'; 
import logo from './logo.svg'; 
import './App.css'; 

class App extends Component { 
  render() { 
    return ( 
      <div className="App"> 
        <header className="App-header"> 
          <img src={logo} className="App-logo" alt="logo" /> 
          <h1 className="App-title">Welcome to React</h1> 
        </header> 
        <p className="App-intro"> 
          To get started... 
        </p> 
      </div> 
    ); 
  } 
} 

export default App; 

现在 Flow 正在检查此模块,我们得到一个错误:

      6: class App extends Component {
                           ^^^^^^^^^ Component. Too few type arguments. Expected at least 1

这是什么意思?Flow 试图在错误输出的下一行提供解释:

Component<Props, State = void> { 
          ^^^^^^^^^^^^ See type parameters of definition here. 

Flow 正在抱怨您使用App扩展的Component类。这意味着您需要为道具向Component提供至少一个type参数。因为App实际上没有使用任何道具,所以现在可能只是一个空类型:

// @flow 
import React, { Component } from 'react'; 
import logo from './logo.svg'; 
import './App.css'; 

type Props = {}; 

class App extends Component<Props> { 
  render() { 
    return ( 
      <div className="App"> 
        <header className="App-header"> 
          <img src={logo} className="App-logo" alt="logo" /> 
          <h1 className="App-title">Welcome to React</h1> 
        </header> 
        <p className="App-intro"> 
          To get started... 
        </p> 
      </div> 
    ); 
  } 
}
export default App; 

现在当您再次运行 Flow 时,App.js中没有任何错误!这意味着您已经成功地使用类型信息对模块进行了注释,这些信息流用于静态分析源代码,以确保一切正常。

那么 Flow 如何知道 React 的Component类在泛型方面期望得到什么呢?事实证明,React 本身就是带注释的流类型,这就是当流检测到问题时获取特定错误消息的方式。

接下来,我们将// @flow指令添加到index.js的顶部:

// @flow 
import React from 'react'; 
import ReactDOM from 'react-dom'; 
import './index.css'; 
import App from './App'; 
import registerServiceWorker from './registerServiceWorker'; 

const root = document.getElementById('root'); 

ReactDOM.render( 
  <App />, 
  root 
); 

registerServiceWorker(); 

如果再次运行npm run flow,将看到以下错误:

    Error: src/index.js:12
     12:   root
    ^^^^ null. This type is incompatible with the expected param 
                type of Element  

这是因为root的值来自document.getElementById('root')。因为这个方法没有 DOM 来返回元素,所以流检测到一个null值并发出抱怨。由于这是一个合理的关注点(可能不存在root元素),并且当没有元素时,我们需要流程遵循的路径,因此您可以添加一些逻辑来处理这种情况:

// @flow 
import React from 'react'; 
import ReactDOM from 'react-dom'; 
import './index.css'; 
import App from './App';
import registerServiceWorker from './registerServiceWorker'; 

const root = document.getElementById('root'); 

if (!(root instanceof Element)) { 
  throw 'Invalid root'; 
} 

ReactDOM.render( 
  <App />, 
  root 
); 

registerServiceWorker(); 

在调用ReactDOM.render()之前,您可以手动检查root的类型,以确保它是 Flow 期望看到的类型。现在运行npm run flow时,没有错误。

你都准备好了!您已经在本地安装和配置了 Flow,并且您有来自create-react-app的初始源通过了类型检查。现在可以继续开发类型安全组件。

验证组件属性和状态

React 的设计考虑了流量静态类型检查。React 应用中最常用的流是验证组件属性和状态是否正确使用。您还可以强制允许作为另一个组件的子组件的组件类型。

在流动之前,React 将依赖于道具类型机制来验证传递给组件的值。这是 React 的一个单独的包,您今天仍然可以使用它。与道具类型相比,Flow 是一个更好的选择,因为它执行静态检查,而道具类型执行运行时验证。这意味着您的应用不需要在运行时运行多余的代码。

基本属性值

通过道具传递给组件的最常见的值类型是基本值,例如字符串、数字和布尔值。使用 Flow,您可以声明自己的类型,该类型表示给定属性允许哪些基元值。

让我们来看一个例子:

// @flow 
import React from 'react'; 

type Props = { 
  name: string, 
  version: number 
}; 

const Intro = ({ name, version }: Props) => ( 
  <p className="App-intro"> 
    <strong>{name}:</strong>{version} 
  </p> 
); 

export default Intro; 

此组件呈现某些应用的名称和版本。这些值通过属性值传入。对于这个组件,假设您只需要name属性的字符串值和version属性的数字值。此模块使用type关键字声明一个新的Props类型:

type Props = { 
  name: string, 
  version: number 
}; 

此流语法允许您创建新类型,然后可用于键入函数参数。在本例中,您有一个功能性的 React 组件,其中 props 作为第一个参数传递。这是告诉 Flow 道具对象应具有特定类型的地方:

({ name, version }: Props) => (...) 

有了它,Flow 可以确定是否有任何地方我们正在将无效的道具类型传递给这个组件!更好的是,这是在浏览器中运行任何东西之前静态完成的。在 Flow 之前,您必须使用prop-types包在运行时验证组件道具。

让我们使用这个组件,然后运行 Flow。下面是使用Intro组件的App.js

// @flow 
import React, { Component } from 'react'; 
import logo from './logo.svg'; 
import './App.css'; 
import Intro from './Intro';

type Props = {}; 

class App extends Component<Props> { 
  render() { 
    return ( 
      <div className="App"> 
        <header className="App-header"> 
          <img src={logo} className="App-logo" alt="logo" /> 
          <h1 className="App-title">Welcome to React</h1> 
        </header> 
        <Intro name="React" version={16} /> 
      </div> 
    ); 
  } 
} 

export default App; 

传递给Intro的属性值满足Props类型的期望:

<Intro name="React" version={16} /> 

您可以通过运行npm run flow来验证这一点。您应该将No errors!视为输出。让我们看看如果更改这些属性的类型会发生什么:

<Intro version="React" name={16} /> 

现在我们要传递一个字符串,其中需要一个数字,一个数字需要一个字符串。如果再次运行npm run flow,应该会看到以下错误:

    Error: src/App.js:17
     17:         <Intro version="React" name={16} />
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ props of React element `Intro`. This type is incompatible with
      9: const Intro = ({ name, version }: Props) => (
                                           ^^^^^ object type. See: src/Intro.js:9
      Property `name` is incompatible:
         17:         <Intro version="React" name={16} />
                                                  ^^ number. This type is incompatible with
          5:   name: string,
                     ^^^^^^ string. See: src/Intro.js:5

    Error: src/App.js:17
     17:         <Intro version="React" name={16} />
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ props of React element `Intro`. This type is incompatible with
      9: const Intro = ({ name, version }: Props) => (
                                           ^^^^^ object type. See: src/Intro.js:9
      Property `version` is incompatible:
         17:         <Intro version="React" name={16} />
                                    ^^^^^^^ string. This type is incompatible with
          6:   version: number
                        ^^^^^^ number. See: src/Intro.js:6

这两个错误将详细说明问题所在。它首先向您显示传递组件属性值的位置:

    <Intro version="React" name={16} />
    ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ props of React element `Intro`. 

然后,它将向您显示使用Props类型声明 properties 参数类型的位置:

    This type is incompatible with
      9: const Intro = ({ name, version }: Props) => (
                                           ^^^^^ object type. See: src/Intro.js:9

最后,它向您展示了类型的确切问题:

    Property `name` is incompatible:
         17:         <Intro version="React" name={16} />
                                                  ^^ number. This type is incompatible with
          5:   name: string,
                     ^^^^^^ string. See: src/Intro.js:5

流错误消息试图为您提供尽可能多的信息,这意味着您花在查找文件上的时间更少。

对象属性值

在上一节中,您学习了如何检查基本属性类型。React 组件还可以接受具有基本体值的对象和其他对象。如果组件希望对象作为属性值,则可以使用与原语值相同的方法。不同之处在于您如何构造Props类型声明:

// @flow 
import React from 'react'; 

type Props = { 
  person: { 
    name: string, 
    age: number 
  } 
}; 

const Person = ({ person }: Props) => ( 
  <section> 
    <h3>Person</h3> 
    <p><strong>Name: </strong>{person.name}</p> 
    <p><strong>Age: </strong>{person.age}</p> 
  </section> 
); 

export default Person; 

此组件需要一个作为对象的person属性。此外,它希望该对象具有一个name字符串属性和一个数字age属性。事实上,如果您有其他需要person属性的组件,您可以将这种类型分解为可重用的部分:

type Person = { 
  name: string, 
  age: number 
}; 

type Props = { 
  person: Person 
}; 

现在让我们看一下作为属性传递给这个组件的值:

// @flow 
import React, { Component } from 'react'; 
import logo from './logo.svg'; 
import './App.css'; 
import Person from './Person'; 

class App extends Component<{}> { 
  render() { 
    return ( 
      <div className="App"> 
        <header className="App-header"> 
          <img src={logo} className="App-logo" alt="logo" /> 
          <h1 className="App-title">Welcome to React</h1> 
        </header> 
        <Person person={{ name: 'Roger', age: 20 }} /> 
      </div> 
    ); 
  } 
} 

export default App; 

它不是向Person组件传递多个属性值,而是传递一个属性值,一个满足Props类型的类型期望的对象。如果没有,Flow 会抱怨。让我们尝试从此对象中删除属性:

<Person person={{ name: 'Roger' }} /> 

现在当您运行npm run flow时,它会抱怨传递给person的对象缺少属性:

    15:         <Person person={{ name: 'Roger' }} />
                 ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ props of React element `Person`. This type is incompatible with
     11: const Person = ({ person }: Props) => (
                                     ^^^^^ object type. See: src/Person.js:11
      Property `person` is incompatible:
         15:         <Person person={{ name: 'Roger' }} />
                                     ^^^^^^^^^^^^^^^^^ object literal. This type is incompatible with
                       v
          5:   person: {
          6:     name: string,
          7:     age: number
          8:   }
               ^ object type. See: src/Person.js:5
          Property `age` is incompatible:
                           v
              5:   person: {
              6:     name: string,
              7:     age: number
              8:   }
                   ^ property `age`. Property not found in. See: src/Person.js:5
             15:         <Person person={{ name: 'Roger' }} />
                                         ^^^^^^^^^^^^^^^^^ object literal

无论您对属性值的使用有多么奇特,Flow 都可以发现您是否误用了它们。试图在运行时使用类似于prop-types的东西来完成同样的事情充其量也很麻烦。

验证组件状态

您可以通过键入传递给组件的 props 参数来验证函数 React 组件的属性。您的一些组件将具有状态,并且您可以验证组件的状态,与验证属性时的状态大致相同。您可以创建一个表示组件状态的类型,并将其作为类型参数传递给Component

让我们来看一个容器组件,它有一个由子组件使用和操作的状态:

// @flow 
import React, { Component } from 'react'; 
import Child from './Child'; 

type State = { 
  on: boolean 
}; 

class Container extends Component<{}, State> { 
  state = { 
    on: false 
  } 

  toggle = () => { 
    this.setState(state => ({ 
      on: !state.on 
    }));
  } 

  render() { 
    return ( 
      <Child 
        on={this.state.on} 
        toggle={this.toggle} 
      />); 
  } 
} 

export default Container; 

Container呈现的Child组件采用on布尔属性和toggle函数。传递到Childtoggle()方法将改变Container的状态。这意味着Child可以调用此函数来更改其父级的状态。在组件类上方的模块顶部,有一个State类型,用于指定允许设置为状态的值。在这种情况下,状态只是一个简单的on布尔值:

type State = { 
  on: boolean 
}; 

该类型在扩展时作为类型参数传递给Component

class Container extends Component<{}, State> { 
  ... 
} 

通过将此类型参数传递给Component,可以根据需要设置组件状态。例如,Child组件调用toggle()方法来改变Container组件的状态。如果此调用设置的状态不正确,Flow 将检测到它并进行投诉。让我们更改toggle()实现,通过将状态设置为与流程不一致的内容,使其失败:

toggle = () => { 
  this.setState(state => ({ 
    on: !state.on + 1 
  })); 
} 

您将得到如下错误:

    Error: src/Container.js:16
     16:       on: !state.on + 1
                   ^^^^^^^^^^^^^ number. This type is incompatible with
      6:   on: boolean
               ^^^^^^^ boolean

在开发过程中,错误地设置组件的状态是很容易的,所以让 Flow 告诉您做错了什么是一个实时节约。

函数属性值

将函数作为属性从一个组件传递到另一个组件是完全正常的。您可以使用 Flow 来确保不仅传递函数到组件,而且传递函数的校正类型。

让我们通过查看 React 应用中的一个常见模式来研究这个想法。假设您有以下呈现Article组件的Articles组件:

// @flow 
import React, { Component } from 'react'; 
import Article from './Article'; 

type Props = {}; 
type State = { 
  summary: string, 
  selected: number | null, 
  articles: Array<{ title: string, summary: string}> 
}; 

class Articles extends Component<Props, State> { 
  state = { 
    summary: '', 
    selected: null, 
    articles: [ 
      { title: 'First Title', summary: 'First article summary' }, 
      { title: 'Second Title', summary: 'Second article summary' }, 
      { title: 'Third Title', summary: 'Third article summary' } 
    ] 
  }
  onClick = (selected: number) => () => { 
    this.setState(prevState => ({ 
      selected, 
      summary: prevState.articles[selected].summary 
    })); 
  } 

  render() { 
    const { 
      summary, 
      selected, 
      articles 
    } = this.state; 

    return ( 
      <div> 
        <strong>{summary}</strong> 
        <ul> 
          {articles.map((article, index) => ( 
            <li key={index}> 
              <Article 
                index={index} 
                title={article.title} 
                selected={selected === index} 
                onClick={this.onClick} 
              /> 
            </li> 
          ))} 
        </ul> 
      </div> 
    ); 
  } 
} 

export default Articles; 

Articles组件是一个容器组件,因为它有状态,并使用此状态来呈现子Article组件。它还定义了一个改变summary状态和selected状态的onClick()方法。想法是Article组件需要访问此方法,以便能够触发状态更改。如果您密切关注onClick()方法,您会注意到它实际上返回了一个新的事件处理函数。这样,当 click 事件实际调用返回的函数时,它将具有对所选参数的作用域访问权限。

现在让我们看一看 AutoT0:Up 组件,看看流如何帮助您确保得到您期望传递给组件的函数:

// @flow 
import React from 'react'; 

type Props = { 
  title: string, 
  index: number, 
  selected: boolean, 
  onClick: (index: number) => Function 
}; 

const Article = ({ 
  title, 
  index, 
  selected, 
  onClick 
}: Props) => ( 
  <a href="#" 
    onClick={onClick(index)} 
    style={{ fontWeight: selected ? 'bold' : 'normal' }} 
  > 
    {title} 
  </a> 
); 

export default Article; 

此组件呈现的<a>元素的onClick处理程序调用作为属性传入的onClick()函数,并期望返回一个新函数。如果您查看Props类型声明,您可以看到onClick属性需要特定类型的函数:

type Props = { 
  onClick: (index: number) => Function, 
  ... 
}; 

这告诉 Flow 该属性必须是接受数字参数并返回新函数的函数。向该组件传递事件处理程序函数而不是返回事件处理程序函数的函数是一个容易犯的错误。Flow 可以很容易地发现这一点,并使您能够很容易地进行纠正。

强制子组件类型

除了验证状态和属性值的类型外,Flow 还可以验证您的组件是否也获得了正确的子组件。以下部分将向您展示一些常见的场景,在这些场景中,当您通过将组件传递给错误的子组件而误用组件时,Flow 可以告诉您。

有特定子女类型的父母

您可以告诉 Flow,组件应该只与特定类型的子组件一起工作。假设您有一个Child组件,并且这是唯一允许作为您正在处理的组件的子组件的组件类型。以下是如何告诉 Flow 有关此约束的信息:

// @flow 
import * as React from 'react'; 
import Child from './Child'; 

type Props = { 
  children: React.ChildrenArray<React.Element<Child>>, 
}; 

const Parent = ({ children }: Props) => ( 
  <section> 
    <h2>Parent</h2> 
    {children} 
  </section> 
); 

export default Parent; 

让我们从第一个import语句开始:

 import * as React from 'react'; 

您希望将星号导入为React的原因是,这将引入 React 中可用的所有流类型声明。在本例中,您使用ChildrenArray类型指定该值实际上是组件的子级,并使用Element指定您需要一个 React 元素。本例中使用的 type 参数告诉 Flow,Child组件是这里唯一可以接受的组件类型。

鉴于子约束,此 JSX 将通过流验证:

<Parent> 
  <Child /> 
  <Child /> 
</Parent> 

对于呈现为Parent子级的Child组件的数量没有限制,只要至少有一个。

有一个孩子的父母

对于某些组件,拥有多个子组件是没有意义的。对于这些情况,您将使用React.Element类型而不是React.ChildrenArray类型:

// @flow
import * as React from 'react';
import Child from './Child';

type Props = {
  children: React.Element<Child>,
};

const ParentWithOneChild = ({ children }: Props) => (
  <section>
    <h2>Parent With One Child</h2>
    {children}
  </section>
);

export default ParentWithOneChild; 

与前面的示例一样,您仍然可以指定允许的子组件类型。在这种情况下,子组件称为Child,从'./Child'导入。以下是如何将此组件传递给子组件:

<ParentWithOneChild> 
  <Child /> 
</ParentWithOneChild> 

如果要传递多个Child组件,Flow 会抱怨:

    Property `children` is incompatible:
         24:         <ParentWithOneChild>
                     ^^^^^^^^^^^^^^^^^^^^ React children array. Inexact type is incompatible with exact type
          6:   children: React.Element<Child>,
                         ^^^^^^^^^^^^^^^^^^^^ object type. See: src/ParentWithOneChild.js:6

流错误消息再次向您准确地显示了代码的错误以及错误所在。

有可选子女的父母

总是需要一个子组件是没有必要的,而且实际上可能会导致头痛。例如,如果由于 API 没有返回任何内容而没有任何内容可呈现,该怎么办?下面是一个示例,说明如何使用流语法指定子级是可选的:

// @flow
import * as React from 'react';
import Child from './Child';

type Props = {
  children?: React.Element<Child>,
};

const ParentWithOptionalChild = ({ children }: Props) => (
  <section>
    <h2>Parent With Optional Child</h2>
    {children}
  </section>
);

export default ParentWithOptionalChild;

这看起来很像 React 组件,它需要特定类型的元素。区别在于问号:children?。这意味着可以传递类型为Child的子组件,或者根本不传递子组件。

具有原始子值的父对象

渲染将基元值作为子元素的 React 组件是很常见的。在某些情况下,您可能希望接受字符串或布尔类型。以下是您将如何做到这一点:

// @flow
import * as React from 'react';

type Props = {
  children?: React.ChildrenArray<string|boolean>,
};

const ParentWithStringOrNumberChild = ({ children }: Props) => (
  <section>
    <h2>Parent With String or Number Child</h2>
    {children}
  </section>
);

export default ParentWithStringOrNumberChild;

同样,您可以使用React.ChildrenArray类型指定允许多个子元素。要指定特定的子类型,请将其作为类型参数传递给React.ChildrenArray,在本例中为字符串和布尔并集。现在可以使用字符串渲染此组件:

<ParentWithStringOrNumberChild>
  Child String
</ParentWithStringOrNumberChild>

或使用布尔值:

<ParentWithStringOrNumberChild> 
  {true} 
</ParentWithStringOrNumberChild> 

或两者兼有:

<ParentWithStringOrNumberChild> 
  Child String 
  {false} 
</ParentWithStringOrNumberChild> 

验证事件处理程序函数

React 组件使用函数来响应事件。这些被称为事件处理函数,当 React 事件系统调用它们时,它们被传递一个事件对象作为参数。使用 Flow 显式地键入这些事件参数,以确保事件处理程序获得它所期望的元素类型,这是非常有用的。

例如,假设您正在处理一个组件,该组件响应<a>元素的点击。您的事件处理程序函数还需要与单击的元素交互,以获取href属性。使用 React 公开的流类型,可以确保正确的元素类型确实触发了导致函数运行的事件:

// @flow
import * as React from 'react';
import { Component } from 'react';

class EventHandler extends Component<{}> {
  clickHandler = (e: SyntheticEvent<HTMLAnchorElement>): void => {
    e.preventDefault();
    console.log('clicked', e.currentTarget.href);
  }

  render() {
    return (
      <section>
        <a href="#page1" onClick={this.clickHandler}>
          First Link
        </a>
      </section>
    );
  }
}

export default EventHandler;

本例中的clickHandler()函数被指定为元素的onClick处理程序。注意事件参数的类型:SyntheticEvent<HTMLAnchorElement>。Flow 将使用它来确保使用事件的代码只访问事件的适当属性和事件的currentTarget

currentTarget是触发事件的元素,在本例中,您已经指定它应该是HTMLAnchorElement。如果您使用了另一种类型,Flow 会抱怨您引用了href属性,因为其他 HTML 元素中不存在这种属性。

将流引入开发服务器

如果将 React 代码的类型检查更紧密地集成到create-react-app开发过程中,那不是很好吗?有人说在create-react-app的未来版本中实现这一点。现在,如果您想在项目中使用此功能,您必须从create-react-app中弹出。

这种方法的目标是让开发服务器在检测到更改时为您运行流。然后,您可以在 dev 服务器控制台输出和浏览器控制台中看到流输出。

一旦您通过运行npm ejectcreate-react-app弹出,您需要安装以下网页包插件:

npm install flow-babel-webpack-plugin --save-dev

然后,您需要通过编辑config/webpack.config.dev.js来启用插件。首先,您需要包括插件:

const FlowBabelWebpackPlugin = require('flow-babel-webpack-plugin');

然后,您需要将插件添加到plugins选项中的数组中。该数组在后面应该是这样的:

plugins: [ 
  new InterpolateHtmlPlugin(env.raw), 
  new HtmlWebpackPlugin({ 
    inject: true, 
    template: paths.appHtml, 
  }), 
  new webpack.NamedModulesPlugin(), 
  new webpack.DefinePlugin(env.stringified), 
  new webpack.HotModuleReplacementPlugin(), 
  new CaseSensitivePathsPlugin(), 
  new WatchMissingNodeModulesPlugin(paths.appNodeModules), 
  new webpack.IgnorePlugin(/^./locale$/, /moment$/), 
  new FlowBabelWebpackPlugin() 
], 

就这些。现在,当您启动 dev 服务器时,Flow 将自动运行,并在 Webpack 构建过程中键入检查代码。让我们将@flow指令添加到App.js的顶部并运行npm start。由于App组件不会作为Component的子类进行验证,因此在 dev 服务器控制台输出中应该会出现错误:

    Failed to compile.

    Flow: Type Error
    Error: src/App.js:6
      6: class App extends Component {
                           ^^^^^^^^^ Component. Too few type arguments. Expected at least 1
     26: declare class React$Component<Props, State = void> {
                                       ^^^^^^^^^^^^ See type parameters of definition here.

    Found 1 error

我真正喜欢这种方法的地方是,即使出现流错误,dev 服务器仍然会启动。如果您在浏览器中查看该应用,您将看到以下内容:

这意味着您甚至不必在开发过程中查看 dev 服务器控制台来捕获类型错误!由于它是 dev 开发服务器的一部分,每次进行更改时,Flow 都会重新检查代码。因此,让我们通过传递一个属性类型参数(<{}>来修复App.js中的当前错误:

class App extends Component<{}> { 
  ... 
} 

进行此更改后,保存文件。就这样,错误消失了,你又回到了正轨。

将流引入编辑器

最后一个使用流验证 React 代码的选项是将流程集成到代码编辑器中。我使用的是流行的 Atom 编辑器,所以我将以它为例,但是有可能将 Flow 与其他编辑器集成。

在 Atom 中启用流功能(https://atom.io/ 编辑器,您需要安装linter-flow软件包:

安装后,您需要更改linter-flow的可执行路径设置。默认情况下,插件假定您在全局安装了 Flow,而您可能没有。您必须告诉插件在本地node_modules目录中查找流可执行文件:

你都准备好了。要验证这是否按预期工作,请从新的create-react-app安装打开App.js,并在文件顶部添加@flow指令。这将触发 Flow 中的错误,并应显示在 Atom 中:

Linter 还将突出显示导致 Flow 抱怨的问题代码:

在编辑器中使用流的方法,您甚至不需要保存,更不用说切换窗口来检查代码类型了,您所要做的就是编写它。

总结

在本章中,您了解了类型检查代码的重要性。您还了解了用于键入 check React 代码的工具 Flow。类型检查对于 React 应用很重要,因为在大多数情况下,它不需要对值执行运行时检查。这是因为流能够静态地跟踪代码路径,并确定是否所有内容都按预期使用。

然后,您将 Flow 本地安装到 React 应用,并学习了如何运行它。接下来,您学习了验证 React 组件的属性和状态值的基础知识。然后,您学习了如何验证函数类型以及如何强制子组件类型。

流可以在create-react-app开发服务器中使用,但必须先弹出。在create-react-app的未来版本中,作为开发服务器的一部分,可能会对运行流提供更好的集成支持。另一种选择是在代码编辑器(如 Atom)中安装流插件,并在编写代码时将错误显示在眼前。

在下一章中,您将学习如何在工具的帮助下使用 React 代码强制执行高质量级别。