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
可以将变量的作用域限制在声明变量的代码块中1
2
3
4
5function() {
let num = 5;
num = 6;
}
console.log(num); // will throw an errorconst
声明的变量的值不能被修改1
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中的整个持续时间内对组件进行操作
函数式组件用于显示易于阅读、调试和测试的静态数据。
1
2
3const Democomponent = () => {
return <h1>welcome Message!</h1>;
}当组件有属性但生命周期不需要管理时最有用。
函数式组件可以接受用户自定义的属性作为参数:
1
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;1
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
处理程序在功能组件中使用的最多:1
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')
);1
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功能。
1
2
3
4
5class Democomponent extends React.Component {
render() {
return <h1>Welcome Message!</h1>;
}
}由于其多功能性,类组件要比函数式组件更受青睐。由于它们继承了
React.Component
,因此必须要覆盖render()
方法。1
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
在类组件外部设置,而状态要在类组件内部设置:1
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不提供高阶组件。它们返回组件的函数,用于与其他组件共享逻辑。
1
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后执行一些操作
1
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更新后执行一些操作
1
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()
:用于在组件被卸载前执行一些操作
1
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
:从服务器获取数据1
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
:将数据发送到服务器1
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
数组中
1
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的单向数据流解决了这个问题。