尚硅谷React教程(2022加更,B站超火react教程)
react入门
React:用于构建用户界面的 JavaScript 库。由 Facebook
开发且开源。
原生 JavaScript 的痛点:
- 操作 DOM 繁琐、效率低
- 使用 JavaScript 直接操作 DOM,浏览器进行大量重绘重排
- 原生 JavaScript 没有组件化编码方案,代码复用率低
React 的特点:
- 采用组件化模式、声明式编码,提高开发效率和组件复用率
- 在
React Native
中可用 React 语法进行移动端开发 - 使用虚拟 DOM 和 Diffing 算法,减少与真实 DOM 的交互
入门案例
react.development.js
:React 核心库react-dom.development.js
:提供 DOM 操作的 React 扩展库babel.min.js
:解析 JSX 语法,转换为 JS 代码
<!-- 引入react核心库 -->
<script type="text/javascript" src="../js/react.development.js"></script>
<!-- 引入react-dom,用于支持react操作DOM -->
<script type="text/javascript" src="../js/react-dom.development.js"></script>
<!-- 引入babel,用于将jsx转为js -->
<script type="text/javascript" src="../js/babel.min.js"></script>
<!-- 准备容器 -->
<div id="app"></div>
<!-- 此处一定要写babel,表示写的不是 JS,而是 JSX,并且靠 babel 翻译 -->
<script type="text/babel">
// 创建虚拟dom
let VDOM = <div>Hello React</div>
// 渲染
ReactDOM.render(VDOM,document.getElementById('app'))
</script>
创建虚拟DOM的两种方式
两种方式是:js和jsx
- 使用 JS 创建虚拟 DOM 比 JSX 繁琐
- JSX 可以让程序员更加简单地创建虚拟 DOM,相当于语法糖
- 最终 babel 会把 JSX 语法转换为 JS
开发中一般使用jsx的方式,也就是入门案例中使用的方式,会jsx就行
第一种:使用js创建
//1.使用 React 提供的 API 创建虚拟DOM
const VDOM = React.createElement('h1', { id: 'title' }, React.createElement('span', {}, 'Hello,React'))
//2.渲染虚拟DOM到页面
ReactDOM.render(VDOM, document.getElementById('app'))
第二种:使用jsx创建(掌握) 例子见入门案例
虚拟DOM和真实DOM
关于虚拟 DOM:
- 本质是 Object 类型的对象(一般对象)
- 虚拟 DOM 比较“轻”,真实 DOM 比较“重”,因为虚拟 DOM 是 React 内部在用,无需真实 DOM 上那么多的属性。
- 虚拟 DOM 最终会被 React 转化为真实 DOM,呈现在页面上。
//如果VDOM如果有多行,可以用()包起来
const VDOM = (
<h1 id="title">
<span>Hello,React</span>
</h1>
)
ReactDOM.render(VDOM, document.getElementById('app'))
const TDOM = document.getElementById('demo')
console.log('虚拟DOM', VDOM)
console.log('真实DOM', TDOM)
JSX
- 全称:JavaScript XML
- React 定义的类似于 XML 的 JS 扩展语法;本质是
React.createElement()
方法的语法糖 - 作用:简化创建虚拟 DOM
JSX的语法规则
- 定义虚拟 DOM 时,不要写引号
- 标签中混入 JS 表达式需要使用
{}
- 指定类名不用
class
,使用className
- 内联样式,使用
style={ { key: value } }
的形式 - 只能有一个根标签
- 标签必须闭合,单标签结尾必须添加
/
:<input type="text" />
- 标签首字母小写,则把标签转换为 HTML 对应的标签,若没有,则报错
- 标签首字母大写,则渲染对应组件,若没有定义组件,则报错
let title = 'HeLLOjSx'
let VDOM = (
<div title={title.toUpperCase()}>
<div className="info">info</div>
<div style={{fontSize: '26px',color: 'red'}}>style内联样式</div>
</div>
)
ReactDOM.render(VDOM,document.getElementById("app"))
js语句和表达式的区别
- 表达式:一个表达式会产生一个
值
,可以放在任何一个需要值的地方
a
a + b
demo(1)
arr.map()
function test() {}
- 语句(代码):
if(){}
for(){}
switch(){case:xxxx}
let arr = ['vue','react','angular']
let VDOM = (
<ul>
{
arr.map( v => <li>{v}</li> )
}
</ul>
)
ReactDOM.render(VDOM,document.getElementById("app"))
react面向组件编程
函数组件
要点:
- 组件名称首字母必须大写,否则会解析成普通标签导致报错,详见 JSX 语法规则
- 函数需返回一个虚拟 DOM
- 渲染组件时需要使用标签形式,同时标签必须闭合
渲染组件的过程:
- React 解析标签,寻找对应组件
- 发现组件是函数式组件,则调用函数,将返回的虚拟 DOM 转换为真实 DOM ,并渲染到页面中
//1.创建函数式组件
function MyComponent() {
console.log(this)//undefined
return <h2>我是用函数定义的组件(适用于【简单组件】的定义)</h2>
}
//2.渲染组件到页面
ReactDOM.render(<MyComponent />, document.getElementById('app'))
注意: 上面函数组件内部this 是 undefined,因为 babel 编译后开启了严格模式
类组件
要点:
- 使用class关键字来定义组件
- 使用extends来继承React.Component类
- 类中使用render方法来返回虚拟DOM
组件渲染过程:
- React 解析组件标签,寻找组件
- 发现是类式组件,则
new
该类的实例对象,通过实例调用原型上的render
方法 - 将
render
返回的虚拟 DOM 转为真实 DOM ,渲染到页面上·
// 创建类式组件
class MyComponent extends React.Component {
render() {
console.log('render中的this:', this)
return <h2>我是用类定义的组件(适用于【复杂组件】的定义)</h2>
}
}
ReactDOM.render(<MyComponent />, document.getElementById('app'))
组件实例的三大属性
state
state
是组件实例对象最重要的属性,值为对象。又称为状态机,通过更新组件的 state
来更新对应的页面显示。
要点:
- 初始化
state
- React 中事件绑定
this
指向问题setState
修改state
状态constructor
、render
、自定义方法的调用次数
class Sum extends React.Component{
constructor(props){// 调用一次
super(props)
//初始化state
this.state = {count: 0}
//解决this为undefined问题,将原型上面的add方法改变this之后,挂在实例上
this.add = this.add.bind(this)
console.log(this)
}
add(){//这样写是挂在原型上
console.log("add",this)
//更新state的值
this.setState({
count: this.state.count + 1
})
}
render(){//调用 1+N次
return (
// 注意绑定事件的方式:a)驼峰 b)带上{} onClick={this.add}
<div>
sum的值:{ this.state.count }<br/>
<button onClick={this.add}>+1</button>
</div>
)
}
}
ReactDOM.render(<Sum />,document.getElementById("app"))
简化版(推荐)
class Sum extends React.Component{
// = 表示直接挂在组件实例身上
state = {count: 0}
//采用箭头函数 + 赋值语句形式 this指向外面组件的this
add = () => {
console.log("add",this) //this为组件实例
//更新state的值
this.setState({count: this.state.count + 1})
}
render(){//调用 1+N次
return (
<div>
sum的值:{ this.state.count }<br/>
<button onClick={this.add}>+1</button>
</div>
)
}
}
ReactDOM.render(<Sum />,document.getElementById("app"))
props
每个组件对象都有 props
属性,组件标签的属性都保存在 props
中。props
是只读
的,不能修改。
props的基本使用
通过标签属性传值,会包装在组件的props属性上面。
class Person extends React.Component {
render(){
console.log(this.props)//{name: '青阳', age: 18, gender: '男'}
const {name , age, gender} = this.props
return (
<div>
<div>name: {name}</div>
<div>age: {age}</div>
<div>gender: {gender}</div>
</div>
)
}
}
// 类似于标签属性传值 注意:数值要用{}
ReactDOM.render(<Person name="青阳" age={18} gender="男"/>,document.getElementById('app'))
批量传递props
class Person extends React.Component {
render(){
console.log(this.props)
const {name , age, gender} = this.props
return (
<div>
<div>name: {name}</div>
<div>age: {age}</div>
<div>gender: {gender}</div>
</div>
)
}
}
const p = {name: '青阳',age: 19,gender: '男'}
//通过...批量传递props
ReactDOM.render(<Person {...p} />,document.getElementById('app'))
props的类型限制
在 React 15.5
以前,React
身上有一个 PropTypes
属性可直接使用,即 name: React.PropTypes.string.isRequired
,没有把 PropTypes
单独封装为一个模块。
从 React 15.5
开始,把 PropTypes
单独封装为一个模块,需要额外导入使用。
了解即可,用的时候再看,老项目可能使用这种,最新一般使用ts来做类型校验.
<!-- 引入prop-types,用于对组件标签属性进行限制 -->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel">
class Person extends React.Component {
render() {
const { name, age, sex } = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
// 类型和必要性限制
Person.propTypes = {
name: PropTypes.string.isRequired,
sex: PropTypes.string,
age: PropTypes.number,
// 限制 speak 为函数
speak: PropTypes.func,
}
// 指定默认值
Person.defaultProps = {
sex: 'male',
age: 19,
}
ReactDOM.render(<Person name="Vue" sex="male" age={11} speak={speak} />, document.getElementById('test'))
function speak() {
console.log('speaking...')
}
</script>
Person.propTypes
和 Person.defaultProps
可以看作在类身上添加属性,利用 static
关键词就能在类内部进行声明。因此所谓简写只是从类外部移到类内部。
<!-- 引入prop-types,用于对组件标签属性进行限制 -->
<script type="text/javascript" src="../js/prop-types.js"></script>
<script type="text/babel">
class Person extends React.Component {
static propTypes = {
name: PropTypes.string.isRequired,
sex: PropTypes.string,
age: PropTypes.number,
// 限制 speak 为函数
speak: PropTypes.func,
}
static defaultProps = {
sex: 'male',
age: 19,
}
render() {
const { name, age, sex } = this.props
return (
<ul>
<li>姓名:{name}</li>
<li>性别:{sex}</li>
<li>年龄:{age}</li>
</ul>
)
}
}
ReactDOM.render(<Person name="Vue" sex="male" age={11} speak={speak} />, document.getElementById('test'))
function speak() {
console.log('speaking...')
}
</script>
函数组件使用props
由于函数可以传递参数,因此函数式组件可以使用 props
。
function Person(props){
const {name,age,gender} = props
return <div>个人信息 name:{name} , age:{age} , gender:{gender}</div>
}
Person.propTypes = {
age: PropTypes.number.required
}
Person.defaultProps = {
age: 10
}
const p = {name: 'tom',gender: '男'}
ReactDOM.render(<Person {...p}/>,document.getElementById("app"))
类组件的构造器和props
官网文档说明 构造函数一般用在两种情况:
- 通过给
this.state
赋值对象来初始化内部state
- 为事件处理函数绑定实例
constructor(props) {
super(props)
// 初始化 state
this.state = { isHot: true, wind: '微风' }
// 解决 this 指向问题
this.changeWeather = this.changeWeather.bind(this)
}
因此构造器一般都不需要写。如果要在构造器内使用
this.props
才声明构造器,并且需要在最开始调用super(props)
refs
Refs 提供了一种方式,允许我们访问 DOM 节点或在 render 方法中创建的 React 元素。
字符串形式的ref
这种形式已过时,效率不高,官方 不建议使用。
class Demo extends React.Component{
showInput = () => {
//2.使用ref
const { myRef } = this.refs
console.log(myRef) //input标签dom
console.log(myRef.value) //input标签的值
}
render(){
return (
<div>
{/*1.定义ref*/}
<input type="text" ref="myRef" />
<button type="button" onClick={this.showInput}>点击显示输入框内容</button>
</div>
)
}
}
ReactDOM.render(<Demo/>,document.getElementById("app"))
回调形式的ref
class Demo extends React.Component{
showInput = () => {
console.log(this.myInput.value);
}
render(){
return (
<div>
{/*
通过回调将input标签的dom 挂在实例的 myInput属性上
由于是箭头函数,因此 `this` 是 `render` 函数里的 `this` ,即组件实例
*/}
<input type="text" ref={input => this.myInput = input} />
<button type="button" onClick={this.showInput}>点击显示输入框内容</button>
</div>
)
}
}
ReactDOM.render(<Demo/>,document.getElementById("app"))
createRef形式的ref
该方式通过调用 React.createRef
返回一个容器用于存储节点,且一个容器只能存储一个节点。
class Demo extends React.Component{
//1.创建ref对象
myRef = React.createRef()
showInput = () => {
//3.使用ref对象
console.log(this.myRef.current) //input dom对象
console.log(this.myRef.current.value);
}
render(){
return (
<div>
{/* 2.绑定ref对象 */}
<input type="text" ref={this.myRef} />
<button type="button" onClick={this.showInput}>点击显示输入框内容</button>
</div>
)
}
}
ReactDOM.render(<Demo/>,document.getElementById("app"))
事件处理
- React 使用自定义事件,而非原生 DOM 事件,即
onClick、onBlur
:为了更好的兼容性 - React 的事件通过事件委托方式进行处理:为了高效
- 通过
event.target
可获取触发事件的 DOM 元素:勿过度使用ref
当触发事件的元素和需要操作的元素为同一个时,可以不使用 ref
:
class Demo extends React.Component {
showData2 = (event) => {
alert(event.target.value)
}
render() {
return (
<div>
<input onBlur={this.showData2} type="text" placeholder="失去焦点提示数据" />
</div>
)
}
}
受控组件和非受控组件
包含表单的组件分类:
- 非受控组件:现用现取。即需要使用时,再获取节点得到数据
- 受控组件:类似于
Vue 双向绑定
的从视图层绑定到数据层
尽量使用受控组件,因为非受控组件需要使用大量的 ref
。
非受控组件
主要是通过ref来实现
class Login extends React.Component{
handleSubmit = (e) => {
e.preventDefault();
const {username,password} = this
console.log(username.value,",",password.value);
}
render(){
return (
<div>
<form onSubmit={this.handleSubmit}>
账号:<input type="text" ref={c => this.username = c}/><br/>
密码:<input type="password" ref={c => this.password = c}/><br/>
<button type="submit">登录</button>
</form>
</div>
)
}
}
ReactDOM.render(<Login/>,document.getElementById("app"))
受控组件
受控组件基础
通过表单的onChange事件,当内容变化时,将数据更新到state中
class Login extends React.Component{
handleSubmit = e => {
e.preventDefault();//阻止默认提交
const {username,password} = this.state
console.log(username,",",password);
}
setUsername = e => {
this.setState({username: e.target.value})
}
setPassword = e => {
this.setState({password: e.target.value})
}
render(){
return (
<div>
<form onSubmit={this.handleSubmit}>
账号:<input type="text" onChange={this.setUsername}/><br/>
密码:<input type="password" onChange={this.setPassword}/><br/>
<button type="submit">登录</button>
</form>
</div>
)
}
}
ReactDOM.render(<Login/>,document.getElementById("app"))
对上述受控组件的代码进行优化,希望把 saveUsername
和 savePassword
合并为一个函数。有如下两种方案。
- 高阶函数:参数为函数或者返回一个函数的函数,如
Promise、setTimeout、Array.map()
- 函数柯里化:通过函数调用继续返回函数的方式,实现多次接收参数最后统一处理的函数编码形式
受控组件柯里化
class Login extends React.Component{
handleSubmit = (e) => {
e.preventDefault();
const {username,password} = this.state
console.log(username,",",password);
}
setFormData = name => {
return e => {//函数调用返回一个函数,多次接受参数统一处理
this.setState({[name]: e.target.value})
}
}
render(){
return (
<div>
<form onSubmit={this.handleSubmit}>
账号:<input type="text" onChange={this.setFormData('username')}/><br/>
密码:<input type="password" onChange={this.setFormData('password')}/><br/>
<button type="submit">登录</button>
</form>
</div>
)
}
}
ReactDOM.render(<Login/>,document.getElementById("app"))
受控组件不用柯里化
和柯里化差不多,只不过是在onChange里面定义一个箭头函数来接受参数
class Login extends React.Component{
handleSubmit = (e) => {
e.preventDefault();
const {username,password} = this.state
console.log(username,",",password);
}
setFormData = (e,name) => {
this.setState({[name]: e.target.value})
}
render(){
return (
<div>
<form onSubmit={this.handleSubmit}>
账号:<input type="text" onChange={e => this.setFormData(e,'username') }/>
密码:<input type="password" onChange={e => this.setFormData(e,'password') }/>
<button type="submit">登录</button>
</form>
</div>
)
}
}
ReactDOM.render(<Login/>,document.getElementById("app"))
生命周期
生命周期-旧版
初始化阶段:ReactDOM.render()
触发的初次渲染
constructor
componentWillMount
render
componentDidMount
更新阶段
- 父组件重新
render
触发的更新
componentWillReceiveProps
shouldComponentUpdate
:控制组件是否更新的阀门,返回值为布尔值,默认为true
。若返回false
,则后续流程不会进行。componentWillUpdate
render
componentDidUpdate
- 组件内部调用
this.setState()
修改状态
shouldComponentUpdate
componentWillUpdate
render
componentDidUpdate
- 组件内部调用
this.forceUpdate()
强制更新
componentWillUpdate
render
componentDidUpdate
卸载阶段:ReactDOM.unmountComponentAtNode()
触发
componentWillUnmount
生命周期-新版
更改内容:
- 废弃三个钩子:
componentWillMount
、componentWillReceiveProps
、componentWillUpdate
。在新版本中这三个钩子需要加UNSAFE_
前缀才能使用,后续可能会废弃。 - 新增两个钩子(实际场景用得很少):
getDerivedStateFromProps
、getSnapshotBeforeUpdate
static getDerivedStateFromProps(props, state):
- 需使用
static
修饰 - 需返回一个对象更新
state
或返回null
- 适用于如下情况:
state
的值任何时候都取决于props
getSnapshotBeforeUpdate(prevProps, prevState):
- 在组件更新之前获取快照
- 得组件能在发生更改之前从 DOM 中捕获一些信息(如滚动位置)
- 返回值将作为参数传递给
componentDidUpdate()
static getDerivedStateFromProps(props,state){
console.log('getDerivedStateFromProps',props,state);
return null
}
getSnapshotBeforeUpdate(){
console.log('getSnapshotBeforeUpdate');
return 'atguigu'
}
componentDidUpdate(preProps,preState,snapshotValue){
console.log('componentDidUpdate',preProps,preState,snapshotValue);
}
// getSnapshotBeforeUpdate 案例
class NewsList extends React.Component {
state = { newsArr: [] }
componentDidMount() {
setInterval(() => {
//获取原状态
const { newsArr } = this.state
//模拟一条新闻
const news = '新闻' + (newsArr.length + 1)
//更新状态
this.setState({ newsArr: [news, ...newsArr] })
}, 1000)
}
getSnapshotBeforeUpdate() {
return this.refs.list.scrollHeight
}
componentDidUpdate(preProps, preState, height) {
this.refs.list.scrollTop += this.refs.list.scrollHeight - height
}
render() {
return (
<div className="list" ref="list">
{this.state.newsArr.map((n, index) => {
return (
<div key={index} className="news">
{n}
</div>
)
})}
</div>
)
}
}
ReactDOM.render(<NewsList />, document.getElementById('test'))
最重要的三个钩子
render
:初始化渲染和更新渲染componentDidMount
:进行初始化,如开启定时器、发送网络请求、订阅消息componentWillUnmount
:进行收尾,如关闭定时器、取消订阅消息
虚拟DOM和diff算法
key
的作用:
key
是虚拟 DOM 对象的标识,可提高页面更新渲染的效率。
当状态中的数据发生变化时,React 会根据新数据生成新的虚拟 DOM ,接着对新旧虚拟 DOM 进行 Diff 比较,规则如下:
- 旧虚拟 DOM 找到和新虚拟 DOM 相同的 key:
- 若内容没变,直接复用真实 DOM
- 若内容改变,则生成新的真实 DOM ,替换页面中之前的真实 DOM
- 旧虚拟 DOM 未找到和新虚拟 DOM 相同的 key:根据数据创建新的真实 DOM ,渲染到页面
使用 index
作为 key
可能引发的问题:
- 若对数据进行逆序添加、逆序删除等破坏顺序的操作,会进行没有必要的真实 DOM 更新。界面效果没问题,但效率低下。
- 如果结构中包含输入类的 DOM(如 input 输入框) ,则会产生错误的 DOM 更新。
- 若不存在对数据逆序添加、逆序删除等破坏顺序的操作,则没有问题。
// 使用 index 作为 key 引发的问题
class Person extends React.Component {
state = {
persons: [
{ id: 1, name: '小张', age: 18 },
{ id: 2, name: '小李', age: 19 },
],
}
add = () => {
const { persons } = this.state
const p = { id: persons.length + 1, name: '小王', age: 20 }
this.setState({ persons: [p, ...persons] })
}
render() {
return (
<div>
<h2>展示人员信息</h2>
<button onClick={this.add}>添加小王</button>
<h3>使用index作为key</h3>
<ul>
{this.state.persons.map((personObj, index) => {
return (
<li key={index}>
{personObj.name}---{personObj.age}
<input type="text" />
</li>
)
})}
</div>
)
}
}
react脚手架
创建项目
- 全局安装 React 脚手架:
npm i -g create-react-app
- 创建项目:
create-react-app 项目名称
- 进入文件夹:
cd 项目名称
- 启动项目:
npm start
上述方式已经过时,改用下方命令。详见官方说明。
npx create-react-app my-app
cd my-app
npm start
项目结构说明
public
:静态资源文件
manifest.json
:应用加壳(把网页变成安卓/IOS 软件)的配置文件robots.txt
:爬虫协议文件
src
:源码文件
App.test.js
:用于给App
组件做测试,一般不用- index.js :入口文件
reportWebVitals.js
:页面性能分析文件,需要web-vitals
库支持setupTests.js
:组件单元测试文件,需要jest-dom
库支持
index.html
代码分析:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8" />
<!-- %PUBLIC_URL% 代表 public 文件夹的路径 -->
<link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
<!-- 开启理想视口,用于做移动端网页的适配 -->
<meta name="viewport" content="width=device-width, initial-scale=1" />
<!-- 用于配置浏览器页签+地址栏的颜色(仅支持安卓手机浏览器) -->
<meta name="theme-color" content="red" />
<!-- 网站描述 -->
<meta name="description" content="Web site created using create-react-app" />
<!-- 用于指定网页添加到手机主屏幕后的图标 -->
<link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
<!-- 应用加壳时的配置文件 -->
<link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
<title>React App</title>
</head>
<body>
<!-- 若浏览器不支持 js 则展示标签中的内容 -->
<noscript>You need to enable JavaScript to run this app.</noscript>
<div id="root"></div>
</body>
</html>
删除项目中不必要的文件,并且使用jsx文件来定义组件
TodoList 案例总结
- 拆分组件、实现静态组件,注意:
className
、style
的写法 - 动态初始化列表,如何确定将数据放在哪个组件的
state
中?
- 某个组件使用:放在其自身的
state
中 - 某些组件使用:放在他们共同的父组件
state
中,即状态提升
- 关于父子之间通信:
- 父传子:直接通过
props
传递 - 子传父:父组件通过
props
给子组件传递一个函数,子组件调用该函数
// 父组件
class Father extends Component {
state: {
todos: [{ id: '001', name: '吃饭', done: true }],
flag: true,
}
addTodo = (todo) => {
const { todos } = this.state
const newTodos = [todo, ...todos]
this.setState({ todos: newTodos })
}
render() {
return <List todos={this.state.todos} addTodo={this.addTodo} />
}
}
// 子组件
class Son extends Component {
// 由于 addTodo 是箭头函数,this 指向父组件实例对象,因此子组件调用它相当于父组件实例在调用
handleClick = () => {
this.props.addTodo({ id: '002', name: '敲代码', done: false })
}
render() {
return <button onClick={this.handleClick}>添加</button>
}
}
- 注意
defaultChecked
和checked
的区别,类似的还有:defaultValue
和value
- 状态在哪里,操作状态的方法就在哪里
配置代理
方法一:
在 package.json
文件中进行配置:
"proxy": "http://localhost:5000"
- 优点:配置简单,前端请求资源可不加前缀
- 缺点:不能配置多个代理
- 工作方式:当请求了 3000 端口号(本机)不存在的资源时,就会把请求转发给 5000 端口号服务器
方法二:
在 src
目录下创建代理配置文件 setupProxy.js
,进行配置:
const proxy = require('http-proxy-middleware')
module.exports = function (app) {
app.use(
//api1是需要转发的请求(所有带有/api1前缀的请求都会转发给5000)
proxy('/api1', {
//配置转发目标地址(能返回数据的服务器地址)
target: 'http://localhost:5000',
//控制服务器接收到的请求头中host字段的值
/*
changeOrigin设置为true时,服务器收到的请求头中的host为:localhost:5000
changeOrigin设置为false时,服务器收到的请求头中的host为:localhost:3000
changeOrigin默认值为false,但一般将changeOrigin改为true
*/
changeOrigin: true,
//去除请求前缀,保证交给后台服务器的是正常请求地址(必须配置)
pathRewrite: { '^/api1': '' },
}),
proxy('/api2', {
target: 'http://localhost:5001',
changeOrigin: true,
pathRewrite: { '^/api2': '' },
})
)
}
消息订阅发布机制
即 React 中兄弟组件或任意组件之间的通信方式。(类似vue的bus)
- 先订阅,再发布(隔空对话)
- 适用于任意组件间通信
- 要在
componentWillUnmount
钩子中取消订阅
使用的工具库:PubSubJS
npm i pubsub-js --save
基础用法:
import PubSub from 'pubsub-js'
// 发布消息
PubSub.publish('topic', 'hello react')
// 订阅消息 token是一个标识,类似setTimeout会有个返回值作为标识
let token = PubSub.subscribe('topic', (msg, data) => {
console.log(msg, data)
})
// 取消订阅
PubSub.unsubscribe(token)
解构赋值再认识
熟悉一下连续解构赋值、连续解构赋值+重命名
let obj = { a: { b: 1 } }
//传统解构赋值
const { a } = obj
//连续解构赋值
const {
a: { b },
} = obj
//连续解构赋值 + 重命名
const {
a: { b: value },
} = obj
nanoid生成唯一标识
https://gitee.com/mirrors/nanoid
生成唯一标识的一个库
npm install --save nanoid
使用
import {nanoid} from 'nanoid'
console.log(nanoid())
react-router5
路由的理解
何为路由?
- 一个路由是一个映射关系
key
为路径,value
可能是function
或 组件
后端路由:
value
是function
,用于处理客户端的请求- 注册路由:
router.get(path, function(req, res))
- 工作过程:Node 接收到请求,根据路径匹配路由,调用对应函数处理请求,返回响应数据
前端路由:
value
是组件- 注册路由:
<Route path="/test" component={Test}>
- 工作过程:浏览器路径变为
/test
,展示Test
组件
路由的基本使用
安装 react-router-dom
// 安装 5.X 版本路由
npm install react-router-dom@5.2.0 -S
最新默认是安装的6.x版本,这里以 5.x
版本为例展示基本使用,后面再写6.x版本的使用
导航区使用 <Link>
,展示区使用 <Route>
。
Link组件
定义链接,用来替换a标签
to属性:定义点击之后切换的路径
className属性: 定义类名
标签体:定义显示的名称
【例如】
<Link className="list-group-item" to="/about">about</Link>
Route组件
路由组件匹配成功之后的展示区
- path属性:匹配的路径
- component属性:匹配成功后展示的组件
【例如】
<Route path="/about" component={About} />
路由入门案例
- 复制boostrap.css到public/css下,并且在index.html中引入
<App>
的最外侧包裹<BrowserRouter>
或<HashRouter>
:
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById('root')
)
- App.jsx
// App.jsx
import React, { Component } from 'react'
import { Link, Route } from 'react-router-dom'
import Home from './components/Home'
import About from './components/About'
export default class App extends Component {
render() {
return (
<div>
<div className="list-group">
<Link className="list-group-item" to="/about">
About
</Link>
<Link className="list-group-item" to="/home">
Home
</Link>
</div>
<div className="panel-body">
<Route path="/about" component={About} />
<Route path="/home" component={Home} />
</div>
</div>
)
}
}
路由组件和一般组件
存放位置不同
- 一般组件:
components
- 路由组件:
pages
写法不同
- 一般组件:
<Demo/>
- 路由组件:
<Route path="/demo" component={Demo}/>
接收到的 props不同
- 一般组件:标签属性传递
- 路由组件:接收到三个固定的属性(history、location、match)
history:
go: ƒ go(n)
goBack: ƒ goBack()
goForward: ƒ goForward()
push: ƒ push(path, state)
replace: ƒ replace(path, state)
location:
pathname: "/home/message/detail/2/hello"
search: ""
state: undefined
match:
params: {}
path: "/home/message/detail/:id/:title"
url: "/home/message/detail/2/hello"
NavLink组件
NavLink
可以实现路由链接的高亮,通过 activeClassName
指定样式名,默认追加类名为 active
。
<NavLink activeClassName="navLinkActive" to="/about">About</NavLink>
<NavLink activeClassName="navLinkActive" to="/home">Home</NavLink>
封装 NavLink
组件:由于 NavLink
组件中重复的代码太多,因此进行二次封装。
※ 细节点:组件标签的内容会传递到 this.props.children
属性中,反过来通过指定标签的 children
属性可以修改组件标签内容
// MyNavLink 组件
import React, { Component } from 'react'
import { NavLink } from 'react-router-dom'
export default class MyNavLink extends Component {
render() {
// this.props.children 可以取到标签内容,如 About, Home
// 反过来通过指定标签的 children 属性可以修改标签内容
return <NavLink activeClassName="active" className="list-group-item" {...this.props}/>
}
}
<MyNavLink to="/about">About</MyNavLink>
<MyNavLink to="/home">Home</MyNavLink>
Switch 的使用
Switch
可以提高路由匹配效率,如果匹配成功,则不再继续匹配后面的路由,即单一匹配。
<!-- 只会展示 Home 组件 -->
<Switch>
<Route path="/about" component="{About}" />
<Route path="/home" component="{Home}" />
<Route path="/home" component="{Test}" />
</Switch>
多级路径刷新样式丢失
public/index.html
中 引入样式时不写./
写/
(常用)public/index.html
中 引入样式时不写./
写%PUBLIC_URL%
(常用)- 使用
HashRouter
<link rel="stylesheet" href="/css/bootstrap.css" />
<link rel="stylesheet" href="%PUBLIC_URL%/css/bootstrap.css" />
路由的严格匹配与模糊匹配
- 默认使用模糊匹配(
Route
组件的path
必须包含Link
组件要匹配的路径,且顺序一致) - 开启严格匹配:
<Route exact path="/about" component={About}/>
- 严格匹配需要再开,开启可能会导致无法继续匹配二级路由
Redirect (重定向)
一般写在所有路由注册的最下方,当所有路由都无法匹配时,跳转到 Redirect 指定的路由
<Switch>
<Route path="/about" component="{About}" />
<Route path="/home" component="{Home}" />
<Redirect to="/about" />
</Switch>
嵌套路由
- 注册子路由需写上父路由的
path
- 路由的匹配是按照注册路由的顺序进行的
父组件
<!-- 父组件 -->
<MyNavLink to="/about">About</MyNavLink>
<MyNavLink to="/home">Home</MyNavLink>
<Switch>
<Route path="/about" component={About} />
<Route path="/home" component={Home} />
<Redirect to="/about" />
</Switch>
子组件
<ul className="nav nav-tabs">
<li>
<MyNavLink to="/home/news">News</MyNavLink>
</li>
<li>
<MyNavLink to="/home/message">Message</MyNavLink>
</li>
</ul>
<Switch>
<Route path="/home/news" component={News} />
<Route path="/home/message" component={Message} />
<Redirect to="/home/news" />
</Switch>
路由传参
三种方式:params, search, state
参数
三种方式对比:
state
方式当前页面刷新可保留参数,但在新页面打开不能保留。前两种方式由于参数保存在 URL 地址上,因此都能保留参数。params
和search
参数都会变成字符串
<!--1.params方式传参 -->
<Link to='/home/message/detail/tom/21'>params</Link>
<Link to={`/home/message/detail/${item.name}/${item.age}`}>{item.name}</Link>
<!--2.search方式传参 -->
<Link to='/home/message/detail/?name=tom&age=21'>search</Link>
<Link to={`/home/message/detail/?id=${item.name}&title=${item.age}`}>{item.name}</Link>
<!--3.state方式传参 -->
<Link to={{pathname:'/home/message/detail',state: {name: 'tom', age: 21}}}>state</Link>
<!-- params 注册路由 -->
<Route path='/home/message/detail/:name/:age' component={Detail} />
<!-- search 和 state 按正常注册即可 -->
<Route path='/home/message/detail' component={Detail} />
接收参数
//1.params方式接受参数
const { name, age } = this.props.match.params
//2.search方式接受参数
import qs from 'querystring'
const { search } = this.props.location
const { name, age } = qs.parse(search.slice(1))
//3.state方式接受参数
const { name, age } = this.props.location.state
编程式导航
编程式导航是使用路由组件 this.props.history
提供的 API 进行路由跳转:
this.props.history.push(path, state)
this.props.history.replace(path, state)
this.props.history.goForward()
this.props.history.goBack()
this.props.history.go(n)
三种传参方式的编程式导航
// 编程式导航传参
this.props.history.push(`/home/message/detail/${id}/${title}`)
this.props.history.push(`/home/message/detail?id=${id}&title=${title}`)
this.props.history.push(`/home/message/detail`, { id: id, title: title })
withRouter 的使用
withRouter
的作用:加工一般组件,让其拥有路由组件的 API ,如 this.props.history.push
等。
import React, {Component} from 'react'
import {withRouter} from 'react-router-dom'
class Header extends Component {
...
}
export default withRouter(Header) //导出前用withRouter加工一下
BrowserRouter 和 HashRouter
底层原理不一样:
BrowserRouter
使用的是 H5 的 history API,不兼容 IE9 及以下版本。HashRouter
使用的是 URL 的哈希值。
路径表现形式不一样
BrowserRouter
的路径中没有#
,如:localhost:3000/demo/test
HashRouter
的路径包含#,如:localhost:3000/#/demo/test
刷新后对路由 state
参数的影响
BrowserRouter
没有影响,因为state
保存在history
对象中。HashRouter
刷新后会导致路由state
参数的丢失!
备注:HashRouter
可以用于解决一些路径错误相关的问题。
redux
概述
Redux 为何物,类似vue中的vuex
- Redux 是用于做 状态管理 的 JS 库
- 可用于 React、Angular、Vue 等项目中,常用于 React
- 集中式管理 React 应用多个组件共享的状态
何时用 Redux
- 某个组件的状态,需要让其他组件拿到(状态共享)
- 一个组件需要改变另一个组件的状态(通信)
- 使用原则:不到万不得已不要轻易动用
Redux 工作流程
- 组件想操作 Redux 中的状态:把动作类型和数据告诉
Action Creators
Action Creators
创建action
:同步action
是一个普通对象,异步action
是一个函数Store
调用dispatch()
分发action
给Reducers
执行Reducers
接收previousState
、action
两个参数,对状态进行加工后返回新状态Store
调用getState()
把状态传给组件
核心概念
action
表示动作的对象,包含 2 个属性
type
:标识属性,值为字符串,唯一,必须属性data
:数据属性,类型任意,可选属性。
例如:{type: 'increment', data: 2}
reducer
- 用于初始化状态、加工状态
- 根据旧状态和
action
产生新状态 - 是纯函数
纯函数:输入同样的实参,必定得到同样的输出
- 不能改写参数数据
- 不产生副作用,如网络请求、输入输出设备(网络请求不稳定)
- 不能调用
Date.now()
、Math.random()
等不纯方法
store
- Redux 核心对象,内部维护着
state
和reducer
- 核心 API
store.getState()
:获取状态store.dispatch(action)
:分发任务,触发reducer
调用,产生新状态store.subscribe(func)
:注册监听函数,当状态改变自动调用
求和案例
- 安装redux
npm i redux@4.0.5
- App.jsx
import React, { Component } from 'react'
import Count from './components/Count'
export default class App extends Component {
render() {
return (
<div>
<Count />
</div>
)
}
}
- index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import store from './redux/store'
ReactDOM.render(<App />, document.getElementById('root'))
// 状态改变重新渲染 App 组件
store.subscribe(() => {
ReactDOM.render(<App />, document.getElementById('root'))
})
//react18的写法如下
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(<App />)
store.subscribe(()=>{
root.render(<App />)
})
- redux/constant.js 定义常量
// 保存常量值
export const INCREMENT = 'increment'
export const DECREMENT = 'decrement'
- redux/count_reducer.js
import { INCREMENT, DECREMENT } from './constant'
//初始化状态
const initState = 0
export default function countReducer(preState = initState, action) {
const { type, data } = action
switch (type) {
case INCREMENT:
return preState + data
case DECREMENT:
return preState - data
default:
return preState
}
}
- redux/store.js
import { createStore } from 'redux'
//引入为 Count 组件服务的 reducer
import countReducer from './count_reducer'
export default createStore(countReducer)
- count_action.js
import {INCREMENT,DECREMENT} from './constant'
export const createIncrementAction = data => ({type: INCREMENT,data})
export const createdecrementAction = data => ({type: DECREMENT,data})
- components/Count/index.jsx
import React, { Component } from 'react'
import store from '../../redux/store'
import { createIncrementAction, createDecrementAction } from '../../redux/count_action'
export default class Count extends Component {
// 可在组件单独监听 Redux 状态变化
// componentDidMount() {
// store.subscribe(() => {
// this.setState({})
// })
// }
increment = () => {
const { value } = this.selectNumber
// 将 value 转为数值
// 手动写 Action 对象
store.dispatch({ type: 'increment', data: value * 1 })
// 专门创建 Action 对象
store.dispatch(createIncrementAction(value * 1))
}
decrement = () => {
const { value } = this.selectNumber
store.dispatch(createDecrementAction(value * 1))
}
incrementAsync = () => {
const { value } = this.selectNumber
setTimeout(() => {
store.dispatch(createIncrementAction(value * 1))
}, 500)
}
render() {
return (
<div>
<h1>当前求和为:{store.getState()}</h1>
<select ref={(c) => (this.selectNumber = c)}>
<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.incrementAsync}>异步加</button>
</div>
)
}
}
编写步骤总结:
1.创建constant.js定义常量
2.创建count_reducer.js
3.根据count_reducer创建store
4.创建action
5.在组件中使用 store.dispatch(action)更新数据 ,store.getState()获取数据
注意:
- redux 只负责管理状态,状态改变驱动页面展示要自己写
- 可以在
index.js
中统一监听状态变化,也可以在组件中单独监听。注意不能直接this.render()
调用render
函数,要通过this.setState({})
间接调用 reducer
由store
自动触发首次调用,传递的preState
为undefined
,action
为{type: '@@REDUX/ININT_a.5.v.9'}
类似的数据格式,只有type
异步编程
1.延迟的动作不想交给组件,而是
action
2.当操作状态所需数据要靠异步任务返回时,可用异步action
3.创建action
的函数返回一个函数,该函数中写异步任务
4.异步任务完成后,分发一个同步action
操作状态
5.异步action
不是必要的,完全可以在组件中等待异步任务结果返回在分发同步action
安装中间件
npm i redux-thunk@2.3.0
对上面的求和案例进行修改
- store.js
import { createStore, applyMiddleware } from 'redux'
import countReducer from './count_reducer'
import thunk from 'redux-thunk' //引入中间件
//创建store的时候,使用中间件
export default createStore(countReducer, applyMiddleware(thunk))
- count_action.js
import { INCREMENT, DECREMENT } from './constant.js'
export const createIncrementAction = (data) => ({ type: INCREMENT, data })
export const createDecrementAction = (data) => ({ type: DECREMENT, data })
// 异步 action 返回一个函数
export const createIncrementAsyncAction = (data, time) => {
return (dispatch) => {
setTimeout(() => {
dispatch(createIncrementAction(data))
}, time)
}
}
- Count.jsx
incrementAsync = () => {
const { value } = this.selectNumber
store.dispatch(createIncrementAsyncAction(value * 1))
}
整个过程简单理解:store
在分发 action
时,发现返回一个函数,那它知道这是个异步 action
。因此 store
帮忙执行这个函数(ps:使用redux-thunk才会执行,否则报错),同时给这个函数传递 dispatch
方法,等待异步任务完成取到数据后,直接调用 dispatch
方法分发同步 action
。
react-redux
React-Redux 是一个插件库,用于简化 React 中使用 Redux 。
React-Redux 将组件分为两类:
- UI 组件
- 只负责 UI 呈现,不带有业务逻辑
- 通过
props
接收数据 - 不能使用 Redux 的 API
- 保存在
components
文件夹下
- 容器组件
- 负责管理数据和业务逻辑,和 Redux 通信,将结果交给 UI 组件
- 可使用 Redux 的 API
- 保存在
containers
文件夹下
基本使用
要点:
- 装包:
npm i react-redux
connect()()
:创建容器组件mapStateToProps(state)
:映射状态为 UI 组件标签属性,即传递状态mapDispatchToProps(dispatch)
:传递操作状态的方法- 容器组件中的
store
是靠props
传进去,而不是在容器组件中直接引入
【react-redux使用案例】
Count 容器组件
// containers/Count/index.jsx
// Count 容器组件
import CountUI from '../../components/Count'
import { connect } from 'react-redux'
import { createIncrementAction, createDecrementAction, createIncrementAsyncAction } from '../../redux/count_action'
function mapStateToProps(state) {
return {
count: state,
}
}
function mapDispatchToProps(dispatch) {
return {
add: (number) => dispatch(createIncrementAction(number)),
sub: (number) => dispatch(createDecrementAction(number)),
addAsync: (number) => dispatch(createIncrementAsyncAction(number, time)),
}
}
export default connect(mapStateToProps, mapDispatchToProps)(CountUI)
App.jsx
// App.jsx
import React, { Component } from 'react'
import Count from './containers/Count'
import store from './redux/store.js'
export default class App extends Component {
render() {
return (
<div>
<Count store={store} />
</div>
)
}
}
Count UI 组件
// components/Count/index.jsx
// Count UI 组件
increment = () => {
const { value } = this.selectNumber
this.props.add(value * 1)
}
decrement = () => {
const { value } = this.selectNumber
this.props.sub(value * 1)
}
incrementAsync = () => {
const { value } = this.selectNumber
this.props.addAsync(value * 1, 500)
}
优化写法
mapDispatchToProps
可以写成对象形式,React-Redux 底层会帮助自动分发。
// 函数写法
export default connect(
state => ({count:state}),
dispatch => ({
add: number => dispatch(createIncrementAction(number)),
sub: number => dispatch(createDecrementAction(number)),
addAsync: (number,time) => dispatch(createIncrementAsyncAction(number,time)),
})
)(CountUI)
// 对象写法
export default connect(
state => ({ count: state }),
{
add: createIncrementAction,
sub: createDecrementAction,
addAsync: createIncrementAsyncAction,
}
)(CountUI)
React-Redux 容器组件可以自动监测 Redux 状态变化,因此 index.js
不需要手动监听:
//不用写下面这些代码了
store.subscribe(() => {
ReactDOM.render(<App />, document.getElementById('root'))
})
Provider
组件的使用:让所有组件都能获得状态数据,不必一个一个传递
// index.js
import React from 'react'
import ReactDOM from 'react-dom'
import App from './App'
import { Provider } from 'react-redux'
import store from './redux/store'
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
整合容器组件和 UI 组件为一个文件:
import React, { Component } from 'react'
import {createIncrementAction,createDecrementAction} from '../../redux/count_action'
import {connect} from 'react-redux'
// 定义 UI 组件
class Count extends Component {
...
}
// 创建容器组件
export default connect(
state => ({count: state}),
{
add: createIncrementAction,
sub: createDecrementAction
}
)(Count)
多组件数据共享
首先规范化文件结构,容器组件和 UI 组件合为一体后放在 containers
文件夹。redux
文件夹新建 actions
和 reducers
文件夹分别用于存放每个组件对应的 action
和 reducer
。
新建 Person
组件对应的 action
和 reducer
:
// redux/actions/person.js
import { ADD_PERSON } from '../constant.js'
export const createAddPersonAction = (personObj) => ({ type: ADD_PERSON, data: personObj })
// redux/reducers/person.js
import { ADD_PERSON } from '../constant.js'
const initState = [{ id: 'lsfd', name: 'china', age: '9999' }]
export default function personReducer(preState = initState, action) {
const { type, data } = action
switch (type) {
case ADD_PERSON:
return [data, ...preState]
default:
return preState
}
}
关键步骤:在 store.js
中使用 combineReducers()
整合多个 reducer
来创建 store
对象。
这样 Redux 中就以对象的形式存储着每个组件的数据。类似于这样:
{
total: 0,
personList: []
}
// redux/store.js
import { createStore, applyMiddleware, combineReducers } from 'redux'
import countReducer from './reducers/count'
import personReducer from './reducers/person'
import thunk from 'redux-thunk'
const Reducers = combineReducers({ //关键代码!!!
total: countReducer,
personList: personReducer,
})
export default createStore(Reducers, applyMiddleware(thunk))
Person
组件中获取 Redux 保存的状态,包括其他组件的数据。
import React, { Component } from 'react'
import { connect } from 'react-redux'
import { createAddPersonAction } from '../../redux/actions/person'
import { nanoid } from 'nanoid'
class Person extends Component {
addPerson = () => {
const name = this.nameInput.value
const age = this.ageInput.value
const personObj = { id: nanoid(), name, age }
this.props.addPerson(personObj)
this.nameInput.value = ''
this.ageInput.value = ''
}
render() {
return (
<div>
<h2>在Person组件拿到Count组件的数据:{this.props.count}</h2>
<input type="text" ref={(c) => (this.nameInput = c)} placeholder="Please input name" />
<input type="text" ref={(c) => (this.ageInput = c)} placeholder="Please input age" />
<button onClick={this.addPerson}>添加</button>
<ul>
{this.props.personList.map((item) => {
return (
<li key={item.id}>
{item.name} -- {item.age}
</li>
)
})}
</ul>
</div>
)
}
}
export default connect(
// state 是 Redux 保存的状态对象
// 容器组件从 Redux 中取出需要的状态,并传递给 UI 组件
state => ({personList: state.personList, count: state.total}),
{
addPerson: createAddPersonAction
}
)(Person)
一个细节,在 personReducer
中,是按如下方式修改状态的,而没有使用 unshift
方法。在第二种方式,React 会认为状态没有变化从而不会重新渲染页面,因为 preState
保存的是数组地址值,返回的地址和之前的地址是一样的,尽管数组内容发生了改变。而第一种方式返回一个新的数组的地址值,和之前不一样,因此会重新渲染页面。
// 方式一
switch (type) {
case ADD_PERSON:
return [data, ...preState]
default:
return preState
}
// 方式二
switch (type) {
case ADD_PERSON:
preState.unshift(data)
return preState
default:
return preState
}
纯函数
概念:输入同样的参数,返回同样的输出。
约束:
- 不能修改参数数据
- 不产生任何副作用,如网络请求、输入和输出设备
- 不能调用
Date.now()
或Math.random()
等不纯的方法
reducer
的函数必须是纯函数。
redux开发工具
Chrome 安装 Redux DevTools 开发者工具
项目下载依赖包
npm i redux-devtools-extension --save-dev
,最后在
store.js
进行配置:
import { composeWithDevTools } from 'redux-devtools-extension'
...
export default createStore(Reducers, composeWithDevTools(applyMiddleware(thunk)))
// 不需要异步中间件
export default createStore(Reducers, composeWithDevTools())
react扩展
setState 更新状态的两种写法
对象式:setState(stateChange, [callback])
stateChange
为状态改变对象(该对象可以体现出状态的更改)callback
是可选的回调函数, 它在状态更新完毕、界面也更新后才被调用
函数式:setState(updater, [callback])
- updater 为返回 stateChange 对象的函数。
- updater 可以接收到 state 和 props。
说明:
- React 状态更新是异步的。下述代码打印的
count
值是上一次的值,而非更新后的。可在第二个参数回调中获取更新后的状态。
add = () => {
this.setState({ count: this.state.count + 1 })
console.log(this.state.count)
}
add = () => {
this.setState({ count: this.state.count + 1 }, () => {
console.log(this.state.count)
})
}
callback
回调在componentDidMount
钩子之后执行- 对象式写法可以看做函数式写法的语法糖
add = () => {
this.setState((state, props) => {
return { count: state.count + props.step }
})
}
路由组件懒加载 lazyLoad
import React, { Component, lazy, Suspense } from 'react'
import Loading from './Loading'
// 通过 lazy 函数配合 import() 函数动态加载路由组件
// 路由组件代码会被分开打包
const Home = lazy(() => import('./Home'))
const About = lazy(() => import('./About'))
export default Demo extends Component {
render() {
return (
<div>
<h1>Demo 组件</h1>
<Link to="/home">Home</Link>
<Link to="/about">About</Link>
// 通过 <Suspense> 指定在加载得到路由打包文件前显示一个自定义 Loading 界面
<Suspense fallback={Loading}>
<Switch>
<Route path="/home" component={Home}>
<Route path="/about" component={About}>
</Switch>
</Suspense>
</div>
)
}
}
React Hook
Hook 是 React 16.8.0 增加的新特性,让我们能在函数式组件中使用
state
和其他特性
State Hook
State Hook
让函数式组件也可拥有state
状态。- 语法:
const [Xxx, setXxx] = React.useState(initValue)
useState()
参数:状态初始化值;返回值:包含 2 个元素的数组,分别为状态值和状态更新函数setXxx()
的 2 种用法:
setXxx(newValue)
setXxx(value => newValue)
注意!新状态值会覆盖原状态值!因此若有多个状态,只能多次调用 React.useState ,不能使用对象!
import React from 'react'
export default function StateHook() {
const [count, setCount] = React.useState(0);
const add = () => setCount(count + 1);
const sub = () => setCount((count) => count - 1)
return (
<div>
<h2>状态值:{count}</h2>
<button onClick={add}>+1</button>
<button onClick={sub}>-1</button>
</div>
)
}
Ref Hook
Ref Hook
可以在函数式组件存储或查找组件内的标签或其他数据- 语法:
const refContainer = React.useRef()
- 保存标签对象的容器,和
React.createRef()
类似,也是专人专用
import React from 'react'
export default function RefHook() {
const myref = React.useRef()
const submit = () => console.log('myref.current.value', myref.current.value)
return (
<div>
<input type="text" ref={myref}/>
<button onClick={submit}>submit</button>
</div>
)
}
Effect Hook
Effect Hook
让我们能在函数式组件中执行副作用操作(就是模拟生命周期钩子)- 副作用操作:发送 Ajax 请求、定时器、手动更改真实 DOM
Effect Hook
可以模拟三个钩子:componentDidMount
、componentDidUpdate
、componentWillUnmount
React.useEffect
第一个参数return
的函数相当于componentWillUnmount
,若有多个会按顺序执行
// 语法
React.useEffect(() => {
...
return () => {
// 组件卸载前执行,即 componentWillUnmount 钩子
...
}
}, [stateValue])
// 模拟 componentDidMount
// 第二个参数数组为空,表示不监听任何状态的更新
// 因此只有页面首次渲染会执行输出
React.useEffect(() => {
console.log('DidMount')
return () => {
console.log('WillUnmount 1')
}
}, [])
// 模拟全部状态 componentDidUpdate
// 若第二个参数不写,表示监听所有状态的更新
React.useEffect(() => {
console.log('All DidUpdate')
return () => {
console.log('WillUnmount 2')
}
})
// 模拟部分状态 componentDidUpdate
// 第二个参数数组写上状态,表示只监听这些状态的更新
React.useEffect(() => {
console.log('Part DidUpdate')
return () => {
console.log('WillUnmount 3')
}
}, [count, name])
// 若调用 ReactDOM.unmountComponentAtNode(document.getElementById('root'))
// 会输出 WillUnmount 1、2、3
Fragment
Fragment
标签本身不会被渲染成一个真实 DOM 标签,有点像 Vue 的template
。- 用空标签也有相同效果,但是空标签不能传递任何属性,
Fragment
标签可以传递key
属性,遍历时候可用。
import React, { Component, Fragment } from 'react'
export default class Demo extends Component {
render() {
return (
<Fragment key={1}>
<input type="text" />
<input type="text" />
</Fragment>
)
// 或
return (
<>
<input type="text" />
<input type="text" />
</>
)
}
}
Context (了解)
Context 是一种组件间通信方式,常用于祖父组件与子孙组件。实际开发一般不用,一般用 React-Redux
用法说明:
1) 创建Context容器对象:
const XxxContext = React.createContext()
2) 渲染子组时,外面包裹xxxContext.Provider, 通过value属性给后代组件传递数据:
<XxxContext.Provider value={数据}>
子组件
</XxxContext.Provider>
3) 后代组件读取数据:
// 第一种方式:仅适用于类组件
// 声明接收context
static contextType = xxxContext
// 读取context中的value数据
this.context
//第二种方式: 可用于函数组件与类组件
<XxxContext.Consumer>
{
// value就是context中的value数据
value => (
...
)
}
</XxxContext.Consumer>
举个例子:
// context.js
import React from 'react'
export const MyContext = React.createContext()
export const { Provider, Consumer } = MyContext
// A.jsx
import React, { Component } from 'react'
import B from './B.jsx'
import { Provider } from './context.js'
export default class A extends Component {
state = { username: 'tom', age: 18 }
render() {
const { username, age } = this.state
return (
<div>
<h3>A组件</h3>
<h4>用户名是:{username}</h4>
<Provider value={{ username, age }}>
<B />
</Provider>
</div>
)
}
}
// B.jsx
import React, { Component } from 'react'
import C from './C.jsx'
export default class B extends Component {
render() {
return (
<div>
<h3>B组件</h3>
<C />
</div>
)
}
}
// C.jsx
import React, { Component } from 'react'
import { MyContext } from './context.js'
export default class C extends Component {
static contextType = MyContext
render() {
const { username, age } = this.context
return (
<div>
<h3>C组件</h3>
<h4>
从A组件接收到的用户名:{username},年龄:{age}
</h4>
</div>
)
}
}
// C.jsx 为函数式组件
import { Consumer } from './context.js'
export default function C() {
return (
<div>
<h3>我是C组件</h3>
<h4>
从A组件接收到的用户名:
<Consumer>{(value) => `${value.username},年龄是${value.age}`}</Consumer>
</h4>
</div>
)
}
组件渲染优化
问题:
- 只要调用
setState()
,即使没有修改状态,组件也会重新render()
- 只要父组件重新渲染,即使子组件没有使用父组件的状态,也会重新
render()
原因:
shouldComponentUpdate()
钩子默认总是返回true
改进:
- 只有组件的
state
或props
的数据发生改变时才重新渲染
方式:
- 手动重写
shouldComponentUpdate(nextProps, nextState)
的逻辑,只有数据发生改变才返回true
- 使用
PureComponent
,它重写了shouldComponentUpdate()
, 只有state
或props
数据有变化才返回true
TIP
- 它只是进行
state
和props
数据的浅比较, 如果只是数据对象内部数据变了, 返回false
。即对于引用数据类型,比较的是地址引用- 不要直接修改
state
数据, 而是要产生新数据
import React, { PureComponent } from 'react'
class Demo extends PureComponent {
...
addStu = () => {
// 不会渲染
const { stus } = this.state
stus.unshift('小刘')
this.setState({ stus })
// 重新渲染
const { stus } = this.state
this.setState({ stus: ['小刘', ...stus] })
}
...
}
render props (插槽)
类似于 Vue 中的插槽技术
如何向组件内部动态传入带内容的结构(即标签或组件)?
- Vue:插槽技术
- React:
- 使用
children props
:通过组件标签体传入结构 - 使用
render props
:通过组件标签属性传入结构,可携带数据
- 使用
children props
方式:
- 组件标签体内容会存储到
this.props.children
中 - 缺点:A 组件无法向 B 组件传递数据
import React, { Component } from 'react'
export default class Parent extends Component {
render() {
return (
<div>
<h3>Parent组件</h3>
<A>
<B />
</A>
</div>
)
}
}
class A extends Component {
state = { name: 'tom' }
render() {
return (
<div>
<h3>A组件</h3>
{this.props.children}
</div>
)
}
}
class B extends Component {
render() {
return (
<div>
<h3>B组件</h3>
</div>
)
}
}
render props
方式:
<A render={(name) => <B name={name} />} />
{this.props.render(name)}
import React, { Component } from 'react'
export default class Parent extends Component {
render() {
return (
<div>
<h3>Parent组件</h3>
<A render={(name) => <B name={name} />} />
</div>
)
}
}
class A extends Component {
state = { name: 'tom' }
render() {
const { name } = this.state
return (
<div>
<h3>A组件</h3>
{this.props.render(name)}
</div>
)
}
}
class B extends Component {
render() {
return (
<div>
<h3>B组件,{this.props.name}</h3>
</div>
)
}
}
错误边界
TIP
错误边界(Error boundary):用来捕获后代组件错误,渲染出备用页面。
注意:只在生产环境(项目上线)起效
特点:
- 只能捕获后代组件生命周期产生的错误,不能捕获自己组件产生的错误和其他组件在合成事件、定时器中产生的错误
- 简单理解就是只能捕获后代组件生命周期钩子里面代码的错误
import React, { Component } from 'react'
import Child from './Child'
export default class Parent extends Component {
state = {
//用于标识子组件是否产生错误
hasError: '',
}
// 当子组件出现错误,会触发调用,并携带错误信息
static getDerivedStateFromError(error) {
// render 之前触发
// 返回新的 state
return { hasError: error }
}
// 子组件产生错误时调用该钩子
componentDidCatch(error, info) {
console.log(error, info)
console.log('此处统计错误,反馈给服务器')
}
render() {
return (
<div>
<h2>Parent组件</h2>
{this.state.hasError ? <h2>网络不稳定,稍后再试</h2> : <Child />}
</div>
)
}
}
组件通信方式总结
props
- 消息订阅发布:
pubs-sub
- 集中管理:Redux、dva 等
- Context
推荐搭配:
- 父子组件:
props
- 兄弟组件:消息订阅-发布、集中式管理
- 祖孙组件(跨级组件):消息订阅-发布、集中式管理、
conText
(开发用的少,封装插件用的多即 React-Redux)
react-router6
React Router 发布了三个不同的包:
react-router
:路由核心库,提供许多组件、钩子react-router-dom
:包括了react-router
所有内容,同时添加了用于 DOM 的组件,如<BrowserRouter>
react-router-native
:包括了react-router
所有内容,同时添加了用于 ReactNative 的 API,如<NativeRouter>
与 React Router 5.x 版本的区别:
- 内置组件的变化:移除
<Switch/>
,新增<Routes/>
…… - 语法变化:
component={About}
变成element={<About/>}
…… - 新增 hook:
useParams
、useNavigate
、useMatch
…… - 官方明确表示推荐使用函数式组件
基本使用
安装 6 版本的 React Router。
npm install react-router-dom
index.js
文件引入 <BrowserRouter>
。
// 从 react-dom/client 引入 ReactDOM
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
// React 18 的语法发生改变了
ReactDOM.createRoot(document.getElementById('root')).render(
<BrowserRouter>
<App />
</BrowserRouter>
)
App.js
设置路由链接和注册路由。<Route caseSensitive>
属性用于指定匹配时是否区分大小写(默认为 false)
import { NavLink, Routes, Route } from 'react-router-dom'
import About from './components/About'
import Home from './components/Home'
// React 18 默认使用函数式组件了
export default function App() {
return (
<div>
<NavLink to="/about">About</NavLink>
<NavLink to="/home">Home</NavLink>
<hr />
<Routes>
<Route path="/about" element={<About />}></Route>
<Route path="/home" element={<Home />}></Route>
</Routes>
</div>
)
}
BrowserRouter
<BrowserRouter>
用于包裹整个应用。
import ReactDOM from 'react-dom/client'
import { BrowserRouter } from 'react-router-dom'
import App from './App'
ReactDOM.createRoot(document.getElementById('root')).render(
<BrowserRouter>
<App />
</BrowserRouter>
)
HashRouter
作用与 <BrowserRouter>
一样,但 <HashRouter>
修改的是地址栏的 hash 值。
6.x 版本中 <HashRouter>
、<BrowserRouter>
的用法与 5.x 相同。
Routes
6 版本中移出了 <Switch>
,引入了新的替代者:<Routes>
。
<Routes>
和 <Route>
要配合使用,且必须要用 <Routes>
包裹 <Route>
。
Navigate-重定向
只要 <Navigate>
组件被渲染,就会修改路径,切换视图。可用于路由重定向
。
replace
属性用于控制跳转模式(push 或 replace,默认是 push)。
import { NavLink, Routes, Route, Navigate } from 'react-router-dom'
import About from './components/About'
import Home from './components/Home'
export default function App() {
return (
<div>
<NavLink to="/about">About</NavLink>
<NavLink to="/home">Home</NavLink>
<hr />
<Routes>
<Route path="/about" element={<About />}></Route>
<Route path="/home" element={<Home />}></Route>
<Route path="/" element={<Navigate to="/about" />}></Route>
</Routes>
</div>
)
}
import React, { useState } from 'react'
import { Navigate } from 'react-router-dom'
export default function Home() {
const [sum, setSum] = useState(1)
return (
<div>
<h1>Home</h1>
{/* 根据sum的值决定是否切换视图 */}
{sum === 1 ? <h4>sum的值为{sum}</h4> : <Navigate to="/about" replace={true} />}
<button onClick={() => setSum(2)}>将sum变为 2</button>
</div>
)
}
useRoutes() 路由表
路由规则可以单独抽出一个模块。
// 路由表
// routes/index.js
import { Navigate } from 'react-router-dom'
import About from '../components/About'
import Home from '../components/Home'
const routes = [
{
path: '/about',
element: <About />
},
{
path: '/home',
element: <Home />
},
{
path: '/',
element: <Navigate to="/home" />
}
]
export default routes
// 引入路由表
// App.js
import { NavLink, useRoutes } from 'react-router-dom'
import routes from './routes'
export default function App() {
// 生成路由规则
const element = useRoutes(routes)
return (
<div>
<NavLink to="/about">About</NavLink>
<NavLink to="/home">Home</NavLink>
<hr />
{element}
</div>
)
}
Outlet 嵌套路由
- 嵌套路由中,需要使用
<Outlet>
设置子路由的路由出口,即在何处渲染子路由。 - 设置二级路由链接时,可以是
to="news"
、to="./news"
,但不能是to="/news"
。
不使用路由表的嵌套路由:
// App.js
export default function App() {
return (
<div>
<NavLink to="about">About</NavLink>
<NavLink to="home">Home</NavLink>
<hr />
<Routes>
<Route path="about" element={<About />} />
<Route path="home" element={<Home />}>
<Route path="news" element={<News />} />
<Route path="message" element={<Message />} />
</Route>
<Route path="/" element={<Navigate to="about" />} />
</Routes>
</div>
)
}
使用路由表的嵌套路由:
// 路由表
const routes = [
{
path: '/about',
element: <About />,
},
{
path: '/home',
element: <Home />,
// 定义二级路由,注意不要加 /
children: [
{
path: 'news',
element: <News />,
},
{
path: 'message',
element: <Message />,
},
],
},
{
path: '/',
element: <Navigate to="/about" />,
},
]
// Home 子组件
import React, { Fragment } from 'react'
import { NavLink, Outlet } from 'react-router-dom'
export default function Hello() {
return (
<Fragment>
<h2>I am Hello!</h2>
{/* 子路由链接 */}
<NavLink to="news">News</NavLink>
<NavLink to="message">Message</NavLink>
<hr />
{/* 子路由出口 */}
<Outlet></Outlet>
</Fragment>
)
}
NavLink 路由高亮
实现导航的 “高亮” 效果,6 版本不能直接指定高亮类名,需要通过函数返回。该函数传入一个对象,类似于 {isActive: true}
标识路由是否被激活。
默认情况下,当 Home
的子组件匹配成功,Home
的导航也会高亮,end
属性可移除该效果。
// NavLink 默认类名是 active,下面是指定自定义类名
//自定义样式
<NavLink
to="login"
className={({ isActive }) => {
console.log('home', isActive)
return isActive ? 'base MyClass' : 'base'
}}
>about</NavLink>
// 默认情况下,当 Home 的子组件匹配成功,Home 的导航也会高亮
// 当 NavLink 上添加了 end 属性后,若 Home 的子组件匹配成功,则 Home 的导航没有高亮效果。
<NavLink to="home" end >home</NavLink>
路由传参
传递 params参数
注册路由时声明 params
参数,和 React Router 5 一样。
export default function App() {
return (
<div>
<Routes>
<Route path="home" element={<Home />}>
<Route path="message" element={<Message />}>
<Route path="detail/:id/:name/:age" element={<Detail />} />
</Route>
</Route>
</Routes>
</div>
)
}
传递参数
<Link to={`detail/${item.id}/${item.name}/${item.age}`}>{item.name}</Link>
使用 useParams()
接收 params
参数。useParams()
返回一个参数对象。
import React from 'react'
import { useParams, useMatch } from 'react-router-dom'
export default function Detail() {
// 解构赋值
const { id, name, age } = useParams()
return (
<div>
<li>id:{id}</li>
<li>name:{name}</li>
<li>age:{age}</li>
</div>
)
}
传递 search参数
和 5 版本一样,正常注册路由即可。
<Route path="detail" element={<Detail />} />
传递参数。
<Link to={`detail?id=${item.id}&name=${item.name}&age=${item.age}`}>{item.name}</Link>
使用 useSearchParams()
接收参数。该方法返回一个包含两个元素的数组:search
参数和修改 search
参数的方法。
import React from 'react'
import { useSearchParams } from 'react-router-dom'
export default function Detail() {
// 数组的解构赋值
const [searchParams, setSearchParams] = useSearchParams()
// 需要调用 get() 方法获取对应的参数值
const id = searchParams.get('id')
const name = searchParams.get('name')
const age = searchParams.get('age')
function change() {
setSearchParams('id=666&name=Lily&age=888')
}
return (
<div>
<li>id:{id}</li>
<li>name:{name}</li>
<li>age:{age}</li>
<button onClick={change}>Change search params</button>
</div>
)
}
传递 state 参数
和 5 版本一样,正常注册路由即可。
<Route path="detail" element={<Detail />} />
传递参数,这里相较于 5 版本有所不同,不必写到一个对象里面。
<Link to="detail" state={{ id: item.id, name: item.name, age: item.age }}>
{item.name}
</Link>
使用 useLocation()
接收参数。该方法返回路由组件的 location
对象,就是 5 版本路由组件的 location
属性,其中包含 state
参数。
import { useLocation } from 'react-router-dom'
export default function Detail() {
// 连续解构赋值
const {
state: { id, name, age },
} = useLocation()
return (
<div>
<li>id:{id}</li>
<li>name:{name}</li>
<li>age:{age}</li>
</div>
)
}
useNavigate() 编程式路由导航
useNavigate()
返回一个函数,调用该函数实现编程式路由导航。函数有两种参数传递方式。
第一种方式传递两个参数:路由和相关参数。参数只能设置 replace
和 state
。想要传递 params
和 search
参数直接在路由带上。
第二种方式传递数字,代表前进或后退几步。
import React, { useState } from 'react'
import { Outlet, useNavigate } from 'react-router-dom'
export default function Message() {
const [list] = useState([
{ id: 1, name: 'Bruce', age: 33 },
{ id: 2, name: 'You', age: 3 },
{ id: 3, name: 'React', age: 333 },
])
const navigate = useNavigate()
function showDetail(item) {
navigate('detail', {
replace: true,
state: {
id: item.id,
name: item.name,
age: item.age,
},
})
}
function back() {
navigate(1)
}
function forward() {
navigate(-1)
}
return (
<div>
<ul>
{list.map((item) => {
return (
<li key={item.id}>
<button onClick={() => showDetail(item)}>查看详情</button>
<button onClick={back}>后退</button>
<button onClick={forward}>前进</button>
</li>
)
})}
</ul>
<Outlet></Outlet>
</div>
)
}
Other Hooks(了解)
useMatch()
返回路由组件的 match
数据,即 5 版本中的 match
属性。
必须传入该组件对应的路由规则才能正确返回,否则返回 null
。
// Detail.jsx
import { useParams, useMatch } from 'react-router-dom'
export default function Detail() {
const match = useMatch('home/message/detail/:id/:name/:age')
console.log(match)
return (
<div>
<li>id</li>
</div>
)
}
/*
params: {id: '1', name: 'tom', age: '33'}
pathname: "/home/message/detail/1/tom/33"
pathnameBase: "/home/message/detail/1/tom/33"
pattern: {path: 'home/message/detail/:id/:name/:age', caseSensitive: false, end: true}
*/
useInRouterContext
如果组件在 <Router>
的上下文中呈现,则 useInRouterContext
钩子返回 true
,否则返回 false
。即组件有没有被包裹在 <BrowserRouter>
这种东西里面。这个对第三方组件库有用处。
useNavigationType
返回当前的导航类型(用户是如何来到当前页面的)。
返回值:POP
、PUSH
、REPLACE
。
POP
是指在浏览器中直接打开了这个路由组件(刷新页面
)。
useOutlet
用来呈现当前组件中渲染的嵌套路由。
const result = useOutlet()
console.log(result)
// 如果嵌套路由没有挂载,则返回 null
// 如果嵌套路由已经挂载,则展示嵌套的路由对象
useResolvedPath
给定一个 URL 值,解析其中的:path
、search
、hash
值。
const res = useResolvedPath('/user?id=001&name=tom#React')
console.log(res)
/*
hash: '#React'
pathname: '/user'
search: '?id=001&name=tom'
*/