Sorry, your browser cannot access this site
This page requires browser support (enable) JavaScript
Learn more >

前端框架React学习笔记

前言

上研究生后老师让直接去实习,实习单位使用React + React Native进行安卓混合开发。于是先学习一下React

React概述

React是一个构建用户见面的JS库

React特点

  1. 声明式

    只需要描述UI(HTML)看起来是啥样,即描述结构,React负责渲染UI以及在数据变化时更新UI

  2. 基于组件

    • 用于表示页面中的部分内容
    • 通过组合、复用多个组件,就能实现完整的页面功能
  3. 学习一次随处使用

    • 使用React开发Web应用

    • 使用React-native开发移动端应用

    • React 360开发VR应用

React 安装

1
npm i react react-dom
  • react包是核心,提供创建元素,组件的功能
  • react-dom包提供DOM相关功能

React初见

  1. 选择一个目录

  2. 创建index.html

  3. 在该目录下安装react包

    1
    npm i react react-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
<!DOCTYPE html>
<html lang="cn">

<head>
<meta charset="utf-8">
<meta name="viewport" content="width-device-width, initial-1.0 ">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>react</title>
</head>

<body>

<div id="root"></div>
<!-- 引入reactjs -->
<script src="./node_modules/react/umd/react.development.js"></script>
<script src="./node_modules/react-dom/umd/react-dom.development.js"></script>
<script>
// 创建React元素
// 参数:
// 1. 元素名称
// 2. 元素属性
// 3. 第三个及其之后的参数:元素子节点
const title = React.createElement('h1', null, "Hello React")

// 渲染React元素
// 参数:
// 1. 要渲染的react元素
// 2. 挂载点,一个DOM对象,指定渲染位置
ReactDOM.render(title, document.getElementById("root"))
</script>
</body>

</html>

React脚手架

脚手架的意义

  1. 脚手架是开发现代Web应用的必备
  2. 充分利用Webpack、Babel、ESLint等工具辅助项目开发
  3. 零配置,无需手动配置繁琐的工具即可使用
  4. 关注业务二部是工具配置

使用脚手架初始化项目

1
npx create-react-app my-app

使用

1
npm start

启动项目

npx 的意义

原来使用npm上下载的包时,如果想在任意地方使用,需要全局安装这个包,但npx之后无需全局安装,即可使用。

脚手架中导入React包

由于React基于Webpack,因此导入时可直接使用ES6中的模块化语法进行导入:

1
2
import React from 'react'
import ReactDOM from 'react-dom' //使用react制作web应用时导入该渲染组件

JSX

JSX基本使用

即JavaScript XML,表示在JS中写XML格式的代码

优点:

  1. 声明式语法更直观,与HTML结构相同,降低了学习成本、提升开发效率

使用步骤

使用JSX语法创建react元素

1
const title = <h1>Hello JSX</h1>

使用reactDOM.render渲染react元素到页面

1
ReactDOM.render(title, root)

JSX原理

react脚手架中配置了babel,JSX会经过babel编译为标准的JS语法。

编译JSX语法的包为@babel/preset-react

注意事项

  1. React元素的属性名采用驼峰命名法
  2. 使用JSX为标签设置属性时,应修改为驼峰命名:
    1. class属性 -> className
    2. for -> htmlFor
    3. tabindex -> tabIndex
  3. 没有子节点的React元素,可以使用但标签<span />结束
  4. 推荐使用小括号包裹JSX,从而避免JS中的自动插入分号陷阱

JSX中嵌入JS表达式

语法:

1
2
3
4
const name = 'Jack'
const dv = (
<div>你好,我叫:{name} </div>
)

JSX条件渲染

根据条件渲染特定的JSX结构

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
const isLoading = false
cont loadData = () => {
if (isLoading) {
return <div>loading...</div>
}

return <div>数据加载完成,此处显示加载后的数据</div>
}

const title = (
<h1>
条件渲染:
{loadData()}
</h1>
)

ReactDOM.render(title, document.getElementById('root'))

此外还可以使用三目运算符和逻辑与运算符

1
2
3
4
5
6
7
8
9
10
11
12
13
const isLoading = false
cont loadData = () => {
return isLoading && (<div>loading...</div>)
}

const title = (
<h1>
条件渲染:
{loadData()}
</h1>
)

ReactDOM.render(title, document.getElementById('root'))

JSX列表渲染

使用数组的map()方法

需要注意的是渲染列表时应该添加key属性,key属性的值要保证唯一

通过便利创建什么元素,就要把key通过遍历加上。

尽量避免使用索引号作为key

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React from 'react'
import ReactDOM from 'react-dom'

const songs = [
{id: 1, name: '111'},
{id: 2, name: '222'},
{id: 3, name: '333'},
]

const title = (
<ul>
{songs.map(item = > <li key={item.id}>{item.name}</li>)}
</ul>
)

ReactDOM.render(title, document.getElementById('root'))

JSX样式处理

两种方式:

  1. 行内样式——style
  2. 类名——className

使用样式时需要引入

1
2
3
4
5
6
7
8
9
10
11
import React from 'react'
import ReactDOM from 'react-dom'

//引入css
import './css/index.css'

const list = (
<h1 className="title" style={{ clolr: 'red', backgroundColor: 'skyblue'}}>
)

ReactDOM.render(list, document.getElementById('root'))

JSX总结

  1. JSX是React的核心内容
  2. JSX表示在JS中写HTML结构,是声明式的体现
  3. 使用JSX配合嵌入JS表达式、条件渲染、列表渲染、可以描述任意UI结构
  4. 推荐使用className的方式给JSX添加样式
  5. React完全利用JS语言自身的能力来编写UI,而不是造轮子增强HTML功能

React组件

组件的特点:

  1. 可复用
  2. 独立
  3. 可组合

组件的两种创建方式

  1. 函数

函数组件

为了和函数进行区分,对函数组件进行如下约定:

  1. 函数名必须以大写字母开头
  2. 函数组件必须有返回值

但函数返回值可以为NULL

1
2
3
4
5
function Hello() {
return (
<div>我是一个函数组件</div>
)
}

渲染组件时直接使用函数名作为组件标签即可

1
ReactDOM.render(<Hello />, root)

也能使用箭头函数来构造组件

1
const Hello = () => <div>我是一个箭头函数组件</div>

React根据名称首字母是否大写来区分组件和普通React元素

类组件

使用ES6中的class创建的组件,为了与普通类进行区分,使用如下约定:

  1. 类名首字母大写
  2. 类组件需要继承自React.Component,从而使用父类中提供的方法和属性
  3. 类组件必须提供**render()**方法
  4. render方法必须有返回值,表示组件的结构
1
2
3
4
5
6
7
class Hello extends React.Component {
render() {
return <div>Hello Class Component</div>
}
}

ReactDOM.render(<Hello />, root)

组件的组织

组件作为一个单独的个体,一般会放到一个单独的JS文件中

  1. 创建JS文件,对应组件名称.js
  2. 在JS文件中导入React
  3. 创建组件
  4. 在JS文件中导出该组件
  5. 在index.js中导入该组件
  6. 渲染组件

组件JS文件

1
2
3
4
5
6
7
8
9
10
// Hello.js
import React from 'react'
class Hello extends React.Component {
render() {
return <div>Hello Class Component</div>
}
}

// 导出Hello组件
export default Hello

index中渲染组件

1
2
3
4
// index.js
import Hello from './Hello'
// 渲染导入的Hello组件
ReactDOM.render(<Hello />, root)

React事件处理

事件绑定

  1. React事件绑定语法与DOM事件绑定相似
  2. on+事件名称={事件处理程序},如onClick={() => {}}
  3. React事件采用驼峰命名法
1
2
3
4
5
6
7
8
9
10
11
class App exntends React.Component {
handleClick() {
console.log('单击事件被触发')
}

render() {
return (
<button onClick={this.handleClick}>点我</button>
)
}
}

函数组件绑定事件,使用内部函数定义

1
2
3
4
5
6
7
8
9
function App() {
function handleClick() {
console.log('单击事件出发了')
}

return(
<button onClick={handleClick}>点我</button>
)
}

事件对象

  1. 可以通过事件处理程序的参数获取到事件对象
  2. React中的事件对象叫做:**合成事件(重要,需补充)**(对象)
  3. 合成事件:兼容所有浏览器,无需担心跨浏览器兼容性问题
1
2
3
4
5
6
function handleClick(e) {
e.preventDefault() //组织浏览器的默认行为
console.log('事件对象', e)
}

<a onClick={handleClick}>点我,不会跳转页面</a>

有状态组件和无状态组件

  1. 函数组件又称为无状态组件,类组件叫做有状态组件
  2. 状态(state)即数据
  3. 函数组件没有自己的状态,只负责数据展示(静)
  4. 类组件有自己的状态,负责更新UI

组件的state和setState

state的基本使用

  1. 状态即数据,时组件内部的私有数据,只能在组件内部使用
  2. state的值是对象,表示一个组件中可以有多个数据

状态的初始化方式如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
class Hello extends React.Component {
constructor() {
// ES6中必须包含
super();
// 初始化state
this.state = {
count: 0,
};
}

render() {
return (
<div>有状态组件</div>
)
}
}

也可以使用ES6中的简化语法:

1
2
3
4
5
6
7
8
9
10
11
12
13
class Hello extends React.Component {
// 简化语法
// 初始化state
state = {
count: 0
}

render() {
return (
<div>有状态组件</div>
)
}
}

可以使用this获取状态的数据

1
2
3
4
5
6
7
8
9
10
11
12
13
class Hello extends React.Component {
// 简化语法
// 初始化state
state = {
count: 0
}

render() {
return (
<div>计数器: {this.state.count}</div>
)
}
}

setState()修改状态

  1. 状态是可变的
  2. 语法:this.setState({要修改的数据})
  3. 不要直接修改state中的值
  4. setState()作用:
    1. 修改state
    2. 更新UI
  5. 思想:数据驱动视图
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
class App extends React.Component {
state = {
count: 0
}

render() {
return (
<div>
<h1>计数器: { this.state.count }</h1>
<button onClick={() => {
this.setState({
count: this.state.count + 1
})
}}>+1</button>
</div>
)
}
}

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

从JSX中抽离事件处理程序

  1. JSX中掺杂了过多JS逻辑代码会使得JSX逻辑混乱
  2. 将逻辑抽离到单独的方法中,保证JSX结构清晰
  3. 由于箭头函数不具备this,因此会向外层寻找this的指向,render()函数中的this指向组件实例,因此不会有问题,但进行抽离后的函数不具备this指向。
  4. 可以使用如下解决方式:
    1. 箭头函数
    2. Function.prototype.bind()
    3. class的实例方法
箭头函数

利用箭头函数自身不绑定this的特点,会根据外部环境推断。对于方法中的this,谁调用就指向谁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class App extends React.Component {
state = {
count: 0
}

// 事件处理程序
onIncrement(){
console.log('事件处理程序中的this', this)
this.setState({
count: this.state.count + 1
})
}

render() {
return (
<div>
<h1>计数器: { this.state.count }</h1>
<button onClick={() => this.onIncrement()}>+1</button>
</div>
)
}
}

ReactDOM.render(<App />, document.getElementById('root'))
Function.prototype.bind()
  1. 利用ES5中的bind方法,将事件处理程序中的this与组件实列绑定

  2. 类似于小程序中的

    1
    2
    let that = this
    that.onIncrement()
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
class App extends React.Component {

constructor() {
super()
this.state = {
count: 0
}
this.onIncrement = this.onIncrement.bind(this)
}

// 事件处理程序
onIncrement(){
console.log('事件处理程序中的this', this)
this.setState({
count: this.state.count + 1
})
}

render() {
return (
<div>
<h1>计数器: { this.state.count }</h1>
<button onClick={this.onIncrement}>+1</button>
</div>
)
}
}
class实例方法

使用箭头函数形式的class实例方法

注意,该语法为实验性语法,但由于babel的存在,可以直接使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class App extends React.Component {
state = {
count: 0
}

// 事件处理程序
onIncrement = () => {
console.log('事件处理程序中的this', this)
this.setState({
count: this.state.count + 1
})
}

render() {
return (
<div>
<h1>计数器: { this.state.count }</h1>
<button onClick={this.onIncrement}>+1</button>
</div>
)
}
}

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

表单处理

React两种表单处理方式

  1. 受控组件
  2. 非受控组件(DOM方式)

受控组件

  1. HTML中的表单元素是可输入的,也就是有自己的可变状态
  2. React中可变状态通常保存在state中,使用setState来i需改
  3. React将state与表单元素值value绑定到一起,由state的值来控制表单元素的值
  4. 受控组件:即值收到React控制的表单元素

受控组件的使用包括以下步骤:

  1. 在state中添加一个状态,作为表单元素的value值(控制表单元素值的来源)
  2. 给表单元素绑定change事件,将表单元素的值设置为state的值(控制表单元素值的变化)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
class App extends React.Component {
state = {
txt: '',
}

handleChange = e => {
this.setState({
txt: e.target.value
})
}

render() {
return (
<div>
<input type="text" value={this.state.txt} onChange={this.handleChange} />
</div>
)
}
}

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

如下练习将不同的表单标签转化为受控组件。

  1. 文本框,富文本框,下拉框操作value属性
  2. 复选框操作checked属性
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
import './index.css';
import React from 'react';
import ReactDOM from 'react-dom';

class App extends React.Component {
constructor(){
super()
this.state = {
txt: '',
content: '',
city: 'bj',
Selection: false,
}
}

handleChange = e => {
this.setState({
txt: e.target.value
})
}

handleContent = e => {
this.setState({
content: e.target.value
})
}

handleCity = e => {
this.setState({
city: e.target.value
})
}

handleSelection = e => {
this.setState({
Selection: e.target.checked
})
}

render(){
return (
<div>
<input type="text" value={this.state.txt} onChange={this.handleChange}></input>
<br/>

<textarea value={this.state.content} onChange={this.handleContent}></textarea>
<br/>

<select value={this.state.city} onChange={this.handleCity}>
<option value="sh">上海</option>
<option value="bj">北京</option>
<option value="gz">广州</option>
</select>
<br/>

<input type="checkbox" checked={this.state.Selection} onChange={this.handleSelection}></input>
</div>
)
}

}

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

但是可以发现用如上方法控制表单元素会产生很多重复新的onChange函数,因此需要对多表单元素进行优化,即使用一个事件处理程序同时处理多个表单元素。

多表单元素优化

  1. 给表单元素添加name属性,名称与state相同
  2. 根据表单元素类型获取对应值
  3. 在change事件处理程序中通过[name]来修改对应的state
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import './index.css';
import React from 'react';
import ReactDOM from 'react-dom';

class App extends React.Component {
constructor(){
super()
this.state = {
txt: '',
content: '',
city: 'bj',
Selection: false,
}
}

handleChange = e => {
// 获取当前DOM对象
const target = e.target;

// 根据类型获取值
const value = target.type === 'checkbox'
? target.checked
: target.value;

// 获取name
const name = target.name;

this.setState({

// 此处定义属性使用了ES6中的新特性 属性名表达式
// 即使用表达式作为属性或函数名
// 此处会将name转化为string
[name]: value,
});
}

render(){
return (
<div>
<input type="text" name='txt' value={this.state.txt} onChange={this.handleChange}></input>
<br/>

<textarea name='content' value={this.state.content} onChange={this.handleChange}></textarea>
<br/>

<select name='city' value={this.state.city} onChange={this.handleChange}>
<option value="sh">上海</option>
<option value="bj">北京</option>
<option value="gz">广州</option>
</select>
<br/>

<input name='Selection' type="checkbox" checked={this.state.Selection} onChange={this.handleChange}></input>
</div>
)
}

}

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

非受控组件

借助于ref,使用原生DOM方法来获取表单元素值

ref的作用:获取DOM或组件

这种方式通过直接操作DOM实现

使用步骤:

  1. 通过React.createRef()方法创建一个ref对象
  2. 将创建好的ref对象添加到文本框中
  3. 通过ref对象获取到文本框中的值
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
import './index.css';
import React from 'react';
import ReactDOM from 'react-dom';

class App extends React.Component {
constructor(){
super()
this.txtRef = React.createRef()
}

// 获取文本框的值
getTxt = () => {
console.log('文本框的值为:', this.txtRef.current.value);
}

render(){
return (
<div>
<input type="text" ref={this.txtRef} />

<button onClick={this.getTxt}>获取文本框的值</button>
</div>
)
}

}

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

案例一

式样以上知识,实现一个无回复功能的评论版

渲染评论列表

  1. 在state总初始化评论列表数据
  2. 使用map循环渲染列表数据
  3. 注意给每个被渲染的元素添加一个key

评论区条件渲染

  1. 判断列表长度是否为0
  2. 如果为0则渲染暂无评论
  3. 注意讲逻辑与JSX分离

获取评论信息

  1. 使用受控组件的方式实现
  2. 注意设置handle方法和name属性

发表评论

  1. 为按钮绑定单击事件
  2. 在事件处理程序中通过state获取评论信息
  3. 将评论添加到state中,更新state
  4. 边界情况:清空文本框,文本框判空

最终实现

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
import './index.css';
import React from 'react';
import ReactDOM from 'react-dom';

class App extends React.Component {

constructor() {
super()
this.state = {
comments: [
{ id: 1, name: 'jack', comment: 'You jump'},
{ id: 2, name: 'rose', comment: 'I jump'},
{ id: 3, name: 'joker', comment: 'I see you jump'},
],
// 当前评论人
userName: '',
// 当前评论内容
userContent: '',
}
}

renderList() {
const {comments} = this.state
if(comments.length === 0) {
return (
<div className='no-comment'>暂无评论,快去评论吧</div>
)
} else {
return (
<ul>
{
comments.map(item => (
<li key={item.id}>
<h3>评论人:{item.name}</h3>
<p>评论内容:{item.comment}</p>
</li>
))
}
</ul>
)
}
}

handleChange = (e) => {
const {name, value} = e.target;
this.setState({
[name]: value,
});
}

addComment = () => {
const {comments, userName, userContent} = this.state

//判空,使用trim去除空格
if(userName.trim() === '' || userContent.trim === '' ) {
alert('请输入评论人和评论内容');
return;
}
// 此处使用了ES6的新特性:拓展运算符...
// 该运算符用于将可便利对象拆分为单个
const newIndex = comments.length + 1;
const newComments = [...comments,
{
id: newIndex,
name: userName,
comment: userContent,
}
];

console.log(newComments);

this.setState({
comments: newComments,
userName: '',
userContent: '',
});
}

render(){
const {userName, userContent} = this.state;

return (
<div className='app'>
<div>
<input
name='userName'
className='user'
value={userName}
type='text'
placeholder='请输入评论人'
onChange={this.handleChange} />
<br/>

<textarea
className='content'
name='userContent'
cols='30'
row = '10'
placeholder='请输入评论内容'
value={userContent}
onChange={this.handleChange}
/>
<br />
<button onClick={this.addComment}>发表评论</button>
</div>
{/* 通过条件渲染决定渲染什么内容 */}
{this.renderList()}
</div>
)
}

}

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

组件间通讯

组件props

由于组件的封闭性,要接受外部数据应该通过props来实现

  • props的作用:接受传递给组件的数据
  • 传递数据:给组件标签添加属性
  • 接受数据:
    • 函数组件通过参数props接收数据
    • 类组件通过this.props接受数据
1
<Hello name='jack' age = {19} />
1
2
3
4
5
6
function Hello(props) {
console.log(props)
return (
<div>接受到数据: {props.name}</div>
)
}
1
2
3
4
5
6
7
8
9
10
11
12
class Hello extends React.Component {

// 推荐写法
constructor(props) {
super(props)
}
render() {
return (
<div>接收到数据:{this.props.age}</div>
)
}
}

组件props的特点

  1. 可以给组件传递任意类型的值
  2. props是一个只读属性,无法修改
  3. 使用类组件时,如果写了构造函数,应该将props传递给super(),否则,无法在构造函数中获取到props

组件通讯的三种方式

  1. 父组件 -> 子组件
  2. 子组件 -> 父组件
  3. 兄弟组件

父组件到子组件

  1. 父组件提供要传递的state数据
  2. 给子组件标签添加属性,值为state中的数据
  3. 子组件中通过props接受父组件中传递的数据
1
2
3
4
5
6
7
8
9
10
class Parent extends React.Component {
state = { lastName: '王' }
render() {
return (
<div>
传递给子组件:<Child name={this.state.lastName} />
</div>
)
}
}
1
2
3
function Child(props) {
return <div>子组件接受:{props.name} </div>
}

子组件到父组件

思路:利用回调函数,父组件提供回调,子组件调用,将要传递的数据作为回调函数的参数

  1. 父组件提供一个回调函数,用于接收数据
  2. 将该函数作为属性值传递给子组件
  3. 子组件通过props调用回调函数
  4. 将子组件的数据作为参数传递给回调函数
  5. 注意回调函数中this指向的问题
1
2
3
4
5
6
7
8
9
10
11
12
13
14
class Parent extends React.Component {
// 提供回调
getChildMsg = (msg) => {
console.log('接收到子组件数据', msg)
}
render() {
return (
// 传递给子组件
<div>
子组件:<Child getMsg={this.getChildMsg} />
</div>
)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
class Child extends React.Component {
// 提供回调
stats = {childMsg: 'React'}
handleClick = () => {
this.props.getMsg(this.state.childMsg)
}
render() {
return (
// 传递给子组件
<button onClick={this.handleClick}>点我,给父组件传递数据</button>
)
}
}

兄弟组件通讯

  • 思路:将共享状态提升到最近的公共父组件中,由公共父组件管理这个状态

  • 将这种思想称为状态提升

  • 公共父组件职责:

    1. 提供共享状态(父到子通讯)
    2. 提供操作共享状态的方法(子到父通讯)
  • 要通讯的子组件只需要通过props接受状态或操作状态的方法

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
class Counter extends React.Component {
// 提供共享状态
state = {
count: 0
}

// 提供修改状态的方法
onIncrement = () => {
this.setState({
count: this.state.count + 1
})
}

render() {
return (
<div>
<Child1 count={this.state,count} />
<Child2 onIncrement={this.onIncrement} />
</div>
)
}
}

const Child1 = (props) => {
return <h1>计数器:{props.count}</h1>
}

const CHild2 = (props) => {
return <button onClick={() => props.onIncrement}> +1 </button>
}

Context

考虑这样一个实际应用中的问题:对于相互嵌套很深层次的组件,我们应该如何进行通讯?例如下面这个结构中组件App想要给Child传递数据

常规的思路可能是使用Props一层一层往下传递,但是这样操作起来会十分繁琐。

1
2
3
4
5
6
7
8
<App>
<Node>
<SubNode>
<Child>
</Child>
</SubNode>
</Node>
</App>

React为我们提供了更好的方式:Context

它的作用就是跨组件传递数据(比如:主题,语言设置等需要在根组件配置数据)

使用方法如下:

  1. 调用React.createContext()创建Provider(提供数据)和Consumer(消费数据)两个组件
  2. 使用Provider组件作为父节点
  3. 设置Value属性,表示要传递的数据
  4. 调用Consumer组件接受数据
  5. 即Provider包裹父节点,Consumer被子组件包裹
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
39
40
41
42
43
44
45
46

// 创建context
const {Provider, Consumer} = React.getContext()

class App extends React.Component {
render() {
return (
// 使用Provider作为父节点
// 设置要传递的数据
<Provider value="pink">
<div className='app'>
<Node />
</div>
</Provider>
)
}
}

const Node = props => {
return (
<div className="node">
<subNode />
</div>
)
}

const SubNode = props => {
return (
<div className="subnode">
<Child />
</div>
)
}

const Child = props => {
return <div className='child'>
<!-- 使用consumer组件接受数据 -->
<Consumer>
{
data => <span>我是子节点 -- {data}</span>
}
</Consumer>
</div>
}

ReactDOm.render(<App />, document.getElementById('root'))

props深入

children属性

表示组件标签的子节点,当组件标签有子节点时,props就会有该属性。

children属性和普通的props一样,值可以任意。

1
2
3
4
5
6
7
8
9
10
const App = props => {
return (
<div>
<h1>组件标签的子节点:</h1>
{props.children}
</div>
)
}

ReactDOM.render{<App>我是子节点</App>, document.getElementById('root')}

props校验

对于组件而言,props是外来的,无法保证使用组件时传入的值的格式。

那么会出现以下问题:

  1. 传入数据不对可能导致组件内部报错
  2. 组件的使用者不知道明确的错误原因

React提供了props校验作为解决方法。

props校验:

  1. 运行在创建组件时只当props的类型、格式等

  2. 能捕获使用组件时因为props导致的错误,给出明确的错误提示,增加组件的健壮性

使用步骤:

  1. 安装prop-types包
  2. 导入prop-types
  3. 使用组件名.propTypes={}来给组件的props添加校验规则
  4. 校验规则通过PropTypes对象指定
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import './index.css';
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'

const App = props => {
const arr = props.colors
const lis = arr.map((item, index) => <li key={index}>{item}</li>)

return <ul>{lis}</ul>
}

// 添加props校验
App.propTypes = {
colors: PropTypes.array
}

ReactDOM.render(<App colors={['red', 'blue']} />, document.getElementById('root'))

props校验常见校验规则:

常见类型 array,bool,func,number,object,string
React元素类型 element
必填项 isRequired
特定结构对象 shape({})

详见官方文档:PropTypes

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
import './index.css';
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'

const App = props => {
return (
<div>
<h1>props校验:</h1>
</div>
)
}

// 添加props校验
// 属性a的类型: 数值(number)
// 属性fn的类型: 函数(func)且为必填
// 属性tag的类型: React元素(element)
// 属性filter的类型: 对象({area: '上海', price: 1999})
App.propTypes = {
a: PropTypes.number,
fn: PropTypes.func.isRequired,
tag: PropTypes.element,
filter: PropTypes.shape({
area: PropTypes.string,
price: PropTypes.number,
})
}

ReactDOM.render(<App fn={() => {}} />, document.getElementById('root'))

props默认值

例如当我们设计一个分页组件时,每页显示条数可以为默认值。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import './index.css';
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'

const App = props => {
return (
<div>
<h1>props默认值:{props.pageSize}</h1>
</div>
)
}

// 添加props默认值
App.defaultProps = {
pageSize: 10
}

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

组件的生命周期

学习组件的生命周期有助于理解组件的运行方式、从而完成更复杂的组件功能、分析组件错误原因等等

组件的生命周期指:组件从被创建到挂载在页面中运行,再到组件不用时卸载的过程。

钩子函数:生命周期的每个阶段总伴随着一些方法调用,这些方法就是生命周期的钩子函数,为开发人员在不同阶段操作组件提供了时机。

只有类组件才有生命周期

生命周期的三个阶段

  1. 创建时
  2. 更新时
  3. 卸载时
创建时(挂在阶段)
  • 执行时机:组件创建时(页面加载时)
  • 钩子函数执行顺序:
    1. constructor()
    2. render()
    3. componentDidMount()
钩子函数 触发时机 作用
constructor 创建组件时,最先执行 1. 初始化state2. 为事件处理程序绑定this
render 每次组件渲染都会触发 渲染UI(注意:不能调用setState())
componentDidMount 组件挂载(完成DOM渲染)后 1. 发送网络请求2. DOM操作

不能在render中调用setState的原因是:调用setState会导致数据更新以及UI更新(渲染),即setState方法将会调用render方法,因此如果在render中调用setState会导致递归效用

componentDidMount会紧跟render方法触发,由于DOM操作需要DOM结构已经渲染,因此DOM操作应被放置于该钩子函数内。

更新阶段
  • 更新阶段的执行时机包括:
    1. New props,组件接收到新属性
    2. setState(),调用该方法时
    3. forceUpdate(),调用该方法时

其中forceUpdate用于使组件强制更新,即使没有数值上的改变。

  • 钩子函数执行顺序:
    1. shouldComponentUpdate
    2. render()
    3. componentDidUpdate()
钩子函数 触发时机 作用
shouldComponentUpdate 更新阶段的钩子函数,组件重新渲染前执行(即在render前执行) 通过该函数的返回值来决定组件是否重新渲染。
render 每次组件渲染都会触发 渲染UI(与挂载阶段是同一个)
componentDidUpdate 组件更新(完成DOM渲染)后 1. 发送网络请求2. DOM操作

需要注意的是在componentDidUpdate中调用setState()必须放在一个if条件中,原因与在render中调用setState相同,render执行完后会立即执行componentDidUpdate导致递归调用。通常会比较更新前后的props是否相同,来决定是否重新渲染组件。可以使用componentDidUpdate(prevProps)得到上一次的props,通过this.props获取当前props

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
39
40
41
42
43
44
45
46
47
48
49
50
51
class App extends React.Component {

constructor(props) {
super(props)

this.state = {
count: 0
}
console.warn('生命周期钩子函数:constructor')
}

componentDidMount(){
console.warn('生命周期钩子函数:componentDidMount')
}

handleClick = () =>{
this.setState({
count: this.state.count + 1
})
}

render() {
return (
<div>
<Counter count={this.state.count} />
<button onClick={this.handleClick}>打豆豆</button>
</div>
)
}
}

class Counter extends React.Component {
render() {
console.warn('--子组件--生命周期钩子函数:render')
return <h1 id='title'>统计豆豆被打的次数:{this.props.count}</h1>
}



conponentDidUpdate(prevProps) {
console.warn('--子组件--生命周期钩子函数:conponentDidUpdate')

console.log('上一次的props: ', prevProps, ',当前的props:', this.props)
if(prevProps.count !== this.props.count) {
this.setState({})
// 发送ajax请求的代码
}
}
}

ReactDOM.render(<App />, document.getElementById('root'))
卸载时(卸载阶段)
  • 执行时机:组件从页面中消失

  • 钩子函数执行顺序:

    • componentWillUnmount
钩子函数 触发时机 作用
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
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
class App extends React.Component {

constructor(props) {
super(props)

this.state = {
count: 0
}
console.warn('生命周期钩子函数:constructor')
}

componentDidMount(){
console.warn('生命周期钩子函数:componentDidMount')
}

handleClick = () =>{
this.setState({
count: this.state.count + 1
})
}

render() {
return (
<div>
{this.state.count > 3 ? (
<p>豆豆被打死了~</p>
) : (
<Counter count={this.state.count} />
)}
<button onClick={this.handleClick}>打豆豆</button>
</div>
)
}
}

class Counter extends React.Component {

conponentDidMount() {
// 开启定时器
this.timerId = setInterval(() => {
console.log("定时器正在执行~")
}. 500)
}

render() {
console.warn('--子组件--生命周期钩子函数:render')
return <h1 id='title'>统计豆豆被打的次数:{this.props.count}</h1>
}

conponentWillUnmount(){
console.warn('--子组件--生命周期钩子函数:conponentWillUnmount')
// 清理定时器
clearInterval(this.timerId)
}

conponentDidUpdate(prevProps) {
console.warn('--子组件--生命周期钩子函数:conponentDidUpdate')

console.log('上一次的props: ', prevProps, ',当前的props:', this.props)
if(prevProps.count !== this.props.count) {
this.setState({})
// 发送ajax请求的代码
}
}
}

ReactDOM.render(<App />, document.getElementById('root'))
其他钩子函数
  • 旧版本遗留,先已弃用的钩子函数:
    • componentWillMount()
    • ComponentWillReceiveProps()
    • ComponentWillUpdate()
  • 新版完整生命周期钩子函数:
    • 创建时:
      • constructor
      • getDerivedStateFromProps(不常用)
      • render
      • React更新DOM和refs
      • componentDidMount
    • 更新时
      • getDerivedStateFromProps(不常用)
      • shouldComponentUpdate(详见组件性能优化)
      • render
      • getSnapshotBeforeUpdate(不常用)
      • React更新DOM和refs
      • componentDidUpdate
    • 卸载时
      • componentWillUnmount

render-props和高阶组件

组件复用

如果两个组件中的部分功能相似或相同时,该如何处理?

因此对于相似的功能,我们希望能偶复用相似的功能。

复用时事实上时复用以下两点:

  1. state
  2. 操作state的方法(组件状态逻辑

React中组件复用包含两种方式:

  1. render props模式
  2. 高阶组件(HOC)

以上两种方式是利用React自身特点的编码技巧,不是API

render-props模式

思路:将要复用的state和操作state的方法封装到一个组件中

问题:

  1. 如何拿到该组件中复用的state
    • 在使用组件时,添加一个值为函数的prop,通过函数参数来获取(需要组件内部实现)
  2. 如何渲染任意UI
    • 使用该函数的返回值作为要渲染的UI内容(需要组件内部实现)
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
import './index.css';
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'

/*
render props 模式
*/

// 导入图片资源
import img from './images/end_favicon128.ico'

// 创建复用组件
class Mouse extends React.Component {
constructor(props) {
super(props)

// 鼠标位置state
this.state = {
x: 0,
y: 0,
}
}

// 鼠标位置事件处理函数
handleMouseMove = e => {
this.setState({
x: e.clientX,
y: e.clientY,
})
}

// 开启鼠标监听事件
componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}

render() {
return this.props.render(this.state)
}
}

const App = props => {
return (
<div>
<h1>render props 模式</h1>
<Mouse render={(mouse) => {
return (
<p>鼠标位置:{mouse.x}, {mouse.y}</p>
)
}} />

{/* 复用Mouse组件 */}
<Mouse render={mouse => {
return (
<img src={img} style={{
position: 'absolute',
top: mouse.y,
left: mouse.x
}} />
)
}}>
</Mouse>
</div>
)
}


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

在上面这个例子中:

  • Mouse组件负责:封装复用的状态逻辑代码:
    1. 状态:鼠标坐标(x,y)
    2. 操作状态的方法:鼠标移动事件
  • 传入的render prop负责:使用复用的状态来渲染UI结构
children代替render

此外还能使用children替代上例中的render,这种方式更直观,更推荐使用:

1
2
3
4
5
6
7
8
<Mouse>
{(mouse) => <p>鼠标位置是 {mouse.x}, {mouse.y}</p>}
</Mouse>

// 组件内部
render() {
return this.props.children(this.state)
}

此处联想到前文提到的Context:

1
2
3
4
5
6
7
8
9
10
const Child = props => {
return <div className='child'>
<!-- 使用consumer组件接受数据 -->
<Consumer>
{
data => <span>我是子节点 -- {data}</span>
}
</Consumer>
</div>
}

实际上此处context也是使用了render props模式,且使用了children代替render。

现在可以对render props给出一个粗略的定义:

render props 模式是指将一个函数作为prop,并使用该函数告诉组件要渲染什么内容的技术

render props模式代码优化
  1. 给render props模式添加props校验
  2. 组件卸载时应该解除mousemove事件绑定(使用react添加的事件绑定react会帮我们处理)

高阶组件

高阶组件时一种采用包装(装饰)模式实现的状态逻辑复用(例如python中的高阶函数,java中的AOP)

实现思路:

  • 高阶组件(HOC)是一个函数,接收要包装的组件,返回增强后的组件
  • 高阶组件内部创建一个类组件,在这个类组件中提供复用的状态逻辑代码,通过prop将复用的状态传递给包装组件WrappedComponent

使用步骤

  1. 创建一个函数,名称约定以with开头
  2. 指定函数参数,参数应以大写字母开头(因为参数要被作为组件渲染)
  3. 在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回
  4. 在该组件中,渲染参数组件,同时将状态通过prop传递给参数组件
  5. 调用高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面中
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
import './index.css';
import React from 'react';
import ReactDOM from 'react-dom';
import PropTypes from 'prop-types'
import react from 'react';
import img from './images/end_favicon128.ico'
import { render } from '@testing-library/react';

/*
高阶组件模式
*/

// 创建高阶组件
function withMouse(WrappedComponent) {
// 该组件提供复用的状态逻辑
class Mouse extends react.Component {
constructor() {
super()

this.state = {
x: 0,
y: 0,
}
}

componentDidMount() {
window.addEventListener('mousemove', this.handleMouseMove)
}

handleMouseMove = e => {
this.setState({
x: e.clientX,
y: e.clientY,
})
}

render() {
return <WrappedComponent {...this.state}></WrappedComponent>
}

componentWillUnmount() {
window.removeEventListener('mousemove', this.handleMouseMove)
}
}

return Mouse
}


const Cat = props => (
<img src={img} alt='logo' style={{
position: 'absolute',
top: props.y,
left: props.x,
}}
/>
)

const Position = props => (
<p>
鼠标当前位置:(x: {props.x}, y: {props.y})
</p>
)


// 获取增强后的组件
const MousePosition = withMouse(Position)

const CatMouse = withMouse(Cat)

class App extends React.Component {
render (){
return (
<div>
<h1>高阶组件模式</h1>
{/* 渲染增强后的组件 */}
<MousePosition />
<CatMouse />
</div>
)
}
}


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

设置displayName

使用高阶组件存在的问题:

  • 得到的两个组件名称相同
    • 原因是默认情况下React使用组件名称作为displayName
    • 为高阶组件设置displayName便于调试时区分不同的组件
    • displayName:用于设置调试信息(React Develop Tools信息)

设置方法:

1
2
3
4
5
6
7
// 获得组件的displayName的函数
function getDisplayName(WrappedComponent) {
return WrappedComponent.displayName || WrappedComponent.name || 'Component';
}

// 设置displayName
Mouse.displayName = `WithMouse${getDisplayName(WrappedComponent)}`

传递props

使用高阶组件实际上是用一个组将将另一个组件包裹,那么这样会导致渲染时传入的props无法传递到被包裹的组件,而是传递到用于包裹的组件中,因此需要在用于包裹的组件中将props和state一起传递给被包裹的组件。

1
2
3
render() {
return <WrappedComponent {...this.state} {...this.props}></WrappedComponent>
}

React原理

setState方法

更新数据

  • setState方法更新数据时异步
  • 因此使用该语法时,后面的setState不能依赖于前面的setState
  • 另外,待用多次setState方法,只会触发一次重新渲染

推荐语法

推荐使用setState((state, props) => {})语法

  • 参数state表示最新的state

  • 参数props表示最新的props

  • 该方法中state的更新仍然是异步的,但该方法利用了回调函数的特性:setState本身是异步的,但setState函数内部的语句依然是同步进行的。解决数据不一致的问题,使得其参数中的state每次都是获取到最新的state,这样连续使用setState方法不会出现异步问题

1
2
3
4
5
6
7
8
9
10
11
this.setState((state, props) => {
return {
count: state.count + 1
}
})
this.setState((state, props) => {
return {
count: state.count + 1
}
})
// 两次setState将导致count+2
回调函数

思考这样一个实际引用中的问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
function postApi (url, data) {
var result = {};
$.ajax({
url: url,
type: 'post',
data: data ? data : {},
success: (res) => {
result = res
},
fail: (err) => {
result = res
}
})
return result
}

我们需要通过调用请求API得到一些数据,请求API中使用Ajax请求数据,但Ajax是异步的。

于是我们调用:

1
var res = postApi(url, data)

得到的res将是{}。

原因就在于JS这类脚本语言的执行机制,当js代码运行到调用postAPI的语句时,对于这些同步语句,JS将顺序执行,知道遇到异步语句,而此时,JS已经执行完postApi的传参,那么下一步将会创建一个result变量并将其初始化。接下来JS遇到了异步语句ajax,那么JS将会将ajax放入异步队列,然后继续执行下一个同步语句,也就是return result,同时位于异步队列中的ajax会进行计时器等待,取出并执行等操作。因此res接收到数据时,postApi中并没有完成对result的赋值。

当然ajax可以通过设置async:false将其设置为同步语句,但这样会导致进程阻塞效率下降。因此我们现在希望在执行完ajax中的语句后再对res赋值。

于是我们想到可以把postApi中的res作为参数传递给一个函数,由于函数时在异步语句内调用的,而异步语句的内部的操作实际上是同步的,因此ajax内部的函数调用会顺序执行。

下面我们给出回调函数的定义:

回调函数值函数的应用方式,出现在两个函数之间,用于指定异步的语句做完之后要做的事情

下发如下:

  • 把函数a当做参数传递到函数b中
  • 在函数b中以形参的方式进行调用
1
2
3
4
5
6
7
function a(cb){
cb()
}
function b(){
console.log('函数b')
}
a(b)

这一定义很像python中的高阶函数,高阶函数的定义为:以函数作为参数的函数,称为高阶函数。

可见高阶函数是对上例中的a进行了定义,而回调函数是对上例中的b进行了定义。

那么我们就可以使用回调函数来解决之前提到的这个问题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function postApi ( url, data, cb ) {
$.ajax({
url: url,
type: 'post',
data: data ? data : {},
success: (res) => {
cb && cb(res)
},
fail: (err) => {
cb && cb(err)
}
})
}

postApi(url, data, (res) => {
console.log(res)
})

此时我们就可以在调用postApi时传入的箭头函数中得到正确的res值,并在其中对res值进行一些操作。

甚至还可以使用闭包这一概念去理解这一方法的应用,使用回调函数时,实际上是利用了闭包的思想,保存了函数执行时的作用域,使得异步操作能在这个作用域中拿到准确的数据。

第二个参数

事实上setState函数还存在第二个参数:

1
2
3
4
this.setState(
(state, props) => {},
() => {console.log('这个回调函数会在状态更新后立即执行')}
)
  • 使用场景:在状态更新后并且页面完成重修渲染后立即执行某个操作
  • 注意这个执行时机与componentDidUpdate钩子函数执行时机相同
  • 语法setState(updater[, callback])

JSX语法转化过程

  • JSX仅仅是React.createElement的语法糖
  • JSX语法会被@babel/preset-react插件编译为createElement方法
  • createElement方法最终又会被转化为React元素(React Element),该元素是一个JS对象,用来描述UI内容

组件更新机制

对于多层树结构的组件结构,组件的更新过程如下:

  • 父组件重新渲染时,子组件也会被重修渲染
  • 渲染只发生在当前组件的子树中
  • 更新顺序按中序遍历序更新

image-20220111111224656

组件性能优化

减轻state

  • 只存储根组件渲染相关的数据(如列表数据/loading等)
    • 不用做渲染的数据不要放在state中,比如定时器id
    • 这些数据可以直接放在this中

避免不必要的重新渲染

  • 父组件的更新将会引起子组件更新
  • 但如果子组件没有任何变换也会重新渲染
  • 可以使用钩子函数shouldComponentUpdate(nextProps, nextState)
    • 触发时机:更新阶段的钩子函数,组件重新渲染前执行(即在render前执行)
    • 作用:返回一个boolean,通过该函数的返回值来决定组件是否重新渲染。
    • 两个参数表示了最新的state与最新的props
    • 在该函数中使用this.state能够获取到更新前的状态

纯组件

考虑上文提到的使用shouldComponentUpdate方法实现的避免重新渲染,如果每一个组件都需要我们手动地去实现这样的一个钩子函数,将会产生非常多的重复代码,但是有时候使用该方法运行我们进行一些特殊的操作,比如深比较,因此React为我们提供了更方便的方法:PureComponent

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
class Father extends Component {
constructor(props) {
super(props);
this.state = { value:0 }
}
onClick=()=>{
this.setState({
value : this.state.value+1
})
}
render() {
console.log('father render')
return (<div>
<button onClick={this.onClick}>click me</button>
<Son value={this.state.value}></Son>
</div> );
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import React, { Component,PureComponent } from 'react';

class Son extends PureComponent {
constructor(props) {
super(props);
this.state = { }
}
render() {
console.log('son render')
return (<div>
{this.props.value}
</div> );
}
}

export default Son;

但是使用纯组件时,纯组件内部进行的新旧值对比采用的是shallpw compare(浅对比)的方法:

  • 对于值类型而言:直接比较两个值是否相同
  • 但对于引用类型而言:值对比对象的地址是否相同

因此采用纯组件时,当我们需要更新state或props中的引用类型数据时,应该创建一个新数据,而不是直接修改原数据。

可以使用扩展运算符来创建新数据:

1
2
3
4
5
6
7
8
const newObj = {...state.obj, number:2}
this.setState({obj: newObj})

// 更新数组时不要使用push/unshift等直接修改当前数组的方法
// 可以使用concat或slice等返回新数组的方法
this.setState({
list: [...this.state.list, {/*新数据*/}]
})

虚拟DOM与Diff算法

React更新的思路是:只要state发生变化,就需要重新渲染视图。

但有这么一个问题:如果组件中有多个DOM元素,当只有一个DOM元素需要更新时,是不是也需要将整个组件全部更新?

实际上React通过虚拟DOM与Diff算法实现了组件的部分更新

实际上虚拟DOM对象就是React元素,用于描述UI。

React部分渲染的实现流程如下:

  1. 初次渲染时,React根据初始state(Model),创建一个虚拟DOM对象(虚拟DOM树)
  2. 根据虚拟DOM生产真正的DOM,渲染到页面中
  3. 当数据变化后,重新根据新数据,创建新的虚拟DOM对象
  4. 与上一次得到的虚拟DOM对象,使用Diff算法对比得到需要更新的内容
  5. 最终,React只将变化的内容更新(patch)到DOM中,重新渲染得到页面

image-20220111134757555

实际上虚拟DOM最大的价值在于:

虚拟DOM让React脱离了浏览器环境的束缚,为跨平台提供了基础

路由

React路由

现代前端应用大多数时SPA(单页应用程序),也就是只有一个HTML页面的应用程序,因为他的用户体验更好、对服务器的压力更小。为了有效地使用单个页面来管理原来多个页面的功能,前端路由应运而生。

  • 前端路由功能:让用户从一个视图(页面)导航到另一个视图(页面)
  • 前端路由是一套映射规则,在React中,是URL路径与组件的对应关系
  • 使用React路由简单来说,就是配置路径和组件(配对)

React路由基本使用

  1. 安装:yarn add react-router-dom
  2. 导入路由的三个核心组件:BrowserRouter/Route/Link
    • import {BrowserRouter as Router, Route, Link} from 'react-router-dom'
  3. 使用Router组件包裹整个应用
  4. 使用Link组件作为导航菜单(路由入口)
    • <Link to="/first">页面一</Link>
  5. 使用Route组件配置路由规则和要展示的组件(路由出口)
    • path表示路径,与Link中的to属性的内容对应
    • component表示要展示的组件
    • <Route path="/first" component={First}></Route>
    • 注意react-router-domV6版本之后使用方法有所改动

常用组件声明

  • Router组件:包裹整个应用,以恶搞React应用只需要使用一次
  • 两种常用的Router:
    • HashRouter(使用URL的哈希值实现(localhost:3000/#/first))在Vue中兼容性更好
    • BrowserRouter(使用H5中的history API实现(localhost:3000/first))
  • Link组件:用于指定导航链接
    • 最终会被编译为a标签;to属性被编译为href,即浏览器地址栏中的pathname
    • 可以通过location.pathname来获取to中的值
  • Route组件:指定路由展示组件相关信息
    • path属性:路由规则
    • component属性:展示的组件
    • Route组件写在哪,组件就会被渲染在哪

路由执行过程

  1. 点击Link组件,修改了浏览器地址中的url
  2. React路由监听到地址栏url变化
  3. React路由内部遍历所有Route组件,使用路由规则(path)与pathname进行匹配
  4. 当路由规则与pathname匹配时,展示该Route组件的内容

编程式导航

  • 编程式导航:通过JS代码实现页面跳转
1
this.props.history.push('/home')
  • history是Reract路由提供的,用于获取浏览器历史记录的相关信息
  • push(path):跳转到某个页面,参数path表示要跳转的路径
  • 注意react-route-domV6版本不支持此方法,应使用useNavigate()API
    • const navigate = userNavigate();navigate('/home')
  • go(n):前进或后退到某个页面,参数n表示前进或后退页面的数量(-1表示后退一页)

默认路由

  • 进入页面时默认的展示页面
  • 默认路由:进入页面时就会默认匹配的路由
  • 默认路由的path:/
    • <Route path="/" component={Home} />

匹配模式

模糊匹配模式

  • 问题:默认路由在路由切换时仍然会被显示(V6没有这个问题)
  • 原因:默认情况下React路由是模糊匹配模式
  • 模糊匹配规则:之哟啊pathname以path开头就会被匹配成功

精确匹配

  • 给Route组件添加exact属性,就能让其变为精确匹配模式
  • 精确匹配:只有当path和pathname 完全匹配时才会展示该路由

redux

  • redux是一个专门用于状态管理的JS库(不是react插件库)
  • 可用于三大框架,但基本与React配合
  • 集中式管理react应用中多个组件共享状态

使用redux的情况

  1. 某个组件的状态需要让其他组件共享
  2. 一个组件需要改变另一个组件的状态(通讯)

核心概念

action

  • 动作对象
  • 包含2个属性
    • type:标识属性,字符串,唯一,必要属性
    • data:数据属性,任意类型,可选属性
  • 例如:{ type: 'TOGGLE_TODO', index: 1 }

reducer

  • 用于初始化状态、加工状态
  • 加工时,根据旧的state和action,产生新的state的纯函数

store

  • 将state、action、reducer联系在一起的对象

  • 如何得到此对象:

    1
    2
    3
    import {createStore} from 'redux'
    import reducer from './reducers'
    const store = createStore(reducer)
  • 此对象的功能:

    • getState():得到state
    • dispatch(action): 分发action,触发reducer调用,产生新的state
    • subscribe(listener): 注册监听,当产生了新的state时,自动调用

Redux案例

该案例实现了一个计数器,计数器提供如下功能:

  • 可以从1,2,3中选择每次增加的步长
  • 实现加法
  • 实现减法
  • 实现奇数加
  • 实现异步加,等待时间为5秒

纯react写法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { Component } from "react";
import Count from './component/Count'

//App.js
class App extends Component {
render() {
return (
<div>
<Count />
</div>
)
}
}

export default App
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
import React, { Component } from "react";

// Count组件
class Count extends Component {

constructor() {
super()
this.state = {
count:0,
selectNumber: '1',
}
}

increment = () => {
// 函数体
const {count, selectNumber} = this.state
console.log(count, typeof(selectNumber));
this.setState({
count: count + (+ selectNumber)
})
}
decrement = () => {
// 函数体
const {count, selectNumber} = this.state
console.log( count, typeof(selectNumber));
this.setState({
count: count - (+ selectNumber),
})
}
incrementIfOdd = () => {
// 函数体
const {count, selectNumber} = this.state
console.log(count, selectNumber);
if(count % 2 !=0){
this.setState({
count: count + + selectNumber
})
}
}
incrementAsync = () => {
// 函数体
const {count, selectNumber} = this.state
console.log(count, selectNumber);
setTimeout(() => {
this.setState({
count: count + + selectNumber
})
}, 500)
}
handleChange = (e) => {
const target = e.target

const value = target.type === "checkbox" ? target.checked : target.value;
const name = target.name

this.setState({
[name]: value
})
}
render() {
return (
<div>
<h1>当前求和为:{this.state.count}</h1>
<select name="selectNumber" value={this.state.selectNumber} onChange={this.handleChange}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>&nbsp;
<button onClick={this.increment}>+</button>&nbsp;
<button onClick={this.decrement}>-</button>&nbsp;
<button onClick={this.incrementIfOdd}>当前求和为奇数则加</button>&nbsp;
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}

export default Count

redux精简实现

该精简版并未实现Redux中的Action Creators

目录结构:

1
2
3
4
5
6
7
8
9
src
- component
- Count
- index.js // count组件
- redux
- countReducer.js // 为Count组件提供共享数据
- store.js // 管理Reducer
- App.js // 根组件
- index.js // 项目入口
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
import React, { Component } from "react";
import store from "../../redux/store";

/**
* count组件
*/
class Count extends Component {

constructor() {
super()
this.state = {
selectNumber: '1',
}
}

componentDidMount() {
// 由于共享状态的更新不会引起本组件重新渲染,因此需要监听共享数据的变化
// 在组件挂载完成后为组件添加一个检测store中state变化的功能,通过回调函数重新渲染
store.subscribe(() => {
this.forceUpdate()
})
}

increment = () => {
// 函数体
const {selectNumber} = this.state
// 分发一个action
store.dispatch({type:'increment', data:selectNumber*1})
}
decrement = () => {
// 函数体
const {selectNumber} = this.state
store.dispatch({type:'decrement', data:selectNumber*1})
}
incrementIfOdd = () => {
// 函数体
const {selectNumber} = this.state
const count = store.getState()
if(count % 2 !=0){
store.dispatch({type:'increment', data:selectNumber*1})
}
}
incrementAsync = () => {
// 函数体
const {selectNumber} = this.state
setTimeout(() => {
store.dispatch({type: 'increment', data: selectNumber*1})
}, 500)
}
handleChange = (e) => {
const target = e.target

const value = target.type === "checkbox" ? target.checked : target.value;
const name = target.name

this.setState({
[name]: value
})
}
render() {
return (
<div>
{/* 获取利用store,Reducer中的公共状态 */}
<h1>当前求和为:{store.getState()}</h1>
<select name="selectNumber" value={this.state.selectNumber} onChange={this.handleChange}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>&nbsp;
<button onClick={this.increment}>+</button>&nbsp;
<button onClick={this.decrement}>-</button>&nbsp;
<button onClick={this.incrementIfOdd}>当前求和为奇数则加</button>&nbsp;
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}

export default Count
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* countReducer
* 该文件用于创建一个为Count组件服务的reducer,本质是一个函数
* reducer函数将接收到两个参数,分别为:1. 之前的状态preState,动作对象action
*/


const initState = 0
// 事实上该函数是一个纯函数
export default function countReducer(preState = initState, action) {
console.log(preState, action)
// 从action对象中解析type、data
const {type, data} = action
// 根据type类型判断操作
switch (type) {
case 'increment':
return preState + data
case 'decrement':
return preState - data
default:
return preState
}
}
1
2
3
4
5
6
7
8
9
/**
* store
* 暴露一个store对象
*/
import {createStore} from 'redux'
// 引入为count服务的reducer
import countReducer from './countReducer'

export default createStore(countReducer)

完整redux实现

完整版redux使用action creator来创建action,不需要我们自己创建

目录结构:

1
2
3
4
5
6
7
8
9
10
11
src
- component
- Count
- index.js // count组件
- redux
- constant.js // 定义action对象中type类型的常量值
- countActionCreator.js // 为Count组件生产action对象
- countReducer.js // 为Count组件提供共享数据
- store.js // 管理Reducer
- App.js // 根组件
- index.js // 项目入口
1
2
3
4
5
6
/**
* 该模块用于定义action对象中type类型的常量值
*/

export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* countActionCreator.js
* 该组件专门为Count组件生产action对象
*/
import { INCREMENT, DECREMENT } from "./constant"

// redux中规定同步action指action的值为对象
export const createIncrementAction = data => ({type:INCREMENT, data})

export const createDecrementAction = data => ({type:DECREMENT, data})

// redux中规定异步action指action的值为函数
// 异步action中一般都会使用同步action,因此如果使用异步action,store会将dispatch传入
// 异步action不是必须的,逻辑可以自己在组件中实现
export const createIncrementAsyncAction = (data, time) => {
return (dispatch) => {
setTimeout(() => {
dispatch(createIncrementAction(data))
},time)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* countReducer
* 该文件用于创建一个为Count组件服务的reducer,本质是一个函数
* reducer函数将接收到两个参数,分别为:1. 之前的状态preState,动作对象action
*/
import { INCREMENT, DECREMENT } from "./constant"

const initState = 0
// 事实上该函数是一个纯函数
export default function countReducer(preState = initState, action) {
console.log(preState, action)
// 从action对象中解析type、data
const {type, data} = action
// 根据type类型判断操作
switch (type) {
case INCREMENT:
return preState + data
case DECREMENT:
return preState - data
default:
return preState
}
}
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
import React, { Component } from "react";
import store from "../../redux/store";
// 引入actionCreator, 用于创建action对象
import {
createIncrementAction,
createDecrementAction,
createIncrementAsyncAction,
} from '../../redux/countActionCreator'
/**
* count组件
*/
class Count extends Component {

constructor() {
super()
this.state = {
selectNumber: '1',
}
}

componentDidMount() {
// 由于共享状态的更新不会引起本组件重新渲染,因此需要监听共享数据的变化
// 在组件挂载完成后为组件添加一个检测store中state变化的功能,通过回调函数重新渲染
store.subscribe(() => {
this.forceUpdate()
})
}

increment = () => {
// 函数体
const {selectNumber} = this.state
// 分发一个action
store.dispatch(createIncrementAction(selectNumber*1))
}
decrement = () => {
// 函数体
const {selectNumber} = this.state
store.dispatch(createDecrementAction(selectNumber*1))
}
incrementIfOdd = () => {
// 函数体
const {selectNumber} = this.state
const count = store.getState()
if(count % 2 !=0){
store.dispatch(createIncrementAction(selectNumber*1))
}
}
incrementAsync = () => {
// 函数体
const {selectNumber} = this.state
store.dispatch(createIncrementAsyncAction(selectNumber*1))
}
handleChange = (e) => {
const target = e.target

const value = target.type === "checkbox" ? target.checked : target.value;
const name = target.name

this.setState({
[name]: value
})
}
render() {
return (
<div>
{/* 获取利用store,Reducer中的公共状态 */}
<h1>当前求和为:{store.getState()}</h1>
<select name="selectNumber" value={this.state.selectNumber} onChange={this.handleChange}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>&nbsp;
<button onClick={this.increment}>+</button>&nbsp;
<button onClick={this.decrement}>-</button>&nbsp;
<button onClick={this.incrementIfOdd}>当前求和为奇数则加</button>&nbsp;
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}

export default Count
1
2
3
4
5
6
7
8
9
10
11
/**
* store
* 暴露一个store对象
*/
import { createStore, applyMiddleware } from 'redux'
// 引入为count服务的reducer
import countReducer from './countReducer'
// 引入redux-thunk,用于支持异步action
import thunk from 'redux-thunk'

export default createStore(countReducer, applyMiddleware(thunk))

异步Action

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* countActionCreator.js
* 该组件专门为Count组件生产action对象
*/
import { INCREMENT, DECREMENT } from "./constant"

// redux中规定同步action指action的值为对象
export const createIncrementAction = data => ({type:INCREMENT, data})

export const createDecrementAction = data => ({type:DECREMENT, data})

// redux中规定异步action指action的值为函数
// 异步action中一般都会使用同步action,因此如果使用异步action,store会将dispatch传入
// 异步action不是必须的,逻辑可以自己在组件中实现
export const createIncrementAsyncAction = (data, time) => {
return (dispatch) => {
setTimeout(() => {
dispatch(createIncrementAction(data))
},time)
}
}

使用时需要利用redux-thunk库开启异步action支持

1
2
3
4
5
6
7
8
9
10
11
/**
* store
* 暴露一个store对象
*/
import { createStore, applyMiddleware } from 'redux'
// 引入为count服务的reducer
import countReducer from './countReducer'
// 引入redux-thunk,用于支持异步action
import thunk from 'redux-thunk'

export default createStore(countReducer, applyMiddleware(thunk))

react-redux

react-redux是react官方开发的,为了实现react的redux支持

react-redux中将组件分类两类:

  1. 容器组件
  2. UI组件
  • 容器组件是UI组件的父组件,负责和redux进行通讯,可随意使用reduxAPI,且容器组件能够检测redux中state的改变,不需要再自己添加检测
  • UI组件中不能使用reduxAPI
  • 容器组件负责将redux中保存的状态以及操作状态的方法传递给UI组件,并且通过props传递

UI组件就是我们常写的组件,而容器组件则需要使用react-redux的connect函数创建,

connect函数包含两参数,然会一个函数:

  • mapStateToProps,该函数的返回的对象将作为传递给UI组件的props,用于传递redux状态
  • mapDispatchToProps,该函数的返回的对象将作为传递给UI组件的props,用于传递redux操作状态的方法
  • 返回的函数接收一个UI组件作为参数,最终返回一个容器组件

此外,react-redux还提供了Provider组件,用于自动的将store传递给所有容器组件

下面使用React-Redux来写一下之前的案例:

目录结构:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
src
- container
- Count
- index.js // count组件
- redux
- store.js // 管理Reducer
- constant.js // 定义action对象中type类型的常量值
- actions
- count.js // 为Count组件生产action对象
- reducers
- count.js // 为Count组件提供共享数据

- App.js // 根组件
- index.js // 项目入口
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
/**
* Count组件
*/
// 引入connect用于连接UI组件与容器组件
import { connect } from "react-redux";
// 引入action
import { createDecrementAction, createIncrementAction, createIncrementAsyncAction } from "../../redux/actions/count";

import React, { Component } from "react";

/**
* 定义UI组件
*/
class Count extends Component {

constructor() {
super()
this.state = {
selectNumber: '1',
}
}

increment = () => {
// 函数体
const {selectNumber} = this.state
this.props.jia(selectNumber*1)
// 分发一个action
}
decrement = () => {
// 函数体
const {selectNumber} = this.state
this.props.jian(selectNumber*1)
}
incrementIfOdd = () => {
// 函数体
const {selectNumber} = this.state
if(this.props.count % 2 !== 0){
this.props.jia(selectNumber * 1)
}
}
incrementAsync = () => {
// 函数体
const {selectNumber} = this.state
this.props.jiaAsync(selectNumber * 1, 500)
}
handleChange = (e) => {
const target = e.target

const value = target.type === "checkbox" ? target.checked : target.value;
const name = target.name

this.setState({
[name]: value
})
}
render() {
return (
<div>
<h2>我是Count组件</h2>
{/* 获取利用store,Reducer中的公共状态 */}
<h4>当前求和为:{this.props.count}</h4>
<select name="selectNumber" value={this.state.selectNumber} onChange={this.handleChange}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>&nbsp;
<button onClick={this.increment}>+</button>&nbsp;
<button onClick={this.decrement}>-</button>&nbsp;
<button onClick={this.incrementIfOdd}>当前求和为奇数则加</button>&nbsp;
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}

/**
* 定义容器组件
*/

// 需要连接UI组件与Redux的store,但store需要通过props传入该组件
// connect函数包含两个参数,均为回到函数
const CountContainer = connect(
// mapStateToProps
state => ({count:state}),
// mapDispatchToProps
// dispatch =>
// ({
// jia:(number) => dispatch(createIncrementAction(number)),
// jian:(number) => dispatch(createDecrementAction(number)),
// jiaAsync:(number, time) => dispatch(createIncrementAsyncAction(number, time)),
// })
// 还剋使用如下简写方式触发react-redux的默认分发,即自动调用dispatch
{
jia:createIncrementAction,
jian:createDecrementAction,
jiaAsync:createIncrementAsyncAction,
}
)(Count)

export default CountContainer
1
2
3
4
5
6
7
8
9
10
11
/**
* store
* 暴露一个store对象
*/
import { createStore, applyMiddleware } from 'redux'
// 引入为count服务的reducer
import countReducer from './reducers/count'
// 引入redux-thunk,用于支持异步action
import thunk from 'redux-thunk'

export default createStore(countReducer, applyMiddleware(thunk))
1
2
3
4
5
6
/**
* 该模块用于定义action对象中type类型的常量值
*/

export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* countActionCreator.js
* 该组件专门为Count组件生产action对象
*/
import { INCREMENT, DECREMENT } from "../constant"

// redux中规定同步action指action的值为对象
export const createIncrementAction = data => ({type:INCREMENT, data})

export const createDecrementAction = data => ({type:DECREMENT, data})

// redux中规定异步action指action的值为函数
// 异步action中一般都会使用同步action,因此如果使用异步action,store会将dispatch传入
// 异步action不是必须的,逻辑可以自己在组件中实现
export const createIncrementAsyncAction = (data, time) => {
return (dispatch) => {
setTimeout(() => {
dispatch(createIncrementAction(data))
},time)
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* countReducer
* 该文件用于创建一个为Count组件服务的reducer,本质是一个函数
* reducer函数将接收到两个参数,分别为:1. 之前的状态preState,动作对象action
*/
import { INCREMENT, DECREMENT } from "../constant"

const initState = 0
// 事实上该函数是一个纯函数
export default function countReducer(preState = initState, action) {
console.log(preState, action)
// 从action对象中解析type、data
const {type, data} = action
// 根据type类型判断操作
switch (type) {
case INCREMENT:
return preState + data
case DECREMENT:
return preState - data
default:
return preState
}
}

使用react-redux实现数据共享

现有两个组件

  • count
  • person

希望这两个组件使用的state能够相互共享。

目录结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
src
- container
- Count
- index.js // count组件
- Person
- index.js // person组件
- redux
- store.js // 管理Reducer
- constant.js // 定义action对象中type类型的常量值
- actions
- count.js // 为Count组件生成action对象
- person.js // 为Person组件生成action对象
- reducers
- count.js // 为Count组件提供共享数据
- person.js // 为Person组件提供共享数据

- App.js // 根组件
- index.js // 项目入口

Count组件与前文提到的案例相同再次不赘述。

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import React, { Component } from "react";
import { nanoid } from "nanoid";
import { connect } from "react-redux";
import { createAddPersonAction } from '../../redux/actions/person'

class Person extends Component {
constructor() {
super()
this.state = {
userName: '',
userAge: '',
}
}

handleChange = e => {
const target = e.target;
const value = target.type === "checkbox" ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
})
}

addPerson = e => {
const {userName, userAge} = this.state
const personObj = {id:nanoid(), name: userName, age: userAge}
this.props.add_person(personObj)
this.setState({
userName: '',
userAge: '',
})
// console.log(personObj);
}

render() {
return (
<div>
<h2>我是Pserson组件,上方组件和为{this.props.sum}</h2>
<input name="userName" type="text" placeholder="输入名字" value={this.state.userName} onChange={this.handleChange} />
<input name="userAge" type="text" placeholder="输入年龄" value={this.state.userAge} onChange={this.handleChange} />
<button onClick={this.addPerson}>添加</button>
<ul>
{
this.props.persons.map((person) => {
return <li key={person.id}>{person.name}--{person.age}</li>
})
}
</ul>
</div>
)
}
}

export default connect(
state => ({
persons: state.persons,
sum: state.count,
}),
{
add_person: createAddPersonAction,
}
)(Person)
1
2
3
4
import { ADD_PERSON } from "../constant";

// 创建增加一个人的action
export const createAddPersonAction = (personObj) => ({type:ADD_PERSON, data: personObj})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ADD_PERSON } from "../constant";

// 初始化人列表
const initState = [{id:'001', name:'init', age:0}]


export default function personReducer(perState = initState, action) {
const {type, data} = action
switch (type) {
case ADD_PERSON:
return [data,...perState]

default:
return perState;
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* store
* 暴露一个store对象
*/
import { createStore, applyMiddleware, combineReducers } from 'redux'
// 引入为count服务的reducer
import countReducer from './reducers/count'
// 引入为person服务的reducer
import personReducer from './reducers/person'
// 引入redux-thunk,用于支持异步action
import thunk from 'redux-thunk'


const allReducer = combineReducers({
count: countReducer,
persons: personReducer,
})

export default createStore(allReducer, applyMiddleware(thunk))

纯函数

上文中提到redux的reducer必须是一个纯函数,那么什么样的函数是纯函数呢?

如果一个函数具有如下特性,则认为该函数是一个纯函数:

只要给定同样的输入,则必定返回同样的输出。

纯函数应该具有如下约束:

  1. 不得改写参数数据
  2. 不会产生任何副作用,例如网络请求,输入和输出设备
  3. 不能调用Data.now()或者Math.random()等不纯函数

redux开发者工具

  1. 为浏览器安装Redux Dev Tools插件
  2. 为项目安装库redux-devtools-extension
  3. 在store.js中引入
    • import { composeWithDevTools } from 'redux-devtools-extension'
  4. 修改暴露为:
    • export default createStore(allReducer, composeWithDevTools(applyMiddleware(thunk)))

也可以使用如下方式开启调试,无需下载redux-devtools-extension库:

  1. import {compose} from 'redux'
  2. export default createStore(allReduers, composeEnhancers(applyMiddleware(thunk)))

案例最终版

如上案例进行优化后,例如使用index将所有reducer汇总,统一暴露,尽量触发对象简写方式等。

项目结构如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
src
- container
- Count
- index.js // count组件
- Person
- index.js // person组件
- redux
- store.js // 管理Reducer
- constant.js // 定义action对象中type类型的常量值
- actions
- count.js // 为Count组件生成action对象
- person.js // 为Person组件生成action对象
- reducers
- index.js // 将所有reducers汇总并暴露
- count.js // 为Count组件提供共享数据
- person.js // 为Person组件提供共享数据

- App.js // 根组件
- index.js // 项目入口
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
/**
* Count组件
*/
// 引入connect用于连接UI组件与容器组件
import { connect } from "react-redux";
// 引入action
import { increment, incrementAsync, decrement } from "../../redux/actions/count";

import React, { Component } from "react";

/**
* 定义UI组件
*/
class Count extends Component {

constructor() {
super()
this.state = {
selectNumber: '1',
}
}

increment = () => {
// 函数体
const {selectNumber} = this.state
this.props.increment(selectNumber*1)
// 分发一个action
}
decrement = () => {
// 函数体
const {selectNumber} = this.state
this.props.decrement(selectNumber*1)
}
incrementIfOdd = () => {
// 函数体
const {selectNumber} = this.state
if(this.props.count % 2 !== 0){
this.props.increment(selectNumber * 1)
}
}
incrementAsync = () => {
// 函数体
const {selectNumber} = this.state
this.props.incrementAsync(selectNumber * 1, 500)
}
handleChange = (e) => {
const target = e.target

const value = target.type === "checkbox" ? target.checked : target.value;
const name = target.name

this.setState({
[name]: value
})
}
render() {
return (
<div>
<h2>我是Count组件,下方组件总人数为{this.props.personNum}</h2>
{/* 获取利用store,Reducer中的公共状态 */}
<h4>当前求和为:{this.props.count}</h4>
<select name="selectNumber" value={this.state.selectNumber} onChange={this.handleChange}>
<option value="1">1</option>
<option value="2">2</option>
<option value="3">3</option>
</select>&nbsp;
<button onClick={this.increment}>+</button>&nbsp;
<button onClick={this.decrement}>-</button>&nbsp;
<button onClick={this.incrementIfOdd}>当前求和为奇数则加</button>&nbsp;
<button onClick={this.incrementAsync}>异步加</button>
</div>
)
}
}

/**
* 定义容器组件
*/

// 需要连接UI组件与Redux的store,但store需要通过props传入该组件
// connect函数包含两个参数,均为回到函数
const CountContainer = connect(
// mapStateToProps
state => ({
count:state.count,
personNum:state.persons.length,
}),
// mapDispatchToProps
// dispatch =>
// ({
// jia:(number) => dispatch(createIncrementAction(number)),
// jian:(number) => dispatch(createDecrementAction(number)),
// jiaAsync:(number, time) => dispatch(createIncrementAsyncAction(number, time)),
// })
// 还可使用如下简写方式触发react-redux的默认分发,即自动调用dispatch
{
increment,
decrement,
incrementAsync,
}
)(Count)

export default CountContainer
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
import React, { Component } from "react";
import { nanoid } from "nanoid";
import { connect } from "react-redux";
import { addPerson } from '../../redux/actions/person'

class Person extends Component {
constructor() {
super()
this.state = {
userName: '',
userAge: '',
}
}

handleChange = e => {
const target = e.target;
const value = target.type === "checkbox" ? target.checked : target.value;
const name = target.name;
this.setState({
[name]: value
})
}

addPerson = e => {
const {userName, userAge} = this.state
const personObj = {id:nanoid(), name: userName, age: userAge}
this.props.addPerson(personObj)
this.setState({
userName: '',
userAge: '',
})
// console.log(personObj);
}

render() {
return (
<div>
<h2>我是Pserson组件,上方组件和为{this.props.sum}</h2>
<input name="userName" type="text" placeholder="输入名字" value={this.state.userName} onChange={this.handleChange} />
<input name="userAge" type="text" placeholder="输入年龄" value={this.state.userAge} onChange={this.handleChange} />
<button onClick={this.addPerson}>添加</button>
<ul>
{
this.props.persons.map((person) => {
return <li key={person.id}>{person.name}--{person.age}</li>
})
}
</ul>
</div>
)
}
}

export default connect(
state => ({
persons: state.persons,
sum: state.count,
}),
{
addPerson,
}
)(Person)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
/**
* countActionCreator.js
* 该组件专门为Count组件生产action对象
*/
import { INCREMENT, DECREMENT } from "../constant"

// redux中规定同步action指action的值为对象
export const increment = data => ({type:INCREMENT, data})

export const decrement = data => ({type:DECREMENT, data})

// redux中规定异步action指action的值为函数
// 异步action中一般都会使用同步action,因此如果使用异步action,store会将dispatch传入
// 异步action不是必须的,逻辑可以自己在组件中实现
export const incrementAsync = (data, time) => {
return (dispatch) => {
setTimeout(() => {
dispatch(increment(data))
},time)
}
}
1
2
3
4
import { ADD_PERSON } from "../constant";

// 创建增加一个人的action
export const addPerson = (personObj) => ({type:ADD_PERSON, data: personObj})
1
2
3
4
5
6
7
8
9
10
11
12
13
// 引入为count服务的reducer
import count from './count'
// 引入为person服务的reducer
import persons from './person'

// 用于汇总多个reducer
import { combineReducers } from 'redux'

// 当key,value同名时触发对象简写方式
export default combineReducers({
count,
persons,
})
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
/**
* countReducer
* 该文件用于创建一个为Count组件服务的reducer,本质是一个函数
* reducer函数将接收到两个参数,分别为:1. 之前的状态preState,动作对象action
*/
import { INCREMENT, DECREMENT } from "../constant"

const initState = 0
// 事实上该函数是一个纯函数
export default function countReducer(preState = initState, action) {
console.log(preState, action)
// 从action对象中解析type、data
const {type, data} = action
// 根据type类型判断操作
switch (type) {
case INCREMENT:
return preState + data
case DECREMENT:
return preState - data
default:
return preState
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
import { ADD_PERSON } from "../constant";

// 初始化人列表
const initState = [{id:'001', name:'init', age:0}]


export default function personReducer(perState = initState, action) {
const {type, data} = action
switch (type) {
case ADD_PERSON:
return [data,...perState]

default:
return perState;
}
}
1
2
3
4
5
6
7
/**
* 该模块用于定义action对象中type类型的常量值
*/

export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
export const ADD_PERSON = 'add_person'
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
/**
* store
* 暴露一个store对象
*/
import { createStore, applyMiddleware } from 'redux'

// 引入汇总reducer
import allReducer from './reducers'

// 引入redux-thunk,用于支持异步action
import thunk from 'redux-thunk'

// 引入开发者工具
import { composeWithDevTools } from 'redux-devtools-extension'




export default createStore(allReducer, composeWithDevTools(applyMiddleware(thunk)))
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import React, { Component } from "react";
import Count from './containers/Count'
import Person from "./containers/Person";


class App extends Component {
render() {
return (
<div>
<Count />
<hr />
<Person />
</div>
)
}
}

export default App
1
2
3
4
5
6
7
8
9
10
11
12
13
import React from "react";
import reactDom from "react-dom";
import App from './App'
import store from './redux/store'
import { Provider } from 'react-redux'


reactDom.render(
<Provider store={store}>
<App></App>
</Provider>,
document.getElementById('root')
)

React-Native

项目结构

react-native目录结构简介:

1
2
3
4
5
6
7
8
- android 与安卓客户端编译相关的配置
- ios 与ios哭护短编译相关的配置
- .eslintrc.js 代码风格配置
- .prettierrc.js 代码格式化风格配置
- App.js 项目的根组件
- index.js 项目的入口文件
- package.json 项目第三方包相关信息
- babel

RN布局

flex布局

  • 所有容器默认为felxbox
  • 并且默认为纵向排列,即felx-direction: colum

样式继承

  • 在RN中样式没有继承关系

单位

  • 在RN中不能为表示宽高的数字增加单位,RN会自动处理单位。

  • RN中可以使用百分比表示宽高。

  • RN中的默认单位为dp

  • px与dp转换:

    • $$
      dp宽高 = 屏幕宽高(px) * 元素宽高(px) / 设计稿宽高(px)
      $$

可以通过构造如下工具来解决px转dp的问题

1
2
3
4
5
6
7
8
9
10
import {Dimensions} from "react-native"

export const screenWidth = Dimensions.get("window").width;

export const screenHeight = Dimension.get("window").height;

/**
* 次数假设设计稿宽为375
*/
export const pxToDp = (elePx) => screenWidth * elePx / 375

屏幕宽高

1
2
3
import {Dimensions} from "react-native";
const screenWidth = Math.round(Dimensions.get('window').width);
const screenHeight = Math.round(Dimensions.get('window').height);

变换

1
<Text style={{transform:[{translateY:300}, {scale:2}]}}>变换</Text>

标签

View
  • 相当于div
  • 不支持字体大小,字体颜色
  • 不能直接放文本内容
  • 不支持直接绑定点击事件(一般使用TouchableOpacity代替)
Text

文本标签

  • 文本标签,可以设置字体颜色、大小
  • 支持绑定点击事件
TouchableOpacity

可以绑定点击事件的块标签

  • 相当于块容器
  • 支持绑定点击事件onPress
  • 可以设置点击时的透明度
1
<TouchableOpecity activeOpacity={0.5} onPress={this.handleOnPress}></TouchableOpecity>
Image

图片标签

  • 渲染本地图片时
1
<Image source={require("../img.png")} />
  • 渲染网络图片
1
<Image source={{url:"https://z3.ax1x.com/2021/08/05/fego40.png"}} style={{width:200,height:300}} />

​ 注意一定要加宽高,不然无法显示

  • Android设备上渲染GIF和WebP

    默认不支持,需要在Android/app/build/gradle中手动添加模块:

    1
    2
    3
    4
    5
    6
    7
    8
    9
    dependencies {
    // 如果需要支持Android4.0之前的版本
    implementation 'com.facebook.fresco:animated-base-support:1.3.0'
    // GIF支持
    implementation 'com.facebook.fresco:animated-gif:2.0.0'
    // Webp格式,包括Webp动图支持
    implementation 'com.facebook.fresco:animated-webp:2.1.0'
    implementation 'com.facebook.fresco:webpsupport:2.0.0'
    }

    注意当更改依赖时需要重启调试

  • ImageBackground

    用于实现带有背景的块级元素

    1
    2
    3
    <ImageBackground source={...} style={{width:'100%', height: '100%'}}>
    <Text>Inside</Text>
    </ImageBackground>

    必须就有style属性

  • TextInpute

    输入框组件

    通过onChangeText事件来获取输入框的值

    1
    <TextInpute onChangeText={handleChangeText} ></TextInpute>

    注意他初始状态是没有样式的

调试

RN有两种调试方式:

  1. 谷歌浏览器

    • 不能查看标签结构
    • 不能查看网络请求
  2. 使用RN推荐的工具react-native-debugger

    • 可以查看标签结构

    • 不能查看网络请求

想要查看网络请求则需要进行如下配置:

  1. 找到项目入口文件index.js

  2. 加入以下代码:

    1
    GLOBAL.XMLHttpRequest = GLOBAL.originalXMLHttpRequest || GLOBAL.XMLHttpRequest

this指向问题

可以使用如下四种方式解决this指向问题:

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
39
40
41
42
43
44
import React, { Component } from 'react'
import { View, Text } from 'react-native'
class Index extends Component {
state = { num:100 }
// 丢失 state
handlePress1() {
console.log(this.state);
}
// 正常
handlePress2 = () => {
console.log(this.state);
}
// 正常
handlePress3() {
console.log(this.state)
}
// 正常
handlePress4() {
console.log(this.state)
}

// 正常
handlePress5() {
console.log(this.state)
}
constructor() {
super()
this.handlePress = this.handlePress.bind(this);
}

// 正常
render() {
return (
<View>
{/* 导致事件函数中获取不到 */}
<Text onPress={this.handlePress1} >事件1</Text>
<Text onPress={this.handlePress2} >事件2</Text>
<Text onPress={this.handlePress3.bind(this)} >事件3</Text>
<Text onPress={()=>this.handlePress4()}>事件4</Text>
<Text onPress={handlePress5()}>事件5</Text>
</View>
)
}
}

RN生命周期

与React相同

mobx

react中全局数据管理库,可以简单实现数据的跨组件共享

安装依赖

  • mobx核心库
  • mobx-react方便在react中使用mobx技术的库
  • @babel/plugin-proposal-decorators让rn项目支持es7中的装饰器语法库
1
yarn add mobx mobx-react @babel/plugin-proposal-decorators

添加配置

在babel.config.js添加如下配置:

1
2
3
plugins: [
['@babel/plugin-proposal-decorators', {'legacy':true}]
]

新建全局数据文件

新建文件mobx\index.js添加如下配置

1
2
3
4
5
6
7
8
9
10
11
12
13
import {observable, action} from 'mbox'

class RootStore {
// es7装饰器语法,使用Object.defineProperty实现
// observable 表示数据可以监控,表示是全局数据
@observable name = "Hello"
// action行为 表示 changeName是个可以修改全局共享数据的方法
@action changeName(name) {
this.name = name;
}
}

export default new RootStore()

挂载

通过provider来挂载和传递

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { Component } from 'react';
import { View } from 'react-native';
import rootStore from './mobx';
import { Provider } from 'mbox-react';
class Index extends Component {
render() {
return (
<View>
<Provider rootStore={rootStore}>
<Sub1></Sub1>
</Provider>
</View>
)
}
}

使用

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
import React, { Component } from 'react';
import { View, Text } from 'react-native';
import { inject,observer } from 'mbox-react';

@inject("rootStore") // 注入Provider的属性名,用来获取全局数据
@observer // 接收全局遍历改变,当全局发生改变,组件重新渲染从而显示最新数据
class Sub1 extends Component {
changeName = () => {
// 修改全局数据
this.props.rootStore.changeName("goodbye");
}
render() {
return (
<View>
<Text onPress={this.changeName}>{this.props.rootStore.name}</Text>
</View>
);
}
}

RN常用图表库

RN打包APK

生成签名密钥

使用如下命令进入jdk\bin目录,利用jdk提供的ketytool生成一个私有密钥:

1
2
$ cd D:\java\jdk8\bin\
$ keytool -genkeypair -v -storetype PKCS12 -keystore my-release-key.keystore -alias my-key-alias -keyalg RSA -keysize 2048 -validity 1000

这条命令会要求你输入密钥库(keystore)和对应密钥的密码,然后设置一些发行相关的信息。最后生成一个叫做my-release-key.keystore的密钥库文件。

在运行上面这条语句之后,密钥库里应该已经生成了一个单独的密钥,有效期为 10000 天。–alias 参数后面的别名是将来为应用签名时所需要用到的。

设置Gradle变量

  1. my-release-key.keystore文件放到工程中的android/app文件夹下。
  2. 编辑~/.gradle/gradle.properties(全局配置,对所有项目有效)或是项目目录/android/gradle.properties(项目配置,只对所在项目有效)。如果没有gradle.properties文件就自己创建一个,添加如下的代码:
1
2
3
4
MYAPP_RELEASE_STORE_FILE=my-release-key.keystore
MYAPP_RELEASE_KEY_ALIAS=my-key-alias
MYAPP_RELEASE_STORE_PASSWORD=*****
MYAPP_RELEASE_KEY_PASSWORD=*****

上面的这些会作为 gradle 的变量,在后面的步骤中可以用来给应用签名。

将签名加入项目

编辑项目目录下的android/app/build.gradle,添加如下的签名配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
...
android {
...
defaultConfig { ... }
signingConfigs {
release {
if (project.hasProperty('MYAPP_RELEASE_STORE_FILE')) {
storeFile file(MYAPP_RELEASE_STORE_FILE)
storePassword MYAPP_RELEASE_STORE_PASSWORD
keyAlias MYAPP_RELEASE_KEY_ALIAS
keyPassword MYAPP_RELEASE_KEY_PASSWORD
}
}
}
buildTypes {
release {
...
signingConfig signingConfigs.release
}
}
}
...

生成发行 APK 包

运行以下命令生成APK:

1
2
$ cd android
$ ./gradlew assembleRelease

Gradle 的assembleRelease参数会把所有用到的 JavaScript 代码都打包到一起,然后内置到 APK 包中。如果想调整下这个行为(比如 js 代码以及静态资源打包的默认文件名或是目录结构等),可以在android/app/build.gradle文件中进行配置。

生成的 APK 文件位于android/app/build/outputs/apk/release/app-release.apk

测试

输入以下命令可以在设备上安装发行版本:

1
$ npx react-native run-android --variant=release

注意--variant=release参数只能在完成了上面的签名配置之后才可以使用。

RN网络请求——Axios

安装

使用如下命令安装Axios:

1
2
$ npm install axios
$ yarn add react-native-axios

安装antdUI组件库:

1
$ npm install antd-mobile --save

封装

直接使用Axios进行请求时,为了完成请求地址拼接,参数设置,异步操作处理,JSON格式转化等等操作,将会产生许多冗余代码,为了简化代码,需要对Axios进行二次封装:

  • 在src目录下创建utils目录,该目录下创建http目录
  • 在http目录下创建文件httpBaseConfig.jsrequest.js
    • httpBaseConfig.js用于配置服务器域名,端口号,API地址
    • request.js用于编写Axios请求逻辑

Axios基于ES6中的Promise对象进行开发,因此可以使用then链来处理同步问题,而ES7加入async函数后,可以在async函数中使用await关键词实现更方便的处理,await会阻塞后续代码直到得到返回的Promise对象,具体可以参考如下博客:

理解 JavaScript 的 async/await - SegmentFault 思否

Axios还为我们提供了方便的基础设置、拦截器等操作,通过设置回调函数可以完成发送请求前,和得到返回的数据后进行处理。

最后我们将不同类型的请求封装到一个http类中。

最后得到的request.js如下:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
import axios from "axios";
import baseConfig from "./httpBaseConfig";

// 默认域名
axios.defaults.baseURL =
baseConfig.baseUrl + ":" + baseConfig.port + baseConfig.prefix;
// 默认请求头
axios.defaults.headers["Content-Type"] = "application/json";

// 响应时间
axios.defaults.timeout = 10000;

// 请求拦截器
axios.interceptors.request.use(
(config) => {
// TODO:在发送前做点什么
// showLoading(); //显示加载动画
return config;
},
(error) => {
// hideLoading(); //关闭加载动画
// TODO:对响应错误做点什么
return Promise.reject(error);
}
);

// 响应拦截器
axios.interceptors.response.use(
(response) => {
// TODO:请求返回数据后做点什么
if (response.status === "200" || response.status === 200) {
return response.data.data || response.data;
} else {
// TODO:请求失败后做点什么
throw Error(response.opt || "服务异常");
}
return response;
},
(error) => {
// TODO:对应响应失败做点什么
return Promise.resolve(error.response);
}
);

// 请求类
export default class http {
// ES7异步get函数
static async get(url, params) {
try {
let query = await new URLSearchParams(params).toString();
let res = null;
if (!params) {
res = await axios.get(url);
} else {
res = await axios.get(url + "?" + query);
}
return res;
} catch (error) {
return error;
}
}

static async post(url, params) {
try {
let res = await axios.post(url, params);
return res;
} catch (error) {
return error;
}
}

static async patch(url, params) {
try {
let res = await axios.patch(url, params);
return res;
} catch (error) {
return error;
}
}

static async put(url, params) {
try {
let res = await axios.put(url, params);
return res;
} catch (error) {
return error;
}
}

static async delete(url, params) {
/**
* params默认为数组
*/
try {
let res = await axios.post(url, params);
return res;
} catch (error) {
return error;
}
}
}

httpBaseConfig.js中的配置如下:

1
2
3
4
5
export default httpBaseConfig = {
baseUrl: 'http://www.*****.***',
port: '****',
prefix: '/AppServer/ajax/'
}

最后使用时,调用请求后,我们得到的将是一个Promise对象,使用then链将其保存到状态中即可完成数据显示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
handleRequest() {
let param = {
userName: "mingming",
classTimeId: "50648",
type: "3",
// 'callback': 'ha'
};
http.get("teacherApp_lookNotice.do", param)
.then((res) => {
console.log(res);
let data = JSON.parse(res);
this.setState({
message: data.message,
});
console.log(data);
})
.catch((error) => {
console.log(error);
});
}

最终打印到控制台的结果如下:

1
{"data": {"author": "明茗", "content": "这是一个测试通知测试通知测试通知测试通知测试通知测试通知,测试***功等等,还有***,和大***等等", "isAuthor": true, "isUpdate": false, "noReadNum": 53, "noreadList": ["索夏利", "何一繁", "段莹", " 孙亮亮", "李冯石", "贺玉婷", "张立新", "龚夏萌", "刘驰誉", "王玲", "张俊", "王楠", "姜克杰", "孙丽园", "李波", "代麦玲", "李妮", "李坤江", "李杰", "黄运科", "陈雨菲", "黄萍", "王致远", "李杰", "柯团团", "陈雯慧", "彭思毅", "张昌", "段怡欣", "管雅", "严彤鑫", "徐文莉", "朱景洲", "刘乔瑞", "王子豪", "孙红", "赵美婷", "李雕坛", "黄楠", "张静静", "刘祎璠", "冯健强", "王俊杰", "张辉", "彭诗雨", "叶刚", "何萍", "何健", "王锦婷", "周骏", "杨千骏", "李娇", "郭聪聪"], "num": 60, "readList": ["李龙龙", "杨文选", "刘佳璇", "方建辉", "卢文静", "左亚东", "李盈斌"], "readNum": 7, "title": "测试通知"}, "message": "数据保存成功!", "success": true}

RN使用nanoid生成key

react强调组件式开发,因此有时我们可能会碰到如下情况:

  • 一个同样的组件需要多次渲染

例如我所模拟的IOS计算器中,将所有按钮抽象为一个组件,通过传入按钮类型,大小,事件来区分他们,并以矩阵的形式对他们进行渲染。

首先对于这样的布局渲染,在JS中可以很轻松的使用map实现:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
buttonsRender = () => {
const buttons = this.state.buttons;
return buttons.map((row) => {
return row.map((button) => {
return (
<ButtonBasic
type={button.type}
buttonSize={button.size}
label={button.label}
onClick={this.handleClick}
/>
);
});
});
};

但此时,发出如下警告:

1
Warning: Each child in a list should have a unique "key" prop. See https://reactjs.org/link/warning-keys for more information.

旨在告诉我们list中每一个需要渲染的JSX都应该具有一个独立的key属性,这是因为React会依照这一属性来决定该JSX的渲染。因此,如果没有key属性,React可能将无法对于list中元素的位置变化等操作做出正确的渲染。

在纯React我们通常会使用nanoid这一第三方库利用伪随机,来生成一个独立的key:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
buttonsRender = () => {
const buttons = this.state.buttons;
return buttons.map((row) => {
return row.map((button) => {
return (
<ButtonBasic
key={nanoid()}
type={button.type}
buttonSize={button.size}
label={button.label}
onClick={this.handleClick}
/>
);
});
});
};

但如果我们直接将这一库应用到RN应用中,将会获得如下报错:

1
Error: Requiring module "node_modules\nanoid\index.browser.js", which threw an exception: Error: React Native does not have a built-in secure random generator. If you don’t need unpredictable IDs use `nanoid/non-secure`. For secure IDs, import `react-native-get-random-values` before Nano ID.

可见RN并不像浏览器DOM那样提供随机数生成内联函数,因此他推荐的做法有如下两种:

  1. 使用nanoid/non-secure代替,即使用非随机ID代替
  2. 在nanoid前手动引入随机数生成器:react-native-get-random-values

如上两种方法在stackOverflow中均有人使用并调试成功,具体可见:Nanoid can’t be used in react-native - Stack Overflow

我们使用第二种方式。由于本项目中所有的组件都由App组件统一渲染,因此只需在App.js中引入随机数函数即可,但这样会导致额外的依赖,因此还是推荐哪里用到nano,哪里再引入该组件:

1
2
3
4
5
6
7
8
9
10
11
import "react-native-get-random-values";
import React, { Component, useState } from "react";
import Calculator from "./src/component/Calculator";

class App extends Component {
render() {
return <Calculator />;
}
}

export default App;

React-Navigation

React-Native在0.44版本后,取消了Navigator组件,该组件曾负责RN中的路由。因此,在之后的RN版本中,官方推荐使用第三方路由组件:React Navigation

该组件提供了三种基本路由方式:

  • StackNavigation
  • TabNavigation
  • DrawerNavigation

安装

使用该第三方组件实现路由时,首先安装该组件库:

1
$ yarn add @react-navigation/native

然后安装同级依赖:

1
$ yarn add react-native-screens react-native-safe-area-context

该组件负责组织app中的路由,将顶级路由链接到app环境中。一般情况下,一个APP只有一个,使用时通常使用该组件将App中的组件包裹,如下所示:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import React, { Component } from "react";
import { SafeAreaView, View } from "react-native";
import MainNavigation from "./src/Components/Navigation/MainNavigation";
import MyTabBar from "./src/Components/Navigation/TabBar";
import "react-native-get-random-values";
import { NavigationContainer, navigationRef } from "@react-navigation/native";

export default class App extends Component {
constructor(props) {
super(props);
}

render() {
return (
<NavigationContainer>
<MyTabBar />
</NavigationContainer>
);
}
}
Ref

还可以通过ref的方式获取到NavigationContainer实例,从而调用其中的API,除了使用React提供的React.useRefReact.createRef外,还能使用React-Navigtaion为我们提供的API:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
import { NavigationContainer, useNavigationContainerRef } from '@react-navigation/native';

function App() {
const navigationRef = useNavigationContainerRef(); // You can also use a regular ref with `React.useRef()`

return (
<View style={{ flex: 1 }}>
<Button onPress={() => navigationRef.navigate('Home')}>
Go home
</Button>
<NavigationContainer ref={navigationRef}>{/* ... */}</NavigationContainer>
</View>
);
}

但当我们使用React提供的方法来创建Ref时,在一些情况下Ref对象的初始值将被初始化为null,因此需要使用onReady回调函数来等到navigationContainer挂载结束再创建Ref对象。

实例对象将包含一些通用的方法,具体可查阅:docs for CommonActions

Props

此外,该组件还提供一些参数可供选择:

  • initialState用于设置初始状态
  • onStateChange每次navigation state改变时都将调用该函数。
  • onReady每次navigationContainer和其所有子组件均挂载完后调用,常用于在此确保ref对象时可用的。

Screen

Screen组件用于控制导航内部各个页面的配置,需要通过调用CreateXNavigator函数得到:

1
const Stack = createNativeStackNavigator()

创建好navigator后使用如下方法即可组织路由:

1
2
3
4
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Profile" component={ProfileScreen} />
</Stack.Navigator>

每个Screen组件必须包含一个name属性与一个component属性,name属性用于唯一的标识一个组件,并且该name将用于后续跳转页面:

1
navigation.navigate('Profile');

需要注意的是官网推荐避免使用空格和特殊符号作为name。

options

该参数用于配置该页组件的表现形式,可接受一个对象或一个函数:

1
2
3
4
5
6
7
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={{
title: 'Awesome app',
}}
/>

可以通过函数接收route或navigation参数,可使用route获取其他页面传来的参数或使用navigation进行一些路由操作。

1
2
3
4
5
6
7
<Stack.Screen
name="Profile"
component={ProfileScreen}
options={({ route, navigation }) => ({
title: route.params.userId,
})}
/>
initialParams

初始化参数,让将一个页面使用initialRouteName设为默认页面后,将为为该页面传递initialRouteName中的参数。当从新跳转到该页面后,新传入的参数将与initialParams中的参数进行浅合并(shallow merge):

1
2
3
4
5
<Stack.Screen
name="Details"
component={DetailsScreen}
initialParams={{ itemId: 42 }}
/>

如果需要进行深合并,请使用下文提到的方法:
(39条消息) 如何深层合并而不是浅层合并?_xfxf996的博客-CSDN博客

component

用于在该路由页渲染的组件:

1
<Stack.Screen name="Profile" component={ProfileScreen} />
getComponent

使用回调函数的方式渲染组件,该方法通常用于组件的懒加载,能够一定程度上提升性能,用法如下:

1
2
3
4
<Stack.Screen
name="Profile"
getComponent={() => require('./ProfileScreen').default}
/>

该属性用于控制不同条件下组件的显示,例如不登陆和登录时显示不同的组件,在Stack Navigator中,当条件改变时,使用该名字的组件将被移除。在Tab或Drawer Navigator中使用该名字的组件将被重置,使用方法如下:

1
2
3
4
5
<Stack.Screen
navigationKey={isSignedIn ? 'user' : 'guest'}
name="Profile"
component={ProfileScreen}
/>

Route prop

每一个Screen组件都将被自动的提供route prop,该参数主要包含被其他路由页传递到当前页面的参数。当使用函数组件时需要通过如下方式访问:

1
2
3
4
5
6
7
8
function ProfileScreen({ route }) {
return (
<View>
<Text>This is the profile screen of the app</Text>
<Text>{route.name}</Text>
</View>
);
}

当使用类组件时可直接通过组件的props得到:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
export default class Details extends Component {
constructor(props) {
super(props);
this.state = {};
}

render() {
const routeParams = this.props.route.params;
console.log(this.props.route.params?.post);
const item = routeParams.article;
// console.log(item);
return (
<View>
<View style={styles.container}>
<Text style={styles.title}>{item.value.name}</Text>
<Text style={styles.content}>{item.value.description}</Text>
</View>
</View>
);
}
}

该参数同样会被自动的提供,使用方法同route props,其中包含许多方法用于路由动作,例如如下函数:

  • navigation
    • navigate - 跳转到指定screen
    • reset - 擦除导航器状态并将其替换为新路线
    • goBack - 关闭当前screen,返回到路由栈的上一个screen
    • setParams - 变更route参数
    • setOptions - 变更screen 的 option参数
    • isFocused - 检查当前screen是否聚焦
    • addListener - 订阅路由事件更新

setParams/setOptions等应该在useEffect/useLayoutEffect/componentDidMount/componentDidUpdate等生命周期中调用,不应再渲染或构造的过程中调用。

但需要注意的是Navigation并不会自动的向下传递,它只会被传递到被screen指定的第一层组件,其子组件中将访问不到navigation,如果希望再所有组件中都能访问navigation,则需要使用useNavigaion钩子函数。

特殊参数

navigation中也包含一些需要配合特定路由使用的方法:

  • stack navigator
    • navigation.replace - 将当前层级screen替换为新的
    • navigation.push - 向路由栈中添加一个新的screen
    • navigation.pop - 返回栈中上一级screen
    • navigation.popToTop - 返回栈顶
  • Tab navigator
    • navigation.jumpTo - 跳转到tab navigator中的特定screen
  • drawer navigator
    • navigation.jumpTo - 跳转到drawer navigator中的特定screen
    • navigation.openDrawer - 打开抽屉
    • navigation.closeDrawer - 关闭抽屉
    • navigation.toggleDrawer - 转换抽屉的开关状态

StackNavigation

接下来我们尝试StackNavigation:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
* @flow strict-local
*/

import React from "react";
import Calculator from "./src/component/Calculator/Calculator";
import "react-native-get-random-values";
import {
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
useColorScheme,
View,
} from "react-native";

// In App.js in a new project

import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";

function HomeScreen() {
return (
<View
style={{ flex: 1, alignItems: "center", justifyContent: "center" }}
>
<Text>Home Screen</Text>
</View>
);
}

const Stack = createNativeStackNavigator();

function App() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={HomeScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}

export default App;
// const App = () => {
// return <Calculator />;
// };

// export default App;

但此时webpak提示我们缺少依赖:react-native-safe-area-context

让我们安装该依赖:

1
$ yarn add react-native-safe-area-context

再次启动仪式我们缺少另一个依赖:react-native-screens

image-20220124154453933

于是安装该依赖:

1
$ yarn add react-native-screens

再次启动项目,由于该依赖较大,安装时间将会显著加长。

image-20220124154705198

启动后便能得到如下画面:

image-20220124155250181

此时我们得到了一个页面,原因是我们只在代码中注册了一个页面,那么接下来我们添加第二个页面:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
function DetailsScreen() {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
</View>
);
}

function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen name="Home" component={HomeScreen} />
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}

使用Screen组件即可方便地排列多个页面,该组件接收一个name参数用以区分各页面,component参数用于指定页面需要渲染的组件,通过设置Navigator组件的initialRouteName参数即可设置初始显示的页面。

我们还可以通过传入options参数,设置Screen组件的一些特性,例如标题:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
function App() {
return (
<NavigationContainer>
<Stack.Navigator initialRouteName="Home">
<Stack.Screen
name="Home"
component={HomeScreen}
options={{ title: "Overview" }}
/>
<Stack.Screen name="Details" component={DetailsScreen} />
</Stack.Navigator>
</NavigationContainer>
);
}

如果我们希望为所有navigator中的screen组件均设置某个属性,可以通过navigator的screenOptions参数进行。

有时我们能可能跟希望使用Screen所渲染的组件,例如HomeScreen向Screen传递一些参数,那么就和父子组件通讯一样即可,使用React context或回调函数均可,使用回调函数操作如下:

1
2
3
<Stack.Screen name="Home">
{props => <HomeScreen {...props} extraData={someData} />}
</Stack.Screen>

现在我们两个页面准备好了,于是便可以通过按钮来实现跳转,Stack Navigation中通过向每个Screen组件传入参数navigation来进行跳转操作,调用navigation参数中的navigate方法来指定需要跳转到的Screen,navigator会通过比对当前screen的name与将要跳转的name来决定是否进行跳转,如下例子中在Home页面点击将发生跳转,而Deatils界面点击将没有任何反应:

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
function HomeScreen({ navigation }) {
return (
<View
style={{ flex: 1, alignItems: "center", justifyContent: "center" }}
>
<Text>Home Screen</Text>
<Button
title="Go to Details"
onPress={() => navigation.navigate("Details")}
></Button>
</View>
);
}

function DetailsScreen({ navigation }) {
return (
<View
style={{ flex: 1, alignItems: "center", justifyContent: "center" }}
>
<Text>Details Screen</Text>
<Button
title="Go to Details... again"
onPress={() => navigation.navigate("Details")}
/>
</View>
);
}

导航

如果我们希望进入同一个页面多次,那么可以使用push方法代替navigat方法,即:

1
2
3
4
<Button
title="Go to Details... again"
onPress={() => navigation.push('Details')}
/>

此外,navigation还提供了goBack()方法用于返回上一级

1
2
3
4
5
6
7
8
9
10
11
12
13
function DetailsScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
<Button
title="Go to Details... again"
onPress={() => navigation.push('Details')}
/>
<Button title="Go to Home" onPress={() => navigation.navigate('Home')} />
<Button title="Go back" onPress={() => navigation.goBack()} />
</View>
);
}

如果进入了太深的层级,一次一次点显然不合逻辑,此时可以使用navigate(‘Home’)直接回到首页,或是通过popToTop()方法回到深度为1的页。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
function DetailsScreen({ navigation }) {
return (
<View style={{ flex: 1, alignItems: 'center', justifyContent: 'center' }}>
<Text>Details Screen</Text>
<Button
title="Go to Details... again"
onPress={() => navigation.push('Details')}
/>
<Button title="Go to Home" onPress={() => navigation.navigate('Home')} />
<Button title="Go back" onPress={() => navigation.goBack()} />
<Button
title="Go back to first screen in stack"
onPress={() => navigation.popToTop()}
/>
</View>
);
}

TabNavigation

tabNavigation的使用与StackNavigation类似,此处介绍Bottom-Tab的使用:

先后先安装Bottom-Tab组件:

1
$ yarn add @react-navigation/bottom-tabs
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
/**
* Sample React Native App
* https://github.com/facebook/react-native
*
* @format
* @flow strict-local
*/

import React from "react";
import Calculator from "./src/component/Calculator/Calculator";
import "react-native-get-random-values";
import {
Button,
SafeAreaView,
ScrollView,
StatusBar,
StyleSheet,
Text,
useColorScheme,
View,
} from "react-native";

// In App.js in a new project

import { NavigationContainer } from "@react-navigation/native";
import { createBottomTabNavigator } from "@react-navigation/bottom-tabs";
import Ionicons from "react-native-vector-icons/Ionicons";

function HomeScreen({ navigation }) {
return (
<View
style={{ flex: 1, alignItems: "center", justifyContent: "center" }}
>
<Text>Home Screen</Text>
<Button
title="Go to Details"
onPress={() => navigation.navigate("Details")}
></Button>
</View>
);
}

function DetailsScreen({ navigation }) {
return (
<View
style={{ flex: 1, alignItems: "center", justifyContent: "center" }}
>
<Text>Details Screen</Text>
<Button title="Go back" onPress={() => navigation.goBack()} />
</View>
);
}

function SettingsScreen({ navigation }) {
return (
<View
style={{ flex: 1, justifyContent: "center", alignItems: "center" }}
>
<Text>Setting Screen</Text>
</View>
);
}

const Tab = createBottomTabNavigator();

function App() {
return (
<NavigationContainer>
<Tab.Navigator
initialRouteName="Home"
screenOption={({ route }) => ({
tabBarIcon: ({ focus, color, size }) => {
let iconName;
if (route.name === "Home") {
iconName = focused
? "ios-information-circle"
: "ios-information-circle-outline";
} else if (route.name === "Settings") {
iconName = focused ? "ios-list-box" : "ios-list";
} else if (route.name === "Details") {
iconName = focused ? "ios-list-box" : "ios-list";
}
return (
<Ionicons
name={iconName}
size={size}
color={color}
/>
);
},
tabBarActiveTintColor: "tomato",
tabBarInactiveTintColor: "gray",
})}
>
<Tab.Screen name="Home" component={HomeScreen} />
<Tab.Screen name="Details" component={DetailsScreen} />
<Tab.Screen name="Settings" component={SettingsScreen} />
</Tab.Navigator>
</NavigationContainer>
);
}

export default App;
// const App = () => {
// return <Calculator />;
// };

// export default App;

结合一些图标库,例如react-native-vector-icons可以通过Navigator的screenOptions参数或者Screen组件的Options参数为每个tab设置对应图标和显示效果,但此处图标的显示仍然在研究。

组件还为我们提供了很多有意思的选项设置,例如通知气泡:

1
2
3
4
5
<Tab.Screen
name="Home"
component={HomeScreen}
options={{ tabBarBadge: 3 }}
/>

最终效果如下:

tab导航

Antd-Mobile-rn

由阿里开发的适用于RN项目的UI组件库,Ant Design的RN版本,使用如下命令进行安装:

1
$ yarn add @ant-design/react-native

安装完成后,使用如下命令下载并链接字体与图标库:

1
2
$ yarn add @ant-design/icons-react-native
$ react-native link @ant-design/icons-react-native

按需加载

使用 babel-plugin-import可实现按需加载。

使用如下命令安装该组件:

1
$ yarn add babel-plugin-import

然后在项目根目录中添加文件.babelrc,内容如下:

1
2
3
4
5
6
// .babelrc
{
"plugins": [
["import", { libraryName: "@ant-design/react-native" }] // 与 Web 平台的区别是不需要设置 style
]
}

然后就能直接使用如下方式引入模块:

1
import { Button } from '@ant-design/react-native';

下面来尝试写一个带有Toast的点击事件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
import React, { Component } from 'react';
import { AppRegistry } from 'react-native';
import { Button, Provider, Toast } from '@ant-design/react-native';

class HelloWorldApp extends Component {
render() {
return (
<Provider>
<Button onPress={() => Toast.info('This is a toast tips')}>
Start
</Button>
</Provider>
);
}
}

使用Toast组件将会需要如下依赖:

  • react-native-pager-view
  • react-native-gesture-handler
  • @react-native-community/slider
  • @react-native-community/segmented-control
  • @react-native-community/cameraroll
  • fbjs
TabBar

antd同样提供了三种导航方式,但使用起来比React-Navigation更符合直觉,只需要使用导航组件将想要在该页显示的内容包裹起来即可,例如我们可以使用如下方式定义一个TabBar,并将我们之前写过的内容放进去:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
import React from "react";
import { Text, View } from "react-native";
import { Icon, SearchBar, TabBar } from "@ant-design/react-native";
import Calculator from "../Calculator/Calculator";
import BasicTabsExample from "./AntNavigator";
import PopoverExample from "./PopOver";
import BasicPaginationExample from "./Pagination";
export default class BasicTabBarExample extends React.Component {
constructor(props) {
super(props);
this.state = {
selectedTab: "redTab",
};
}
renderContent = (pageText) => {
return (
<View
style={{
flex: 1,
alignItems: "center",
backgroundColor: "white",
}}
>
<Text style={{ margin: 50 }}>{pageText}</Text>
</View>
);
};
onChangeTab = (tabName) => {
this.setState({
selectedTab: tabName,
});
};
render() {
return (
<TabBar
unselectedTintColor="#949494"
tintColor="#33A3F4"
barTintColor="#f5f5f5"
>
<TabBar.Item
title="Home"
icon={<Icon name="home" />}
selected={this.state.selectedTab === "blueTab"}
onPress={(e) => this.onChangeTab("blueTab")}
>
<Calculator />
</TabBar.Item>
<TabBar.Item
icon={<Icon name="ordered-list" />}
title="Todo"
badge={2}
selected={this.state.selectedTab === "redTab"}
onPress={() => this.onChangeTab("redTab")}
>
<BasicPaginationExample />
</TabBar.Item>
<TabBar.Item
icon={<Icon name="like" />}
title="Friend"
selected={this.state.selectedTab === "greenTab"}
onPress={() => this.onChangeTab("greenTab")}
>
<SearchBar placeholder="Search" showCancelButton />
<BasicTabsExample />
</TabBar.Item>
<TabBar.Item
icon={<Icon name="user" />}
title="My"
selected={this.state.selectedTab === "yellowTab"}
onPress={() => this.onChangeTab("yellowTab")}
>
<PopoverExample />
</TabBar.Item>
</TabBar>
);
}
}

其中TabBar组件用于渲染整个TabBar条,其中TabBar.Item组件用于单独渲染bar上该页面的图标、颜色、名称等信息,我们只需要将页面组件,例如CalculatorBasicTabsExample等包裹在想要渲染的页面的TabBar.Item下即可。

tabBar

还可以使用受控组件的形式来控制tabBar,例如上述代码中通过修改selectedTab状态来控制选中图标的颜色,只需要设置TabBar.Item中的selected属性即可,此外TabBar.Item还有其他属性可供使用,具体参见下表:

TabBar.Item

属性 说明 类型 默认值
badge 徽标数 Number \ String
onPress bar 点击触发,需要自己改变组件 state & selecte={true} Function (){}
selected 是否选中 Boolean false
icon 默认展示图片 `Image Source React.ReactNode`
selectedIcon 选中后的展示图片 `Image Source React.ReactNode`
title 标题文字 String
key 唯一标识 String
iconStyle icon 样式 String { width: 28, height: 28 }

TabBar

属性 说明 类型 默认值
barTintColor tabbar 背景色 String white
tintColor 选中的字体颜色 String #108ee9
unselectedTintColor 未选中的字体颜色 String ‘#888’
Tabs

antd直接为我们提供了方便使用的顶部导航可供使用,无需通过修改TabBar实现,并且使用起来和TabBar一样方便。

例如下面的代码中设计了一个较长Tab用于显示State中的所有Tab栏,只需要向Tabs组件的tabs传参即可完成自动渲染,在每一个Tab中,又渲染了8个View包裹的文本,并赋予不同的key值:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
import React from "react";
import {
ScrollView,
Text,
View,
TouchableOpacity,
StyleSheet,
} from "react-native";
import { Tabs } from "@ant-design/react-native";

export default class BasicTabsExample extends React.Component {
constructor(props) {
super(props);
this.state = {
tab: [
{ title: "1st Tab" },
{ title: "2nd Tab" },
{ title: "3rd Tab" },
{ title: "4th Tab" },
{ title: "5th Tab" },
{ title: "6th Tab" },
{ title: "7th Tab" },
{ title: "8th Tab" },
{ title: "9th Tab" },
],
};
}

renderContent = (tab, index) => {
const style = {
paddingVertical: 40,
justifyContent: "center",
alignItems: "center",
margin: 10,
backgroundColor: "#ddd",
};
const content = [1, 2, 3, 4, 5, 6, 7, 8].map((i) => {
return (
<View key={`${index}_${i}`} style={style}>
<Text>
{tab.title} - {i}
</Text>
</View>
);
});
return (
<ScrollView style={{ backgroundColor: "#fff" }}>
{content}
</ScrollView>
);
};
render() {
return (
<Tabs
tabs={this.state.tab}
initialPage={1}
tabBarPosition="top"
animated="true"
>
{this.renderContent}
</Tabs>
);
}
}

const styles = StyleSheet.create({});

Tabs

其中Tabs还提供了许多自定义功能,允许我们设计TabBar组件的渲染方式,以及TabBar组件中的每个Tab的渲染方式(待测试)

Tabs

属性 说明 类型 默认值 必选
tabs tab数据 Models.TabData[] true
tabBarPosition TabBar位置 ‘top’ | ‘bottom’ top false
renderTabBar 替换TabBar ((props: TabBarPropsType) => React.ReactNode) | false false
initialPage 初始化Tab, index or key number | string false
page 当前Tab, index or key number | string false
swipeable 是否可以滑动内容切换 boolean true false
useOnPan 使用跟手滚动 boolean true false
prerenderingSiblingsNumber 预加载两侧Tab数量 number 1 false
animated 是否开启切换动画 boolean true false
onChange tab变化时触发 (tab: Models.TabData, index: number) => void false
onTabClick tab 被点击的回调 (tab: Models.TabData, index: number) => void false
destroyInactiveTab 销毁超出范围Tab boolean false false
distanceToChangeTab 滑动切换阈值(宽度比例) number 0.3 false
usePaged 是否启用分页模式 boolean true false
tabBarUnderlineStyle tabBar下划线样式 React.CSSProperties | any false
tabBarBackgroundColor tabBar背景色 string false
tabBarActiveTextColor tabBar激活Tab文字颜色 string false
tabBarInactiveTextColor tabBar非激活Tab文字颜色 string false
tabBarTextStyle tabBar文字样式 React.CSSProperties | any false
renderTab 替换TabBar的Tab (tab: Models.TabData) => React.ReactNode false
renderUnderline renderUnderline (style: any) => React.ReactNode false

rn-placeholder

为了显示更优雅的加载动画,提出了骨架屏的概念,用于显示加载中的页面,但该组件在antd-mobile-rn中并未提供,因此为了能够在RN上实现该效果,需要寻求另一个第三方组件的帮助,即rn-placeholder。

使用如下命令安装即可:

1
$ yarn add rn-placeholder

该组件库为我们提供了三个基础组件:

  • Placeholder
  • PlaceholderMedia
  • PlaceholderLine

以及一些配合使用的动效组件:

  • Fade
  • Shine
  • ShineOverlay

使用如下代码即可看到三个基础组件的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
import {
Placeholder,
PlaceholderMedia,
PlaceholderLine,
Fade
} from "rn-placeholder";

const App = () => (
<Placeholder
Animation={Fade}
Left={PlaceholderMedia}
Right={PlaceholderMedia}
>
<PlaceholderLine width={80} />
<PlaceholderLine />
<PlaceholderLine width={30} />
</Placeholder>
);

其中Placeholder用于控制包裹在其中的所有相关组件的动效,PlaceholderMedia的显示效果为正方形的小方块

PlaceholderLine的显示效果即为文本条

但由于该组件需要使用三个组件分别设置样式,且不好控制个数与大小,因此需要对该组件进行进一步封装:

即我们需要通过简单的传参实现如下功能:

  • 显示文本框的条数
  • 是否包含标题
  • 显示文本框的长度
  • 动效
  • 根究Loding状态加载骨架或组件
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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
import React from "react";
import {
Placeholder,
PlaceholderMedia,
PlaceholderLine,
Fade,
} from "rn-placeholder";
import { StyleSheet, ScrollView, View } from "react-native";

const Title = (hasTitle) => {
return hasTitle ? (
<Placeholder style={styles.title}>
<PlaceholderLine />
</Placeholder>
) : null;
};
const Placeholders = (props) => {
const {
ParagraphLength,
hasTitle,
firstLineWidth,
lastLineWidth,
width,
style,
} = props;
const PlaceholderContent = [];
let widthList = Array(ParagraphLength).fill(width);
widthList[0] = firstLineWidth ? firstLineWidth : width;
widthList[widthList.length - 1] = lastLineWidth ? lastLineWidth : width;
for (let key = 0; key < ParagraphLength; key++) {
PlaceholderContent.push(
<Placeholder
Animation={Fade}
style={styles.item}
key={`PlaceholderContentKey${key}`}
>
{Title(hasTitle)}
<PlaceholderLine width={widthList[key]} style={{ ...style }} />
</Placeholder>
);
}
return (
<ScrollView style={{ margin: 10 }}>
{PlaceholderContent.map((item) => item)}
</ScrollView>
);
};

const ImageContent = (props) => {
const baseOption = {
ParagraphLength: 5,
hasTitle: false,
style: {
margin: 10,
},
lastLineWidth: 60,
};
const options = { ...baseOption, ...props };
const { isLoading, list } = props;

if (isLoading) {
return Placeholders(options);
}
return typeof list === "function" && list();
};

const Paragraph = (props) => {
const baseOption = {
style: {
margin: 5,
},
width: 90,
lastLineWidth: 70,
firstLineWidth: 50,
};

const options = { ...baseOption, ...props };
const { isLoading, list } = props;

if (isLoading) {
return Placeholders(options);
}
return typeof list === "function" && list();
};

/* 导出
============================================================ */
const ImagePlaceholder = ImageContent;
const ParagraphPlaceholder = Paragraph;
export { ImagePlaceholder, ParagraphPlaceholder };

/* 样式
============================================================ */
const styles = StyleSheet.create({
title: {
marginBottom: 12,
},
item: {
margin: 12,
},
});

之后使用如下方式调用即可:

  • ParagraphLength定义长度
  • isLoading用来绑定加载状态
  • list用于绑定loading结束后需要加载的组件
  • hastTitle用于定义是否需要显示标题占位
1
2
3
4
5
6
<ParagraphPlaceholder
ParagraphLength={10}
isLoading={this.state.isLoading}
list={this.getMy}
hasTitle={true}
></ParagraphPlaceholder>

placeholder

加载动画与上拉加载

加载动画

RN中提供了原生的Loading组件支持,即ActivityIndicator组件,并且可以通过styles文件对其进行样式控制,虽然使用较为方便,但考虑到后续需要使用不同风格的Loading,需对其封装以达到如下效果:

  • 只需引入一条组件即可实现渲染
  • 加载结束后显示应该出现在此处的组件
  • 控制颜色
  • 控制背景

后两个需求只需要通过props参数控制即可,而第二个要求则需要利用父子组件间的通信,传入回调函数,根据父组件的加载需求判断显示加载界面,还是父组件指定的其他组件,因此最终实现如下:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
import React, { Component } from "react";
import {
StyleSheet,
Text,
View,
ActivityIndicator,
Dimensions,
} from "react-native";
const { width, height } = Dimensions.get("window");
_this = null;
class Loading extends Component {
constructor(props) {
super(props);
}
render() {
const { show, list } = this.props;
const color = this.props.color ? this.props.color : "blue";
const background = this.props.background ? true : false;
if (show) {
return (
<View style={styles.LoadingPage}>
<View
style={
background ? styles.hasBackground : styles.Loading
}
>
<ActivityIndicator size="large" color={color} />
<Text
style={{
marginLeft: 10,
color: color,
marginTop: 10,
}}
>
正在加载...
</Text>
</View>
</View>
);
} else {
return typeof list === "function" && list();
}
}
}
export default Loading;
const styles = StyleSheet.create({
LoadingPage: {
position: "absolute",
left: 0,
top: 0,
backgroundColor: "rgba(0,0,0,0)",
width: width,
height: height,
justifyContent: "center",
alignItems: "center",
},
Loading: {
width: 100,
height: 100,
opacity: 1,
justifyContent: "center",
alignItems: "center",
borderRadius: 7,
},
hasBackground: {
width: 100,
height: 100,
opacity: 1,
justifyContent: "center",
alignItems: "center",
borderRadius: 7,
backgroundColor: "rgba(0,0,0,0.6)",
},
});

调用方式如下:

1
2
3
4
5
<Loading
show={this.state.isLoading}
list={this.getHome}
color={"red"}
/>

其中list参数需要传入一个函数,该函数用于渲染加载结束后的组件,show参数用于定义加载状态。

上拉加载

上拉加载使用了RN提供的另一个自定义程度极高的List组件:FlatList

该组件支持如下常用功能:

  • 完全跨平台。
  • 支持水平布局模式。
  • 行组件显示或隐藏时可配置回调事件。
  • 支持单独的头部组件。
  • 支持单独的尾部组件。
  • 支持自定义行间分隔线。
  • 支持下拉刷新。
  • 支持上拉加载。
  • 支持跳转到指定行(ScrollToIndex)。
  • 支持多列布局。

可以完全自定义list中每一个项目的表现形式,分割线表现形式。该组件基于VirtualizedList进行封装,继承了其所有props(也包括所有ScrollView的props)

该组件可定义程度极高,但包含两个必须的参数:

  • renderItem
  • data

其中renderItem用于从data中挨个取出数据并渲染到列表中,即定义data的渲染表示

data用于定义需要渲染的数据,为了简化起见,data 属性目前只支持普通数组。如果需要使用其他特殊数据结构,例如 immutable 数组,请直接使用更底层的VirtualizedList组件。

其余组件在此不一一介绍,实现上拉加载以及其动画需要使用如下参数:

  • ListFooterComponent
  • onEndReached
  • onEndReachedThreshold

最终封装代码如下:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
import React from "react";
import {
ScrollView,
Text,
View,
TouchableOpacity,
StyleSheet,
FlatList,
ActivityIndicator,
} from "react-native";
import { Tabs } from "@ant-design/react-native";
import http from "../../../utils/http/request";
let pageNo = 1; //当前第几页
let totalPage = 5; //总的页数
let itemNo = 0; //item的个数

export default class FlatListExample extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: true,
isRefresh: false,
//网络请求状态
error: false,
errorInfo: "",
dataArray: [],
showFoot: 0, // 控制foot, 0:隐藏footer 1:已加载完成,没有更多数据 2 :显示加载中
isRefreshing: false, //下拉控制
};
}
fetchData(pageNo) {
let params = {
q: "javascript",
sort: "stars",
page: pageNo,
};
http.get("/search/repositories", params)
.then((responseData) => {
let data = responseData.items;
let dataBlob = [];
let i = itemNo;

data.map(function (item) {
dataBlob.push({
key: i,
value: item,
});
i++;
});
itemNo = i;
console.log("itemNo:" + itemNo);
let foot = 0;
if (pageNo >= totalPage) {
foot = 1; //listView底部显示没有更多数据了
}
this.setState({
//复制数据源
dataArray: this.state.dataArray.concat(dataBlob),
isLoading: false,
isRefresh: false,
showFoot: foot,
isRefreshing: false,
});
data = null;
dataBlob = null;
})
.catch((error) => {
this.setState({
error: true,
errorInfo: error,
});
});
}
componentDidMount() {
//请求数据
this.fetchData(pageNo);
}
//加载等待页
renderLoadingView() {
return (
<View style={styles.container}>
<ActivityIndicator animating={true} color="red" size="large" />
</View>
);
}

//加载失败view
renderErrorView() {
return (
<View style={styles.container}>
<Text>Fail</Text>
</View>
);
}
//返回itemView
_renderItemView({ item }) {
return (
<View>
<Text style={styles.title}>name: {item.value.name}</Text>
<Text style={styles.content}>
stars: {item.value.stargazers_count}
</Text>
<Text style={styles.content}>
description: {item.value.description}
</Text>
</View>
);
}

renderData() {
return (
<FlatList
// 定义数据显示效果
data={this.state.dataArray}
renderItem={this._renderItemView}
ItemSeparatorComponent={this._separator}
//下拉刷新相关
onRefresh={() => this._onRefresh()}
refreshing={this.state.isRefresh}
// 上拉加载相关
ListFooterComponent={this._renderFooter.bind(this)}
onEndReached={this._onEndReached.bind(this)}
onEndReachedThreshold={1}
/>
);
}
render() {
//第一次加载等待的view
if (this.state.isLoading && !this.state.error) {
return this.renderLoadingView();
} else if (this.state.error) {
//请求失败view
return this.renderErrorView();
}
//加载数据
return this.renderData();
}
_onRefresh() {
totalPage = 5;
pageNo = 0;
itemNo = 0;
this.setState({
isRefresh: true,
dataArray: [],
showFoot: 0, // 控制foot, 0:隐藏footer 1:已加载完成,没有更多数据 2 :显示加载中
isRefreshing: false, //下拉控制
});
this.fetchData(pageNo);
}
_separator() {
return <View style={{ height: 1, backgroundColor: "#999999" }} />;
}
_renderFooter() {
if (this.state.showFoot === 1) {
return (
<View
style={{
height: 30,
alignItems: "center",
justifyContent: "flex-start",
}}
>
<Text
style={{
color: "#999999",
fontSize: 14,
marginTop: 5,
marginBottom: 5,
}}
>
没有更多数据了
</Text>
</View>
);
} else if (this.state.showFoot === 2) {
return (
<View style={styles.footer}>
<ActivityIndicator />
<Text>正在加载更多数据...</Text>
</View>
);
} else if (this.state.showFoot === 0) {
return (
<View style={styles.footer}>
<Text></Text>
</View>
);
}
}

_onEndReached() {
//如果是正在加载中或没有更多数据了,则返回
if (this.state.showFoot != 0) {
return;
}
//如果当前页大于或等于总页数,那就是到最后一页了,返回
if (pageNo != 1 && pageNo >= totalPage) {
return;
} else {
pageNo++;
}
//底部显示正在加载更多数据
this.setState({ showFoot: 2 });
//获取数据
this.fetchData(pageNo);
}
}

const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
backgroundColor: "#F5FCFF",
},
title: {
fontSize: 15,
color: "blue",
},
footer: {
flexDirection: "row",
height: 24,
justifyContent: "center",
alignItems: "center",
marginBottom: 10,
},
content: {
fontSize: 15,
color: "black",
},
});

下拉刷新与点击跳转

下拉刷新

下拉刷新主要使用过上文提及的FlatList组件中提及的如下两个属性:

  • onRefresh
  • refreshing

其中第一个参数接收一个函数,用于指定刷新触发后的一些列操作。

第二个参数refreshing用来绑定描述刷新状态的属性,具体设置如下:

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
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
import React from "react";
import {
ScrollView,
Text,
View,
TouchableOpacity,
StyleSheet,
FlatList,
ActivityIndicator,
} from "react-native";
import StackNavigator from "react-navigation";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { NavigationContainer } from "@react-navigation/native";
import { Tabs } from "@ant-design/react-native";
import http from "../../../utils/http/request";
import Details from "../Details/Details";
let pageNo = 1; //当前第几页
let totalPage = 5; //总的页数
let itemNo = 0; //item的个数

class FlatListExample extends React.Component {
constructor(props) {
super(props);
this.state = {
isLoading: true,
isRefresh: false,
//网络请求状态
error: false,
errorInfo: "",
dataArray: [],
showFoot: 0, // 控制foot, 0:隐藏footer 1:已加载完成,没有更多数据 2 :显示加载中
isRefreshing: false, //下拉控制
};
}
fetchData(pageNo, onRefresh = false) {
let params = {
q: "javascript",
sort: "stars",
page: pageNo,
};
http.get("/search/repositories", params)
.then((responseData) => {
let data = responseData.items;
let dataBlob = [];
let i = itemNo;

data.map(function (item) {
dataBlob.push({
key: i,
value: item,
});
i++;
});
itemNo = i;
console.log("itemNo:" + itemNo);
let foot = 0;
if (pageNo >= totalPage) {
foot = 1; //listView底部显示没有更多数据了
}
this.setState({
//复制数据源
dataArray: onRefresh
? dataBlob
: this.state.dataArray.concat(dataBlob),
isLoading: false,
isRefresh: false,
showFoot: foot,
isRefreshing: false,
});
data = null;
dataBlob = null;
})
.catch((error) => {
this.setState({
error: true,
errorInfo: error,
});
});
}
componentDidMount() {
//请求数据
this.fetchData(pageNo);
}
//加载等待页
renderLoadingView() {
return (
<View style={styles.container}>
<ActivityIndicator animating={true} color="red" size="large" />
</View>
);
}

//加载失败view
renderErrorView() {
return (
<View style={styles.container}>
<Text>Fail</Text>
</View>
);
}
//返回itemView
_renderItemView = ({ item }) => {
const navigation = this.props.navigation;
return (
<View>
<TouchableOpacity
onPress={() => {
navigation.navigate("Details", {
article: item,
itemId: 10,
});
}}
>
<Text style={styles.title}>name: {item.value.name}</Text>
<Text style={styles.content}>
stars: {item.value.stargazers_count}
</Text>
<Text style={styles.content}>
description: {item.value.description}
</Text>
</TouchableOpacity>
</View>
);
};

renderData() {
return (
<FlatList
// 定义数据显示效果
data={this.state.dataArray}
renderItem={this._renderItemView}
ItemSeparatorComponent={this._separator}
// //下拉刷新相关
onRefresh={() => this._onRefresh()}
refreshing={this.state.isRefresh}
// 上拉加载相关
ListFooterComponent={this._renderFooter.bind(this)}
onEndReached={this._onEndReached.bind(this)}
onEndReachedThreshold={1}
/>
);
}
render() {
//第一次加载等待的view
if (this.state.isLoading && !this.state.error) {
return this.renderLoadingView();
} else if (this.state.error) {
//请求失败view
return this.renderErrorView();
}
//加载数据
return this.renderData();
}
_onRefresh() {
totalPage = 5;
pageNo = 0;
itemNo = 0;
this.setState({
isRefresh: true,
showFoot: 0, // 控制foot, 0:隐藏footer 1:已加载完成,没有更多数据 2 :显示加载中
isRefreshing: false, //下拉控制
});
this.fetchData(pageNo, (onRefresh = true));
}
_separator() {
return <View style={{ height: 1, backgroundColor: "#999999" }} />;
}
_renderFooter() {
if (this.state.showFoot === 1) {
return (
<View
style={{
height: 30,
alignItems: "center",
justifyContent: "flex-start",
}}
>
<Text
style={{
color: "#999999",
fontSize: 14,
marginTop: 5,
marginBottom: 5,
}}
>
没有更多数据了
</Text>
</View>
);
} else if (this.state.showFoot === 2) {
return (
<View style={styles.footer}>
<ActivityIndicator />
<Text>正在加载更多数据...</Text>
</View>
);
} else if (this.state.showFoot === 0) {
return (
<View style={styles.footer}>
<Text></Text>
</View>
);
}
}

_onEndReached() {
//如果是正在加载中或没有更多数据了,则返回
if (this.state.showFoot != 0) {
return;
}
//如果当前页大于或等于总页数,那就是到最后一页了,返回
if (pageNo != 1 && pageNo >= totalPage) {
return;
} else {
pageNo++;
}
//底部显示正在加载更多数据
this.setState({ showFoot: 2 });
//获取数据
this.fetchData(pageNo);
}
}

const Stack = createNativeStackNavigator();
function ModalStack() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={FlatListExample} />
<Stack.Screen name="Details" component={Details} />
</Stack.Navigator>
</NavigationContainer>
);
}

export default ModalStack;

const styles = StyleSheet.create({
container: {
flex: 1,
flexDirection: "row",
justifyContent: "center",
alignItems: "center",
backgroundColor: "#F5FCFF",
},
title: {
fontSize: 15,
color: "blue",
},
footer: {
flexDirection: "row",
height: 24,
justifyContent: "center",
alignItems: "center",
marginBottom: 10,
},
content: {
fontSize: 15,
color: "black",
},
});

实现效果如下:

refresh

文章点击跳转

首先为了提高组件复用性,创建一个组件用于显示文章的详细信息,即Detailes组件:

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
39
40
41
42
43
44
45
import React, { Component } from "react";
import { NavigationContainer } from "@react-navigation/native";
import { createNativeStackNavigator } from "@react-navigation/native-stack";
import { tsConstructorType } from "@babel/types";
import { View, Text, StyleSheet } from "react-native";

export default class Details extends Component {
constructor(props) {
super(props);
this.state = {};
}

render() {
const navigationState =
this.props.navigation.getState().routes[1].params;
const item = navigationState.article;
console.log(item);
return (
<View>
<View style={styles.container}>
<Text style={styles.title}>{item.value.name}</Text>
<Text style={styles.content}>{item.value.description}</Text>
</View>
</View>
);
}
}

const styles = StyleSheet.create({
container: {
padding: 10,
},
title: {
fontSize: 30,
justifyContent: "center",
alignContent: "center",
color: "blue",
margin: 10,
},
content: {
fontSize: 15,
color: "black",
margin: 5,
},
});

其次,页面跳转由点击事件触发,为了不破坏原布局,使用组件TouchableOpacity包裹原List中的每一个条目,为其添加OnPress事件,并具有点击时透明的效果:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
<View>
<TouchableOpacity
onPress={() => {
navigation.navigate("Details", {
article: item,
itemId: 10,
});
}}
>
<Text style={styles.title}>name: {item.value.name}</Text>
<Text style={styles.content}>
stars: {item.value.stargazers_count}
</Text>
<Text style={styles.content}>
description: {item.value.description}
</Text>
</TouchableOpacity>
</View>
);

然后使用组件react-navigation创建NavigationContainer以及StackNavigator,使用这两个组件将原组件FlatListExample组件包裹,并为其添加兄弟组件Details,这样就形成了一个两级的路由结构,只需对其进行命名并传入组件即可,形式如下:

1
2
3
4
5
6
7
8
9
10
11
const Stack = createNativeStackNavigator();
function ModalStack() {
return (
<NavigationContainer>
<Stack.Navigator>
<Stack.Screen name="Home" component={FlatListExample} />
<Stack.Screen name="Details" component={Details} />
</Stack.Navigator>
</NavigationContainer>
);
}

之后只需要在每一个Item的OnPress事件中增加路由逻辑即可:

1
2
3
4
5
onPress={() => {
navigation.navigate("Details", {
article: item,
});
}}

此处NavigationContainer组件会讲过navigation作为参数传递给组件,需要通过以下方式获得:

1
const navigation = this.props.navigation;

现在点击就能够顺利跳转了,但是还有一个问题就是点击不同的标签需要显示不同的内容,此时就需要通过传参来实现,Navigation中,在制动跳转路由之后传入一个对象作为参数,此后,在组件中使用如下方式接收即可:

1
2
const navigationState =     this.props.navigation.getState().routes[1].params;
const item = navigationState.article;

最终效果如下:

TabRout

RN访问摄像机与相册

通常情况下为了能使RN项目能够访问摄像头,需要在项目文件夹中的android目录下进行一些相关的权限配置,RN官方给出的摄像机权限配置如下:

  1. 打开项目中的android->app->src->main->AndroidManifest.xml文件,添加如下配置:
1
2
<uses-permission android:name="android.permission.CAMERA" />
<uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE"/>
  1. 打开项目中的android->app->src->main->java->com->当前项目名称文件夹->MainActivity.java文件,修改配置如下:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package com.native_camera;
import com.facebook.react.ReactActivity;

// 1. 添加以下两行:
import com.imagepicker.permissions.OnImagePickerPermissionsCallback; // <- add this import
import com.facebook.react.modules.core.PermissionListener; // <- add this import

public class MainActivity extends ReactActivity {
// 2. 添加如下一行:
private PermissionListener listener; // <- add this attribute

/**
* Returns the name of the main component registered from JavaScript.
* This is used to schedule rendering of the component.
*/
@Override
protected String getMainComponentName() {
return "task";
}
}

但实际上RN并没有提供很方便的调用摄像头的操作,于是找到了以下第三方插件:react-native-image-picker

Camera

评论