React笔记
React引入
什么是React?
与vue的作用相同,React是前端框架,用来代替DOM来构建用户界面
React的特点
- 虚拟DOM
- 程序员-->虚拟DOM-->DOM
- 好处:
- React比DOM简单,学习成本低
- React会帮助我们解决浏览器兼容问题,减少开发成本
- 使用虚拟DOM的性能会被直接操作DOM好
- 声明式
- 基于组件
- 支持服务器端渲染
- 快速、简单、易学
HelloReact
使用js脚本引入React
- react.development.js
react 是react核心库,只要使用react就必须要引入
下载地址:https://unpkg.com/react@18.0.0/umd/react.development.js- react-dom.development.js
react-dom 是react的dom包,使用react开发web应用时必须引入
下载地址:https://unpkg.com/react-dom@18.0.0/umd/react-dom.development.js<script src="./script/react.development.js"></script>
<script src="./script/react-dom.development.js"></script>代码
<body>
<div id="root"></div>
<script>
//创建一个React元素
const div=React.createElement("div",{},"hello React")
//获取根元素对应的React元素
const root=ReactDOM.createRoot(document.getElementById("root"))
//渲染进页面
root.render(div)
</script>
</body>
三个API
React.createElement()
- 用来创建一个React元素
- 参数:
- 元素的名称(html标签要小写)
- 标签的属性:
- 添加类属性要用className
- 在设置事件时,属性名需要修改为驼峰命名法,属性值为一个函数
- 元素的内容(可以是子元素),多个元素用逗号隔开
- 注意点:
- React元素最终会通过虚拟DOM转换为真实的DOM元素
- React元素一旦创建无法修改,只能通过创建新的元素进行替换
<div id="root"></div>
<script>
const btn=React.createElement("button",{
id:"btn",
className:"my-button",
type:"button",
onClick:()=>{alert('123')}
},"点我一下")
const div=React.createElement("div",{},btn)
const root=ReactDOM.createRoot(document.getElementById("root"))
root.render(div)
</script>
- 修改React元素后,必须重新对根元素进行渲染
- 当调用render渲染页面时,React会自动比较两次渲染的元素,只在真实DOM中更新发生变化的部分,没发生变化的保持不变
- React与vue都采用diff算法,通过修改最少的DOM来实现最好的性能
<button id="btn">修改React元素</button>
<div id="root"></div>
<script>
const button = React.createElement("button", {
className: "my-button",
type: "button",
onClick: () => { alert('123') }
}, "点我一下")
const div = React.createElement("div", {}, "啦啦啦啦啦啦啦啦", button)
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(div)
const btn = document.getElementById("btn")
btn.onclick = () => {
const newButton = React.createElement("button", {
className: "my-button",
type: "button",
onClick: () => { alert('123') }
}, "点我一万下")
const div = React.createElement("div", {}, "啦啦啦啦啦啦啦啦", newButton)
root.render(div)
}
</script>
只有button发生改变
ReactDOM.createRoot(DOM元素)
获取根元素,根元素就是React元素要插入的位置
ReactDOM.render(div, document.getElementById('root')); // 老版本的React中使用方法
root.render()
- 用来将React元素渲染到根元素中
- 根元素中所有的内容都会被删除,被React元素所替换
- 当重复调用render()时,React会将两次的渲染结果进行比较,
- 它会确保只修改那些发生变化的元素,对DOM做最少的修改
JSX
JSX 是 JavaScript 的语法扩展,JSX 使得我们可以以类似于 HTML 的形式去使用 JS。
JSX便是React中声明式编程的体现方式。声明式编程,简单理解就是以结果为导向的编程。
使用JSX将我们所期望的网页结构编写出来,然后React再根据JSX自动生成JS代码。
所以我们所编写的JSX代码,最终都会转换为以调用React.createElement()创建元素的代码。
JSX是React.createElement()的语法糖,简化了React代码的编写。
引入babel
- babel用来帮助React翻译JSX代码
- babel下载地址:https://unpkg.com/babel-standalone@6/babel.min.js
- 引入babel js文件
- script标签内的type="text/babel",设置js代码被babel处理
JSX语法
- JSX不是字符串,不要加引号
- JSX中html标签应该小写,React组件应该大写开头
- JSX中有且只有一个根标签
- JSX的标签必须正确结束(自结束标签必须写/)
- 在JSX中可以使用{}嵌入表达式
- 有值的语句的就是表达式
- 如果表达式是空值、布尔值、undefined,将不会显示【vue的模板会显示布尔值,其他一致】
- 在JSX中,属性可以直接在标签中设置
注意:
- class需要使用className代替
- 事件用驼峰命名法
- style中必须使用对象设置
- style={{backgroundColor:'red'}}
<div id="root"></div>
<script type="text/babel">
const div = <div
id="box"
className="my-box"
onClick={()=>{
alert(123)
}}
style={{width:"100px",height:"100px",backgroundColor:"red"}}
></div>
const root=ReactDOM.createRoot(document.getElementById("root"))
root.render(div)
</script>
渲染列表
jsx会自动解构数组,将数组中的元素在网页中显示
<div id="root"></div>
<script type="text/babel">
const arr=["啊伟","啊强","啊珍"]
const list=<div>{arr}</div>
const root=ReactDOM.createRoot(document.getElementById("root"))
root.render(list)
</script>使用for循环
<div id="root"></div>
<script type="text/babel">
const data=["啊伟","啊强","啊珍"]
const arr=[]
for(let item of data){
arr.push(<li>{item}</li>)
}
const list=<ul>{arr}</ul>
const root=ReactDOM.createRoot(document.getElementById("root"))
root.render(list)
</script>使用map
<div id="root"></div>
<script type="text/babel">
const data = ["啊伟", "啊强", "啊珍"]
const list = <ul>{data.map(item=><li>{item}</li>)}</ul>
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(list)
</script>
虚拟DOM
在React我们操作的元素被称为React元素,并不是真正的原生DOM元素,
React通过虚拟DOM 将React元素和原生DOM进行映射,
虽然操作的React元素,但是这些操作最终都会在真实DOM中体现出来
- 虚拟DOM的好处:
- 降低API复杂度
- 解决兼容问题
- 提升性能(减少DOM的不必要操作)
每当我们调用root.render()时,页面就会发生重新渲染
React会通过diffing算法,将新的元素和旧的元素进行比较
通过比较找到发生变化的元素,并且只对变化的元素进行修改,没有发生的变化不予处理
比较两次数据时,React会先比较父元素,父元素如果不同,直接所有元素全部替换
父元素一致,在去逐个比较子元素,直到找到所有发生变化的元素为止
注意
当我们在JSX中显示数组中,数组中每一个元素都需要设置一个唯一key,否则控制台会显示红色警告
重新渲染页面时,React默认会按照顺序依次比较对应的元素(就地更新策略),如果列表的顺序永远不会发生变化,这么做当然没有问题,但是如果列表的顺序会发生变化,这可能会导致性能问题出现
为了解决这个问题,React为列表设计了一个key属性,key的作用相当于ID,只是无法在页面中查看,当设置key以后,再比较元素时,就会比较相同key的元素,而不是按照顺序进行比较
在渲染一个列表时,通常会给列表项设置一个唯一的key来避免上述问题(这个key在当前列表中唯一即可)
注意:
- 开发中一般会采用数据的id作为key
- 尽量不要使用元素的index作为key
索引会跟着元素顺序的改变而改变,所以使用索引做key跟没有key是一样的
唯一的不同就是,控制台的警告没了
当元素的顺序不会发生变化时,用索引做key也没有什么问题const list = <ul>{data.map(item=> <li key={item}>{item}</li>)}</ul>
创建React项目
手动创建
项目结构
- 常规的React项目需要使用npm(或yarn)作为包管理器来对项目进行管理。并且React官方为了方便我们的开发,为我们提供react-scripts包。包中提供了项目开发中的大部分依赖,大大的简化了项目的开发。
- 服务器使用的动态资源放在public下,静态资源放在src目录下的assets,src目录的内容服务器无法直接通过路径访问,要采用模块的方式导入才能使用
根目录
- public
- index.html(添加标签 <div id="root"></div>)
- src
- App.js
- index.js操作
- 进入项目所在目录,并执行命令:
npm init -y
- 安装项目依赖:
npm install react react-dom react-scripts -S
- 打包
npx react-scripts build
打包项目,初次会询问是否加入一些默认配置(例如提高代码兼容的babel工具),选y- 打包后的index.html中的src路径默认是根目录开头,需手动修改为相对路径
- 在开发阶段使用打包来查看项目效果是不方便的,我们只有在项目开发完成后才对项目打包
- 运行
npx react-scripts start
启动内置测试服务器,实时对代码进行编译,初次也会询问- ctrl+c终止批处理操作
- 进入项目所在目录,并执行命令:
描述文件
"scripts": {
"start": "react-scripts start",
"build": "react-scripts build"
},
/*语法检查*/
"eslintConfig": {
"extends": [
"react-app"
]
}
自动创建
- 使用步骤
- 打开命令行
- 进入到项目所在目录
- 使用如下命令:
npx create-react-app 项目名
- 具体介绍
React组件
在React中网页被拆分为了一个一个组件,组件是独立可复用的代码片段。
具体来说,组件可能是页面中的一个按钮,一个对话框,一个弹出层等。
React中定义组件的方式有两种:基于函数的组件和基于类的组件。
// React组件可以直接通过JSX渲染
root.render(<组件名/>);
vscode:输入rfc快捷生成函数组件模板,输入rcc生成类组件模板
函数组件
- 函数组件就是一个返回JSX的普通函数
- 组件的首字母必须是大写
const App=()=>{
return <div>hello React</div>
}
export default App
类组件
类组件必须要继承React.Component
类组件中,必须添加一个render()方法,且方法的返回值要是一个jsx
相较于函数组件,类组件的编写要麻烦一下,但是他俩的功能是一样的
import React from "react";//import {Component} from "react
class App extends React.Component{//extendes Component
render(){
return <div>我是一个类组件</div>
}
}
export default App
React事件
在React中事件需要通过元素的属性来设置,和原生JS不同,在React中事件的属性需要使用驼峰命名法:
- onclick -> onClick
- onchange -> onChange
属性值不能直接执行代码,而是需要一个回调函数:
onclick="alert(123)"
onClick={()=>{alert(123)}}事件对象
- React事件中同样会传递事件对象,可以在响应函数中定义参数来接收事件对象
- React中的事件对象同样不是原生的事件对象,是经过React包装后的事件对象
- 由于对象进行过包装,所以使用过程中我们无需再去考虑兼容性问题
- 这一点与vue不同,vue传递的是原生的事件对象
阻止事件传导与默认行为
- 在React中,无法通过return false取消默认行为
- event.preventDefault(); // 取消默认行为
- event.stopPropagation(); // 取消事件的冒泡
代码示例:
export default function App() {
function MyClick(event){
alert("123")
event.preventDefault()
event.stopPropagation()
}
return <div>
<button onClick={MyClick}>点我一下</button>
<div onClick={()=>{alert("div被点了")}}><a href="#" onClick={MyClick}>click me</a></div>
</div>
}
函数组件使用
props
- 如果将组件中的数据全部写死,将会导致组件无法动态设置,不具有使用价值。我们希望组件数据可以由外部设置,在组件间,父组件可以通过props(属性)向子组件传递数据
- 在函数组件中,属性就相当于是函数的参数,可以通过参数来访问。
- 可以在函数组件的形参中定义一个props,props指向的是一个对象,它包含了父组件中传递的所有参数
- 父组件
<子组件 属性名1=XXX 属性名2=XXX/>
- 子组件
export default function 子组件名(props){
console.log(props)//{属性名1:XXX,属性名2:XXX}
} - props是只读的不能修改
state
为什么需要state
在React中,当组件渲染完毕后,再修改组件中的变量,不会使组件重新渲染
要使得组件可以收到变量的影响,必须在变量修改后对组件进行重新渲染
这里我们就需要一个特殊变量,当这个变量被修改使,组件会自动重新渲染
state和props类似,都是一种存储属性的方式,但是不同点在于state只属于当前组件,其他组件无法访问。
并且state是可变的,当其发生变化后组件会自动重新渲染,以使变化在页面中呈现。
具体介绍
state实际上就是一个变量,只是这个变量在React中进行了注册,React会监控这个变量的变化,
当state发生变化时,会自动触发组件的重新渲染,使得我们的修改可以在页面中呈现出来
在函数组件中,我们需要通过钩子函数,获取state
- 使用钩子 useState() 来创建state
- 它需要一个值作为参数,这个值就是state的初始值
- 该函数会返回一个数组
- 数组中第一个元素,是初始值
- 初始值只用来显示数据,直接修改不会触发组件的重新渲染(初始值变了并不会改变state的值)
- 数组中的第二个元素,是一个函数,通常会命名为setXxx
- 这个函数用来修改state,调用其修改state后会触发组件的重新渲染,并且使用函数的实参作为新的state值
- 假如函数实参与原本的初始值一致,那仍不会重新渲染
- 数组中第一个元素,是初始值
import {useState} from "react"
import "./App.css"
export default function App() {
let[count,setCount]=useState(0)
return <div className="app">
<h1>{count}</h1>
<button onClick={()=>{setCount(count++)}}>+</button>
<button onClick={()=>{setCount(count--)}}>-</button>
</div>
}
注意
只有state值本身发生变化,组件才会重新渲染
- 直接修改初始值,并不会改变state
- 当state的值是一个对象时,修改时是使用新的对象去替换已有对象
import {useState} from "react"
import "./App.css"
export default function App() {
let[obj,setObj]=useState({name:"阿伟",age:18,gender:"男"})
return <div className="app">
<h1>{obj.name}---{obj.age}---{obj.gender}</h1>
<button onClick={()=>{setObj({...obj,name:"阿强"})}}>改变名字</button>
</div>
}- 当通过setState去修改一个state时,并不表示修改当前的state,它修改的是组件下一次渲染时的state值
- setState()会触发组件的重新渲染,它是异步的
import {useState} from "react"
import "./App.css"
export default function App() {
let[count,setCount]=useState(0)
console.log("我被渲染了!")
function changeCount(){
setCount(1)
setCount(2)
setCount(3)
setCount(4)
setCount(5)
}
return <div className="app">
<h1>{count}</h1>
<button onClick={()=>{changeCount()}}>多次修改</button>
</div>
}- 所以当调用setState()需要用旧state的值时,一定要注意有可能出现计算错误的情况,例如:
import {useState} from "react"
import "./App.css"
export default function App() {
let[count,setCount]=useState(0)
function changeCount(){
setTimeout(()=>{
setCount(count++)
},1000)
}
return <div className="app">
<h1>{count}</h1>
<button onClick={()=>{changeCount()}}>+</button>
</div>
}快速连续点击两次,在经历了1s和1.05s后两个事件入队,
此时由于第一次点击还未入栈执行,第二次拿到的count仍为0,
导致两次点击的结果一致,都为1- 为了避免这种情况,可以通过为setState()传递回调函数的形式来修改state值
setCount(preCount=>preCount++)
//setState()中回调函数的返回值将会成为新的state值
//回调函数执行时,React会将最新的state值作为参数传递组件重新渲染本质上是重新调用render方法
- 函数组件是一个函数,调用render方法就是调用函数组件本身
- 我们可以在函数组件里写一个输出语句,能直观地知道组件渲染了几次
获取DOM对象
在React中可以传统地利用document来对DOM进行操作
直接从React处获取DOM对象
- 创建一个存储DOM对象的容器
- 使用 useRef() 钩子函数
- 钩子函数的注意事项:
- React中的钩子函数只能用于函数组件或自定义钩子
- 钩子函数只能直接在函数组件中调用
- 将容器设置为想要获取DOM对象元素的ref属性
<h1 ref={xxx}>....</h1>
<!--React会自动将当前元素的DOM对象,设置为容器current属性>
useRef()
- 返回值是一个普通的js对象
- {current:undefined}
- 所以我们直接创建一个js对象,也可以代替useRef()
- 区别:
- 我们创建的对象,组件每次重新渲染都会创建一个新对象
- useRef()创建的对象,可以确保每次渲染获取到的都是同一个对象
- 当你需要一个对象不会因为组件的重新渲染而改变时,就可以使用useRef()
代码
import {useRef} from "react"
export default function App() {
const h1Ref=useRef()
return <div className="app">
<h1 id="title" ref={h1Ref}>标题</h1>
<button onClick={()=>{alert(h1Ref.current)}}>获取DOM元素</button>
</div>
}
类组件使用
prop
类组件的props是存储到类的实例对象中,可以直接通过实例对象访问
this.props
传数据语法一致,接收数据时props从实参变为类的属性
root.render(<App name={"阿伟"} age={20}/>)
import React, { Component } from 'react'
export default class App extends Component {
render() {
return (
<div className='app'>
<h1>{this.props.name}</h1>
<h1>{this.props.age}</h1>
</div>
)
}
}
state
类组件中state统一存储到了实例对象的state属性中,可以通过
this.state
来访问通过
this.setState()
对其进行修改- 参数可以为对象或者回调函数
- 当我们通过this.setState()修改state时,React只会修改设置了的属性,其他未修改的属性会保留
- 但这仅限于state中的直接属性
import React, { Component } from 'react'
export default class App extends Component {
state={
count:0,
obj:{name:"阿伟",age:18}
}
render() {
return (
<div className='app'>
<h1>{this.state.count}</h1>
<button onClick={()=>{this.setState(prevState=>{return {count:prevState.count+1}})}}>加1</button>
<hr/>
<h2>{this.state.obj.name}---{this.state.obj.age}</h2>
<button onClick={()=>{this.setState({obj:{name:"啊强"}})}}>改变名字</button>
</div>
)
}
}
获取DOM对象
1.创建一个属性,用来存储DOM对象
divRef = React.createRef();//除了创建方式不同,其他都与函数组件相同
2.将这个属性设置为指定元素的ref值
方法
- 函数组件中,响应函数直接以函数的形式定义在组件中,但是在类组件中,响应函数是以类的方法来定义
- 为了省事,在类组件中响应函数都应该以箭头函数的形式定义
- 示例中不采用箭头函数的形式声明方法,把该方法绑定给响应函数,由于在调用时没有this所以this为undefined,导致代码报错
- 如果采用箭头函数的方式,箭头函数没有自己的this,this等于外层作用域中的this,那么就可以避免这种情况发生
练习-学习计划表
Card组件
多个组件有共同的样式,我们可以把这些样式写在一个组件里,并把这些组件写在UI文件夹里【一般这个文件夹拿来放和核心数据和业务无关的组件】
import React from 'react'
import "./Card.css"
export default function Card(props) {
return (
<div className={`card ${props.className}`}>{props.children}</div>
)
}
<Card className="logs">
{data.map(item => <Item item={item} key={item.id} />)}
</Card>
- props.children获取父组件使用本组件时标签体的内容,这一步让Card组件的内容由使用它的组件定义
- html标签内的属性才是有效的,组件里的属性是用来传递的,在最终的html标签内不设置就不会生效
双向绑定
- 非受控组件
- 表单数据由DOM本身处理。即不受setState()的控制,与传统的HTML表单输入相似,input输入值即显示最新值。可以通过ref读取非受控组件的内容
- 受控组件 -就是受我们控制的组件,组件的状态全程响应外部数据
将表单中的数据存储到state中,然后将state设置为表单项value值,
这样当表单项发生变化,state会随之变化,
反之,state发生变化,表单项也会跟着改变,
这种操作我们就称为双向绑定,这样一来,表单就成为了一个受控组件
- 注意事项
- 在使用双向绑定声明表单数据的useState()要有初始值,原因是如果没有初始值,生成的表单
<input value=udefined></input>
,React会把它解析成非受控组件,出现代码报错
- 在使用双向绑定声明表单数据的useState()要有初始值,原因是如果没有初始值,生成的表单
- 参考博客
state提升
如果多个组件需要使用同一个数据,那么我们可以把这个数据提升到他们最近的共同祖先,这就是state提升
子传父数据
可以在父组件中定义一个响应函数,通过属性传递给子组件,子组件传参调用响应函数实现传递数据
遮罩层
在这个练习中,我们弹出确认框后不希望用户对网页做其他操作,可以添加一个遮住整个网页的遮罩层,遮罩层应该放在UI里
上述将遮罩层写在item里,这带来一个问题,假如后面代码维护中对修改了item的样式,会带来一系列问题:
- item开启相对定位,遮罩层从相对html到相对item,会出现遮罩层只能盖自己的情况
2. 为解决这个问题,给backdrop开启固定定位,因为排后面的item层级更高,盖不住
3. 于是再给backdrop设置层级=999,这时bug解决了 4. 但如果给item设置层级=1,又会出现第二步的问题
以上的解决办法治标不治本,导致这些bug是因为覆盖整个网页的遮罩层不应该写在item内,但是我们又必须把遮罩层写在item里,因为删除按钮在每一个item上,我们需要根据删除按钮弹出遮罩层,这时就需要一个新的工具:portal
portal
- 组件默认会作为父组件的后代渲染到页面中,但是有些情况下,这种方式会带来一些问题
- 通过portal可以将组件渲染到页面中的指定位置
- 使用方法:
- 在index.html添加一个新的元素
- 修改组件的渲染方式
- 通过ReactDOM.createPortal()作为返回值创建元素
- 参数:
- jsx(修改前return后的代码)
- 目标位置(DOM元素)
样式
内联样式
在React中可以直接通过标签的style属性来为元素设置样式。
style属性需要的是一个对象作为值,来为元素设置样式。
传递样式时,需要注意如果样式名不符合驼峰命名法,需要将其修改为符合驼峰命名法的名字。比如:background-color改为backgroundColor。
如果内联样式编写过多,会导致JSX变得异常混乱,此时也可以将样式对象定义到JSX外,然后通过变量引入。
import React from 'react'
export default function App() {
const appStyle={color:"white",witdh:"200px",height:"200px",backgroundColor:"red"}
return (
<div style={appStyle}>我是app</div>
)
}使用内联样式会使我们代码变得复杂,不利于后期维护,只建议在项目局部偶尔使用。
样式表
- 外部样式是将样式编写到外部的css文件中,然后直接通过import进行引入
- 直接通过import引入的样式都是全局样式,如果不同的样式表中出现了相同的类名,会出现相互覆盖情况。
- 样式表这种形式一般用于写一些普遍的全局样式。
CSS模块
如果没有类名冲突的问题,外部CSS样式表不失为是一种非常好的编写样式的方式。为了解决这个问题React为我们提供了另一种方式,CSS Module。
在于使用CSS Module后,网页中元素的类名会自动计算生成并确保唯一。
使用步骤:
- 使用CSS Module编写的样式文件的文件名必须为xxx.module.css
- 在组件中引入样式的格式为import xxx from './xxx.module.css'
- 设置类名时需要使用xxx.yyy的形式来设置
动态设置样式
内联样式
const pStyle = {
color: 'red',
backgroundColor: '#bfa',
border: redBorder ? "red solid 1px" : "blue solid 1px"
};样式表
<p className={`p1 ${redBorder?'':'blueBorder'}`}>我是一个段落</p>
样式模块
<p className={`${classes.p1} ${showBorder ? classes.Border : ''}`}>我是一个段落</p>
样式表和样式模块都是采用模板字符串实现类名的拼接来动态设置样式
Fragment
在React中,JSX必须有且只有一个根元素,但有时我们又想去掉最外层根元素,只显示最里面的内容,这时候只需要设置根元素 <React.Fragment></React.Fragment>
,或者用语法糖 <></>
import React from 'react';
//底层原理
const MyFragment = (props) => {
return props.children;
};
export default MyFragment;
引入FontAwesome
- 文档
- 安装依赖
- npm i --save @fortawesome/fontawesome-svg-core
- npm i --save @fortawesome/free-solid-svg-icons
- npm i --save @fortawesome/free-regular-svg-icons
- npm i --save @fortawesome/react-fontawesome@latest
- 使用
- 引入组件
import {FontAwesomeIcon} from "@fortawesome/react-fontawesome";
- 引入图标
import {faPlus} from "@fortawesome/free-solid-svg-icons";
- 使用组件
<FontAwesomeIcon icon={faPlus}/>
- 引入组件
Context
在React中组件间的数据通信是通过props进行的,父组件给子组件设置props,
子组件给后代组件设置props,props在组件间自上向下(父传子)的逐层传递数据。
但并不是所有的数据都适合这种传递方式,比如我们的数据在App组件里,
要用到数据的组件同APP隔好几代,如果还采用props的形式传递数据,不仅特别麻烦还使得代码不容易维护。
Context相当于一个公共的存储空间,我们可以将多个组件中都需要访问的数据统一存储到一个Context中,这样无需通过props逐层传递,即可使组件访问到这些数据
存放Context的文件放在store文件夹下
创建Context
- 通过
React.createContext()
创建
import React from 'react'
const TestContext=React.createContext('默认值')
export default TestContext- 通过
使用Context 方式1:采用消费者这种模式
- 引入Context
- 使用 Xxx.Consumer 组件来创建元素
- Consumer 的标签体需要一个回调函数,它会将Context设置为回调函数的参数,通过参数就可以访问到Context中存储的数据
import React from 'react'
import TestContext from "./store/TestContext"
export default function A() {
return (
<TestContext.Consumer>
{(ctx)=>{
return <div>{ctx}</div>//默认值
}}
</TestContext.Consumer>
)
}
方式2:使用钩子函数
- 导入Context
- 使用钩子函数useContext()获取到Context
- useContext() 需要一个Context作为参数
- 它会将Context中数据获取并作为返回值返回
import React, { useContext } from 'react'
import TestContext from './store/TestContext'
export default function B() {
const ctx=useContext(TestContext)
return (
//默认值
<div>{ctx}</div>
)
}
- 向Context传值:Xxx.Provider
- 表示数据的生产者,可以使用它来指定Context中的数据
- 通过value来指定Context中存储的数据,这样一来,在该组件的所有的后代组件中都可以通过Context来访问它所指定数据
- 当我们通过Context访问数据时,他会读取离他最近的Provider中的数据,如果没有Provider,则读取Context中的默认数据
<TestContext.Provider value={"新值"}>
<A />//新值
<B />//新值
</TestContext.Provider>
汉堡到家练习
注意点:
- CleanConfirm的Backdrop的层级应该更高,给它添加一个样式覆盖掉购物车
- 使用模板字符串拼接样式记得样式中间要用空格隔开
- 在函数组件体内修改state,会报to many re-renders错误,原因是这导致组件陷入死循环,一直在重新渲染
- 至今还有一个bug,加购后搜索Meals中的食品数量会丢失,因为用MEAL_DATA过滤
- 利用Effect降低数据过滤的次数,提高用户体验,当用户停止输入动作1秒后,才提交查询
- 使用Reducer优化state结构,由于我们将修改meal.amount的代码放进reducer函数里,在严格模式下会被调用两次,导致开发时结果不准确(meal.amount加了两次)
Effect
React组件有部分逻辑都可以直接编写到组件的函数体中的,像是对数组调用filter、map等方法,像是判断某个组件是否显示等。
但是有一部分逻辑如果直接写在函数体中,会影响到组件的渲染,这部分会产生“副作用”的代码,是一定不能直接写在函数体中。
例如,如果直接将修改state的逻辑编写到了组件之中,就会导致组件不断的循环渲染,直至调用次数过多内存溢出。
React.StrictMode
编写React组件时,我们要极力的避免组件中出现那些会产生“副作用”的代码。同时,如果你的React使用了严格模式,也就是在React中使用了React.StrictMode标签,那么React会非常“智能”的去检查你的组件中是否写有副作用的代码:在处于开发模式下,会主动的重复调用一些函数,以使副作用显现。
重复调用的函数
类组件的的 constructor, render, 和 shouldComponentUpdate 方法
类组件的静态方法 getDerivedStateFromProps
函数组件的函数体
参数为函数的setState
参数为函数的useState, useMemo, or useReducer
工具
通过浏览器安装React Developer Tools可以在控制台对二次调用的结果标灰
函数组件setState的流程
- 底层调用dispatchSetDate(),会先判断组件处于什么阶段
- 如果在渲染阶段(第一次初始化组件),不会去比较state值是否改变,直接重新渲染
- 如果在非渲染阶段(组件已经渲染完毕),state值不改变,不渲染,state值改变,则重新渲染
- 值不改变,React在一些情况下会继续执行当前组件的重新渲染,但是这个渲染不会触发子组件的渲染,这次渲染不会产生实际的效果
- 这种情况一般发生在state值第一次不改变的时候
使用useEffect
- useEffect是一个钩子函数,需要一个回调函数作为参数
- 回调函数会在组件渲染完毕后调用
- 在开发将产生“副作用”的代码写到useEffect的回调函数中,这样就可以避免这些代码影响组件的渲染
- 相当于把回调函数里的代码放进了一个定时器
- 可以传递第二个参数,第二个参数是一个数组,表示依赖项,当依赖项发生变化useEffect才会执行
- 通常会将useEffect中使用的变量都设置成依赖项,这样一来可以确保这些值发生变化时,会触发useEffect的执行
- useState会确保组件每次渲染都获取到相同的setState()对象,所以这个依赖项可写可不写
- 如果依赖项设置了一个空数组,则意味着Effect只会在组件初始化时执行一次
useEffect清理函数
- 在useEffect参数1(回调函数),可以指定一个函数作为返回值
- 这个函数可以称其为清理函数,它会在下次Effect执行前调用
- 可以在这个函数中,做一些工作来清除上次Effect执行所带来的影响
Reducer
为什么需要Reducer
由于使用state只有调用setState能修改state的值,当我们实现和state相关功能时不得不
利用setState封装多种方法,如果这些方法很多,不利于后期对代码进行维护
Reducer可以翻译为“整合器”,它的作用就是将那些和同一个state相关的所有函数都整合到一起,
方便在组件中进行调用,使用Reducer解决复杂state带来的不便,优化代码结构。
useReducer
参数
- reducer
- 一个回调函数,对于我们当前state的所有操作都应该在该函数中定义
- 该函数的返回值,会成为state的新值
- 回调函数的第一个参数(state)是当前最新的state,React自动帮我们传
- 回调函数的第二个参数(action)是一个对象,在对象中会存储dispatch指令,指令在调用dispath时传参定义。
- reducer封装多个操作state的函数,可以根据action中的指令调用不同的函数
- 如果把reducer写在函数组件内部,组件每渲染一次,reducer就会重复创建一次,所以可以把reducer定义在函数组件外部
- initialArg
- 表示state的初始值,作用和useState()的参数是一样的
返回值:返回一个数组
- 数组[0]xxx,用来获取state的值
- 数组[1]xxxDispatch,用来修改state的派发器
- 通过派发器可以发送修改state的命令
- 具体的修改行为会由另外一个函数(reducer)执行
React.memo
为什么需要memo
React组件会在两种情况下发生重新渲染。
第一种,当组件自身的state发生变化时。
第二种,当组件的父组件重新渲染时。
第一种情况下的重新渲染无可厚非,state都变了,组件自然应该重新进行渲染。但是第二种情况似乎并不是总那么的必要。
使用
- 只用于函数组件
- React.memo()是一个高阶组件,它接收另一个组件作为参数,并且会返回一个包装过的新组件
- 包装后的新组件拥有缓存功能,父组件触发渲染后,只能当前组件的props发生变化,才会触发组件的重新渲染,否则总是返回缓存的结果
- 开发中组件数据比较庞大复杂,可以使用memo减少该组件非必要的渲染
useCallBack
为什么需要CallBack?
上述使用memo使得父组件重新渲染,如果props发生改变,子组件才会重新渲染
如果父组件给子组件传的是一个引用数据类型(例如一个响应函数),
父组件重新渲染,该值会发生改变,子组件依然会渲染,对于这种情况,memo是无效的
针对这个情况,React采用useCallBack这个钩子函数
使用
- useCallBack()用来创建React响应函数且创建的响应函数不会在组件重新渲染时重新创建
- 参数
- 回调函数
- 依赖数组:
- 当依赖数组中的变量发生变化时,回调函数才会重新创建
- 如果不指定依赖数组,回调函数每次都会重新创建
- 依赖数组为空,意味着回调函数只在初始化时创建一次
- 一定要将回调函数中使用到的所有变量都设置成依赖项(除了setState)
Strapi
作用
- Strapi,它可以帮助我们方便快捷的搭建起一个供我们使用的API服务器。
- Strapi就是一个API的管理系统,通过Strapi我们可以直接以网页的形式去定义自己的API、包括设置模型、权限等功能。
- 有了Strapi我们无需编写代码便可开发出功能强大的API。
创建项目
- 安装strapi,使用下面命令
npx create-strapi-app@latest 项目名 --quickstart
//安装之前要保证node和npm的版本合适
"engines": {
"node": ">=14.19.1 <=18.x.x",
"npm": ">=6.0.0"
}
- 使用命令后如果npm报错,则删除node_module,使用
cnpm install
安装所需要的包 - 如果以上操作安装strapi仍有出错(例如后来我发现我的strapi汉化后构建会出现报错:缺少XXX文件),可以尝试下载其他版本的strapi
npx create-strapi-app@4.3.8 项目名称
- 创建项目后弹出一个注册页面,可以注册一个管理员
汉化项目
在src下的admin有一个app.example.js的模板文件,复制它并重命名为app.js,配置语言为"zh-Hans"
对项目进行重新构建
在网页修改界面语言,管理员名->Profile->修改语言
具体使用
创建新的数据类型:模型构建器->创建一个新的Content Type
创建多个字段,保存后等待服务器重启
- 无需创建id字段,strapi会自动给我们生成id
创建数据:内容管理器->添加条目->发布
- 不发布处于草稿状态,不会真的生效
- 在模型构建器,点击相关模型的编辑按钮->高级设置->设置自动发布
设置权限:设置->用户及权限插件->角色列表->编辑各个角色的权限
使用Postman访问api
React中使用fetch发生请求
fetch-Promise
useEffect(() => {
//表示正在加载
setLoading(true)
fetch("http://localhost:1337/api/students",{
method:"get"
}).then((res)=>{
if(res.status===200){
return res.json()
}else{
throw new Error("数据加载失败!")
}
}).then((res)=>{
setStudents(res.data)
setLoading(false)
}).catch((e)=>{
//收到异常则数据加载完毕
setLoading(false)
//记录错误信息
setFault(e.toString())
})
},[])
fetch-await
const fetchData = useCallback(async () => {
try {
//表示正在加载
setLoading(true)
setError(null)
const res = await fetch("http://localhost:1337/api/students", {
method: "get"
})
if (res.status === 200) {
const data = await res.json()
setData(data.data)
} else {
throw new Error("数据加载失败")
}
} catch (e) {
setError(e.toString())
} finally {
setLoading(false)
}
}, [])
- 注意点:useCallBack里有state,不设置依赖会出现拿不到更新后的state,可以把state作为参数传递进回调函数
自定义钩子
React中的钩子函数只能直接在函数组件或自定义钩子中调用,当我们需要将React中的钩子函数提取到一个公共区域时,就可以使用自定义钩子
自定义钩子就是一个普通函数,只是它的名字需要使用use开头
自定义钩子函数就是可以调用其他钩子函数的钩子函数
import { useState,useCallback} from 'react'
/*
reqObj:{
way:路径,
option:{路由信息配置对象},
errorDesc:错误描述信息
},
cb:回调函数,用来刷新数据
*/
export default function useFetch(reqObj,cb) {
//记录数据的state
const [data, setData] = useState([])
/*添加一个state来记录数据是否正在加载 ,ture表示正在加载,false表示加载完毕*/
const [loading, setLoading] = useState(false)
/*
/*添加一个state记录错误信息 */
const [error, setError] = useState(null)
let {way,option,errorDesc}=reqObj
const fetchF = useCallback(async (id,body) => {
try {
setError(null)
setLoading(true)
//修改和添加数据会传参设置body
if(body){
option.body=JSON.stringify({ data: body })
}
//修改会设置id
if(id){
// eslint-disable-next-line react-hooks/exhaustive-deps
way=way+`/${id}`;
}
console.log(option,way);
const res = await fetch(way,option)
if (res.status === 200) {
const data = await res.json()//res.json()返回一个Promise,使用await获得PromiseResult
setData(data.data)
cb && cb()
} else {
throw new Error(errorDesc)
}
} catch (e) {
setError(e.toString())
} finally {
setLoading(false)
}
}, [])
return {data,loading,error,fetchF}
}
Redux
什么是Redux
一个专为JS应用设计的可预期的状态容器,它使用一个store对多个state进行统一管理
- 状态:
状态就是一个变量,用以记录程序执行的情况。
- 容器:
容器当然是用来装东西的,状态容器即用来存储状态的容器。
状态多了,自然需要一个东西来存储,但是容器的功能却不是仅仅能存储状态,它实则是一个状态的管理器。
除了存储状态外,它还可以用来对state进行查询、修改等所有操作。 - 可预期:
之前学习的state使用useState的钩子函数获得初始值和修改state值的方法,我们使用方法可以对state值做任意修改,所以这种方式是不可预期的。
Redux中对状态所有的操作都封装到了容器内部,外部只能通过调用容器提供的方法来操作state,而不能直接修改state。
这就意味着外部对state的操作都被容器所限制,对state的操作都在容器的掌控之中,也就是可预测。
为什么需要Redux?
- 之前学习Reducer解决state复杂的问题,学习Context解决state传递问题,但是如果开发大型应用只是使用Reducer和Context,不会非常便利
- Redux可以理解为是reducer和context的结合体,使用Redux即可管理复杂的state,又可以在不同的组件间方便的共享传递state。
- Redux多用于大型应用的开发
使用
网页使用
引入redux核心包
<script src="https://unpkg.com/redux@4.2.0/dist/redux.js"></script>
创建reducer整合函数,同useReducer里的reducer
function reducer(state, action) {//可以在这里指定state的初始值:state=xxx
switch (action.type) {
case "SUB":
return {...state,count:state.count-1}
case "ADD":
return {...state,count:state.count+1}
default:
return state
}
}创建store(容器)
- 参数1:reducer
- 参数2:state的初始值,由于redux把所有state整合在一起,所以一般state是一个对象
const store=Redux.createStore(reducer,{count:1})
对store中的state进行订阅
- 参数是一个回调函数,当state发生变化时,回调函数会执行
store.subscribe(()=>{
num.textContent=store.getState().count
})
- 通过dispatch派发state的操作指令
subBtn.onclick=()=>{
store.dispatch({type:"SUB"})
}
addBtn.onclick=()=>{
store.dispatch({type:"ADD"})
}
- dispatch触发reducer函数,reducer函数内部修改state值,触发订阅回调函数的执行
问题
- 如果state过于复杂,将会非常难以维护
- 可以通过对state分组来解决这个问题,创建多个reducer,然后将其合并成一个
- state每次操作时,都需要对state进行复制,然后再去修改
- case后面的常量维护起来比较麻烦
- 如果state过于复杂,将会非常难以维护
Redux Toolkit(RTK)
- 上述中我们通过Redux核心库来使用Redux,除了核心库React还为我们提供了一种使用Redux的方式————Redux Toolkit
- RTK,即Redux工具包,帮助我们处理使用Redux的重复性工作,简化Redux中的各种操作
引入RTK
需要安装两个包:react-redux和@reduxjs/toolkit
npm install react-redux @reduxjs/toolkit -S
使用RTK
创建reducer切片
- 调用createSlice
- 参数:options,有以下属性:
- name:用来自动生成action中的type
- initialState:state的初始值
- 可以传一个回调函数,回调函数的返回值会作为state初始值
- reducers:是一个对象,通过方法指定state的各种操作
- 方法有两个参数:state和action
- 这里的state是一个原生state的代理对象,可以直接修改state的值
- action同之前
import {configureStore, createSlice} from "@reduxjs/toolkit"
const stuSlice=createSlice({
name:"stu",
initialState:{
name:"啊伟",
age:18,
gender:"男",
address:"广东"
},
reducers:{
setName(state,action){
state.name="阿强"
},
setAge(state,action){
state.age=20
}
}
})
获得action
- 切片对象.actions;
- actions存储切片自动生成action创造器(函数),调用函数会自动创建action对象
- 生成的action对象:{type:name/函数名,payload:xxx}
console.log(stuSlice.actions);
const nameAction=setName("哈哈")
const ageAction=setAge(18)
console.log(nameAction,ageAction);
生成store
const store=configureStore({
reducer:{
stu:stuSlice.reducer
}
})
使用store
根节点使用Provider注入store
root.render(
<Provider store={store}>
<App />
</Provider>
)useSelector()
- 用来加载state中的数据
- 参数是一个回调函数,回调函数的参数会接收一整个state
const student=useSelector(state=>state.student)
useDispatch
- 用来获取派发器对象
- dispatch(调用action创造器)派发指令
const dispatch=useDispatch()
const changeNameHandler=()=>{
dispatch(setName("阿强"))
}
RTK拆分
- 在开发中应该将不同切片封装在不同文件(模块),再暴露出actions和reducer,使得项目文件结构更加清晰,便于维护
RTKQ的定义
上述我们使用RTK管理state,目前我们的state是本地的,并不与服务器关联。
RTK Query是一个强大的数据获取和缓存工具,在它的帮助下,我们不再需要自己编写获取数据和缓存数据的逻辑
Web应用中加载数据时需要处理的问题:
- 根据不同的加载状态显示不同UI组件
- 减少对相同数据重复发送请求
- 使用乐观更新,提升用户体验
- 乐观更新:只在数据加载成功才更新数据,否则维持旧数据
- 在用户与UI交互时,管理缓存的生命周期
- 管理缓存数据的生命周期,例如增加完数据我们需要更新数据,即让缓存数据死亡。
这些问题,RTKQ都可以帮助我们处理。
首先,可以直接通过RTKQ向服务器发送请求加载数据,并且RTKQ会自动对数据进行缓存,避免重复发送不必要的请求。
其次,RTKQ在发送请求时会根据请求不同的状态返回不同的值,我们可以通过这些值来监视请求发送的过程并随时中止。
RTKQ的使用
导包
import { createApi, fetchBaseQuery } from "@reduxjs/toolkit/query/react"
//路径/react 会自动生成钩子函数,一般我们用这个路径
创建Api对象
createApi()
创建RTKQ中的API对象,RTKQ的所有功能都需要通过该对象来进行需要一个options作为参数:
reducerPath:""
Api的标识,不能和其他Api或reducer重复baseQuery:fetchBaseQuery()
指定查询的基础信息,指定发送请求使用的工具。fetchBaseQuery是RTKQ对fetch的简单封装,是一个函数
需要一个options作为参数:
baseUrl
指定基本路径prepareHeaders
用于统一设置请求头,是一个回调函数参数1:headers,需要修改headers后对它进行返回
参数2:
prepareHeaders:(headers)=>{
headers.set("Authorization",`Bearer ${localStorage.getItem("token")}`)
return headers
}
tagTypes:[标签名1,标签名2...]
用于指定API有什么类型的标签endpoints
const studentApi = createApi({
reducerPath: "studentApi",
baseQuery: fetchBaseQuery({ baseUrl: "http://localhost:1337/api" }),
endpoints(build) {
return {
getStudents: build.query({
query() {
return "/students"
}
})
}
}
})
endpoints
- 用来指定Api的各种功能,是一个方法
- 其实是一个回调函数,创建完API对象后会自动调用endpoints,传一个build
- 参数:build是请求的构建器
- 需要一个对象作为返回值,每一个属性设置一个请求,这里就需要用到build
build.方法()
- 需要一个options作为参数,用来定义我们这次查询所用到的信息
- 属性
keepUnusedDataFor
rtkq的缓存机制,第一次操作发送请求,会把所获得的数据缓存下来。
在缓存数据有效期内,相同的操作不会重复发送请求
endpoints每一个端点都有自己单独的缓存
keepUnusedDataFor:10//设置数据缓存的时间,单位秒,默认是60
数据未使用时开始计时,10s过后缓存数据失效,也就是说10s内相同操作不会重复发送请求providesTags:[...]
- 给请求打上标签
- 同下,可以传一个对象
{type:"标签名",id:xxx}
或者回调函数,对标签进行细化
invalidatesTags:[...]
- 让标签失效,绑定该标签的请求缓存会失效
- 标签可以对应一个对象,让更具体的缓存项失效
- 属性值为一个回调函数时,可以操作以下参数:
- data:请求响应数据
- error:错误信息
- stu:钩子函数所传的第一个参数
invalidatesTags:(data,err,stu)=>{
return [{type:"student",id:stu.id}]
}
- 方法
- transformResponse
//用来转换响应数据的格式
//baseQueryReturnValue就是原本的响应数据
transformResponse(baseQueryReturnValue){
return baseQueryReturnValue.data
}
build.query
- 通过
build.query()
来设置查询的相关信息//用来指定请求的子路径
query(){返回一个字符串作为子路径}
build.mutation
通过
build.mutation()
来定义我们这次请求所用到的信息query(){
//如果发送的不是get请求,需要返回一个对象来设置请求的信息
return {
url:...,
method:XXX,
}
}
暴露Api对象
- Api对象创建后,对象中会根据各种方法自动的生成对应的钩子函数
- 通过这些钩子函数,可以向服务器发送请求
- 钩子函数的命名规则:getStudents-->useGetStudentsQuery
export const {useGetStudentsQuery} =studentApi
//用于store的配置
export default studentApi
使用Api对象
使用store
配置store
配置reducer
设置中间件
import { configureStore, getDefaultMiddleware } from "@reduxjs/toolkit";
import studentApi from "./studentApi";
const store=configureStore({
reducer:{
//Api的标识:reducer
[studentApi.reducerPath]:studentApi.reducer
},
//getDefaultMiddleware获取当前store所有默认的中间件
//将studentApi自动生成的中间件添加到store
//middleware使得缓存生效,rtkq应用到缓存
//多个api,concat传进一个数组
middleware:getDefaultMiddleware=>getDefaultMiddleware().concat(studentApi.middleware)
})
export default store
注入store
使用钩子函数发送请求
钩子函数会返回一个对象作为返回值,请求过程中相关数据都在该对象中存储
钩子函数传的参数会被传进query()方法里
import { useGetStudentsQuery } from "./store/studentApi"
export default function App() {
const { data ,isSuccess,isLoading} = useGetStudentsQuery()
return (<div>
{isLoading && <div>数据加载中</div>}
{isSuccess && data.data.map(item => <div key={item.id}>
{item.attributes.name}--
{item.attributes.age}--
{item.attributes.gender}--
{item.attributes.address}
</div>)}
</div>)
}
useQuery()的返回值
currentData: undefined //当前参数的最新数据,当参数发生变化时它会变成undefined
data: undefined //最新加载到的数据
isError: false //布尔值,是否有错误
error:Error() //对象,有错才存在
isFetching: true //布尔值,数据是否正在加载
isLoading: true //布尔值,数据是否第一次加载,第一次加载时为true,加载完毕后变为false,以后的加载值依旧是false
isSuccess: false //布尔值,请求是否成功
isUninitialized: false //布尔值,请求是否还没有开始发送
refetch: ƒ () //一个函数,用来重新加载数据
status: "pending" //字符串,请求的状态,pending表示数据加载中,fulfilled表示数据加载完成
- 例如如果用户发送请求后不想显示之前的数据而是显示一个加载中的图标,可以使用currentData,反之使用data
useQuery()的参数
- 参数1:会传到query()方法里
- 参数2:接收一个对象,通过该对象可以对请求进行配置
- 属性1:selectFromResult,一个回调函数(result)=>{返回值指定结果},用于指定useQuery返回的结果
- 属性2:pollingInterval,默认值是0,设置轮询的间隔,单位毫秒,如果为0则表示不轮询
- 属性3:skip,默认值是false,设置是否跳过当前请求
- 属性4:refetchOnMountOrArgChange,默认值为false,设置是否每次都重新加载数据
- 还可以指定一个数值表示数据缓存的时间,单位是秒
- 属性5:refetchOnFocus,默认值是false,是否在重新获取焦点(切换回来页面)时重载数据
- 属性6:refetchOnReconnect,默认值是false,是否在网络重新连接后重载数据
- 属性5和属性6要想生效,需要在store里设置监听器
setupListeners(store.dispatch)
useMutation的返回值
- 返回一个数组[操作的触发器,结果集]
- 调用触发器,发送相应的请求
- rtkq会自动转换为json,无需类型转换
ReactRouter
简介
- 使用React这些工具所编写的项目通常都是单页应用(SPA)。单页应用中,整个应用中只含有一个页面,React会根据不同的状态在应用中显示出不同的组件。但是我们之前所编写应用还存在着一个问题,整个应用只存在一个页面,一个请求地址,这就使得用户只能通过一个地址访问应用,当我们点击组件中的不同链接时应用的地址是不会发生变化的。也就是说我们无法随意刷新页面和分享网页。
- 为了解决这个问题,我们需要引入一个新的工具React Router,React Router为我们提供一种被称为客户端路由的东西,通过客户端路由可以将URL地址和React组件进行映射,当URL地址发生变化时,它会根据设置自动的切换到指定组件。并且这种切换完全不依赖于服务器。换句话说,在用户看来浏览器的地址栏确实发生了变化,但是这一变化并不由服务器处理,而是通过客户端路由进行切换。
- react router适用于web和原生项目,我们在web项目中使用,所以需要引入的包是react-router-dom。
引入版本5
npm install react-router-dom@5 -S
使用Router
- 在index.js中引入Router
- 将Router设置根标签
- BrowserRouter根据url地址切换组件,使用过程中和普通的url地址没有区别。
- 此时存在一个问题:
- 当我们使用Link构建的链接进行跳转,此行为是发生在客户端,不经过服务器,但是如果在跳转后的页面进行刷新或者点击普通链接,会向服务器发送请求,由于服务器没有关于该url地址的路由,所以会返回404
- 我们可以修改服务器的配置,将所有请求都转发到index.html来解决这个问题
- nginx->conf->nginx.conf修改配置文件
location / {
root html;
try_files $uri /index.html;
} - 补充nginx相关命令:
.\nginx.exe -s stop
停止服务器.\nginx.exe -s reload
重新加载服务器
- HashRouter根据url地址的hash值切换组件
- 服务器不会处理地址栏的哈希值,所以使用HashRouter能很好解决上述问题
- BrowserRouter根据url地址切换组件,使用过程中和普通的url地址没有区别。
- 示例:
import ReactDOM from "react-dom/client"
import App from "./App"
import { BrowserRouter } from "react-router-dom"
const root = ReactDOM.createRoot(document.getElementById("root"))
root.render(<BrowserRouter><App /></BrowserRouter>)
使用Route
将组件和url进行映射
使用Route组件来映射地址和组件,需要以下参数:
- path:映射的url地址
- exact:布尔值,表示路径是否完全匹配
- 当Route被访问,其对应的组件会自动挂载,默认情况下不是严格匹配。例如在非严格匹配的情况下url="/about",组件a关联的path="/",b是"/about",那么a和b都会被挂载。
- component:指定要挂载的组件,需要直接传递组件的类,而不是JSX
- 通过component构建的组件,它会自动创建组件并且会自动传递参数。
- match--匹配的信息
- isExact检查路径是否完全匹配
- params请求的参数
- location--地址信息
- history--历史记录信息,用于控制页面的跳转
- push()跳转页面,A->B->C,从B
push
到C,可以回退到B - replace()替换页面,A->B->C,从B
replace
到C,只能回退到A - 参数都是location对象
- push()跳转页面,A->B->C,从B
- render:指定要挂载的组件,需要传一个回调函数,回调函数的返回值最终会被挂载
- render不会像component自动传递match、location、history
- 回调函数的第一个参数为roteProps,与component中的props内容相同,可以使用它自定义组件
- children:可以用来指定被挂载的组件
- 和render类似,传递回调函数
- 当children设置一个回调函数时,该组件无论路径是否匹配都会被挂载
- 可以直接传递组件
- 无法用props获得三个参数
- 和render类似,传递回调函数
示例:
<Route path="/" component={Home} exact={true}/>
<Route path="/about" render={(roteProps)=><About {...roteProps}/>} exact={true}/>
<Route path="/about" children={<About/>} exact={true}/>
{/*注意:children表示标签体,可以直接在标签体直接设置,这样写结构更清晰*/}
<Route path="/about" exact={true}>
<About/>
</Route>除了使用props获取三个参数外,也可以使用钩子函数来获取
const match=useRouteMatch()
const history=useHistory()
const location=useLocation()
const params=useParams()路由嵌套
- 可以在路由里嵌套子组件,只有访问子路由,子组件才显示
<div>
这是About页面
<Link to={`${path}/hello`}>sayHello</Link>
<Route path={`${path}/hello`}component={Hello}/>
</div>
创建超链接访问组件
在使用react router时,一定不要使用a标签来创建超链接,因为a标签创建的超链接,会自动向服务器发送请求重新加载页面,而我们不希望这种情况发生
可以使用Link组件创建超链接
- exact:布尔值,表示路径是否完全匹配
与Link相比,NavLink组件可以指定链接激活样式
- activeClassName:字符串
- activeStyle:对象
<li><NavLink activeClassName={classes.active} to="/" exact>主页</NavLink></li>
其他组件
- Switch
- 放入Switch组件中后,匹配路径时会自动自上向下对Route进行匹配,如果匹配到则挂载组件,并且一个Switch中只会有一个Route被挂载。
- 如果将Route组件单独使用,那么所有的路径匹配的Route中的组件都会被挂载。
- Prompt确定框
- when:为true显示,反之不显示
- message:提示信息
- Redirect重定向
- to相当于history的replace("to的值")
- push to相当于history的push("to的值")
- from,to,从from的路径跳转到to的路径
引入版本6
npm install react-router-dom@6 -S
使用Router
与版本5相同
使用Route
- Route必须被
<Routes></Routes>
包含 - Routes作用和switch类似,都是Route的容器,Routes中的Route只有一个会被匹配
- 不同的是switch可写可不写,Route必须写在Routes里面否则报错
- Route的component,render,children都没了,需要通过element来指定要挂载的组件,传一个JSX对象
- path
- 第一个斜杠可以省略
- 版本6默认是严格匹配,要想变成非严格匹配。可以修改路径为/xxxx/*
使用钩子函数
- useParams(),useLocation()同版本5
- useMatch("路由路径"),用来检查当前url是否匹配某个路由,如果匹配则返回对象,否则返回null
- useNavigate代替useHistory,获取用于跳转页面的函数,默认使用push的方式
- 参数1:要跳转的路径
- 要切换成替换的方式,只需传第二个参数{replace:true}
路由嵌套
写法1:
- 父路由非严格匹配
- 子路由写在父组件里
- 子路由的路径直接写子路径名,无需引用父路径
<Route path="/about/*" element={<About/>}></Route>
import React from 'react'
import { Route,Routes } from 'react-router-dom'
import Hello from './Hello'
export default function About() {
return (
<div>
About页面
<Routes>
<Route path='hello' element={<Hello/>}></Route>
</Routes>
</div>
)
}写法2:
- 子路由直接写在父路由标签体内,方便我们统一管理和维护所有路由
- 子路由的路径直接写子路径名,无需引用父路径
- Outlet用来表示嵌套路由中的组件
- 当嵌套路由中的路径匹配成功了,Outlet则表示嵌套路由中的组件,否则Outlet就什么都不是
<Route path="/about" element={<About/>}>
{/*写成/hello会报错,因为这是绝对路径的写法,绝对路径必须写完整,这里直接写路径名作为相对路径*/}
<Route path="hello" element={<Hello/>}/>
</Route>import React from 'react'
import { Outlet } from 'react-router-dom'
export default function About() {
return (
<div>
About页面
<Outlet/>
</div>
)
}
创建超链接访问组件
- Link与版本5相同
- NavLink
- style:回调函数,回调函数的返回值会成为生效的样式
- 参数里传进一个对象{isActive:ture/false}
- ture表示链接被选中,false反之
其他组件
- Navigate作用同版本5的Redirect
- 默认使用push跳转,加一个replace属性,切换到replace方式
- 添加一个state属性,会作为下一个Location的sate值