vlambda博客
学习文章列表

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》Reaction快速入门

Chapter 6. Getting Started with React

本章描述了 React 编程的基础知识。我们将介绍为 React 前端创建基本功能所需的技能。在 JavaScript 中,我们使用 ES6 语法,因为它提供了许多使编码更简洁的特性。

在本章中,我们将看到以下内容:

  • How to create React components
  • How to use state and props in components
  • Useful ES6 features 
  • What JSX is
  • How to handle events and forms in React

Technical requirements


在本书中,我们使用 Windows 操作系统,但所有工具也可用于 Linux 和 macOS。

Basic React components

根据 Facebook 的说法,React 是一个用于用户界面的 JavaScript 库。从版本 15 开始,React 是在 MIT 许可下开发的。 React 是基于组件的,组件是独立且可重用的。这些组件是 React 的基本 构建块。当你开始使用 React 开发用户界面时,最好从创建开始 一个模拟接口。这样,很容易确定您必须创建什么样的组件以及它们如何交互。

从下面的 mock 图中,我们可以看到如何将用户界面拆分为组件。在这种情况下,将有一个应用程序根组件、一个搜索栏组件、一个表格组件和一个表格行组件:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》Reaction快速入门

然后可以将组件排列在以下树层次结构中。使用 React 要理解的重要一点是数据流是从父组件流向子组件的:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》Reaction快速入门

React 使用虚拟 DOM 来选择性地重新渲染用户界面,这使得它更具成本效益。 Virtual DOM 是 DOM 的轻量级copy,对虚拟 DOM 的操作比真实 DOM 快得多。更新虚拟 DOM 后,React 会将其与更新运行之前从虚拟 DOM 中获取的快照进行比较。比较之后,React 就知道哪些部分被改变了,只有这些部分被更新到真实的 DOM 中。

可以使用 JavaScript 函数或 ES6 JavaScript 类来定义 React 组件。我们将在下一节更深入地介绍 ES6。以下是呈现 Hello World 文本的简单组件源代码。第一个代码块使用 JavaScript 函数:

// Using JavaScript function
function Hello() {
  return <h1>Hello World</h1>;
}

而这个是使用类来创建一个组件:

// Using ES6 class
class Hello extends React.Component {
  render() {
    return <h1>Hello World</h1>;
  }
}

使用该类实现的组件包含必需的 render() 方法。此方法显示并更新组件的渲染输出。用户定义组件的名称应以大写字母开头。

让我们更改组件的 render 方法,并在其中添加新的 header 元素:

class App extends Component {
  render() {
    return (
      <h1>Hello World!</h1>
      <h2>From my first React app</h2>
    );
  }
}

当您运行应用程序时,您会收到 相邻的 JSX 元素必须包含在封闭标记中 错误。要修复此错误,我们必须将 headers 包装在一个元素中,例如  div;从 React 16.2 版本开始,我们还可以使用 Fragments,看起来像空的 JSX 标签:

// Wrap headers in div
class App extends Component {
  render() {
    return (
      <div>
        <h1>Hello World!</h1>
        <h2>From my first React app</h2>
      </div>
    );
  }
}

// Or using fragments
class App extends Component {
  render() {
    return (
      <>
        <h1>Hello World!</h1>
        <h2>From my first React app</h2>
      </>
    );
  }
}

让我们更仔细地看一下我们在上一章中使用 create-react-app 创建的第一个 React 应用程序。根目录下Index.js文件源码如下: 

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

ReactDOM.render(<App />, document.getElementById('root'));
registerServiceWorker();

在文件的开头,有 import 将组件或资产加载到我们的文件中的语句。例如,第二行从 node_modules 文件夹中导入 react-dom 包,第四行行导入 App (根文件夹中的 App.js文件)组件。  react-dom 包为我们提供了特定于 DOM 的方法。要将 React 组件渲染到 DOM,我们可以使用 render 方法来自 react-dom 包裹。第一个参数是要渲染的组件,第二个参数是 element 或容器将被渲染。在这种情况下, root 元素是 <div id="root"></div> ,可以在 public 文件夹内的 index.html 文件中找到。请参阅以下 index.html 文件:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, shrink-to-fit=no">
    <meta name="theme-color" content="#000000">

    <link rel="manifest" href="%PUBLIC_URL%/manifest.json">
    <link rel="shortcut icon" href="%PUBLIC_URL%/favicon.ico">

    <title>React App</title>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>

以下源代码显示了我们第一个 React 应用程序中的 App.js 组件。您可以看到 import 也适用于资产,例如图像和样式表。在源代码的末尾,有一个 export 语句,用于导出组件,并通过 import 提供给其他组件。每个文件只能有一个默认导出,但可以有多个命名导出:

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, edit <code>src/App.js</code> and save to reload.
        </p>
      </div>
    );
  }
}

export default App;

以下示例显示如何导入默认导出和命名导出:

import React from 'react' // Import default value
import { Component } from 'react' // Import named value

导出如下所示:

export default React // Default export
export {Component} // Named export

Basics of ES6

ES6ECMAScript 2015)于 2015 年发布,它 < span>引入了 很多新功能。 ECMAScript 是一种标准化的脚本语言 JavaScript 是它的一种实现。在这里,我们将介绍 ES6 中发布的最重要 特性,我们将在下一节中使用这些特性。

Understanding constants

常量或不可变变量可以使用 const 关键字来定义。当使用 const 关键字时,变量内容 不能 被重新赋值:

const PI = 3.14159;

 const 的作用域是块作用域,与 let 的作用域相同。这意味着 const 变量只能在定义它的块内使用。实际上,块是大括号 { } 之间的区域。以下示例代码显示了作用域的工作原理。第二个console.log 语句给出了错误,因为我们试图在范围之外使用 total 变量:

var count = 10;
if(count > 5) {
  const total = count * 2;
  console.log(total); // Prints 20 to console
}
console.log(total); // Error, outside the scope

很高兴知道如果 const 是对象或数组,则可以更改内容。以下示例说明:

const myObj = {foo : 3};
myObj.foo = 5; // This is ok

Arrow functions

箭头函数使函数声明 much 更加紧凑。在 JavaScript 中定义函数的传统方式是使用 function 关键字。以下函数获取一个参数并仅返回参数值:

function hello(greeting) {
    return greeting;
}

通过使用 ES6 箭头函数,函数如下所示:

const hello = greeting => { greeting }

// function call
hello('Hello World'); // returns Hello World

如果您有多个参数,则必须将参数括在括号中并用逗号分隔参数。以下函数获取两个参数并返回参数的总和。如果函数体是表达式,则不需要使用 return 关键字。表达式总是从函数中隐式返回:

const calcSum = (x, y) => { x + y }

// function call
calcSum(2, 3); // returns 5

如果函数没有任何参数,则语法如下:

() => { ... }

Template literals

模板文字可以使用 来连接字符串。连接字符串的传统方法是使用加号运算符:

var person = {firstName: 'John', lastName: 'Johnson'};
var greeting = "Hello " + ${person.firstName} + " " + ${person.lastName};

使用模板文字,语法如下。您必须使用反引号 (` `) 而不是单引号或双引号:

var person = {firstName: 'John', lastName: 'Johnson'};
var greeting = `Hello ${person.firstName} ${person.lastName}`;

Classes and inheritance

ES6 中的类定义类似于 Java 或 C# 等其他面向对象的语言。定义类的关键字是class。类可以有字段、构造函数和类方法。以下示例代码显示了 ES6 类:

class Person {
    constructor(firstName, lastName) {
        this.firstName = firstName;
        this.lastName = lastName;
    }  
}

继承是通过一个 extends 关键字完成的。以下 sample 代码显示了一个 Employee 类,它继承了一个 < code class="literal">Person 类。因此,它继承 来自父类的所有字段,并且可以拥有自己的特定于员工的字段。在构造函数中,我们首先使用 super 关键字调用父类构造函数。该调用是必需的,如果丢失,您将收到错误消息:

class Employee extends Person {
    constructor(firstName, lastName, title, salary) {
        super(firstName, lastName);
        this.title= title;
        this.salary = salary;
    }  
}

尽管 ES6 已经很老了,但它仍然只被现代 Web 浏览器部分支持。 Babel 是一个 JavaScript 编译器,用于将 ES6 编译为兼容所有浏览器的旧版本。你可以在 Babel 网站(https://babeljs.io)上测试编译器。以下屏幕截图显示了编译回旧 JavaScript 语法的箭头函数:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》Reaction快速入门

JSX and styling

JSX 是 JavaScript 的语法扩展。将 JSX 与 React 一起使用并不是强制性的,但有一些好处可以使开发更容易。例如,JSX 可以防止注入攻击,因为在呈现 它们 之前,所有值都在 JSX 中进行了转义。最有用的功能是您可以将 JavaScript 表达式嵌入到 JSX 中,方法是用 with 大括号在后面的章节中使用了很多。在这个例子中,我们可以在使用 JSX 时访问组件的 props。组件道具将在下一节中介绍:

class Hello extends React.Component {
  render() {
    return <h1>Hello World {this.props.user}</h1>;
  }
}

您还可以将 JavaScript 表达式作为道具传递:

<Hello count={2+2} />

JSX 被 Babel 编译为React.createElement() 调用。您可以对 React JSX 元素使用内部或外部样式。以下是内联样式的两个示例。第一个直接在 div 元素内定义样式:

<div style={{height: 20, width: 200}}>
  Hello
</div>

第二个示例首先创建样式对象,然后将其用于 div 元素。对象名称应使用 camelCase 命名约定:

const divStyle = {
  color: 'red',
  height: 30
};

const MyComponent = () => (
  <div style={divStyle}>Hello</div>
);

如上一节所示,您可以将样式表导入 React 组件。要从外部 CSS 文件中引用类,您应该使用 className 属性:

import './App.js';

...

<div className="App-header">
  This is my app
</div>

Props and state

Props 和 state 是用于渲染组件的输入 data。 props 和 state 实际上都是 JavaScript 对象,当 props 或 state 发生变化时,组件会重新渲染。

props 是不可变的,因此组件无法更改其 props。 props 是从父组件接收的。组件可以通过 this.props 对象访问props。例如,看一下以下组件:

class Hello extends React.Component {
  render() {
    return <h1>Hello World {this.props.user}</h1>;
  }
}

父组件可以通过以下方式向 Hello 组件发送 props:

<Hello user="John" />

Hello 组件被渲染时,它会显示 Hello World John 文本。

可以在组件内部更改状态。状态的初始值在组件的构造函数中给出。可以使用 this.state 对象访问状态。状态的范围是组件,因此它不能在定义它的组件之外使用。正如您在下面的示例中看到的,道具作为参数传递给构造函数,并且状态在构造函数中被初始化。然后可以使用大括号在 JSX 中呈现状态值, {this.state.user}

class Hello extends React.Component {
  constructor(props) {
    super(props);
    this.state = {user: 'John'}
  }

  render() {
    return <h1>Hello World {this.state.user}</h1>;
  }
}

状态可以包含多个不同类型的值,因为它是一个 JavaScript 对象,如下例所示:

  constructor(props) {
    super(props);
    this.state = {firstName: 'John', lastName: 'Johnson', age: 30}
  }

使用 setState 方法更改状态的值:

this.setState({firstName: 'Jim', age: 31});  // Change state value

您永远不应该通过 使用 等号运算符来更新状态,因为这样 React 不会重新渲染组件。更改状态的唯一方法是使用 setState 方法,该方法会触发重新渲染:

this.state.firstName = 'Jim'; // WRONG

 setState 方法是异步的,因此您无法确定何时更新状态。  setState 方法有一个回调函数,在状态更新时执行。

状态的使用始终是可选的,它增加了组件的复杂性。只有 props 的组件被称为 stateless< /span> 组件。当输入相同时,它总是会呈现相同的输出,这意味着它们非常容易测试。同时具有状态和 props 的组件称为 stateful 组件。以下是简单无状态组件的示例,它是使用类定义的。您还可以使用以下函数定义它:

export default class MyTitle extends Component {
  render() {
    return (
     <div>
      <h1>{this.props.text}</h1>
     </div>
    );
 };
};

// The MyTitle component can be then used in other component and text value is passed to props
<MyTitle text="Hello" />
// Or you can use other component's state
<MyTitle text={this.state.username} />

如果要更新依赖于当前状态的状态值,则应将更新函数传递给 setState() 方法而不是对象。演示这种情况的一个常见案例是此处显示的反例:

// This solution might not work correctly
incerementCounter = () => {
 this.setState({count: this.state.count + 1});
}

// The correct way is the following
incrementCounter = () => {
  this.setState((prevState) => {
    return {count: prevState.count + 1}
  });
}

Component life cycle methods

React 组件有许多可以覆盖的生命周期方法。这些方法在组件生命周期的某些阶段执行。 生命周期方法的名称是合乎逻辑的,您几乎可以猜到 什么时候 它们将被执行。带有前缀的生命周期方法在发生任何事情之前执行,带有前缀的方法在发生事情之后执行。挂载是组件生命周期的一个阶段,它是组件被创建并插入 DOM 的时刻。 我们已经介绍过的两个生命周期方法在组件挂载时执行: constructor()render()

挂载阶段一个有用的方法是 componentDidMount(),在组件被挂载后调用。此方法适用于调用一些 REST API 来获取数据,例如。以下示例代码给出了使用 componentDidMount() 方法的示例。

在下面的示例代码中,我们首先将 this.state.user 的初始值设置为 John。然后,当组件被挂载时,我们将值更改为 Jim

class Hello extends React.Component {
  constructor(props) {
    super(props);
    this.state = {user: 'John'}
  }

  componentDidMount() {
    this.setState({user: 'Jim'});
  }

  render() {
    return <h1>Hello World {this.state.user}</h1>;
  }
}

还有一个 componentWillMount() 在组件挂载之前调用的生命周期方法,但Facebook建议不要使用它,因为它可能用于内部开发目的.

shouldComponentUpdate() 方法在 state 或 props 已经更新并且组件被渲染之前被调用。该方法将新的道具作为第一个参数,将新的状态作为第二个参数,并返回布尔值。如果返回值为true,则重新渲染组件;否则,它不会被重新渲染。此方法允许您避免无用的渲染并提高性能:

shouldComponentUpdate(nextProps, nextState) {
  // This function should return a boolean, whether the component should re-render.
  return true; 
}

componentWillUnmount() 生命周期方法在组件从 DOM 中移除之前被调用。这是清理资源、清除计时器或取消请求的好方法。

错误边界是在其子组件树中捕获 JavaScript 错误的组件。他们还应该记录这些错误并在用户界面中显示回退。为此,有一个名为 componentDidCatch() 的生命周期方法。它适用于 React 组件,例如标准 JavaScript catch 块。

Handling lists with React

对于列表处理,我们引入了 new JavaScript 方法, map()< /code>,这在您必须操作列表时很方便。 map() 方法创建一个新数组,其中包含对原始数组中每个元素调用函数的结果。在以下示例中,每个数组元素都乘以 2:

const arr = [1, 2, 3, 4];

const resArr = arr.map(x => x * 2); // resArr = [2, 4, 6, 8]

map() 方法还有 index 第二个参数,这在 React 中处理列表时很有用。 React 中的列表项需要一个唯一键,用于检测已更改、添加或删除的行。

以下示例显示了将整数数组转换为列表项数组并在 ul 元素中呈现它们的组件:

class App extends React.Component {
  render() { 
    const data = [1, 2, 3, 4, 5];
    const rows = data.map((number, index) =>
     <li key={index}>Listitem {number}</li>
    );

    return (
     <div>
      <ul>{rows}</ul>
     </div>
    );
  }
}

以下屏幕截图显示了组件在渲染时的样子:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》Reaction快速入门

如果数据是一个对象数组,那么更好 以表格形式呈现数据。这个想法和列表一样,但是现在我们只是将数组映射到表格行并在表格元素中呈现它们,如下面的代码所示:

class App extends Component {
  render() { 
    const data = [{brand: 'Ford', model: 'Mustang'}, 
    {brand:'VW', model: 'Beetle'}, {brand: 'Tesla', model: 'Model S'}];
    const tableRows = data.map((item, index) =>
     <tr key={index}><td>{item.brand}</td><td>{item.model}</td></tr>
    );

    return (
     <div>
      <table><tbody>{tableRows}</tbody></table>
     </div>
    );
  }
}

以下屏幕截图显示了组件在渲染时的样子:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》Reaction快速入门

Handling events with React

React 中的事件处理类似于处理 DOM 元素事件。与 HTML 事件处理相比,不同之处在于事件 naming 在 React 中使用 camelCase。以下示例代码为按钮添加了一个事件侦听器,并在按下按钮时显示警报消息:

class App extends React.Component {
  // This is called when the button is pressed
  buttonPressed = () => {
    alert('Button pressed');
  }

  render() { 
    return (
     <div>
      <button onClick={this.buttonPressed}>Press Me</button>
     </div>
    );
  }
}

在 React 中,您不能从事件处理程序返回 false 以防止默认行为。相反,您应该调用 preventDefault() 方法。在下面的示例中,我们正在使用一个表单并且我们想要阻止表单提交:

class MyForm extends React.Component {
  // This is called when the form is submitted
  handleSubmit(event) {
    alert('Form submit');
    event.preventDefault();  // Prevents default behavior
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <input type="submit" value="Submit" />
      </form>
    );
  }
}

Handling forms with React

表单处理与 React 有点不同。提交时,HTML 表单将导航到下一页。一个常见的情况是 我们希望调用一个 JavaScript 函数,该函数在提交后可以访问表单数据并避免导航到下一个页。我们已经在上一节中介绍了如何使用 preventDefault() 来避免提交。 

让我们首先创建一个带有一个输入字段和提交按钮的简约表单。为了能够获取输入字段的值,我们使用了 onChange 事件处理程序。当输入字段的值改变时,新的值将被保存到状态。  this.setState({text: event.target.value}); 语句从输入字段中获取值并将其保存到名为 文本 。最后,我们将在用户按下提交按钮时显示键入的值。以下是我们第一个表单的源代码:

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {text: ''};
  }

  // Save input box value to state when it has been changed
  inputChanged = (event) => {
    this.setState({text: event.target.value});
  }

  handleSubmit = (event) => {
    alert(`You typed: ${this.state.text}`);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <input type="text" onChange={this.inputChanged} value={this.state.text}/>
        <input type="submit" value="Press me"/>
      </form>
    );
  } 
}

下面是我们的表单组件在 Submit 按钮被按下后的截图:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》Reaction快速入门

现在是查看 React 开发人员工具的好时机,它们是调试 React 应用程序的便捷工具。如果我们使用我们的 React 表单应用程序打开 React 开发者工具并在输入字段中输入一些内容,我们可以看到 value的状态变化。我们可以查看 的当前值道具和状态。以下屏幕截图显示了当我们在输入字段中输入内容时状态如何变化:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》Reaction快速入门

通常,我们在表单中有多个输入字段。处理多个输入字段的一种方法是添加与输入字段一样多的更改处理程序。但这会产生很多样板代码,这是我们想要避免的。因此,我们将名称属性添加到我们的输入字段中,我们可以在更改处理程序中使用它来识别哪个输入字段触发更改处理程序。输入字段的名称属性值必须与我们要保存该值的状态的名称相同。

处理程序现在如下所示。如果触发处理程序的输入字段是名字字段,则 event.target.namefirstName 和键入的值将保存到名为 firstName 的状态。通过这种方式,我们可以使用一个更改处理程序来处理所有输入字段:

 inputChanged = (event) => {
    this.setState({[event.target.name]: event.target.value});
  }

以下是该组件的完整源代码:

class App extends Component {
  constructor(props) {
    super(props);
    this.state = {firstName: '', lastName: '', email: ''};
  }

  inputChanged = (event) => {
    this.setState({[event.target.name]: event.target.value});
  }

  handleSubmit = (event) => {
    alert(`Hello ${this.state.firstName} ${this.state.lastName}`);
    event.preventDefault();
  }

  render() {
    return (
      <form onSubmit={this.handleSubmit}>
        <label>First name </label>
        <input type="text" name="firstName" onChange={this.inputChanged} value={this.state.firstName}/><br/>
        <label>Last name </label>
        <input type="text" name="lastName" onChange={this.inputChanged} value={this.state.lastName}/><br/>
        <label>Email </label>
        <input type="email" name="email" onChange={this.inputChanged} value={this.state.email}/><br/>
        <input type="submit" value="Press me"/>
      </form>
    );
  } 
}

下面是我们的form组件在Submit 按钮已被按下:

读书笔记《hands-on-full-stack-development-with-spring-boot-2-0-and-react》Reaction快速入门

Summary


在本章中,我们开始发现 React,我们将使用它来构建我们的前端。在开始使用 React 进行开发之前,我们介绍了一些基础知识,例如 React 组件、JSX、props 和 state。在我们的前端开发中,我们使用的是 ES6,这使得我们的代码更加简洁。我们完成了进一步开发所需的功能。我们还学习了如何使用 React 处理表单和事件。

Questions


  1. What is the React component?
  2. What are state and props?
  3. How does data flow in the React app?
  4. What is the difference between stateless and stateful components?
  5. What is JSX?
  6. What are component life cycle methods?
  7. How should we handle events in React?
  8. How should we handle forms in React?