七、验证部件属性

在本章中,您将了解 React 组件中的属性验证。乍一看,这似乎很简单,但这是一个重要的话题,因为它会带来无 bug 的组件。首先,我们将讨论可预测的结果,以及这将如何导致组件在整个应用中可移植。

接下来,我们将浏览 React 附带的一些简单类型检查属性验证器的示例。然后,我们将介绍一些更复杂的属性验证场景。最后,我们将用一个如何实现您自己的自定义验证器的示例来结束本章。

知道该期待什么

React 组件中的属性验证类似于 HTML 表单中的字段验证。验证表单字段的基本前提是用户知道他们提供了一个不可接受的值。理想情况下,验证错误消息足够清晰,用户可以轻松修复该情况。使用 React 组件属性验证,我们正尝试做同样的事情,以便于修复提供意外值的情况。属性验证增强了开发人员体验,而不是用户体验。

属性验证的关键方面是了解作为属性值传递到组件中的内容。例如,如果我们期望的是一个数组,而传递的是一个布尔值,则可能会出错。如果使用内置的 React 验证机制验证属性值,那么您就知道传递了意外的消息。如果组件需要一个数组以便调用map()方法,那么如果传递了布尔值,它将失败,因为它没有map()方法。但是,在发生此故障之前,您将看到属性验证警告。

我们的想法不是通过属性验证快速失败。这是为了向开发者提供信息。当属性验证失败时,您会知道某些内容是作为组件属性提供的,而不应该这样做。因此,找到值在代码中传递的位置并修复它是一个简单的问题。

Fail fast 是软件的一种体系结构特性,在这种特性中,系统将完全崩溃,而不是在不一致的状态下继续运行。

推广便携式组件

当我们知道从组件属性中可以得到什么时,使用组件的上下文就变得不那么重要了。这意味着,只要组件能够验证其属性值,在哪里使用组件就不重要了;任何功能都可以轻松使用它。

如果您想要一个跨应用功能可移植的通用组件,您可以编写组件验证代码,也可以编写在渲染时运行的防御代码。防御性编程的挑战在于它稀释了声明性组件的价值。使用 React 样式的属性验证,可以避免编写防御代码。相反,属性验证机制在某些内容未通过时发出警告,通知您需要修复某些内容。

防御代码是在生产环境中运行时需要考虑大量边缘情况的代码。当在开发过程中无法检测到潜在问题时,如 React 组件属性验证,需要进行防御性编码。

简单属性验证器

在本节中,您将学习如何使用PropTypes对象中提供的简单属性类型验证器。然后,您将学习如何接受任何属性值,以及如何将属性设置为 required而不是可选

基本类型验证

让我们来看看处理最原始类型的 JavaScript 值的验证器。您将经常使用这些验证器,因为您希望知道属性是字符串还是函数。本示例还将向您介绍在组件上设置验证所涉及的机制。这是组件本身;它只是使用基本标记呈现一些属性:

import React, { PropTypes } from 'react'; 

const MyComponent = ({ 
  myString, 
  myNumber, 
  myBool, 
  myFunc, 
  myArray, 
  myObject, 
}) => ( 
  <section> 
    { /* Strings and numbers can be rendered 
         just about anywhere. */ } 
    <p>{myString}</p> 
    <p>{myNumber}</p> 

    { /* Booleans are typically used as property  
         values. */ } 
    <p><input type="checkbox" defaultChecked={myBool} /></p> 

    { /* Functions can return values, or be assigned as 
         event handler property values. */ } 
    <p>{myFunc()}</p> 

    { /* Arrays are typically mapped to produce new  
         JSX elements. */ } 
    <ul> 
      {myArray.map(i => ( 
        <li key={i}>{i}</li> 
      ))} 
    </ul> 

    { /* Objects typically use their properties in some  
         way. */ } 
    <p>{myObject.myProp}</p> 
  </section> 
); 

// The "propTypes" specification for this component. 
MyComponent.propTypes = { 
  myString: PropTypes.string, 
  myNumber: PropTypes.number, 
  myBool: PropTypes.bool, 
  myFunc: PropTypes.func, 
  myArray: PropTypes.array, 
  myObject: PropTypes.object, 
}; 

export default MyComponent; 

属性验证机制有两个关键部分。首先,您拥有静态propTypes属性。这是类级属性,不是实例属性。当 React 找到propTypes时,它使用此对象作为组件的属性规范。其次,您有PropTypes工具,它有几个内置的验证器功能。

在本例中,MyComponent有六个属性,每个属性都有自己的类型。当您查看propTypes规范时,应该清楚该组件将接受哪种类型的值。现在让我们来看看这个,并用一些属性值渲染这个组件:

import React from 'react'; 
import { render as renderJSX } from 'react-dom'; 

import MyComponent from './MyComponent'; 

// The properties that we'll pass to the component. 
// Each property is a different type, and corresponds 
// to the "propTypes" spec of the component. 
const validProps = { 
  myString: 'My String', 
  myNumber: 100, 
  myBool: true, 
  myFunc: () => 'My Return Value', 
  myArray: ['One', 'Two', 'Three'], 
  myObject: { myProp: 'My Prop' }, 
}; 

// These properties don't correspond to the "<MyComponent>" 
// spec, and will cause warnings to be logged. 
const invalidProps = { 
  myString: 100, 
  myNumber: 'My String', 
  myBool: () => 'My Reaturn Value', 
  myFunc: true, 
  myArray: { myProp: 'My Prop' }, 
  myObject: ['One', 'Two', 'Three'], 
}; 

// Renders "<MyComponent>" with the given "props". 
function render(props) { 
  renderJSX( 
    (<MyComponent {...props} />), 
    document.getElementById('app') 
  ); 
} 

render(validProps); 
render(invalidProps); 

第一次渲染<MyComponent>时,它使用validProps属性。这些值都符合组件属性规范,因此控制台中不会记录任何警告。第二次,使用了invalidProps属性,但属性验证失败,因为每个属性都使用了错误的类型。控制台输出应如下所示:

Invalid prop `myString` of type `number` supplied to `MyComponent`, expected `string` 
Invalid prop `myNumber` of type `string` supplied to `MyComponent`, expected `number` 
Invalid prop `myBool` of type `function` supplied to `MyComponent`, expected `boolean` 
Invalid prop `myFunc` of type `boolean` supplied to `MyComponent`, expected `function` 
Invalid prop `myArray` of type `object` supplied to `MyComponent`, expected `array` 
Invalid prop `myObject` of type `array` supplied to `MyComponent`, expected `object` 
TypeError: myFunc is not a function 

最后一个错误很有趣。您可以清楚地看到,属性验证正在抱怨无效的属性类型。这包括传递给myFunc的无效函数。因此,尽管对属性进行了类型检查,JSX 仍然会尝试调用值 is(如果它是函数)。

以下是渲染输出的外观:

Basic type validation

同样,React 组件中属性验证的目的是帮助您在开发过程中发现 bug。当 React 处于生产模式时,属性验证将完全关闭。这意味着您不必担心编写昂贵的属性验证代码;它永远不会在生产中运行。但是,错误仍然会发生,因此请修复它。

需要值

让我们对前面的示例进行一些调整。组件属性规范需要特定类型的值,但只有当属性作为 JSX 属性传递给组件时,才会检查这些值。例如,您可能完全省略了myFunc属性,它将被验证。值得庆幸的是,PropTypes函数有一个工具,可以让您指定必须提供的属性以及它必须具有特定的值。以下是修改后的组件:

const MyComponent = ({ 
  myString, 
  myNumber, 
  myBool, 
  myFunc, 
  myArray, 
  myObject, 
}) => ( 
  <section> 
    <p>{myString}</p> 
    <p>{myNumber}</p> 
    <p><input type="checkbox" defaultChecked={myBool} /></p> 
    <p>{myFunc()}</p> 
    <ul> 
      {myArray.map(i => ( 
        <li key={i}>{i}</li> 
      ))} 
    </ul> 
    <p>{myObject.myProp}</p> 
  </section> 
); 

// The "propTypes" specification for this component. Every 
// property is required, because they each have the 
// "isRequired" property. 
MyComponent.propTypes = { 
  myString: PropTypes.string.isRequired, 
  myNumber: PropTypes.number.isRequired, 
  myBool: PropTypes.bool.isRequired, 
  myFunc: PropTypes.func.isRequired, 
  myArray: PropTypes.array.isRequired, 
  myObject: PropTypes.object.isRequired, 
}; 

export default MyComponent; 

正如您所看到的,这个组件与我们在上一节中实现的组件之间没有太大的变化。主要区别在于propTypes中的规格。您会注意到,isRequired值被追加到所使用的每个类型验证器中。例如,string.isRequired意味着属性值必须是字符串,并且属性不能丢失。现在让我们对该组件进行测试:

import React from 'react'; 
import { render as renderJSX } from 'react-dom'; 

import MyComponent from './MyComponent'; 

const validProps = { 
  myString: 'My String', 
  myNumber: 100, 
  myBool: true, 
  myFunc: () => 'My Return Value', 
  myArray: ['One', 'Two', 'Three'], 
  myObject: { myProp: 'My Prop' }, 
}; 

// The same as "validProps", except it's missing 
// the "myObject" property. This will trigger a 
// warning. 
const missingProp = { 
  myString: 'My String', 
  myNumber: 100, 
  myBool: true, 
  myFunc: () => 'My Return Value', 
  myArray: ['One', 'Two', 'Three'], 
}; 

// Renders "<MyComponent>" with the given "props". 
function render(props) { 
  renderJSX( 
    (<MyComponent {...props} />), 
    document.getElementById('app') 
  ); 
} 

render(validProps); 
render(missingProp); 

第一次使用所有正确的特性类型呈现组件。第二次,渲染组件时不使用myObject属性。控制台错误应如下所示:

Required prop `myObject` was not specified in `MyComponent`. 
Cannot read property 'myProp' of undefined 

您可以看到,由于myObject的属性规范,显然需要为myObject属性提供对象值。最后一个错误是因为组件假定存在一个属性为myProp的对象。但这很好,因为由于属性验证,问题很容易发现和修复。

理想情况下,我们将在本例中验证myProp对象属性,因为它直接用于 JSX。JSX 标记中用于对象形状和形状的特定属性可以进行验证,您将在本章后面看到。

任何财产价值

本节的最后一个主题是any属性验证器。也就是说,它实际上并不关心它得到什么值,任何东西都是有效的,包括根本不传递值。事实上,isRequired验证器可以与any验证器组合使用。例如,如果您正在处理一个组件,并且您只想确保传递了某些内容,但还不确定需要哪种类型,那么您可以执行类似于myProp: PropTypes.any.isRequired的操作。

另一个原因是为了一致性而使用any属性验证器。每个组件都应该有属性规范。当我们不确定属性类型时,any验证器在一开始很有用。我们至少可以开始属性规范,然后随着事情的发展完善它。

现在让我们来看看一些代码:

import React, { PropTypes } from 'react'; 

// Renders a component with a header and a simple 
// progress bar, using the provided property 
// values. 
const MyComponent = ({ 
  label, 
  value, 
  max, 
}) => ( 
  <section> 
    <h5>{label}</h5> 
    <progress {...{ max, value }} /> 
  </section> 
); 

// These property values can be anything, as denoted by 
// the "PropTypes.any" prop type. 
MyComponent.propTypes = { 
  label: PropTypes.any, 
  value: PropTypes.any, 
  max: PropTypes.any, 
}; 

该组件实际上不会验证任何内容,因为其属性规范中的三个属性将接受任何内容。但是,这是一个很好的起点,因为我一眼就能看到这个组件使用的三个属性的名称。所以稍后,当我决定这些属性应该具有哪些类型时,更改很简单。现在让我们看看这个组件的运行情况:

import React from 'react'; 
import { render } from 'react-dom'; 

import MyComponent from './MyComponent'; 

render(( 
  <section> 
    { /* Passes a string and two numbers to 
         "<MyComponent>". Everything works as 
         expected. */ } 
    <MyComponent 
      label="Regular Values" 
      max={20} 
      value={10} 
    /> 

    { /* Passes strings instead of numbers to the 
         progress bar, but they're correctly 
         interpreted as numbers. */} 
    <MyComponent 
      label="String Values" 
      max="20" 
      value="10" 
    /> 

    { /* The "label" has no issue displaying 
         "MAX_SAFE_INTEGER", but the date that's 
         passed to "max" causes the progress bar 
         to break. */ } 
    <MyComponent 
      label={Number.MAX_SAFE_INTEGER} 
      max={new Date()} 
      value="10" 
    /> 
  </section> 
  ), 
  document.getElementById('app') 
); 

如您所见,字符串和数字在多个地方可以互换。因此,仅限于其中一种似乎过于严格。正如您将在下一节中看到的,React 具有其他属性验证器,允许您进一步限制组件中允许的属性值。

下面是渲染时组件的外观:

Any property value

类型和值验证器

在本节中,我们将了解 ReactPropTypes设施中提供的更高级的验证器功能。首先,您将了解检查可在 HTML 标记内呈现的值的元素和节点验证器。然后,您将看到如何检查特定类型,而不仅仅是上一节中看到的基本类型检查。最后,我们将实现查找特定值的验证。

可以呈现的事物

有时,您只需要确保属性值可以由 JSX 标记呈现。例如,如果属性值是数组,则无法通过将其放入{}来呈现。您必须将数组项映射到 JSX 元素。

如果组件将属性值作为子元素传递给其他元素,这种检查尤其有用。让我们看一个这样的例子:

import React, { PropTypes } from 'react'; 

const MyComponent = ({ 
  myHeader, 
  myContent, 
}) => ( 
  <section> 
    <header>{myHeader}</header> 
    <main>{myContent}</main> 
  </section> 
); 

// The "myHeader" property requires a React 
// element. The "myContent" property requires 
// a node that can be rendered. This includes 
// React elements, but also strings. 
MyComponent.propTypes = { 
  myHeader: PropTypes.element.isRequired, 
  myContent: PropTypes.node.isRequired, 
}; 

export default MyComponent; 

此组件有两个属性,需要可以呈现的值。myHeader属性需要一个element。这可以是任何 JSX 元素。myContent物业需要一个node。这可以是任何 JSX 元素或任何字符串值。让我们向该组件传递一些值并渲染它:

import React from 'react'; 
import { render } from 'react-dom'; 

import MyComponent from './MyComponent'; 

// Two React elements we'll use to pass to 
// "<MyComponent>" as property values. 
const myHeader = (<h1>My Header</h1>); 
const myContent = (<p>My Content</p>); 

render(( 
  <section> 
    { /* Renders as expected, both properties are passed 
         React elements as values. */ } 
    <MyComponent 
      {...{ myHeader }} 
      {...{ myContent }} 
    /> 

    { /* Triggers a warning because "myHeader" is expecting 
         a React element instead of a string. */ } 
    <MyComponent 
      myHeader="My Header" 
      {...{ myContent }} 
    /> 

    { /* Renders as expected. A string is a valid type for 
         the "myContent" property. */ } 
    <MyComponent 
      {...{ myHeader }} 
      myContent="My Content" 
    /> 

    { /* Renders as expected. An array of React elements 
         is a valid type for the "myContent" property. */ } 
    <MyComponent 
      {...{ myHeader }} 
      myContent={[myContent, myContent, myContent]} 
    /> 
  </section> 
  ), 
  document.getElementById('app') 
); 

正如您所看到的,myHeader属性对它将接受的值有更严格的限制。myContent属性将接受字符串、元素或元素数组。这两个验证器在从属性传递子数据时很重要,就像这个组件一样。例如,尝试将普通对象或函数作为子对象传递将不起作用,最好使用验证器检查这种情况。

下面是渲染时此组件的外观:

Things that can be rendered

需要特定类型

有时,您需要一个属性验证器来检查应用定义的类型。例如,假设您有以下用户类:

import cuid from 'cuid'; 

// Simple class the exposes an API that the 
// React component expects. 
export default class MyUser { 
  constructor(first, last) { 
    this.id = cuid(); 
    this.first = first; 
    this.last = last; 
  } 

  get name() { 
    return `${this.first} ${this.last}`; 
  } 
} 

现在,假设您有一个组件想要使用这个类的实例作为属性值。您需要一个验证器来检查属性值是否是MyUser的实例。让我们实现一个组件,该组件可以实现以下功能:

import React, { PropTypes } from 'react'; 
import MyUser from './MyUser'; 

const MyComponent = ({ 
  myDate, 
  myCount, 
  myUsers, 
}) => ( 
  <section> 
    { /* Requires a specific "Date" method. */ } 
    <p>{myDate.toLocaleString()}</p> 

    { /* Number or string works here. */ } 
    <p>{myCount}</p> 
    <ul> 
      { /* "myUsers" is expected to be an array of 
           "MyUser" instances. So we know that it's 
           safe to use the "id" and "name" property. */ } 
      {myUsers.map(i => ( 
        <li key={i.id}>{i.name}</li> 
      ))} 
    </ul> 
  </section> 
); 

// The properties spec is looking for an instance of 
// "Date", a choice between a string or a number, and 
// an array filled with specific types. 
MyComponent.propTypes = { 
  myDate: PropTypes.instanceOf(Date), 
  myCount: PropTypes.oneOfType([ 
    PropTypes.string, 
    PropTypes.number, 
  ]), 
  myUsers: PropTypes.arrayOf(PropTypes.instanceOf(MyUser)), 
}; 

export default MyComponent; 

这个组件有三个属性,它们需要特定的类型,每个属性都超出了本章中到目前为止所看到的基本类型验证器。现在让我们来看看这些:

  • myDate需要Date的实例。它使用instanceOf()函数来构建验证程序函数,以确保值实际上是Date实例。
  • myCount要求通过数字或字符串输入值。此验证器功能是通过组合oneOfTypePropTypes.number()PropTypes.string()创建的。
  • myUsers需要一个MyUser实例数组。此验证器由arrayOf()instanceOf()组合而成。

这个例子说明了我们可以通过组合 React 提供的属性验证器来处理的场景数量。以下是渲染输出的外观:

Requiring specific types

需要特定值

到目前为止,我们的重点是验证属性值的类型,但这并不总是我们想要检查的。有时,具体的价值观很重要。让我们看看如何验证特定属性值:

import React, { PropTypes } from 'react'; 

// Any one of these is a valid "level" 
// property value. 
const levels = new Array(10) 
  .fill(null) 
  .map((v, i) => i + 1); 

// This is the "shape" of the object we expect 
// to find in the "user" property value. 
const userShape = { 
  name: PropTypes.string, 
  age: PropTypes.number, 
}; 

const MyComponent = ({ 
  level, 
  user, 
}) => ( 
  <section> 
    <p>{level}</p> 
    <p>{user.name}</p> 
    <p>{user.age}</p> 
  </section> 
); 

// The property spec for this component uses 
// "oneOf()" and "shape()" to define the required 
// property vlues. 
MyComponent.propTypes = { 
  level: PropTypes.oneOf(levels), 
  user: PropTypes.shape(userShape), 
}; 

export default MyComponent; 

level属性应该是levels数组中的数字。这很容易使用oneOf()功能进行验证。user属性需要一个特定的形状。形状是对象的预期属性和类型。本例中定义的userShape需要name字符串和age编号。shape()instanceOf()之间的关键区别在于,我们不一定关心类型。我们可能只关心组件的 JSX 中使用的值。

让我们来看看这个组件是如何使用的:

import React from 'react'; 
import { render } from 'react-dom'; 

import MyComponent from './MyComponent'; 

render(( 
  <section> 
    { /* Works as expected. */ } 
    <MyComponent 
      level={10} 
      user={{ name: 'Name', age: 32 }} 
    /> 

    { /* Works as expected, the "online" 
         property is ignored. */ } 
    <MyComponent 
      user={{ name: 'Name', age: 32, online: false }} 
    /> 

    { /* Fails. The "level" value is out of range, 
         and the "age" property is expecting a 
         number, not a string. */ } 
    <MyComponent 
      level={11} 
      user={{ name: 'Name', age: '32' }} 
    /> 
  </section> 
  ), 
  document.getElementById('app') 
); 

下面是渲染组件时的外观:

Requiring specific values

编写自定义属性验证器

在最后一节中,您将看到如何构建自己的自定义属性验证函数,并在属性规范中应用它们。一般来说,只有在绝对必要的情况下,才应该实现自己的属性验证器。PropTypes中提供的默认验证器涵盖了广泛的场景。

但是,有时需要确保将非常特定的属性值传递给组件。请记住,这些将不会在生产模式下运行,因此对集合进行迭代是完全可以接受的。现在让我们实现两个自定义验证器函数:

import React from 'react'; 

const MyComponent = ({ 
  myArray, 
  myNumber, 
}) => ( 
  <section> 
    <ul> 
      {myArray.map(i => ( 
        <li key={i}>{i}</li> 
      ))} 
    </ul> 
    <p>{myNumber}</p> 
  </section> 
); 

MyComponent.propTypes = { 
  // Expects a property named "myArray" with a non-zero 
  // length. If this passes, we return null. Otherwise, 
  // we return a new error. 
  myArray: (props, name, component) => 
    (Array.isArray(props[name]) && 
      props[name].length) ? null : new Error( 
        `${component}.${name}: expecting non-empty array` 
      ), 

  // Expects a property named "myNumber" that's 
  // greater than 0 and less than 99\. Otherwise, 
  // we return a new error. 
  myNumber: (props, name, component) => 
    (Number.isFinite(props[name]) && 
      props[name] > 0 && 
      props[name] < 100) ? null : new Error( 
        `${component}.${name}: ` + 
            `expecting number between 1 and 99` 
      ), 
}; 

export default MyComponent; 

myArray属性需要一个非空数组,myNumber属性需要一个大于0小于100的数字。让我们尝试向这些验证器传递一些数据:

import React from 'react'; 
import { render } from 'react-dom'; 

import MyComponent from './MyComponent'; 

render(( 
  <section> 
    { /* Renders as expected... */ } 
    <MyComponent 
      myArray={['first', 'second', 'third']} 
      myNumber={99} 
    /> 

    { /* Both custom validators fail... */ } 
    <MyComponent 
      myArray={[]} 
      myNumber={100} 
    /> 
  </section> 
  ), 
  document.getElementById('app') 
); 

如您所见,第一个元素呈现得很好,因为两个验证器都返回 null。但是,空数组和数字100会导致两个验证器返回错误:

MyComponent.myArray: expecting non-empty array 
MyComponent.myNumber: expecting number between 1 and 99 

以下是渲染输出的外观:

Writing custom property validators

总结

本章的重点是 React 组件属性验证。当您实现属性验证时,您知道会发生什么;这促进了可移植性。组件不关心属性值如何传递给它,只要它们是有效的。

然后,我们浏览了几个使用基本 React 验证器检查基本 JavaScript 类型的示例。您还了解到,如果需要属性,则必须明确这一点。然后,您学习了如何通过组合 React 附带的内置验证器来验证更复杂的属性值。

最后,您实现了自己的自定义验证器函数,以执行超出内置 React 验证器可能实现的验证。在下一章中,您将学习如何使用新数据和行为扩展 React 组件。