IBM 全栈开发【4】:React 创建前端应用
近期在学习 IBM 全栈应用开发微学士课程,故此记录学习笔记。
1. 使用 React 和 ES6 创建前端应用
1.1. 前端框架
前端框架用于创建可连接服务器的动态客户端应用程序。它们通常是开源项目:
- Angular
- React
- Vue
1.1.1. Angular
Angular 是一个开源的框架,由谷歌维护。它基于 HTML 和 JavaScript,并且易于实现。
Angular 使用指令来使 HTML 更加动态,所有指令都可用于包含库的 HTML。
1 |
|
1.1.2. Vue
Vue 是一个开源的前端框架,它使用虚拟 DOM 来实现高性能,HTML 被视为一个完整的对象。Vue 非常轻量级、渲染速度快、易于学习。
1 | <html> |
1.1.3. React
React 是一个用于构建客户端动态网络应用程序的框架,使用动态数据绑定和虚拟 DOM 来扩展 HTML 语法,而不需要编写额外的代码,并保持用户界面元素与应用程序状态的同步。
1 | <html> |
React 使用 JavaScript XML 这种类似于 HTML 的特殊语言来创建用户界面,其可被 Babel 编译器编译为 JavaScript。
JavaScript XML 要嵌入在特殊的脚本标签中,其中的 type
属性指定了需要 Babel 的内容。
用于构建 React 应用程序的三个重要软件包:
- React 包:保存组件以及其状态和属性的 React 源代码
- ReactDOM 包:React 和 DOM 之间的粘合剂
- Babel 编译器:将 JavaScript XML 编译为 JavaScript
1 | <html> |
- React 组件要在
<script>
标签中定义,type
属性的类型需要设置为text/babel
,以便 Babel 编译器将其编译为 JavaScript- 定义的组件为
Mycomp
,继承自React.Component
,并重写了render()
方法
- 定义的组件为
ReactDOM.render()
方法用于渲染组件,并指定组件名称、HTML 标签和要设置的任何属性(该例子中就设置了name
属性)- 组件需要被指定呈现在 HTML 页面的哪个位置(该例子中就是
comp1
)
Facebook 提供了一个名为「Create React App」的工具,可以简化创建 React 应用程序的过程。
如果已安装 Node.JS,就可以运行以下命令来安装 Create React App:
1 | npx create-react-app my-app |
当运行完上述命令后,系统会自动创建一个包含所有必要文件的目录结构。该目录结构包含创建和运行 React 应用程序所需的所有文件。
src
目录是我们需要修改的主要目录App.js
文件是我们要添加到 HTML 页面的 React 根组件index.js
文件将应用程序添加到 HTML 页面
1.2. ES6
ES6 的全程为 ECMAScript 6,制定了广泛的全球信息和通信技术标准。
JavaScript 遵循 ECMAScript 6 标准(2015 年),其最主要的更改是:
let
const
- 箭头函数
- Promise 构造函数
- 类
1.2.1. let
和 const
let/const
和 var
不同:
var
声明的变量的作用域是全局的。这很有挑战性,尤其是在大型项目中,代表着有许多变量需要维护let
可以将变量的作用域限制在声明变量的代码块中js1
2
3
4
5function() {
let num = 5;
num = 6;
}
console.log(num); // will throw an errorconst
声明的变量的值不能被修改js1
2
3
4const num = 5;
console.log(num);
num = 6; // will throw an error
console.log(num);
1.2.2. 箭头函数
箭头函数允许函数像变量一样声明,这是一种更简洁的函数声明方式。
1 | // how a function was written in the older ES5 JavaScript |
箭头函数可以被调用,并可以作为回调的参数传递。
1 | const sayHello = ()=> console.log("Hello world!"); |
箭头函数也可以像普通函数一样接受参数。
1 | // takes one parameter |
1.2.3. Promise
Promise 对象表示了一个异步操作的最终完成或失败,以及其返回值。每当你调用异步操作时,Promise 会处于 pending(挂起)状态;当操作成功地执行时,Promise 会处于 fulfilled(履行)状态;当操作失败时,Promise 会处于 rejected(拒绝)状态。
1 | let promiseArgument = (resolve, reject) => { |
1 | let myPromise = new Promise((resolve, reject) => { |
以上两种写法是等价的。
1.2.4. 类
ES6 中的类使面向对象编程在 JavaScript 中更加容易。类创建了对象的模板,且建立在原型(即 prototype,是所有 JavaScript 对象的属性,包括函数,而函数可用于创建对象实例)的基础上。
1 | function Person(name, age) { |
this
指代的是当前对象- 类的概念是在函数原型的前提下建立的,目的是将面向对象编程扩展到 JavaScript 中
构造函数(constructor)是一个特殊的函数,用于创建一个类对象:
1 | class Rectangle { |
- 使用
new
关键字就可以创建一个类的实例
在 JavaScript ES6 中,类可以继承自其他类。继承其他类的类被称为子类(subclass),而超类(superclass)是被子类继承的类。子类会继承超类的所有属性和方法。
子类具有特殊权限,能够使用 super()
方法来调用超类的构造函数。
1 | class Square extends Rectangle { |
1.3. JSX
JSX 是 JavaScript XML 或 JavaScript Syntax Extension 的缩写,是一种类似于 React 使用的 XML 或 HTML 类语法,用于创建 React 元素。 JSX 允许 XML 或 HTML 类文本与 JavaScript 或 React 代码并存。
JSX 使用预处理器将 JavaScript 文件中的 HTML 类文本转换为标准的 JavaScript 对象,例如转译器或编译器(比方说 Babel)。
1 | const el1 = <h1>This is a sample JSX code snippet</h1> |
- JSX 代码的语法就像是 HTML 使用了类似 JavaScript 的变量
1.3.1. React 代码例子
1 | import React from 'react' |
而这是普通的 JavaScript 代码:
1 | import React from 'react' |
可以看出来,如果没有 JSX,React 代码将不得不使用大量嵌套来编写,这会导致代码变得难以阅读和维护。
1.3.2. 组件
组件(component)是 React 的核心构件,是一个可重用的代码块,用于创建用户界面。组件可以是函数或类,它们接受输入并返回 React 元素。
组件可以拥有自己的状态,这些状态是描述了组件行为的对象。有状态的组件的类型为类,而无状态的组件的类型为函数。
React 组件通过三个概念实现这些功能:
- 属性(property):用于从父组件向子组件传递数据
- 事件(event):使组件能够管理 DOM 事件和用户在系统上交互的动作
- 状态(state):根据组件的当前状态更新用户界面
React 应用程序是一颗组件树:根组件就像一个容器,它包含了所有其他组件。 所有组件的名称,无论是函数还是类,都必须以大写字母开头。组件可以通过使用 className
属性和 CSS 来进行样式化。
组件类型:
-
函数式组件通过编写 JavaScript 函数来创建,可以接受也可以不接受数据作为参数,返回 JSX 函数。它们本身没有状态或生命周期方法,因此也被称为无状态组件,但是可以通过实现 React Hooks 来添加这些功能。
- React Hook 是 React 的一项新功能,它能让你在不编写类的情况下使用 React 的特性
- 生命周期方法(lifecycle methods)是 React 内置的方法,可以在 DOM 中的整个持续时间内对组件进行操作
函数式组件用于显示易于阅读、调试和测试的静态数据。
jsx1
2
3const Democomponent = () => {
return <h1>welcome Message!</h1>;
}当组件有属性但生命周期不需要管理时最有用。
函数式组件可以接受用户自定义的属性作为参数:
jsx1
2
3
4
5
6
7
8
9
10
11
12
13
14function App(props) { // props passed as a function parameter
const compStyle = {
color: props.color,
fontSize: props.size + 'px'
};
return (
<div>
<span style={compStyle}>I am a sentence.</span>
</div>
);
}
export default App;jsx1
2
3
4
5
6ReactDOM.render(
<React.StrictMode>
<App color="blue" size="25"/> <!-- props being sent to the component -->
</React.StrictMode>,
document.getElementById('root')
);事件处理程序(event handler)可以通过属性来设置,其中
onClick
处理程序在功能组件中使用的最多:jsx1
2
3
4
5
6
7
8
9
10
11
12import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
ReactDOM.render(
<React.StrictMode>
<App color="blue" size="25" clickEvent={ <!-- setting an event handler method as a property -->
() => { alert("You clicked me!") }
}/>
</React.StrictMode>,
document.getElementById('root')
);jsx1
2
3
4
5
6
7
8
9function App(props) {
return (
<div>
<button onClick={props.clickEvent}>Click Me!</button> <!-- setting an event handler from props -->
</div>
);
}
export default App; -
类组件要比函数式组件更复杂,它们可以将数据传递给其他类组件、可以被 JavaScript ES6 的类创建、可以使用状态、属性和生命周期方法等 React 功能。
jsx1
2
3
4
5class Democomponent extends React.Component {
render() {
return <h1>Welcome Message!</h1>;
}
}由于其多功能性,类组件要比函数式组件更受青睐。由于它们继承了
React.Component
,因此必须要覆盖render()
方法。jsx1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16// import the React module from the react package
import React from 'react';
// create the App class that extends React.Component
class App extends React.Component {
constructor(props) {
super(props)
}
// override the render method
render() {
return <button onClick={this.props.clickEvent}>Click Me!</button>;
}
}
export default App;props
在类组件外部设置,而状态要在类组件内部设置:jsx1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22import Reach from 'react';
class App extends React.Component {
constructor(props) {
super(props)
}
state = {counter: "0"}; // define the state counter of the component App
// a function to increment the counter every time a button is clicked
incrementCounter = () => {
this.setState({counter: parseInt(this.state.counter) + 1});
}
// override the render method
render() {
return <div>
<button onClick={this.incrementCounter}>Click Me!</button>
<br/>
{this.state.counter}
</div>
}
} -
纯组件(pure component)优于函数式组件,主要用于提供优化。它们是编写起来最简单最快的组件,不依赖于其作用域之外的任何变量状态,可以用来替代简单的函数式组件。
-
高阶组件(higher-order component)是 React 中重用组件逻辑的高级技术。API 不提供高阶组件。它们返回组件的函数,用于与其他组件共享逻辑。
jsx1
2
3
4
5
6
7
8
9
10
11
12
13
14// import React and React Native's Text Core Component
import React from 'react';
import { Text } from 'react-native';
// define a component as a function
const Helloworld = () => {
return (
<Text>Hello, World!</Text>
);
}
// export your function component
// the function can then be imported in any application
export default Helloworld;
2. React 组件
2.1. 状态
状态允许你在一个应用程序中修改数据。它被定义为一个对象,使用键值对来存储数据,并帮助你跟踪应用程序中不同类型的数据。
React 组件有一个内置的状态对象,可以在状态对象中存储属于组件的属性值。当状态对象发生变化时,组件会重新渲染。
1 | // component |
- 本代码示例展示出了如何创建一个测试组件,该组件包含
id
、name
和age
三个状态属性 - 组件的
render()
方法返回了状态属性的值 - 包含属性的状态将根据组件的要求进行更改
React 状态的类型:
- 共享状态(shared state):由多个组件共享,比较复杂。例如订单应用程序中的所有订单列表
- 本地状态(local state):存在于单个组件中,不用于其他组件。例如隐藏和显示信息
2.2. 属性
属性用于在 React 组件之间传递数据。工作方式与 HTML 属性类似,它们存储标签的属性值。
React 组件之间的数据流是从父组件到子组件的单向数据流。
属性可以像函数参数一样被传递,但它们是只读的,不能在组件内部更改。属性允许子组件访问父组件中被定义的方法(状态则是由父组件管理,而子组件没有自己的状态),大部分组件将根据接收到的属性来显示信息,并保持无状态。
1 | // component |
- 该代码示例创建了一个类
TestComponent
,该类扩展了 React 组件
2.3. 组件阶段
每个 React 组件在其生命周期中都有三个阶段:
-
挂载阶段(mounting phase):组件被创建并插入 DOM 中。当组件被创建时,会有四个方法被依次调用:
constructor()
:用于初始化组件的状态和属性getDerivedStateFromProps()
:用于更新组件的状态render()
:用于渲染组件;必须且只能返回一个 DOM 元素componentDidMount()
:用于在组件被插入 DOM 后执行一些操作
jsx1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26import React from 'react';
class App extends React.Component {
// when the component App is created, the constructor is invoked
constructor(props) {
super(props)
console.log("Inside the constructor")
}
// the componentDidMount method is invoked
componentDidMount = () => {
console.log("Inside component did mount")
}
// the render method is invoked
render() {
console.log("Inside render method")
return (
<div>
The component is rendered
</div>
);
}
}
export default App; -
更新阶段(updating phase):组件的状态或属性发生变化时,会触发更新阶段。当组件更新时,会有五个方法被依次调用:
getDerivedStateFromProps()
:用于更新组件的状态shouldComponentUpdate()
:每当状态发生变化时被调用;默认返回true
;应当仅在不想渲染状态的变化时返回false
render()
:用于渲染组件;必须且只能返回一个 DOM 元素getSnapshotBeforeUpdate()
:用于在 DOM 更新前获取 DOM 状态componentDidUpdate()
:用于在 DOM 更新后执行一些操作
jsx1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39import React from 'react';
class App extends React.Component {
state = {counter: "0"};
incrementCounter = () => this.setState({counter: parseInt(this.state.counter) + 1});
// returns true by default
// its behavior is rarely changed
shouldComponentUpdate() {
console.log('Inside shouldComponentUpdate')
return true;
}
getSnapshotBeforeUpdate(prevProps, prevState) {
console.log('Inside getSnapshotBeforeUpdate');
console.log('Prev counter is ' + prevState.counter);
console.log('New counter is ' + this.state.counter);
return prevState;
}
componentDidUpdate() {
console.log('Inside componentDidUpdate')
}
// logs on to the console and then renders the component
render() {
console.log('Inside render')
return (
<div>
<!-- With the onClick of the button, incrementCounter is invoked, increasing the counter state by 1 -->
<button onClick={this.incrementCounter}>Click Me!</button>
{this.state.counter}
</div>
);
}
}
export default App; -
卸载阶段(unmounting phase):组件从 DOM 中移除时,会触发卸载阶段。当组件被卸载时,会有一个方法被调用:
componentWillUnmount()
:用于在组件被卸载前执行一些操作
jsx1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32import React from 'react';
class AppInner extends React.Component {
componentWillUnmount() {
console.log('This component will unmount')
}
render() {
return <div>Inner component</div>
}
}
class App extends React.Component {
state = {innerComponent:<AppInner/>}
componentDidMount() {
setTimeout(() => {
this.setState({innerComponent: <div>unmounted</div>})
}, 5000)
}
render() {
console.log('Inside render')
return (
<div>
{this.state.innerComponent}
</div>
);
}
}
export default App;
2.4. 组件之间的数据传递
React 组件之间的数据传递可以有:
- 使用属性的「父到子」数据传递
- 使用回调函数的「子到父」数据传递
- 使用 Redux 的「兄弟」数据传递(此处不做讨论)
父到子:
1 | class App extends React.Component { |
1 | class AppInner extends React.Component { |
- 其中,
App
组件是AppInner
组件的父组件
子到父:
1 | class App extends React.Component { |
1 | class AppInner extends React.Component { |
2.5. 组件的生命周期
组件的生命周期代表了组件从创建到销毁的整个过程。React 组件的生命周期包含四个阶段,每个阶段都有不同的方法:
- 初始化(initialization):组件以给定的属性和默认状态被创建
- 挂载(mounting):渲染由
render()
方法返回的 JSX - 更新(updating):当组件的状态或属性发生变化时,会触发更新阶段
- 卸载(unmounting):组件从 DOM 中移除
2.5.1. 挂载阶段
挂载阶段中,组件被添加到 DOM,并在组件加载前和加载后调用两个预定义方法:
componentWillMount()
componentDidMount()
2.5.2. 更新阶段
组件的状态或属性发生变化时,会触发更新阶段。变化可以在组件内发生,也可以通过后台发生,这些变化都会触发 render()
方法的调用。
getDerivedStateFromProps()
shouldComponentUpdate()
render()
getSnapshotBeforeUpdate()
componentDidUpdate()
2.5.3. 卸载阶段
组件从 DOM 中移除时,会触发卸载阶段。在卸载阶段,只有一个方法被调用:
componentWillUnmount()
2.6. 外部服务
路由器(router)可以连接到外部服务以执行多种操作,例如:
-
GET
:从服务器获取数据jsx1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25class App extends React.Component {
state = {
user: "None Logged In"
}
// connect to a server through an axios request
componentDidMount() {
const req = axios.get("<external server>");
req.then(resp => {
// then the promise is fulfilled, you parse the response and extract the data from it to change user to have the same name as its value
this.setState({user: resp.data.name});
})
.catch(err => {
this.setState({user: "Invalid user"});
});
}
render() {
return (
<div>
Current user - {this.state.user}
</div>
);
}
} -
POST
:将数据发送到服务器jsx1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22const express = require("express");
const app = new express();
// this server uses the CORS middleware to allow cross-origin requests to the server
const cors_app = require("cors");
app.use(cors_app());
let usercollection = [];
app.post("/user", (req, res) => {
let newuser = {"name": req.query.name, "gender": req.query.gender}
usercollection.push(newuser);
return res.send("User successfully added");
});
app.get("/user", (req, res) => {
return res.send(usercollection);
})
app.listen(3333, () => {
console.log("Listening at http://localhost:3333")
})- Express 服务器接收端点
/user
的POST
请求,并将数据存储在usercollection
数组中
jsx1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28class App extends React.Component {
state = {completionstatus: ""}
postDataToServer = () => {
axios.post("http://localhost:3333/user?name=" +
document.getElementById("name").value +
"&gender=" + document.getElementById("gender").value
)
.then(response => {
this.setState({completionstatus: response.data})
}).catch((err) => {
this.setState({completionstatus: "Operation failure"})
})
}
render() {
return (
<div>
Enter the name <input type="text" id="name" />
<br />
Enter the gender <input type="text" id="gender" />
<br />
<button onClick={this.postDataToServer}>Post Data</button>
<span>{this.state.completionstatus}</span>
</div>
);
}
} - Express 服务器接收端点
-
UPDATE
:修改数据 -
DELETE
:删除数据
大多数对外部服务器的请求都是阻塞性的。要异步调用,可以使用 Promise。
2.7. 测试
测试可以是一套由代码组成的,以验证应用程序的无差错执行。
测试 React 组件有多个好处:验证代码运行无误;通过复制最终用户的行为来测试组件;通过测试组件的不同状态来测试组件;防止先前已修复的错误再次出现。
测试有着两种类型:
- 在简单的测试环境中渲染组件树并验证其输出
- 在真实的浏览器环境中运行应用程序,进行端到端的测试
2.7.1. React 组件测试的阶段
- 安排(arrange):组件需要将其 DOM 渲染到用户界面
- 操作(act):注册任何可能以编程方法触发的用户行为
- 断言(assert):验证组件的输出是否与预期的输出相匹配
2.7.2. 测试工具
速度 vs 环境:
- 有些工具能在做出修改和看到结果之间提供非常快的回馈,但无法精确地模拟浏览器行为
- 有些工具可能会使用真实的浏览器环境,但会降低迭代速度,在持续集成环境中使用时可能会导致不稳定
测试工具有:
- Mocha
- Chai:断言库
- Sinon
- Enzyme:渲染组件
- Jest:测试 React 组件,并拥有着 Mocha、Chai、Sinon 以及其他工具的能力
- React Testing Library:测试 React 组件
3. React 进阶
3.1. Hooks
Hooks 是在用户界面中封装有状态的行为的更简单的方法,它们允许函数式组件访问状态和其他 React 功能。Hooks 是常规的 JavaScript 函数,提供使用上下文或状态等功能的方法,且无需编写类,帮助你使代码更简洁。
类组件有时会带来一些问题,例如封装复杂、组件大小难以管理以及类混淆等。
标准的 Hooks:
useState
:为函数式组件添加状态useEffect
:管理副作用(side effects)useContext
:管理上下文useReducer
:管理 Redux 的状态变化
自定义 Hooks 允许你为应用程序添加特殊功能。它们可以由一个或多个 Hooks 组成、可以被重复使用、分解为更小的 Hooks。自定义 Hooks 需要以 use
开头。
1 | import React, { useState } from "react"; |
3.2. 表单
大多数 React 表单都是单页面应用程序(SPA)或者加载单个页面的网络应用程序。表单使用组件处理数据、使用事件处理程序控制变量的变化和状态的更新。
表单标签有:
<input>
<textarea>
<select>
在 HTML,状态由表单元素管理;在 React,组件的状态管理着表单元素。
3.2.1. 输入类型
非受控输入 | 受控输入 |
---|---|
允许浏览器处理大部分表单元素,并通过 React 的变化事件收集数据 | 使用 React 直接设置和更新输入值,从而完全控制元素 |
在输入的 DOM 节点中管理自己的状态 | 函数管理数据的传递 |
元素会在输入值发生变化的时候更新 | 更好地控制表单元素和数据 |
ref 函数用于从 DOM 中获取表单值 |
属性获取当前值并通知更改 |
父组件控制更改 |
表单示例:
1 | import React, { Component } from "react"; |
React Hook Form 是一个创建表单的实用软件包,它可以帮助你创建可重用的表单组件。
3.3. Redux
Redux 是一个状态管理库,它遵循一种称为 Flux 架构的模式,通常在组件数量较多的时候实用。
Redux 提供了一个集中的状态管理系统,它将应用程序的所有状态存储在一个单一的对象中,称为存储(store)。存储是一个 JavaScript 对象,它包含了应用程序的所有状态。
Redux 的工作流程:当用户与应用程序的某个组件交互时,Action
会更新整个应用程序的状态,这反过来又会触发组件的重新渲染,从而更新该组件的属性,这些属性会将结果反馈给用户。
3.3.1. 概念
Action
:【你的应用程序能做什么】。它是一个由选择单选按钮、复选框或点击按钮触发的事件 / JSON 对象;它包含着需要对状态进行更改的信息,并由被称为操作创建器(action creator)的函数创建。Action
由应用程序的各个部分派发,并由存储空间接收Store
:应用程序状态的唯一位置和权威来源。它是一个包含着状态、函数和其他对象的对象,可以调度和接收操作。Store
的更新能够被订阅Reducers
:返回全新的状态的函数。它们从Store
接收Action
,并对状态进行适当更改。作为事件监听器,Reducer
会读取Action
的有效载荷(payload)并更新Store
。Reducer
接收两个参数:先前的应用程序状态和Action
3.3.2 中间件
中间件(middleware)是一个函数,它可以访问 Action
和 Store
,并且可以在 Action
到达 Reducer
之前执行某些操作。它可以用于日志记录、分析、异步请求等。
- Thunk 中间件:允许在操作创建器中传递函数以创建
async
Redux、允许编写操作创建器、允许延迟调度操作、允许调度多个操作。优势是 Thunk 中间件可以无需大量模板代码即可实现异步操作、学习难度小、易于使用;缺点是不能直接对操作做出响应、难以处理可能出现的并发问题、是命令式的、不太容易测试和扩展 - Saga 中间件:使用称为生成器(generator)的 ES6 功能来实现异步操作、允许以纯函数的形式表达复杂的逻辑、易于测试、允许分离关注点、易于扩展具有副作用的复杂操作、易于通过
try/catch
处理错误;缺点是不适合简单的应用程序、需要更多的模板代码、需要具备生成器的知识 - 基于 Promise 的中间件
3.3.3. 数据流
React-Redux 应用程序的数据流是单向的。它只朝一个方向流动。
- 操作创建器(action creator)朝根归纳器(root reducer)流动
- 根归纳器处理
Action
并返回新的状态到储存空间(store) - 存储空间更新用户界面(UI)
- 用户界面调用操作创建器
为什么要选择单向数据流:双向数据绑定会影响浏览器性能,而且很难跟踪数据流,因此 Redux 的单向数据流解决了这个问题。