前端框架React学习笔记
前言
上研究生后老师让直接去实习,实习单位使用React + React Native进行安卓混合开发。于是先学习一下React
React概述
React是一个构建用户见面的JS库
React核心文件及渲染流程:
App.js(入口文件) -> index.js(根组件)-> public/index.html(root挂载点)
React特点
声明式
只需要描述UI(HTML)看起来是啥样,即描述结构,React负责渲染UI以及在数据变化时更新UI
基于组件
用于表示页面中的部分内容
通过组合、复用多个组件,就能实现完整的页面功能
学习一次随处使用
使用React开发Web应用
使用React-native开发移动端应用
React 360开发VR应用
React 安装
react包是核心,提供创建元素,组件的功能
react-dom包提供DOM相关功能
React初见
选择一个目录
创建index.html
在该目录下安装react包
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 > <script src ="./node_modules/react/umd/react.development.js" > </script > <script src ="./node_modules/react-dom/umd/react-dom.development.js" > </script > <script > const title = React .createElement ('h1' , null , "Hello React" ) ReactDOM .render (title, document .getElementById ("root" )) </script > </body > </html >
React脚手架
脚手架的意义
脚手架是开发现代Web应用的必备
充分利用Webpack、Babel、ESLint等工具辅助项目开发
零配置,无需手动配置繁琐的工具即可使用
关注业务二部是工具配置
使用脚手架初始化项目
1 npx create-react-app my-app
使用
启动项目
npx 的意义
原来使用npm上下载的包时,如果想在任意地方使用,需要全局安装这个包,但npx之后无需全局安装,即可使用。
脚手架中导入React包
由于React基于Webpack,因此导入时可直接使用ES6中的模块化语法进行导入:
1 2 import React from 'react' import ReactDOM from 'react-dom'
JSX
JSX基本使用
即JavaScript XML,表示在JS中写XML格式的代码
优点:
声明式语法更直观,与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
注意事项
React元素的属性名采用驼峰命名法
使用JSX为标签设置属性时,应修改为驼峰命名:
class属性 -> className
for -> htmlFor
tabindex -> tabIndex
没有子节点的React元素,可以使用但标签<span />
结束
推荐使用小括号包裹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对假值的处理
值为false,不显示任何内容
值为""
,不显示任何内容
值为null,不显示任何内容
值为undefined,不显示任何内容
值为0,直接显示0
如下代码将现实0: jsx <div>{0 && <p>避免对number类的数据直接判断是否为0</p>}</div>
值为NaN,直接显示NaN
JSX注释
由于bable无法处理html注释,因此需要借助js注释,但需要注释一段html代码时,先用{}
将其变为JS表达式,然后是有/**/
将其注释,即:
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样式处理
两种方式:
行内样式——style
类名——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总结
JSX是React的核心内容
JSX表示在JS中写HTML结构,是声明式的体现
使用JSX配合嵌入JS表达式、条件渲染、列表渲染、可以描述任意UI结构
推荐使用className的方式给JSX添加样式
React完全利用JS语言自身的能力来编写UI,而不是造轮子增强HTML功能
注意:
JSX只能有一个根元素
每个元素都需要是关闭的
class -> className
style接收一个Object,属性名使用驼峰命名
label表情的for属性更改为htmlFor
单个单词的属性名不变
{}总需要包含表达式(不支持语句)
React组件
组件的特点:
可复用
独立
可组合
组件的两种创建方式
函数
类
函数组件
为了和函数进行区分,对函数组件进行如下约定:
函数名必须以大写字母开头
函数组件必须有返回值
但函数返回值可以为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创建的组件,为了与普通类进行区分,使用如下约定:
类名首字母大写
类组件需要继承自React.Component ,从而使用父类中提供的方法和属性
类组件必须提供**render()**方法
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文件中
创建JS文件,对应组件名称.js
在JS文件中导入React
创建组件
在JS文件中导出该组件
在index.js中导入该组件
渲染组件
组件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事件处理
事件绑定
React事件绑定语法与DOM事件绑定相似
on+事件名称={事件处理程序},如onClick={() => {}}
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> ) }
事件对象
可以通过事件处理程序的参数获取到事件对象
React中的事件对象叫做:合成事件(重要,需补充) (对象)
合成事件:兼容所有浏览器,无需担心跨浏览器兼容性问题
1 2 3 4 5 6 function handleClick(e) { e.preventDefault() //组织浏览器的默认行为 console.log('事件对象', e) } <a onClick={handleClick}>点我,不会跳转页面</a>
有状态组件和无状态组件
函数组件又称为无状态组件 ,类组件叫做有状态组件
状态(state)即数据
函数组件没有自己的状态,只负责数据展示 (静)
类组件有自己的状态,负责更新UI
组件的state和setState
state的基本使用
状态即数据,时组件内部的私有数据,只能在组件内部使用
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()修改状态
状态是可变的
语法:this.setState({要修改的数据})
不要直接修改state中的值
setState()作用:
修改state
更新UI
思想:数据驱动视图
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中抽离事件处理程序
JSX中掺杂了过多JS逻辑代码会使得JSX逻辑混乱
将逻辑抽离到单独的方法中,保证JSX结构清晰
由于箭头函数不具备this,因此会向外层寻找this的指向,render()函数中的this指向组件实例,因此不会有问题,但进行抽离后的函数不具备this指向。
可以使用如下解决方式:
箭头函数
Function.prototype.bind()
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()
利用ES5中的bind方法,将事件处理程序中的this与组件实列绑定
类似于小程序中的
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两种表单处理方式
受控组件
非受控组件(DOM方式)
受控组件
HTML中的表单元素是可输入的,也就是有自己的可变状态
React中可变状态通常保存在state中,使用setState来i需改
React将state与表单元素值value绑定到一起,由state的值来控制表单元素的值
受控组件:即值收到React控制的表单元素
受控组件的使用包括以下步骤:
在state中添加一个状态,作为表单元素的value值(控制表单元素值的来源)
给表单元素绑定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'))
如下练习将不同的表单标签转化为受控组件。
文本框,富文本框,下拉框操作value属性
复选框操作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函数,因此需要对多表单元素进行优化,即使用一个事件处理程序同时处理多个表单元素。
多表单元素优化
给表单元素添加name属性,名称与state相同
根据表单元素类型获取对应值
在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实现
使用步骤:
通过React.createRef()方法创建一个ref对象
将创建好的ref对象添加到文本框中
通过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") );
案例一
式样以上知识,实现一个无回复功能的评论版
渲染评论列表
在state总初始化评论列表数据
使用map循环渲染列表数据
注意给每个被渲染的元素添加一个key
评论区条件渲染
判断列表长度是否为0
如果为0则渲染暂无评论
注意讲逻辑与JSX分离
获取评论信息
使用受控组件的方式实现
注意设置handle方法和name属性
发表评论
为按钮绑定单击事件
在事件处理程序中通过state获取评论信息
将评论添加到state中,更新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 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的特点
可以给组件传递任意类型的值
props是一个只读 属性,无法修改
使用类组件时,如果写了构造函数,应该将props传递给super(),否则,无法在构造函数中获取到props
组件通讯的三种方式
父组件 -> 子组件
子组件 -> 父组件
兄弟组件
父组件到子组件
父组件提供要传递的state数据
给子组件标签添加属性,值为state中的数据
子组件中通过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> }
子组件到父组件
思路:利用回调函数,父组件提供回调,子组件调用,将要传递的数据作为回调函数的参数
父组件提供一个回调函数,用于接收数据
将该函数作为属性值传递给子组件
子组件通过props调用回调函数
将子组件的数据作为参数传递给回调函数
注意回调函数中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 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
它的作用就是跨组件传递数据(比如:主题,语言设置等需要在根组件配置数据)
使用方法如下:
调用React.createContext()创建Provider(提供数据)和Consumer(消费数据)两个组件
使用Provider组件作为父节点
设置Value属性,表示要传递的数据
调用Consumer组件接受数据
即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是外来的,无法保证使用组件时传入的值的格式。
那么会出现以下问题:
传入数据不对可能导致组件内部报错
组件的使用者不知道明确的错误原因
React提供了props校验作为解决方法。
props校验:
运行在创建组件时只当props的类型、格式等
能捕获使用组件时因为props导致的错误,给出明确的错误提示,增加组件的健壮性
使用步骤:
安装prop-types包
导入prop-types
使用组件名.propTypes={}
来给组件的props添加校验规则
校验规则通过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'))
组件的生命周期
学习组件的生命周期有助于理解组件的运行方式、从而完成更复杂的组件功能、分析组件错误原因等等
组件的生命周期指:组件从被创建到挂载在页面中运行,再到组件不用时卸载的过程。
钩子函数:生命周期的每个阶段总伴随着一些方法调用,这些方法就是生命周期的钩子函数,为开发人员在不同阶段操作组件提供了时机。
只有类组件才有生命周期
生命周期的三个阶段
创建时
更新时
卸载时
创建时(挂在阶段)
执行时机:组件创建时(页面加载时)
钩子函数执行顺序:
constructor()
render()
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操作应被放置于该钩子函数内。
更新阶段
更新阶段的执行时机包括:
New props,组件接收到新属性
setState(),调用该方法时
forceUpdate(),调用该方法时
其中forceUpdate用于使组件强制更新,即使没有数值上的改变。
钩子函数执行顺序:
shouldComponentUpdate
render()
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
组件卸载(从页面中消失)
执行清理工作(比如:清理定时器等)
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
卸载时
render-props和高阶组件
组件复用
如果两个组件中的部分功能相似或相同时,该如何处理?
因此对于相似的功能,我们希望能偶复用相似的功能。
复用时事实上时复用以下两点:
state
操作state的方法(组件状态逻辑
React中组件复用包含两种方式:
render props模式
高阶组件(HOC)
以上两种方式是利用React自身特点的编码技巧,不是API
render-props模式
思路:将要复用的state和操作state的方法封装到一个组件中
问题:
如何拿到该组件中复用的state
在使用组件时,添加一个值为函数的prop,通过函数参数来获取(需要组件内部实现)
如何渲染任意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组件负责:封装复用的状态逻辑代码:
状态:鼠标坐标(x,y)
操作状态的方法:鼠标移动事件
传入的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模式代码优化
给render props模式添加props校验
组件卸载时应该解除mousemove事件绑定(使用react添加的事件绑定react会帮我们处理)
高阶组件
高阶组件时一种采用包装(装饰)模式 实现的状态逻辑复用(例如python中的高阶函数,java中的AOP)
实现思路:
高阶组件(HOC)是一个函数,接收要包装的组件,返回增强后的组件
高阶组件内部创建一个类组件,在这个类组件中提供复用的状态逻辑代码,通过prop将复用的状态传递给包装组件WrappedComponent
使用步骤
创建一个函数,名称约定以with开头
指定函数参数,参数应以大写字母开头(因为参数要被作为组件渲染)
在函数内部创建一个类组件,提供复用的状态逻辑代码,并返回
在该组件中,渲染参数组件,同时将状态通过prop传递给参数组件
调用高阶组件,传入要增强的组件,通过返回值拿到增强后的组件,并将其渲染到页面中
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) => {})语法
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内容
组件更新机制
对于多层树结构的组件结构,组件的更新过程如下:
父组件重新渲染时,子组件也会被重修渲染
渲染只发生在当前组件的子树中
更新顺序按中序遍历序更新
组件性能优化
减轻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部分渲染的实现流程如下:
初次渲染时,React根据初始state(Model),创建一个虚拟DOM对象(虚拟DOM树)
根据虚拟DOM生产真正的DOM,渲染到页面中
当数据变化后,重新根据新数据,创建新的虚拟DOM对象
与上一次得到的虚拟DOM对象,使用Diff算法对比得到需要更新的内容
最终,React只将变化的内容更新(patch)到DOM中,重新渲染得到页面
实际上虚拟DOM最大的价值在于:
虚拟DOM让React脱离了浏览器环境的束缚,为跨平台提供了基础
路由
React路由
现代前端应用大多数时SPA(单页应用程序),也就是只有一个HTML页面的应用程序,因为他的用户体验更好、对服务器的压力更小。为了有效地使用单个页面来管理原来多个页面的功能,前端路由应运而生。
前端路由功能:让用户从一个视图(页面)导航到另一个视图(页面)
前端路由是一套映射规则,在React中,是URL路径与组件的对应关系
使用React路由简单来说,就是配置路径和组件(配对)
React路由基本使用
安装:yarn add react-router-dom
导入路由的三个核心组件:BrowserRouter/Route/Link
import {BrowserRouter as Router, Route, Link} from 'react-router-dom'
使用Router组件包裹整个应用
使用Link组件作为导航菜单(路由入口)
<Link to="/first">页面一</Link>
使用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组件写在哪,组件就会被渲染在哪
路由执行过程
点击Link组件,修改了浏览器地址中的url
React路由监听到地址栏url变化
React路由内部遍历所有Route组件,使用路由规则(path)与pathname进行匹配
当路由规则与pathname匹配时,展示该Route组件的内容
编程式导航
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的情况
某个组件的状态需要让其他组件共享
一个组件需要改变另一个组件的状态(通讯)
核心概念
action
动作对象
包含2个属性
type:标识属性,字符串,唯一,必要属性
data:数据属性,任意类型,可选属性
例如:{ type: 'TOGGLE_TODO', index: 1 }
reducer
用于初始化状态、加工状态
加工时,根据旧的state和action,产生新的state的纯函数
store
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> <button onClick={this.increment}>+</button> <button onClick={this.decrement}>-</button> <button onClick={this.incrementIfOdd}>当前求和为奇数则加</button> <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 - redux - countReducer.js - store.js - 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> <button onClick={this.increment}>+</button> <button onClick={this.decrement}>-</button> <button onClick={this.incrementIfOdd}>当前求和为奇数则加</button> <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 - redux - constant.js - countActionCreator.js - countReducer.js - store.js - 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> <button onClick={this.increment}>+</button> <button onClick={this.decrement}>-</button> <button onClick={this.incrementIfOdd}>当前求和为奇数则加</button> <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中将组件分类两类:
容器组件
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 - redux - store.js - constant.js - actions - count.js - reducers - count.js - 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> <button onClick={this.increment}>+</button> <button onClick={this.decrement}>-</button> <button onClick={this.incrementIfOdd}>当前求和为奇数则加</button> <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实现数据共享
现有两个组件
希望这两个组件使用的state能够相互共享。
目录结构如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 src - container - Count - index.js - Person - index.js - redux - store.js - constant.js - actions - count.js - person.js - reducers - count.js - person.js - 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必须是一个纯函数,那么什么样的函数是纯函数呢?
如果一个函数具有如下特性,则认为该函数是一个纯函数:
只要给定同样的输入,则必定返回同样的输出。
纯函数应该具有如下约束:
不得改写参数数据
不会产生任何副作用,例如网络请求,输入和输出设备
不能调用Data.now()或者Math.random()等不纯函数
redux开发者工具
为浏览器安装Redux Dev Tools插件
为项目安装库redux-devtools-extension
在store.js中引入
import { composeWithDevTools } from 'redux-devtools-extension'
修改暴露为:
export default createStore(allReducer, composeWithDevTools(applyMiddleware(thunk)))
也可以使用如下方式开启调试,无需下载redux-devtools-extension库:
import {compose} from 'redux'
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 - Person - index.js - redux - store.js - constant.js - actions - count.js - person.js - reducers - index.js - count.js - person.js - 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> <button onClick={this.increment}>+</button> <button onClick={this.decrement}>-</button> <button onClick={this.incrementIfOdd}>当前求和为奇数则加</button> <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
样式继承
单位
可以通过构造如下工具来解决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 ;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 }} />
注意一定要加宽高,不然无法显示
调试
RN有两种调试方式:
谷歌浏览器
使用RN推荐的工具react-native-debugger
想要查看网络请求则需要进行如下配置:
找到项目入口文件index.js
加入以下代码:
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变量
把my-release-key.keystore
文件放到工程中的android/app
文件夹下。
编辑~/.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-aliasMYAPP_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.js
与request.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那样提供随机数生成内联函数,因此他推荐的做法有如下两种:
使用nanoid/non-secure代替,即使用非随机ID代替
在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
NavigationContainer
该组件负责组织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.useRef
和React.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} />
navigationKey
该属性用于控制不同条件下组件的显示,例如不登陆和登录时显示不同的组件,在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> ); } }
Navigation prop
该参数同样会被自动的提供,使用方法同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
于是安装该依赖:
1 $ yarn add react-native-screens
再次启动项目,由于该依赖较大,安装时间将会显著加长。
启动后便能得到如下画面:
此时我们得到了一个页面,原因是我们只在代码中注册了一个页面,那么接下来我们添加第二个页面:
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 }} />
最终效果如下:
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 { "plugins" : [ [ "import" , { libraryName: "@ant-design/react-native" } ] ] }
然后就能直接使用如下方式引入模块:
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
Navigation
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上该页面的图标、颜色、名称等信息,我们只需要将页面组件,例如Calculator
、BasicTabsExample
等包裹在想要渲染的页面的TabBar.Item
下即可。
还可以使用受控组件的形式来控制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还提供了许多自定义功能,允许我们设计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
以及一些配合使用的动效组件:
使用如下代码即可看到三个基础组件的效果:
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>
加载动画与上拉加载
加载动画
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
中挨个取出数据并渲染到列表中,即定义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
组件中提及的如下两个属性:
其中第一个参数接收一个函数,用于指定刷新触发后的一些列操作。
第二个参数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", }, });
实现效果如下:
文章点击跳转
首先为了提高组件复用性,创建一个组件用于显示文章的详细信息,即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;
最终效果如下:
RN访问摄像机与相册
通常情况下为了能使RN项目能够访问摄像头,需要在项目文件夹中的android目录下进行一些相关的权限配置,RN官方给出的摄像机权限配置如下:
打开项目中的android->app->src->main->AndroidManifest.xml文件,添加如下配置:
1 2 <uses-permission android:name ="android.permission.CAMERA" /> <uses-permission android:name ="android.permission.WRITE_EXTERNAL_STORAGE" />
打开项目中的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;import com.imagepicker.permissions.OnImagePickerPermissionsCallback; import com.facebook.react.modules.core.PermissionListener; public class MainActivity extends ReactActivity { private PermissionListener listener; @Override protected String getMainComponentName () { return "task" ; } }
但实际上RN并没有提供很方便的调用摄像头的操作,于是找到了以下第三方插件:react-native-image-picker