Web全栈开发学习笔记—Part5 测试 React 应用—a.props.children 与 proptypes

目录

The components children, aka. props.children

State of the forms

References to components with ref

One point about components

PropTypes

ESlint


Displaying the login form only when appropriate

【在合适的时候展示登录表单】

让我们修改应用,让登录表单在默认情况下不显示

fullstack content

而当用户点击登录按钮时,登录表单再出现

fullstack content

用户可以通过单击 cancel 按钮关闭登录表单

首先将登录组件解耦出来:

import React from 'react'

const LoginForm = ({
   handleSubmit,
   handleUsernameChange,
   handlePasswordChange,
   username,
   password
  }) => {
  return (
    <div>
      <h2>Login</h2>

      <form onSubmit={handleSubmit}>
        <div>
          username
          <input
            value={username}
            onChange={handleUsernameChange}
          />
        </div>
        <div>
          password
          <input
            type="password"
            value={password}
            onChange={handlePasswordChange}
          />
      </div>
        <button type="submit">login</button>
      </form>
    </div>
  )
}

export default LoginForm

状态以及所有相关的函数都在组件外进行定义,并作为属性传递给组件。

注意,属性是通过变量解构出来的,而不是如下这种方式编写:

const LoginForm = (props) => {
  return (
    <div>
      <h2>Login</h2>
      <form onSubmit={props.handleSubmit}>
        <div>
          username
          <input
            value={props.username}
            onChange={props.handleChange}
            name="username"
          />
        </div>
        // ...
        <button type="submit">login</button>
      </form>
    </div>
  )
}

例如当访问 props 对象的 props.handleSubmit 属性时,属性被直接赋值给它们自己的变量。

一个快速的实现方式是改变 App 组件的 loginForm 函数:

const App = () => {
  const [loginVisible, setLoginVisible] = useState(false)
  // ...

  const loginForm = () => {
    const hideWhenVisible = { display: loginVisible ? 'none' : '' }
    const showWhenVisible = { display: loginVisible ? '' : 'none' }

    return (
      <div>
        <div style={hideWhenVisible}>
          <button onClick={() => setLoginVisible(true)}>log in</button>
        </div>
        <div style={showWhenVisible}>
          <LoginForm
            username={username}
            password={password}
            handleUsernameChange={({ target }) => setUsername(target.value)}
            handlePasswordChange={({ target }) => setPassword(target.value)}
            handleSubmit={handleLogin}
          />
          <button onClick={() => setLoginVisible(false)}>cancel</button>
        </div>
      </div>
    )
  }

  // ...
}

App 组件状态当前包含了 loginVisible 这个布尔值,定义了登录表单是否应当展示给用户。

loginVisible 可以通过两个按钮切换,每个按钮都有自己的事件处理函数,这些函数直接定义在组件中。

<button onClick={() => setLoginVisible(true)}>log in</button>

<button onClick={() => setLoginVisible(false)}>cancel</button>

组件是否可见被定义在了一个内联样式中inline ,即display 属性值是 none的时候,组件就看不到了:

const hideWhenVisible = { display: loginVisible ? 'none' : '' }
const showWhenVisible = { display: loginVisible ? '' : 'none' }

<div style={hideWhenVisible}>
  // button
</div>

<div style={showWhenVisible}>
  // button
</div>

我们再次使用三元运算符。如果 loginVisible 是 true,组件的 CSS 规则为:

display: 'none';

如果 loginVisible 是 false, display 不会接受任何与组件可见性相关的值。

The components children, aka. props.children

【组件的 children,又叫 props.children】

用于控制登录表单是否可见的代码,应当被视作它自己的逻辑实体,将它从 App 组件中解耦到自己的组件中。

实现一个新的 Togglable 组件,按照如下方式进行使用:

<Togglable buttonLabel='login'>
  <LoginForm
    username={username}
    password={password}
    handleUsernameChange={({ target }) => setUsername(target.value)}
    handlePasswordChange={({ target }) => setPassword(target.value)}
    handleSubmit={handleLogin}
  />
</Togglable>

之前的组件使用方法有一些不同。包含打开和关闭标签的组件将 LoginForm 包含在了里面。用 React 的术语来说, LoginForm 组件是 Togglable 的子组件。

任何我们想要打开或关闭的组件都可以通过 Togglable 进行包裹,例如:

<Togglable buttonLabel="reveal">
  <p>this line is at start hidden</p>
  <p>also this is hidden</p>
</Togglable>

Togglable 组件的代码如下:

import React, { useState } from 'react'

const Togglable = (props) => {
  const [visible, setVisible] = useState(false)

  const hideWhenVisible = { display: visible ? 'none' : '' }
  const showWhenVisible = { display: visible ? '' : 'none' }

  const toggleVisibility = () => {
    setVisible(!visible)
  }

  return (
    <div>
      <div style={hideWhenVisible}>
        <button onClick={toggleVisibility}>{props.buttonLabel}</button>
      </div>
      <div style={showWhenVisible}>
        {props.children}
        <button onClick={toggleVisibility}>cancel</button>
      </div>
    </div>
  )
}

export default Togglable

这个新的且比较有趣的代码就是 props.children, 它用来引用组件的子组件。子组件就是我们想要控制开启和关闭的 React 组件。

这一次,子组件被渲染到了用于渲染组件本身的代码中:

<div style={showWhenVisible}>
  {props.children}
  <button onClick={toggleVisibility}>cancel</button>
</div>

children被 React 自动添加了,并始终存在,只要这个组件定义了关闭标签 />

<Note
  key={note.id}
  note={note}
  toggleImportance={() => toggleImportanceOf(note.id)}
/>

这时 props.children 是一个空的数组。

Togglable 组件可被重用,我们可以用它创建新的切换可见性的功能,如对添加 Note 的表单添加类似的功能。

在这之前,我们把创建 Note 的表单解耦到自己的组件中。

const NoteForm = ({ onSubmit, handleChange, value}) => {
  return (
    <div>
      <h2>Create a new note</h2>

      <form onSubmit={onSubmit}>
        <input
          value={value}
          onChange={handleChange}
        />
        <button type="submit">save</button>
      </form>
    </div>
  )
}

export default NoteForm

把组件定义在 Togglable 组件中

<Togglable buttonLabel="new note">
  <NoteForm
    onSubmit={addNote}
    value={newNote}
    handleChange={handleNoteChange}
  />
</Togglable>

State of the forms

【表单的状态】

应用的状态当前位于 App 组件中。

React文档阐述了关于在哪里放置状态:

Often, several components need to reflect the same changing data. We recommend lifting the shared state up to their closest common ancestor.
通常,几个组件需要反映相同的变化数据。 我们建议将共享状态提升到它们最接近的共同祖先。

如果我们考虑一下表单的状态,例如一个新便笺的内容在创建之前,App 组件实际上并不需要它做任何事情。

可以将表单的状态移动到相应的组件中。

便笺的组件变化如下:

import React, {useState} from 'react' 

const NoteForm = ({ createNote }) => {
  const [newNote, setNewNote] = useState('') 

  const handleChange = (event) => {
    setNewNote(event.target.value)
  }

  const addNote = (event) => {
    event.preventDefault()
    createNote({
      content: newNote,
      important: Math.random() > 0.5,
    })

    setNewNote('')
  }

  return (
    <div>
      <h2>Create a new note</h2>

      <form onSubmit={addNote}>
        <input
          value={newNote}
          onChange={handleChange}
        />
        <button type="submit">save</button>
      </form>
    </div>
  )
}

newNote state 属性和负责更改它的事件处理程序已经从 App 组件移动到负责记录表单的组件。

现在只剩下一个props,即 createNote 函数,当创建新便笺时,表单将调用该函数。

用于创建新便笺的 addNote 函数接收一个新便笺作为参数,该函数是我们发送到表单的唯一props:

const App = () => {
  // ...
  const addNote = (noteObject) => {
    noteService
      .create(noteObject)
      .then(returnedNote => {
        setNotes(notes.concat(returnedNote))
      })
  }
  // ...
  const noteForm = () => (
    <Togglable buttonLabel='new note'>
      <NoteForm createNote={addNote} />
    </Togglable>
  )

  // ...
}

References to components with ref

【引用具有 ref 的组件】

我们当前的实现还不错,但有个地方可以改进

当我们创建了一个新的 Note,我们应当隐藏新建 Note 的表单。当前这个表单会持续可见,但隐藏这个表单有个小问题。可见性是透过Togglable 组件的visible 变量来控制的,我们怎么从外部进行访问呢?

从父组件来关闭这个表单有许多方法,我们使用 React 的 ref机制,它提供了一个组件的引用。

把 App 组件按如下修改:

import React, { useState, useRef } from 'react'
const App = () => {
  // ...
  const noteFormRef = useRef()
  const noteForm = () => (
    <Togglable buttonLabel='new note' ref={noteFormRef}>      <NoteForm createNote={addNote} />
    </Togglable>
  )

  // ...
}

useRef 方法就是用来创建 noteFormRef 引用,它被加到了能够控制表单创建的 Togglable 组件, noteFormRef 变量就代表了组件的引用。

同样要修改 Togglable 组件:

import React, { useState, useImperativeHandle } from 'react'
const Togglable = React.forwardRef((props, ref) => {  const [visible, setVisible] = useState(false)

  const hideWhenVisible = { display: visible ? 'none' : '' }
  const showWhenVisible = { display: visible ? '' : 'none' }

  const toggleVisibility = () => {
    setVisible(!visible)
  }

  useImperativeHandle(ref, () => {    return {      toggleVisibility    }  })
  return (
    <div>
      <div style={hideWhenVisible}>
        <button onClick={toggleVisibility}>{props.buttonLabel}</button>
      </div>
      <div style={showWhenVisible}>
        {props.children}
        <button onClick={toggleVisibility}>cancel</button>
      </div>
    </div>
  )
})
export default Togglable

创建组件的函数被包裹在了forwardRef 函数调用。利用这种方式可以访问赋给它的引用。

组件利用useImperativeHandle Hook来将toggleVisibility 函数能够被外部组件访问到。

现在可以在 Note 创建后,通过调用 noteFormRef.current.toggleVisibility() 控制表单的可见性了

const App = () => {
  // ...
  const addNote = (noteObject) => {
    noteFormRef.current.toggleVisibility()    noteService
      .create(noteObject)
      .then(returnedNote => {     
        setNotes(notes.concat(returnedNote))
      })
  }
  // ...
}

useImperativeHandle函数是一个 React hook,用于定义组件中的函数,该组件可以从组件外部调用。

One point about components

【关于组件的一个点】

当在 React 定义一个组件:

const Togglable = () => ...
  // ...
}

并按如下方式进行使用:

<div>
  <Togglable buttonLabel="1" ref={togglable1}>
    first
  </Togglable>

  <Togglable buttonLabel="2" ref={togglable2}>
    second
  </Togglable>

  <Togglable buttonLabel="3" ref={togglable3}>
    third
  </Togglable>
</div>

三个单独的组件都有自己的状态:

fullstack content

ref 属性用于为变量 togglable1、 togglable2 和 togglable3 中的每个组件分配一个引用。

PropTypes

Togglable 组件假定使用者会通过 buttonLabel 属性获传递按钮的文本。 如果我们忘记给组件定义:

<Togglable> buttonLabel forgotten... </Togglable>

应用会运行正常,但浏览器呈现一个没有 label text 的按钮。

如果希望使用 Togglable 组件时强制给按钮一个 label text 属性值,可以通过 prop-types 包来定义:

npm install prop-types

可以定义 buttonLabel 属性定义为 mandatory,或按如下加入required 这种字符串类型的属性:

import PropTypes from 'prop-types'

const Togglable = React.forwardRef((props, ref) => {
  // ..
})

Togglable.propTypes = {
  buttonLabel: PropTypes.string.isRequired
}

如果这时属性是 undefined,控制台就会展示如下的错误信息

fullstack content

虽然应用程序仍然可以工作,没有强迫我们定义 PropTypes。 但通过控制台来提醒,因为不处理红色警告是非常不专业的做法。

让我们 LoginForm 组件同样定义一个 PropTypes。

import PropTypes from 'prop-types'

const LoginForm = ({
   handleSubmit,
   handleUsernameChange,
   handlePasswordChange,
   username,
   password
  }) => {
    // ...
  }

LoginForm.propTypes = {
  handleSubmit: PropTypes.func.isRequired,
  handleUsernameChange: PropTypes.func.isRequired,
  handlePasswordChange: PropTypes.func.isRequired,
  username: PropTypes.string.isRequired,
  password: PropTypes.string.isRequired
}

如果传递给 prop 的类型是错误的。例如尝试定义 handleSubmit 成 string,那结果会出现如下警告:

fullstack content

ESlint

ESlint代码控制样式。

Create-react-app 已经默认为项目安装好了 ESlint, 所以我们需要做的就是定义自己的.eslintrc.js 文件

注意: 不要运行 eslint-- init 命令。 它将安装与 create-react-app 创建的配置文件不兼容的最新版本的 ESlint!

下面开始测试前端,为避免不想要和不相关的 lint 错误,先安装eslint-plugin-jest 库:

npm install --save-dev eslint-plugin-jest

为 .eslintrc.js 添加如下内容

module.exports = {
  "env": {
      "browser": true,
      "es6": true,
      "jest/globals": true 
  },
  "extends": [ 
      "eslint:recommended",
      "plugin:react/recommended"
  ],
  "parserOptions": {
      "ecmaFeatures": {
          "jsx": true
      },
      "ecmaVersion": 2018,
      "sourceType": "module"
  },
  "plugins": [
      "react", "jest"
  ],
  "rules": {
      "indent": [
          "error",
          2  
      ],
      "linebreak-style": [
          "error",
          "unix"
      ],
      "quotes": [
          "error",
          "single"
      ],
      "semi": [
          "error",
          "never"
      ],
      "eqeqeq": "error",
      "no-trailing-spaces": "error",
      "object-curly-spacing": [
          "error", "always"
      ],
      "arrow-spacing": [
          "error", { "before": true, "after": true }
      ],
      "no-console": 0,
      "react/prop-types": 0
  },
  "settings": {
    "react": {
      "version": "detect"
    }
  }
}

创建一个 .eslintignore 添加如下内容:

node_modules
build

现在 build 和 node_modules 这两个文件夹就不会被 lint 到了

同样为 lint 创建一个 npm 脚本:

{
  // ...
  {
    "scripts": {
    "start": "react-scripts start",
    "build": "react-scripts build",
    "test": "react-scripts test",
    "eject": "react-scripts eject",
    "server": "json-server -p3001 db.json",
    "eslint": "eslint ."  },
  // ...
}

组件 Togglable 导致了一些烦人的警告:组件定义缺少显示名:

fullstack content

React-devtools 还显示组件没有名称:

fullstack content

这个问题很容易解决

import React, { useState, useImperativeHandle } from 'react'
import PropTypes from 'prop-types'

const Togglable = React.forwardRef((props, ref) => {
  // ...
})

Togglable.displayName = 'Togglable'
export default Togglable

猜你喜欢

转载自blog.csdn.net/qq_39389123/article/details/112383511