学习是个吃苦的过程,而学编程那就是个更吃苦的事情,一个技术点需要上万的字来说明,一个实现页面需要上万的代码数据来凑,既然走上了这条路,愿大家无怨无悔,今天为大家带来了万字编程宝典中关于前端开发项目中javascript库中recact详解,你看或者不看,它就在这里。
一、React是什么?
React是一个声明式,高效且灵活的用于构建用户界面的JavaScript库。使用React可以将一些简短、独立的代码片段组合成复杂的UI界面,这些代码片段被称为”组件”。
二、React特点
- 1.声明式设计 -React采用声明范式,可以轻松描述应用。
- 2.高效 -React通过对DOM的模拟,最大限度地减少与DOM的交互。
- 3.灵活 -React可以与已知的库或框架很好地配合。
- 4.JSX -JSX是JavaScript语法的扩展。React开发不一定使用JSX,但我们建议使用它。
- 5.组件 -通过React构建组件,使得代码更加容易得到复用,能够很好的应用在大项目的开发中。
- 6.单向响应式的数据流 - React实现了单向相应的数据流,从而减少了重复代码,这也是它为什么比传统数据绑定更简单。
三、引入React.js库
React.js框架本身包含两个部分: - 1.react.js:提供了React.js核心功能代码,如:虚拟dom、组件 - 2.React-dom.js:提供了与浏览器交互的DOM功能,如:dom渲染
<!DOCTYPE html>
<html lang="m en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Document</title>
</head>
<body>
<div id="app"></div>
<script src="./js/react.production.min.js"></script>
<script src="./js/react-dom.production.min.js"></script>
<script type="text/jsx" src="js/app.js"></script>
</body>
</html>
// 需注意:最后一个script标签的type属性为text / jsx。这是因为React独有的JSX语法,跟JavaScript不兼容。凡是使用JSX的地方,都要加上type=’text/jsx’。
js/app.js
ReactDOM.render(
'<h1>Hello,world!</h1>',
document.getElementById('app')
);
上面代码将一个h1标签插入app节点,运行结果如下:
- Hello,world!
React.render()
ReactDOM.render(element,container[,callback])
element: 要渲染的内容 container: 要渲染的内容存放容器 callback: 渲染后的回调函数
四、JSX
1.JSX是什么?
- JSX是一个基于JavaScript + XML 的一个- 扩展语法
- 它可以作为 值 使用
- 它并不是 字符串
- 它也不是 HTML
- 它可以配合 JavaScript 表达式 一起使用
2.JSX优点:
- JSX执行更快,因为它在编译为JavaScript代码后进行了优化
- 它是类型安全的,在编译过程中就能发现错误
- 使用JSX编写模块更加简单快速
3.JSX语法:
- 1> 如果输出多行结构,可以使用一对小括号来包含整个结构
const App = (
<div>
<h1>开课吧</h1>
<p>web前端高级工程师</p>
</div>
)
- 2> 注意:JSX只能有一个顶级父元素
const App = (
<div>
<h1>开课吧</h1>
<p>web前端高级工程师</p>
</div>
<div>第二个</div>
)
//以上代码会出错
- 3> 在JSX中可以嵌套表达式:
et name = '开课吧';
let title = 'web前端高级工程师';
const App = (
<div>
<h1>{name}</h1>
<p>{title}</p>
</div>
);
//注意分清楚 表达式 与 语句 的区别
// if、for、while这些都是语句,JSX不支持语句
<h1>{if(true){...}}</h1>//错误
- 4> JSX中的注释
<div>
{/*注释*/}
{/*
多行注释
*/}
</div>
- 5> JSX更偏向于JavaScript,所以对于一些特殊的属性,使用的是JavaScript中的属性名风格
// 错误
const App = (
<div class="box1"></div>
);
// 正确
const App = (
<div className="box1"></div>
);
- 6> 为了更加方便的操作元素的style,针对style这个属性有特殊的处理
const App = (
<div style={{width: '100px', height: '100px', color:'red'}}></div>
);
// 这里的两个 <u>{{}}</u> ,外部的大括号表示的是前面说的表达式语法中的大括号,里面的大括号是表示对象的大括号
let skin = {width: '100px', height: '100px', color:'red'};
const App = (
<div style={skin}></div>
);
- 7> JSX中的表达式也可以使用在属性上,但是使用的时候需要注意:不要使用引号包含
let id = 'kaikeba';
// 错误
const App = (
<div id="{id}"></div>
);
// 正确
const App = (
<div id={id}></div>
);
4.JSX输出类型:
字符串、数字:原样输出 数组:转成字符串,数组.join(‘’) 使用 空字符串 而不是默认的 逗号 连接 其他对象:不能直接输出 布尔值、空、未定义:会被忽略
5.JSX列表渲染:
- 如果需要渲染一组数据,我们可以通过遍历(数组遍历、对象遍历...)等操作,返回一组jsx
数据:
let zMouse = {
name: '张三',
gender: '男',
skills: ['JavaScript', 'Node.js'],
interests: ['音乐', '足球', '编程']
};
数组:
function getSkills(){
return (
<ul>
{zMouse.skills.map(skill => <li></li>)}
</ul>
)
}
const App = (
<div>{getSkills()}</div>
);
// 或者
const App = (
<div>
<ul>
{zMouse.skills.map(skill => <li>{skill}</li>)}
</ul>
</div>
)
对象:
function getKeys(){
let arr = [];
for(let k in zMouse){
arr.push(<li>{k}</li>)
}
return arr;
}
const App = (
<div>
<ul>{getKeys()}</ul>
</div>
)
//或者
const App = (
<div>
<ul>
{Object.keys(zMouse).map(key => <li>{key}</li>)}
</ul>
</div>
)
Key - 默认情况下,React从性能上考虑,会尽可能的复用结构,针对 同组可变列表 结构,为了避免出现某些方面的问题,通常会给每一个列表添加一个 唯一的key
<ul>
{[{id:1,name:'zs',id:2,name:'ls'}].map(user => <li key={user.id}>{user.name}</li>)}
</ul>
// 注意:key 的值不推荐使用数组的下标
条件渲染
function moreInterests() {
if (zMouse.interests.length > 2) {
return <a href="#">更多</a>
}
}
const App = (
<div>
爱好:{zMouse.interests.map(interest=>{
return <span style={{marginRight:"10px"}}>{interest}</span>
})}
{moreInterests()}
</div>
);
三目运算符
const App = (
<div>
爱好:{zMouse.interests.map(interest=>{
return <span style={{marginRight:"10px"}}>{interest}</span>
})}
{zMouse.interests.length > 2 ? <a href="#">更多</a> : null}
</div>
);
与或运算符
const App = (
<div>
爱好:{zMouse.interests.map(interest=>{
<span style={{marginRight:"10px"}}>{interest}</span>
})}
{zMouse.interests.length > 2 && <a href="#">更多</a>}
{zMouse.interests.length < 4 || <a href="#">更多</a>}
</div>
);
五、React脚手架
- create-react-app是用于搭建react项目的脚手架。它的优势在于省略了很多涉及配置的地方,能够更加容易上手。
1.安装与使用:
通过 npm、yarn、npx都可以
1>安装 npm
npm i -g create-react-app
yarn
yarn global add create-react-app
2>使用 安装完成以后,即可使用create-react-app命令
- create-react-app 项目名称 或者通过npx的方式
- npx cerate-react-app 项目名称
2.项目目录结构说明:
运行命令以后,就会在运行命令所在的目录下面创建一个以项目名称为名的目录:
my-app/
README.md //主要是项目的说明文件,在里面写一些关于本项目的说明
node_modules/ //里面放的是项目依赖的第三方的包,这些包不是我们自己写的,是脚手架工具要实现自己的功能,需要依赖外部的一些第三方的包文件来实现
package.json //脚手架自动生成的一个文件,是一个node的文件,里面有一些项目的脚手架的版本,以及一些npm的命令
public/
index.html //项目首页的html模板
favicon.ico //图标
src/ //src里面放的是项目所有的源代码
App.css
App.js
App.test.js //自动化测试的文件
index.css
index.js //项目的入口文件
logo.svg
3.命令脚本:
create-react-app同时也提供了其它一些命令来帮助我们进行开发 1> npm start 启动一个内置的本地WebServer,根目录映射到 ’./public’目录,默认端口:3000 2> npm test 运行 Jest 测试 3> npm run build 打包应用(准备上线)
六、组件之状态与通信
在React.js中,定义一个组件的最简单的方式就是 函数
1> 函数式组件:
function Kaikeba() {
return (
<div>
<h2>开课吧!</h2>
</div>
);
}
ReactDOM.render(
<Kaikeba />,
document.getElementById('app')
);
// 函数的名称就是组件的名称,必须是首字母大写
// 函数的返回值就是组件要渲染的内容
2> 类组件:
class Miaov extends React.Component {
render() {
return (
<div>
<h2>妙味!</h2>
</div>
);
}
}
// 组件类必须继承 React.Component
// 组件类必须有 render方法
// render 方法的返回值就是组件要渲染的内容
// 类的名称(组件的名称),也必须是首字母大写
3> 组件复用 - 数据传入
使用组件的时候通过 标签属性 -property 的方式传入数据,在组件内部通过构造函数参数(如果是函数组件,则通过函数参数)来接收传入的数据
<组件名称 属性名称="值" />
// 使用表达式
<组件名称 属性名称={表达式} />
ReactDOM.render(
<FriendList datas={datas} />,
document.getElementById('app')
);
4> 接收参数 -props
- 函数式组件:通过函数的第一个参数来接收;
- 类式组件:通过类的 props 属性接收;
无论是函数式组件还是类式组件,都会把传入的参数封装成一个对象
<组件名称 属性名称1="值1" 属性名称二={表达式二} />
// 函数式组件:
// 函数创建的组件是无状态组件,没有state,没有生命周期方法,它是一种只负责展示的纯组件
function 组件名称(参数) {
// 参数的结构为:
参数 = {
属性名称1: "值1",
属性名称二: 表达式二的值
}
return <div>组件结构</div>
}
// 类式组件
class 组件名称 extends React.Component {
constructor(参数) {
super(参数);
this.props = {
属性名称1: "值1",
属性名称2: 表达式二的值
}
}
render() {
return <div>组件结构</div>
}
}
// 在类式组件中,需要注意:
// 1.当子类重写 constructor,则必须调用父类 super
// 2.把接收到的参数传入父类构造函数,父类构造函数中会创建一个对象属性:props 类存储传入的参数数据
5> 通过参数动态渲染组件结构
class FriendList extends React.Component {
constructor(props) {
super(props);
}
render() {
let {datas} = this.props;
return (
<div className="friend-list">
{Object.keys(datas).map((key, index) => (
<div className="friend-group" key={key}>
<dt>{datas[key].title}</dt>
{datas[key].list.map((list, index) => (
<dd key={index}>{list}</dd>
)}
</div>
))}
</div>
);
}
}
6> 子组件提取
组件与类一样,是一个不断提取的过程,当我们发现某个部分可复用或者结构复杂的时候,我们可以对它再次进行提取
class FriendGroup extends React.Component {
constructor(props) {
super(props);
}
render() {
let {data} = this.props;
return (
<div className="friend-group">
<dt>{data.title}</dt>
{data.list.map((list, index) => {
<dd key={index}>{list}</dd>
})}
</div>
);
}
}
class FriendList extends React.Component {
constructor(props) {
super(props);
}
render() {
let {datas} = this.props;
return (
<div className="friend-list">
{Object.keys(datas).map((key, index) => (
<FriendGroup data={datas[key]} key={key} />
))}
</div>
);
}
}
7> 组件状态
- 组件状态是什么? 状态被用来存储组件在某段时间内状态改变的信息。用户事件或系统事件会导致一些经典的状态改变。(比如:对用户输入的回应、服务器的请求、生命周期函数)
- 组件 state 工作是这样的:先给组件设置一个默认状态,再获取当前状态,最后更新这个状态
- 修改 state 的正确姿势: 不能直接修改state 在React中,直接修改state并不会触发render函数,所以下面的写法是错误的。
this.state.title = 'React'; // 错误
// 组件的State只能通过setState()方式进行修改。例如:
this.setState({title: 'React'}); // 正确
- setState方法由父类React.Component提供,当该方法被调用时,组件的state会被更新,同时会被重新调用组件的render方法对组件进行渲染。
- 组件构造函数是唯一可以对 state 直接赋值(初始化)的位置
8> 绑定事件
class FriendGroup extends React.Component {
constructor(props) {
super(props);
this.state = {
expanded: false
};
this.expand = this.click.expand();
}
expand() {
this.setState({
expanded: !this.state.expanded
});
}
render() {
let {data} = this.props;
let {expanded} = this.state;
return (
<div className={["friend-group",expanded && "expanded"].join(' ')}>
<dt onClick={this.expand}>{data.title}</dt>
{data.list.map((list, index) => {
<dd key={index}>{list}</dd>
})}
</div>
);
}
}
React.js的事件绑定需要注意:
- 事件名称是驼峰命名的
- 事件绑定函数的this指向
- 通过bind改变this指向,为了能够在方法中调用对象的属性和其他方法, 我们需要把this指向组件
- 通过箭头函数处理
<dt onClick={e=>{this.expand}}>{data.title}</dt>
如上代码: 事件绑定函数的第一个参数是事件对象 因为this指向了组件对象,那么获取当前事件触发元素,可以通过e.target来获取
9> 获取原生DOM对象 有的时候我们也是需要操作原生DOM对象的,除了可以通过事件源来获取,还可以通过ref的方式来获取
class MyComponent extends React.Component {
render() {
return (
<div>
<input ref={el=>this.el=el} />
<button onClick={this.todo.bind(this)}>Button</button>
</div>
);
}
todo() {
console.log(this.el.value);
}
}
10> 更新异步 出于性能考虑,setState方法的修改并不是立即生效的
// this.state.val = 0
this.setState({
val: this.state.val + 1
});
// this.state.val 的值并不是 1
console.log(this.state.val);
11> 更新合并 React会把多个setState合并成一个调用
// this.state.val = 0
this.setState({
val: this.state.val + 1
});
this.setState({ // 因为异步的问题,this.state.val 的值在这里还是0
val: this.state.val + 1
});
12> props与state的区别 - state的主要作用是用于 组件保存、控制、修改自己的可变状态,在组件内部进行初始化,也可以在组件内部进行修改,但是组件外部不能修改组件的state - props的主要作用是让使用该组件的父组件可以传入参数来配置该组件,它是外部传进来的配置参数,组件内部无法控制也无法修改 - state和props都可以决定组件的外观和显示状态。通常,props作为不变数据或者初始化数据传递给组件,可变状态使用state - 能使用props就不要使用state
13> 无状态组件 - 没有状态的组件,我们成为 无状态组件。 - 因为状态会带来复杂性,所以,通常我们推荐使用无状态组件。 - 函数式组件没有state,所以通常我们编写使用函数式组件来编写无状态组件
14> 数据流 - 在React.js中,数据是从 上自下流动(传递) 的,也就是一个父组件可以把它的state/props通过props传递给它的 子组件 ,但是 子组件不能修改props。 - React.js是单向数据流,如果子组件需要修改父组件状态(数据),是通过 回调函数 方式来完成的。
15> React组件通信方式汇总:
需要组件之前进行通信的几种情况: - 父组件向子组件通信 - 子组件向父组件通信 - 跨级组件通信 - 没有嵌套关系组件之间的通信
1.父组件向子组件通信
父组件通过向子组件传递props,子组件得到props后进行相应的处理
2.子组件向父组件通信
3.跨级组件通信
使用context context是一个全局变量,像是一个大容器,在任何地方都可以访问到,我们可以把要通信的信息放在context上,然后在其他组件中可以随意取到
使用context也很简单,需满足: - 1、上级组件要声明自己支持context,提供context中属性的PropTypes,并提供一个函数来返回相应的context对象 - 2、子组件要声明自己主要使用context,并提供其需要使用的context属性的PropTypes - 3、父组件需提供一个getChildContext函数,以返回一个厨师的context对象 如果组件中使用构造函数(constructor),还需要在构造函数中传入第二个参数context,并在super调用父类构造函数时传入context,否则会造成组建中无法使用context。
下面例子中的组件关系:ListItem是List的子组件,List是app的子组件
4.没有嵌套关系的组件通信(兄弟组件)
使用自定义事件机制
实现这样一个功能: 点击List2中的一个按钮,改变List1中的信息显示(List1和List2没有任何嵌套关系,App是他们的父组件)
- 首先需要项目中安装events包:
npm install events --save
- 在src下新建一个util目录里面见一个events.js
import { EvenetEmitter } from 'events';
export default new EventEmitter();
- list1.js
import React, { Component } from 'react';
import emitter from '../util/events';
class List extends Component {
constructor(props) {
super(props);
this.state = {
message: 'List1',
};
}
componentDidMount() {
// 组件装载完成以后声明一个自定义事件
this.eventEmitter = emitter.addListener('changeMessage', (message) => {
this.setState({
message,
});
});
}
componentWillUnmount() {
emitter.removeListener(this.eventEmitter);
}
render() {
return (
<div>
{this.state.message}
</div>
);
}
}
export default List;
- List2.js
import React, { Component } from 'react';
import emitter from '../util/events';
class List2 extends Component {
handleClick = (message) => {
emitter.emit('changeMessage', message);
};
render() {
return (
<div>
<button onClick={this.handleClick.bind(this, 'List2')}>点击我改变List1组件中显示信息</button>
</div>
);
}
}
- APP.js
import React, { Component } from 'react';
import List1 from './components/List1';
import List2 from './components/List2';
export default class App extends Component {
render() {
return (
<div>
<List1 />
<List2 />
</div>
);
}
}
- 自定义事件是典型的发布订阅模式,通过向事件对象上添加监听器和触发事件来实现组件之间的通信
七、React生命周期
1> 第一个是组件初始化阶段:
- super(props)用来调用基类的构造方法(constructor()),也将父组件的props注入给子组件,供子组件读取(组件总props只读不可变,state可变)
- 而constructor()用来做一些组件的初始化工作,如定义this.state的初始内容。
import React, { Component } from 'react';
class Test extends Component {
constructor(props) {
super(props);
}
}
2> 第二个是组件的挂载阶段:
- componentWillMount: 在组件挂载到DOM前调用,且只会被调用一次,在这边调用this.setState不会引起组件重新渲染,也可以把写在这边的内容提前放到constructor()中,所以项目中很少用。
- render: 根据组件的props和state(无论两者的重传递和重赋值,论值是否有变化,都可以引起组件重新render),return一个React元素 (描述组件,即UI),不负责组件实际渲染工作,之后由React自身根据此元素去渲染出页面DOM。render是纯函数(函数的返回结果只依赖它的参数;函数执行过程里面没有副作用),不能在里面执行this.setState,会有改变组件状态的副作用。
- componentDidMount: 组件挂载到DOM后调用,且只会被调用一次
3> 第三个是组件的更新阶段:
在讲述此阶段前需要先明确下react组件更新机制。setState引起的state更新或父组件更新render引起的props更新,更新后的state和props相对之前无论是否有变化,都将引起组件的重新渲染。
造成组件更新有两类(三种)情况: - 1.父组件重新render 父组件重新render引起子组件重新render的情况有两种: a. 直接使用,每当父组件重新render导致的重传props,子组件将直接跟着重新渲染,无论props是否有变化。可通过shouldComponentUpdate方法优化。
class Child extends Component {
shouldComponentUpdate(nextProps){ // 应该使用这个方法,否则无论props是否有变化都将会导致组件跟着重新渲染
if(nextProps.someThings === this.props.someThings){
return false
}
}
render() {
return <div>{this.props.someThings}</div>
}
}
b.在componentWillReceiveProps方法中,将props转换成自己的state
class Child extends Component {
constructor(props) {
super(props);
this.state = {
someThings: props.someThings
};
}
componentWillReceiveProps(nextProps) { // 父组件重传props时就会调用这个方法
this.setState({someThings: nextProps.someThings});
}
render() {
return <div>{this.state.someThings}</div>
}
}
在该函数(componentWillReceiveProps)中调用 this.setState() 将不会引起第二次渲染。 是因为componentWillReceiveProps中判断props是否变化了,若变化了,this.setState将引起state变化,从而引起render,此时就没必要再做第二次因重传props引起的render了,不然重复做一样的渲染了。
- 2.组件本身调用setState,无论state有没有变化。可通过shouldComponentUpdate方法优化。
class Child extends Component {
constructor(props) {
super(props);
this.state = {
someThings:1
}
}
shouldComponentUpdate(nextStates){ // 应该使用这个方法,否则无论state是否有变化都将会导致组件重新渲染
if(nextStates.someThings === this.state.someThings){
return false
}
}
handleClick = () => { // 虽然调用了setState ,但state并无变化
const preSomeThings = this.state.someThings
this.setState({
someThings: preSomeThings
})
}
render() {
return <div onClick = {this.handleClick}>{this.state.someThings}</div>
}
}
此阶段分为componentWillReceiveProps,shouldComponentUpdate,componentWillUpdate,render,componentDidUpdate
- componentWillReceiveProps(nextProps) 此方法只调用于props引起的组件更新过程中,参数nextProps是父组件传给当前组件的新props。但父组件render方法的调用不能保证重传给当前组件的props是有变化的,所以在此方法中根据nextProps和this.props来查明重传的props是否改变,以及如果改变了要执行啥,比如根据新的props调用this.setState出发当前组件的重新render
- shouldComponentUpdate(nextProps, nextState) 此方法通过比较nextProps,nextState及当前组件的this.props,this.state,返回true时当前组件将继续执行更新过程,返回false则当前组件更新停止,以此可用来减少组件的不必要渲染,优化组件性能。 ps:这边也可以看出,就算componentWillReceiveProps()中执行了this.setState,更新了state,但在render前(如shouldComponentUpdate,componentWillUpdate),this.state依然指向更新前的state,不然nextState及当前组件的this.state的对比就一直是true了。
- componentWillUpdate(nextProps, nextState) 此方法在调用render方法前执行,在这边可执行一些组件更新发生前的工作,一般较少用。
- render render方法在上文讲过,这边只是重新调用。
- componentDidUpdate(prevProps, prevState) 此方法在组件更新后被调用,可以操作组件更新的DOM,prevProps和prevState这两个参数指的是组件更新前的props和state
4>卸载阶段:
此阶段只有一个生命周期方法:
- componentWillUnmount
此方法在组件被卸载前调用,可以在这里执行一些清理工作,比如清楚组件中使用的定时器,清楚componentDidMount中手动创建的DOM元素等,以避免引起内存泄漏。
八、React路由
- 使用React构建的单页面应用,要想实现页面间的跳转,首先想到的就是使用路由。在React中,常用的有两个包可以实现这个需求,那就是react-router和react-router-dom。
react-router-dom
安装
- 首先进入项目目录,使用npm安装react-router-dom: npm install react-router-dom --save-dev //这里可以使用cnpm代替npm命令
基本操作
- 然后我们新建两个页面,分别命名为“home”和“detail”。在页面中编写如下代码:
// home.js
import React from 'react';
export default class Home extends React.Component {
render() {
return (
<div>
<a>去detail</a>
</div>
)
}
}
// detail.js
import React from 'react';
export default class Home extends React.Component {
render() {
return (
<div>
<a>回到home</a>
</div>
)
}
}
// 然后再新建一个路由组件,命名为“Router.js”,并编写如下代码:
import React from 'react';
import {HashRouter, Route, Switch} from 'react-router-dom';
import Home from '../home';
import Detail from '../detail';
const BasicRoute = () => (
<HashRouter>
<Switch>
<Route exact path="/" component={Home}/>
<Route exact path="/detail" component={Detail}/>
</Switch>
</HashRouter>
);
export default BasicRoute;
- 如上代码定义了一个纯路由组件,将两个页面组件Home和Detail使用Route组件包裹,外面套用Switch作路由匹配,当路由组件检测到地址栏与Route的path匹配时,就会自动加载响应的页面。
然后在入口文件中——我这里指定的是index.js——编写如下代码:
import React from 'react';
import ReactDOM from 'react-dom';
import Router from './router/router';
ReactDOM.render(
<Router/>,
document.getElementById('root')
);
这里相当于向页面返回了一个路由组件。我们先运行项目看一下效果,在地址栏输入“http://localhost:3000/#/”:
输入“http://localhost:3000/#/detail”:
通过a标签跳转 可以看到其实路由已经开始工作了,接下来我们再来做页面间的跳转。在home.js和detail.js中,我们修改如下代码:
// home.js
import React from 'react';
export default class Home extends React.Component {
render() {
return (
<div>
<a href='#/detail'>去detail</a>
</div>
)
}
}
// detail.js
import React from 'react';
export default class Home extends React.Component {
render() {
return (
<div>
<a href='#/'>回到home</a>
</div>
)
}
}
重新打包运行,在浏览器地址栏输入“http://localhost:3000/”, 试试看页面能否正常跳转。如果不能,请按步骤一步一步检查代码是否有误。以上是使用a标签的href进行页面间跳转,此外react-router-dom还提供了通过函数的方式跳转页面。
通过函数跳转 - 首先我们需要修改router.js中的两处代码:
import {HashRouter, Route, Switch, hashHistory} from 'react-router-dom';
<HashRouter history={hashHistory}>
- 然后在home.js中:import React from 'react';
export default class Home extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<a href='#/detail'>去detail</a>
<button onClick={() => this.props.history.push('detail')}>通过函数跳转</button>
</div>
)
}
}
在a标签下面添加一个按钮并加上onClick事件,通过this.props.history.push这个函数跳转到detail页面。 在路由组件中加入的代码就是将history这个对象注册到组件的props中去,然后就可以在子组件中通过props调用history的push方法跳转页面。
很多场景下,我们还需要在页面跳转的同时传递参数,在react-router-dom中,同样提供了两种方式进行传参。
url传参 - 在router.js中,修改如下代码:
<Route exact path="/detail/:id" component={Detail}/>
- 然后修改detail.js,使用this.props.match.params获取url传过来的参数:
componentDidMount() {
console.log(this.props.match.params);
}
在地址栏输入“http://localhost:3000/#/detail/3”,打开控制台:
可以看到传过去的id=3已经被获取到了。react-router-dom就是通过“/:”去匹配url传递的参数。
隐式传参 此外还可以通过push函数隐式传参。 修改home.js代码如下:
import React from 'react';
export default class Home extends React.Component {
constructor(props) {
super(props);
}
render() {
return (
<div>
<a href='#/detail/3'>去detail</a>
<button onClick={() => this.props.history.push({
pathname: '/detail',
state: {
id: 3
}
})}>通过函数跳转</button>
</div>
)
}
}
在detail.js中,就可以使用this.props.history.location.state获取home传过来的参数:
componentDidMount() {
//console.log(this.props.match.params);
console.log(this.props.history.location.state);
}
跳转后打开控制台可以看到参数被打印:
其他函数
replace 有些场景下,重复使用push或a标签跳转会产生死循环,为了避免这种情况出现,react-router-dom提供了replace。在可能会出现死循环的地方使用replace来跳转:
this.props.history.replace('/detail');
goBack 场景中需要返回上级页面的时候使用:
this.props.history.goBack();
嵌套路由 - 首先定义父级组件MainLayout
import React from 'react';
import './MainLayout.scss';
const { Header, Sider, Content } = Layout;
export default class MainLayout extends React.Component {
render() {
return (
<div className='main-layout'>
父组件
</div>
);
}
}
- 然后定义子组件Home:
import React, {useState} from 'react';
import {Modal, Select} from "antd";
import {connect} from 'react-redux';
import {addCount} from '../../servers/home';
function Home(props) {
const [visible, setVisible] = useState(false);
const {countNum: {count}, dispatch} = props;
return (
<div>
子组件
</div>
)
}
export default Home;
然后将它们添加进路由router.js,并且关联父子关系:
import React from 'react';
import {HashRouter, Route, Switch} from "react-router-dom";
import Home from '../pages/Home/Home';
import MainLayout from '../layout/MainLayout';
const BasicRouter = () => (
<HashRouter>
<Switch>
<Route path="/index" component={
<MainLayout>
<Route exact path="/" component={Home}/>
<Route exact path="/index" component={Home}/>
<Route path="/index/home" component={Home}/>
</MainLayout>
}/>
</Switch>
</HashRouter>
);
export default BasicRouter;
在MainLayout中,修改如下代码:
import React from 'react';
import './MainLayout.scss';
const { Header, Sider, Content } = Layout;
export default class MainLayout extends React.Component {
render() {
return (
<div className='main-layout'>
{this.props.children}
</div>
);
}
}
如此,一个嵌套路由就完成了。
react-router和react-router-dom的区别
- 写法1:
import {Swtich, Route, Router, HashHistory, Link} from 'react-router-dom';
- 写法2:
import {Switch, Route, Router} from 'react-router';
import {HashHistory, Link} from 'react-router-dom';
先简单说下各自的功能: - react-router: 实现了路由的核心功能 react-router-dom: 基于react-router,加入了在浏览器运行环境下的一些功能,例如:Link组件,会渲染一个a标签,Link组件源码a标签行; BrowserRouter和HashRouter组件,前者使用pushState和popState事件构建路由,后者使用window.location.hash和hashchange事件构建路由。 - react-router-native: 基于react-router,类似react-router-dom,加入了react-native运行环境下的一些功能。
结论: - react-router-dom依赖react-router,所以我们使用npm安装依赖的时候,只需要安装相应环境下的库即可,不用再显式安装react-router。基于浏览器环境的开发,只需要安装react-router-dom;基于react-native环境的开发,只需要安装react-router-native。npm会自动解析react-router-dom包中package.json的依赖并安装。
- react-router-dom中package.json依赖:
"dependencies": {
"history": "^4.7.2",
"invariant": "^2.2.2",
"loose-envify": "^1.3.1",
"prop-types": "^15.5.4",
"react-router": "^4.2.0",
"warning": "^3.0.0"
}
安装了react-router-dom,npm会解析并安装上述依赖包。可以看到,其中包括react-router。
所以,回到最开始的写法。基于浏览器环境的开发,写法1就可以了。
Route组件 - Route组件用于组件的显示,Route组件上有path和component属性,对应的path会显示对应的component,路径和组件产生对应的映射关系
<div>
<Route path='/' component={Home}></Route>
<Route path='/page1' component={Page1}></Route>
<Route path='/page2' component={Page2}></Route>
</div>
//以上代码,当浏览器访问/page1或者/page2路径,都会渲染Home组件,因为先和‘/’匹配了
- component属性:component能够接受一个组件名,也可以接受一个函数
<Route path="/" exact render={()=><h1>首页</h1>}/>
- 路由匹配的方式:要路径开头匹配成功即会显示对应的组件,就是当访问/page1路径的时候会匹配 / 的路径 ,解决办法是 exact属性
- exact:作用是严格匹配,路径是什么就匹配什么,避免路径匹配不正确的问题
<div>
<Route path='/' exact component={Home}></Route>
<Route path='/page1' component={Page1}></Route>
</div>
//此时访问'/page1',就不会再匹配‘/’了
Link组件和NavLink组件 - Link 组件是react-router中提供的路由切换的组件,基于它可以实现点击的时候实现路由的切换
Link组件的属性
- to:'/xxx?xxx=123' 跳转到指定的路由地址
- to: {pathname:'/xxx',search:'xxx:123',state:''}//可以基于state的方式传参
- replace 默认值false 是替换HISTORY stack中当前的地址(true) 还是追加一个新的地址 (默认是追加)
- 基于link组件渲染,渲染后的结果是一个A标签,to对应的信息最后会变成href的内容
<Link to={{pathname:'/',search:'lx=logo'}} className="navbar">强国首页</Link>
NavLink组件
NavLink组件和LINK类似,不同在于,NavLink组件在当前页面地址和组件对应地址相吻合的时候,会默认给组件加一个active样式,让其有选中态(可以修改选中状态) - activeClassName:把默认你加的active样式类改为自己设定的(默认是active) - activeStyle:给匹配的NavLink设置行内样式 - exact 控制匹配的时候是否严格匹配(和route的一样) - isActive:()=>{} 匹配成功后执行对应函数(相当于路由的钩子函数)
<NavLinkto={{pathname:'/',search:'lx=logo'}} className="navbar">强国首页</NavLink>
//匹配成功时,此元素会有active类名
//有时会有bug=>类名不切换,可以用hashChange事件监听或者使用withRouter
路由对象中的 history、location、 match、params
1.history对象 (用于编程式导航)
- let {history}=this.props;
- history.go(n)//到任意有的记录中
- history.push() //追加一条记录
- history.replace()//不会追加记录
- history.goback()//回退
- history.goforward()//前进
2.location
- let {location:{pathname,search,state}}=this.props;
- pathname当前的hash路由地址
- search查询字符串
- state 基于 redirect、link、nav-link中的to={},传递的是一个对象,对象中编写的state,就可以通过 loactuion.state获取
3.match 匹配动态路由的值主要使用 params动态路由参数
let {match:{params}}=this.props;
console.log(params.id)
//获取的是动态路由匹配的一些结果,可用于动态传参
二级路由
<Switch>
{/*进入客户管理页面,我们默认为其展示的就是List区域内容,或者使用重定向也可以*/}
<Route path='/custom' exact component={List}></Route>
<Route path='/custom/list' component={List}></Route>
<Route path='/custom/create' component={Create}></Route>
<Route path='/custom/Detail/:id' component={Detail}></Route>
{/*进入客户管理页面,我们默认为其展示的就是List区域内容*/}
{/*<Redirect from='/custom' to='/custom/list'></Redirect>*/}
</Switch>
Switch组件,用于嵌套Route组件 - 和switch case一样,只能 选择一个,避免一个路径匹配多个组件 像这种情况,可能都会被渲染到页面上,此时Switch组件就有用了
<Route path="/about" component={About}/>
<Route path="/:user" component={User}/>
<Route component={NoMatch}/>{/*无论前边路由是否匹配成功,都会走这个*/}
路由数据传输方式 不推荐: - 本地存储 localstroage - redux存储 (页面刷新数据就没了) 点击列表中某一项的时候,把信息存储到本地或者redux中,跳转到详情页面,把信息从本地或者redux中获取即可
推荐: - 1.问号传参
<Link to={{
pathname:'/home/user',
search:`?id=${item.id}`, //问号传参
}}>问号传参</Link>
// 受控组件中获取参数通this.props.location.search
- 2.state传值(一旦页面刷新state传的值就没了)
<Link to={{
pathname:'/home/user',
state:{'id':item.id},
}}>state传值</Link>
// 受控组件中获取参数通过 this.props.location.state
- 3.URL地址参数 path=‘/custom/detail/:id’
//动态路径设置
<Link to={{
pathname:`/home/user/${item.id}`
}}>URL地址参数</Link>
//Route路径配置
<Route path='/custom/Detail/:id' component={User}></Route>
//User组件内获取参数
this.props.match.params.id
路由重定向 Redirect
<Redirect from='/user' to='/user/list'></Redirect>//from 从哪个路由 to定向到哪个路由
处理默认子路由
- Redirect
<Route path='/user/list' component={List}></Route><Redirect from='/user' to='/user/list'></Redirect>//路径 /user下的默认显示的子路由是 /user/list ,/user和/user/list 最后渲染的都是List组件
- 两个路径都指向一个组件
<Route path='/user' exact component={List}></Route><Route path='/user/list' component={List}></Route>
九、Redux
Redux 是一个独立的 JavaScript 状态管理库。 曾经有人说过这样一句话。
"如果你不知道是否需要 Redux,那就是不需要它。"
Redux 的创造者 Dan Abramov 又补充了一句。
"只有遇到 React 实在解决不了的问题,你才需要 Redux 。"
核心概念 理解 Redux 几个核心概念与它们之间的关系: - store - state - action - reducer
Store Store就是保存数据的地方,你可以把它看成一个容器。整个应用只能有一个Store。
Redux提供createStore这个函数,用来生成Store:
import { createStore } from 'redux';
const store = createStore(fn);
上面代码中,createStore函数接受另一个函数作为参数,返回新生成的 Store 对象。 state Store对象包含所有数据。如果想得到某个时点的数据,就要对Store生成快照。这种时点的数据集合,就叫做State。
当前时刻的State,可以通过store.getState()拿到:
import { createStore } from 'redux';
const store = createStore(fn);
const state = store.getState();
Redux 规定, 一个 State 对应一个 View。只要 State 相同,View 就相同。你知道 State,就知道 View 是什么样,反之亦然。
state是只读的 - 这里需要注意的是,为了保证数据状态的可维护和测试,不推荐直接修改state中的原数据 - 通过纯函数修改state 什么是纯函数? 1.相同的输入永远返回相同的输出 2.不修改函数的输入值 3.不依赖外部环境状态 4.无任何副作用 - 使用纯函数的好处? 1.便于测试 2.有利重构
Action State 的变化,会导致 View 的变化。但是,用户接触不到 State,只能接触到 View。所以,State 的变化必须是 View 导致的。Action 就是 View 发出的通知,表示 State 应该要发生变化了。
Action 是一个对象。其中的type属性是必须的,表示 Action 的名称。其他属性可以自由设置:
const action = {
type: 'ADD_TODO',
payload: 'Learn Redux'
};
// 上面代码中,Action 的名称是ADD_TODO,它携带的信息是字符串Learn Redux。
// 可以这样理解,Action 描述当前发生的事情。改变 State 的唯一办法,就是使用 Action。它会运送数据到 Store。
Action Creator View 要发送多少种消息,就会有多少种 Action。如果都手写,会很麻烦。可以定义一个函数来生成 Action,这个函数就叫 Action Creator。
const ADD_TODO = '添加 TODO';
function addTodo(text) {
return {
type: ADD_TODO,
text
}
}
const action = addTodo('Learn Redux');
// 上面代码中,addTodo函数就是一个 Action Creator。
store.dispatch() store.dispatch()是 View 发出 Action 的唯一方法。
import { createStore } from 'redux';
const store = createStore(fn);
store.dispatch({
type: 'ADD_TODO',
payload: 'Learn Redux'
});
// 上面代码中,store.dispatch接受一个 Action 对象作为参数,将它发送出去。
结合 Action Creator,这段代码可以改写如下。
store.dispatch(addTodo('Learn Redux'));
Reducer Store 收到 Action 以后,必须给出一个新的 State,这样 View 才会发生变化。这种 State 的计算过程就叫做 Reducer。
Reducer 是一个函数,它接受 Action 和当前 State 作为参数,返回一个新的 State:
const reducer = function (state, action) {
// ...
return new_state;
};
整个应用的初始状态,可以作为 State 的默认值。下面是一个实际的例子:
const defaultState = 0;
const reducer = (state = defaultState, action) => {
switch (action.type) {
case 'ADD':
return state + action.payload;
default:
return state;
}
};
const state = reducer(1, {
type: 'ADD',
payload: 2
});
// 上面代码中,reducer函数收到名为ADD的 Action 以后,就返回一个新的 State,作为加法的计算结果。
实际应用中,Reducer 函数不用像上面这样手动调用,store.dispatch方法会触发 Reducer 的自动执行。为此,Store 需要知道 Reducer 函数,做法就是在生成 Store 的时候,将 Reducer 传入createStore方法。
import { createStore } from 'redux';
const store = createStore(reducer);
// 上面代码中,createStore接受 Reducer 作为参数,生成一个新的 Store。以后每当store.dispatch发送过来一个新的 Action,就会自动调用 Reducer,得到新的 State。
store.subscribe() Store 允许使用store.subscribe方法设置监听函数,一旦 State 发生变化,就自动执行这个函数。
import { createStore } from 'redux';
const store = createStore(reducer);
store.subscribe(listener);
显然,只要把 View 的更新函数(对于 React 项目,就是组件的render方法或setState方法)放入listen,就会实现 View 的自动渲染。
store.subscribe方法返回一个函数,调用这个函数就可以解除监听:
let unsubscribe = store.subscribe(() =>
console.log(store.getState())
);
unsubscribe();
Reducer 的拆分
- Reducer 函数负责生成 State。由于整个应用只有一个 State 对象,包含所有数据,对于大型应用来说,这个 State 必然十分庞大,导致 Reducer 函数也十分庞大
请看下面的例子:
const chatReducer = (state = defaultState, action = {}) => {
const { type, payload } = action;
switch (type) {
case ADD_CHAT:
return Object.assign({}, state, {
chatLog: state.chatLog.concat(payload)
});
case CHANGE_STATUS:
return Object.assign({}, state, {
statusMessage: payload
});
case CHANGE_USERNAME:
return Object.assign({}, state, {
userName: payload
});
default: return state;
}
};
上面代码中,三种 Action 分别改变 State 的三个属性: - ADD_CHAT:chatLog属性 - CHANGE_STATUS:statusMessage属性 - CHANGE_USERNAME:userName属性 这三个属性之间没有联系,这提示我们可以把 Reducer 函数拆分。不同的函数负责处理不同属性,最终把它们合并成一个大的 Reducer 即可
const chatReducer = (state = defaultState, action = {}) => {
return {
chatLog: chatLog(state.chatLog, action),
statusMessage: statusMessage(state.statusMessage, action),
userName: userName(state.userName, action)
}
};
// 上面代码中,Reducer 函数被拆成了三个小函数,每一个负责生成对应的属性。
// 这样一拆,Reducer 就易读易写多了。而且,这种拆分与 React 应用的结构相吻合:一个 React 根组件由很多子组件构成。这就是说,子组件与子 Reducer 完全可以对应。
Redux 提供了一个combineReducers方法,用于 Reducer 的拆分。你只要定义各个子 Reducer 函数,然后用这个方法,将它们合成一个大的 Reducer:
import { combineReducers } from 'redux';
const chatReducer = combineReducers({
chatLog,
statusMessage,
userName
})
export default todoApp;
// 上面的代码通过combineReducers方法将三个子 Reducer 合并成一个大的函数。
这种写法有一个前提,就是 State 的属性名必须与子 Reducer 同名。如果不同名,就要采用下面的写法:
const reducer = combineReducers({
a: doSomethingWithA,
b: processB,
c: c
})
// 等同于
function reducer(state = {}, action) {
return {
a: doSomethingWithA(state.a, action),
b: processB(state.b, action),
c: c(state.c, action)
}
}
// 总之,combineReducers()做的就是产生一个整体的 Reducer 函数。该函数根据 State 的 key 去执行相应的子 Reducer,并将返回结果合并成一个大的 State 对象。
下面是combineReducer的简单实现。
const combineReducers = reducers => {
return (state = {}, action) => {
return Object.keys(reducers).reduce(
(nextState, key) => {
nextState[key] = reducers[key](state[key], action);
return nextState;
},
{}
)
}}
你可以把所有子 Reducer 放在一个文件里面,然后统一引入:
import { combineReducers } from 'redux'import * as reducers from './reducers'const reducer = combineReducers(reducers)
工作流程
- 首先,用户发出 Action。
store.dispatch(action);
- 然后,Store 自动调用 Reducer,并且传入两个参数:当前 State 和收到的 Action。 Reducer 会返回新的 State 。
let nextState = todoApp(previousState, action);
- State 一旦有变化,Store 就会调用监听函数。
// 设置监听函数
store.subscribe(listener);
- listener可以通过store.getState()得到当前状态。如果使用的是 React,这时可以触发重新渲染 View。
function listerner() {
let newState = store.getState();
component.setState(newState);
}
Provider 组件
- 想在 React 中使用 Redux ,还需要通过 react-redux 提供的 Provider 容器组件把 store 注入到应用中
// index.js
import {Provider} from 'react-redux';
import store from './store';
ReactDOM.render(
<Provider store={store}>
<Router>
<App />
</Router>
</Provider>,
document.getElementById('root')
);
connect 方法
- 有了 connect 方法,我们不需要通过 props 一层层的进行传递, 类似路由中的 withRouter ,我们只需要在用到 store 的组件中,通过 react-redux 提供的 connect 方法,把 store 注入到组件的 props 中就可以使用了
import {connect} from 'react-redux';
class Main extends React.Component {
render() {
console.log(this.props);
}
}
export default connect()(Main);
// 默认情况下,connect 会自动注入 dispatch 方法
注入 state 到 props
export default connect( state => {
return {
items: state.items
}
} )(Main);
connect 方法的第一个参数是一个函数 - 该函数的第一个参数就是 store 中的 state : store.getState() - 该函数的返回值将被解构赋值给 props : this.props.items
redux-chunk
- 这是一个把同步 dispatch 变成异步 dispatch 的中间件
安装
npm i -S redux-thunk
import {createStore, combineReducers, applyMiddleware} from 'redux';
import thunk from 'redux-thunk';
import { composeWithDevTools } from 'redux-devtools-extension';
import user from './reducer/user';
import items from './reducer/items';
import cart from './reducer/cart';
let reducers = combineReducers({
user,
items,
cart
});
const store = createStore(
reducers,
composeWithDevTools(applyMiddleware(
thunk
))
);