项目构建命令
npx create-react-app react-basic
npx:node语法
create-react-app:项目模板
react-basic:项目名称
项目结构
项目打包和本地预览
- 项目打包
npm run build
- 本地预览(模拟服务器运行项目)
- 安装本地服务包
npm i -g serve
- 指定服务启动路径,如./build:
serve -s ./build
- 安装本地服务包
打包优化 - CDN配置
- craco.config.js
// 扩展webpack的配置
const path = require("path");
// 引入辅助函数
const { whenProd, getPlugin, pluginByName } = require("@craco/craco");
module.exports = {
// webpack 配置
webpack: {
// 配置别名
alias: {
// 约定:使用 @ 表示 src 文件所在路径
"@": path.resolve(__dirname, "src"),
},
// 配置CDN
configure: (webpackConfig) => {
let cdn;
whenProd(() => {
// key: 不参与打包的包(由dependencies依赖项中的key决定)
// value: cdn文件中 挂载于全局的变量名称 为了替换之前在开发环境下
webpackConfig.externals = {
react: "React",
"react-dom": "ReactDOM",
};
// 配置现成的cdn资源地址
// 实际开发的时候 用公司自己花钱买的cdn服务器
cdn = {
js: [
"https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.production.min.js",
"https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.production.min.js",
],
};
});
// 通过 htmlWebpackPlugin插件 在public/index.html注入cdn资源url
const { isFound, match } = getPlugin(
webpackConfig,
pluginByName("HtmlWebpackPlugin")
);
if (isFound) {
// 找到了HtmlWebpackPlugin的插件
match.options.cdn = cdn;
}
return webpackConfig;
},
},
};
- index.html:动态创建script标签,导入js文件
<body>
<div id="root"></div>
<% htmlWebpackPlugin.options.cdn.js.forEach(cdnURL=> { %>
<script src="<%= cdnURL %>"></script>
<% }) %>
</body>
包体积可视化分析
- 安装插件:
npm i source-map-explorer
- 配置命令指定要分析的js文件:一般为打包后的js文件
package.json:
"scripts": {
"analyze": "source-map-explorer 'build/static/js/*.js'"
},
配置路径别名
CRA本身把webpack配置包装到了黑盒里无法直接修改,需要借助一个插件 - craco
配置步骤:
- 安装craco:
npm i -D @craco/craco
- 项目根目录下创建配置文件
craco.config.js
- 配置文件中添加路径解析配置
- 包文件中配置启动和打包命令
const path = require('path')
module.exports = {
// webpack配置
webpack: {
// 配置别名
alias: {
// 约定:使用 @ 表示 src 文件所在路径
'@': path.resolve(__dirname, 'src'),
},
},
}
"scripts": {
"start": "craco start",
"build": "craco build"
},
联系路径配置
VsCode的联想配置,需要我们在项目目录下添加jsconfig.json文件,加入配置之后Vscode会自定读取配置帮助我们自动联想提示
配置步骤:
- 根目录下新增配置文件 - jsconfig.json
- 添加路径提示配置
{
"compilerOptions": {
"baseUrl": "./",
"paths": {
"@/*": [
"src/*"
]
}
}
}
React调试插件
浏览器插件:React Developer Tools
JSX基础
概念和本质
概念:jsx是javaScript和XML(HTML)的缩写,表示在js代码中编写html模板结构,它是react中编写UI模板的方式
本质:JSX并不是标准的js语法,它是js的语法扩展,浏览器本身不能识别,需要通过解析工具做解析之后才能在浏览器中运行
优势:
- HTML的声明式模板语法
- JS的可编程能力
JSX中使用JS表达式
在JSX中可以通过 大括号语法{} 识别JavaScript中的表达式,比如常见的变量、函数调用、方法调用等等
- 使用引号传递字符串
- 使用JavaScript变量
- 函数调用和方法调用
- 使用JavaScript对象
注意:if语句、switch语句、变量声明属于语句,不是表达式,不能出现在{}中
const count = 100
const getName = () => {
return 'Bob'
}
function App() {
return (
<div className="App">
{/* 使用引号传递字符串 */}
{ '这是一个字符串' }
{/* 使用JavaScript变量 */}
{ count }
{/* 函数调用 */}
{ getName() }
{/* 方法调用 */}
{ new Date().getDate() }
{/* 使用JavaScript对象 */}
<div style={ {color:'red',fontSize:'30px'} }>this is div</div>
</div>
);
}
export default App;
JSX中实现列表渲染
语法:在JSX中可以使用原生JS中的map方法遍历渲染列表
- 循环哪个结构在map中就return哪个结构
- 结构上需要加上key字段,可以是字符串、number类型
- key的作用:React框架内部使用,提升更新性能
const list = [
{id:1,name:'Vue'},
{id:2,name:'React'},
{id:3,name:'Angle'}
]
function App() {
return (
<div className="App">
{/* 渲染列表 */}
<ul>
{list.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
);
}
export default App;
JSX实现条件渲染
语法:在React中,可以通过逻辑与运算符&&、三元里表达式(?:)实现基础的条件渲染
{flag && <span>this is span</span>}
:当flag为false时,不显示;当flag为true时,显示标签
{loading ? <span>loading...</span> : <span>this is span</span>}
:当loading为true时显示第一个标签;当loading为false时显示第二个标签
const isLogin = true
function App() {
return (
<div className="App">
{/* 条件渲染 */}
{/* 逻辑与 && */}
{isLogin && <div>未登录</div>}
{/* 三元运算符 */}
{isLogin ? <div>已登录(jack)</div> : <div>用户未登录</div>}
</div>
);
}
export default App;
复杂的条件渲染:
const count = 1 // 0 1 2
const getCount = () => {
if(count === 0){
return '当前为0'
}else if(count === 1){
return '当前为1'
}else if(count === 2){
return '当前为2'
}
}
function App() {
return (
<div className="App">
{/* 复杂的条件渲染 */}
{getCount()}
</div>
);
}
export default App;
事件绑定
基础事件绑定
语法:on+事件名称={事件处理程序},整体上遵循驼峰命名法
function App() {
const handleClick = () => {
console.log('BUTTON被点击了')
}
return (
<div className="App">
{/* 基础事件绑定 */}
<button onClick={handleClick}>click事件</button>
</div>
);
}
export default App;
使用事件对象参数
语法:在事件回调函数中设置形参e
function App() {
const handleClick1 = (e) => {
console.log('button被点击了',e)
}
return (
<div className="App">
{/* 使用事件对象参数e */}
<button onClick={handleClick1}>click事件</button>
</div>
);
}
export default App;
传递自定义参数
语法:事件绑定的位置改造成箭头函数的写法,在执行实际处理业务函数的时候传递实参
function App() {
const handleClick2 = (name) => {
console.log('button被点击了',name)
}
return (
<div className="App">
{/* 传递自定义参数 */}
<button onClick={() => handleClick2('bob')}>click事件</button>
</div>
);
}
export default App;
同时传递事件对象和自定义参数
语法:在事件绑定的位置传递事件实参e和自定义参数,clickHandler中声明形参,注意顺序对应
function App() {
const handleClick3 = (name,e) => {
console.log('button被点击了',name,e)
}
return (
<div className="App">
{/* 同时传递自定义参数和事件对象 */}
<button onClick={(e) => handleClick3('bob',e)}>click事件</button>
</div>
);
}
export default App;
组件基础使用
概念:一个组件就是用户界面的一部分,它可以有自己的逻辑和外观,组件之间可以互相嵌套,也可以复用多次
语法:在React中,一个组件就是首字母大写的函数,内部存放了组件的逻辑和视图UI,渲染组件只需要把组件当成标签书写即可
// 定义组件
function Button(){
// 组件业务逻辑
return <button>click me</button>
}
function App() {
return (
<div className="App">
{/* 使用组件方式: */}
{/* 1.自闭和 */}
<Button/>
{/* 2.成对标签 */}
<Button></Button>
</div>
);
}
export default App;
useState基础使用
:::info
useState是一个React Hook(函数),它允许我们向组件添加一个状态变量,从而控制影响组建的渲染结果
本质:和普通的JS变量不同的是,状态变量一旦发生变化组件的视图UI也会跟着变化(数据驱动视图)
:::
- useState是一个函数,返回值是一个数组
- 数组中的第一个参数是状态变量,第二个参数时set函数用来修改状态变量
- useState的参数将作为count的初始值
set函数的作用:
- 使用新值更新状态变量
- 使用更新后的状态变量重新渲染UI
import { useState } from "react";
function App() {
// 1. 调用useState函数创建一个状态变量
// count:状态变量
// setCount:修改状态变量的方法
const [count,setCount] = useState(0)
// 2. 点击事件回调
const handleClick = () => {
// 作用:
// 1. 用传入的新值修改count
// 2. 使用新的count重新渲染UI
setCount(count+1)
}
return (
<div className="App">
<button onClick={handleClick}>{count}</button>
</div>
);
}
export default App;
修改状态的规则
- 状态不可变:在React中,状态被认为是只读的,我们应该始终替换它而不是修改它,直接修改状态不能引发视图更新
- 修改对象状态:对于对象类型的状态变量,应该始终传给set方法一个全新的对象来进行修改
import { useState } from "react";
function App() {
let [count,setCount] = useState(0)
const handleClick = () => {
// 错误写法:直接修改
// 1. 值发生了变化
// 2. 但无法引发视图更新
// count++
// console.log(count) // 自增
// 正确写法:set函数
// 1. 值发生了变化
// 2. 视图重新渲染
setCount(count + 1)
}
// 修改对象状态
const [form,setForm] = useState({name:'bob'})
const changeForm = () => {
// 错误写法:直接修改
// 1. 值发生了变化
// 2. 但无法引发视图更新
// form.name = 'tom'
// console.log(form) // tom
// 正确写法:set函数
// 1. 值发生了变化
// 2. 视图重新渲染
setForm({
...form,
name:'Tom'
})
}
return (
<div className="App">
<button onClick={handleClick}>{count}</button>
<button onClick={changeForm}>修改form-{form.name}</button>
</div>
);
}
export default App;
基础样式控制
React组件基础的样式控制有两种方式:
- 行内样式 — 不推荐
- class类名控制,注意不能使用class,而是使用className — 推荐
.foo{
color: blue;
}
// 导入样式文件
import './index.css'
const myStyle = {
color:'yellow',
fontSize: '50px'
}
function App() {
return (
<div className="App">
{/* 行内样式 */}
<span style={{color:'red',fontSize:'40px'}}>this is span</span>
<span style={myStyle}>this is span</span>
{/* class类名控制 */}
<span className='foo'>this is class</span>
</div>
);
}
export default App;
classnames优化类名控制
classnames是一个简单的JS库,可以非常方便的通过条件动态控制class类名显示
安装:npm install classnames
不使用classnames时,动态控制class类名:采用字符串拼接的方式
<span className={`tab ${item.type === type && 'active'}`}>bob</span>
使用classnames时,动态控制class类名:
<span className={classNames('tab',{active: item.type === type})}>bob</span>
表单受控绑定
概念:使用React组件的状态(useState)控制表单的状态
import { useState } from 'react';
function App() {
const [value,setValue] = useState('')
return (
<div className="App">
{/* 通过value属性绑定状态,通过onChange属性绑定状态同步的函数 */}
<input value={value} type='text' onChange={(e) => setValue(e.target.value)}/>
<div>{value}</div>
</div>
);
}
export default App;
获取DOM
API:useRef(null)
使用方法:
- 使用useRef生成ref对象,绑定到dom标签上
const inputRef = useRef(null)
<input type='text' ref={inputRef}/>
- dom可用时,ref.current获取dom,注意:dom在渲染完毕(dom生成)之后才可用
inputRef.current
// 导入样式文件
import { useRef } from 'react';
function App() {
// 1. 使用useRef生成ref对象,绑定到dom标签上
const inputRef = useRef(null)
const showDom = () => {
// 2. dom可用时,ref.current获取dom
// dom在渲染完毕(dom生成)之后才可用
console.dir(inputRef.current)
}
return (
<div className="App">
<input type='text' ref={inputRef}/>
<button onClick={showDom}>获取Dom</button>
</div>
);
}
export default App;
组件通信
props说明
- props可传递数据类型:
数字、字符串、布尔值、数组、对象、函数、JSX
- props是只读对象
子组件只能读取props中的数据,不能直接进行修改,父组件的数据只能由父组件修改
function Son(props){
console.log(props)
// props.age = 32 错误代码,TypeError: Cannot assign to read only property 'age' of object '#<Object>'
return (
<div>this is Son App,{props.name}</div>
)
}
function App() {
const name = 'this is App'
return (
<div className="App">
<Son
name={name}
age={18}
isTrue={true}
list={['vue','react']}
obj={{name:'bob'}}
cb={() => console.log(123)}
child={<span>this is span child</span>}
/>
</div>
);
}
export default App;
特殊的prop - children
场景:当我们把内容嵌套在子组件标签中时,父组件会自动在名为children的prop属性中接收该内容
语法:
// 子组件中:
function Son(props){
return (
<div>{props.children}</div>
)
}
// 父组件中:
<Son>
<span>this is span</span>
</Son>
比如:
function Son(props){
// 2. 子组件通过props参数接收数据
console.log(props)
return (
<div>this is Son App,{props.name},{props.children}</div>
)
}
function App() {
const name = 'this is App'
return (
<div className="App">
{/* 1. 在子组件标签上绑定属性 */}
<Son name={name}>
<span>this is span</span>
</Son>
</div>
);
}
export default App;
父传子
实现步骤:
- 父组件传递数据 - 在子组件标签上绑定属性
- 子组件接收数据 - 子组件通过props参数接收数据
function Son(props){
// 2. 子组件通过props参数接收数据
return (
<div>this is Son App,{props.name}</div>
)
}
function App() {
const name = 'this is App'
return (
<div className="App">
{/* 1. 在子组件标签上绑定属性 */}
<Son name={name}/>
</div>
);
}
export default App;
子传父
实现核心:在子组件中调用父组件中的函数并传递参数
语法:
// 子组件:
function Son(props){
const {onGetSonMsg,onGetSonName} = props
return <button onClick={() => onGetSonMsg(sonMsg) }>点击</button>
}
// 父组件:
const getSonMsg = (msg) => {
// 接收子组件传递的参数
console.log(msg) // this is son msg
}
<Son name={name} onGetSonMsg={getSonMsg}/>
比如:
import { useState } from "react"
function Son(props){
// 2. 子组件通过props参数接收数据
const {onGetSonMsg,onGetSonName} = props
console.log(props)
const name = 'this is Son'
const sonMsg = 'this is son msg'
return (
<div>
this is Son
<div>{props.name}</div>
{/* 3. 触发父组件传递过来的函数,并传递参数 */}
<button onClick={() => onGetSonMsg(sonMsg) }>点击</button>
<button onClick={() => onGetSonName(name)}>点击2</button>
</div>
)
}
function App() {
const [sonName,setSonName] = useState('')
const name = 'this is App'
const getSonMsg = (msg) => {
// 4. 接收子组件传递的参数
console.log(msg) // this is son msg
}
const getSonName = (sname) => {
// 4. 接收子组件传递的参数
setSonName(sname)
}
return (
<div className="App">
{/* 1. 在子组件标签上绑定属性 */}
<Son name={name} onGetSonMsg={getSonMsg} onGetSonName={getSonName}/>
<div>{sonName}</div>
</div>
);
}
export default App;
使用状态提升实现兄弟组件通信
实现思路:A(子组件) —> App(父组件) —> B(子组件)
实现步骤:
- A组件先通过子传父的方式把数据传给父组件App
- App拿到数据后通过父传子的方式再传递给B组件
import { useState } from "react"
function A({onGetName}) {
const aName = 'this is A'
return (
<div>
this is A
{/* 1.将数据传递父组件App */}
<button onClick={() => onGetName(aName)}>send</button>
</div>
)
}
function B(props) {
const {name} = props
return (
<div>
this is B
<div>{name}</div>
</div>
)
}
function App() {
const [name,setName] = useState('')
const getName = (aName) => {
setName(aName)
}
return (
<div className="App">
<A onGetName={getName}/>
{/* 2.将数据传递给子组件B */}
<B name={name}/>
</div>
);
}
export default App;
使用Context机制跨层级组件通信
语法:createContext
、useContext
实现步骤:
- 使用createContext方法创建一个上下文对象Ctx
- 在顶层组件(App)中通过Ctx.Provider组件提供数据
- 在底层组件(B)中通过useContext钩子函数获取数据
import { createContext,useContext } from "react"
// 1. 使用createContext创建一个上下文对象
const MsgContext = createContext()
function B() {
// 3. 在底层组件中通过useContext钩子函数使用数据
const msg = useContext(MsgContext)
return (
<div>
this is B
<div>{msg}</div>
</div>
)
}
function A() {
return (
<div>
this is A
<B/>
</div>
)
}
function App() {
const msg = 'this is App msg'
return (
<div className="App">
{/* 2. 在顶层组件中通过Provider组件提供数据 */}
<MsgContext.Provider value={msg}>
this is App
<A/>
</MsgContext.Provider>
</div>
);
}
export default App;
传递状态(useState):
import { createContext,useContext,useState } from "react"
// 1. 使用createContext创建一个上下文对象
const NameContext = createContext()
function B() {
// 3. 在底层组件中通过useContext钩子函数使用数据
const name = useContext(NameContext)
return (
<div>
this is B
<div>{name}</div>
</div>
)
}
function A() {
return (
<div>
this is A
<B/>
</div>
)
}
function App() {
const [name,setName] = useState('bob')
return (
<div className="App">
{/* 2. 在顶层组件中通过Provider组件提供数据 */}
<NameContext.Provider value={name}>
this is App
<button onClick={() => setName('tom')}>改变name</button>
<A/>
</NameContext.Provider>
</div>
);
}
export default App;
useEffect
概念理解和基础使用
概念:useEffect是一个React Hook函数,用于在React组件中创建不是由事件引起而是由渲染本身引起的操作,比如发送Ajax请求,更改Dom等等
语法:useEffect(() => {}, [])
参数1是一个函数,可以把它叫做副作用函数,在函数内部可以放置要执行的操作
参数2是一个数组(可选参数),在数组里放置依赖项,不同依赖项会影响第一个参数函数的执行,当是一个空数组的时候,副作用函数只会在组件渲染完毕之后执行一次
import { useEffect, useState } from "react";
const URL = 'http://geek.itheima.net/v1_0/channels'
function App() {
// 创建一个状态
const [list,setList] = useState([])
useEffect(() => {
// 获取列表数据
async function getList(){
const res = await fetch(URL)
const jsonRes = await res.json()
console.log(jsonRes)
setList(jsonRes.data.channels)
}
getList()
},[])
return (
<div className="App">
this is App
<ul>
{list.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
);
}
export default App;
依赖项参数说明
useEffect副作用函数的执行时机存在多种情况,根据传入依赖项的不同,会有不同的执行表现
依赖项 | 副作用函数执行时机 |
---|---|
没有依赖项 | 组件初始渲染 + 组件更新时执行(如状态变化等) |
空数组依赖 | 只在初始渲染时执行一次 |
添加特定依赖项 | 组件初始渲染 + 特性依赖项变化时执行 |
import { useEffect, useState } from "react";
function App() {
// 1. 没有依赖项:初始 + 组件更新
const [count, setCount] = useState(0)
// 改变count会触发,组件发生变化都会触发
useEffect(() => {
console.log('没有依赖项的副作用函数')
})
// 2. 传入空数组依赖:初始执行一次
// 只在初始执行一次
useEffect(() => {
console.log('空数组的副作用函数')
},[])
// 3. 传入特定依赖项:初始 + 依赖项变化时执行
const [name,setName] = useState('Tom')
// 改变name会触发,同时也会触发第一个useEffect
useEffect(() => {
console.log('传入特定依赖项的副作用函数')
},[name])
return (
<div className="App">
this is App
<button onClick={() => setCount(count + 1)}>+{count}</button>
<button onClick={() => setName(name + '+')}>update:{name}</button>
</div>
);
}
export default App;
清除副作用
在useEffect中编写的由渲染本身引起的对接组件外部的操作,社区也经常把它叫做副作用操作,比如在useEffect中开启了一个定时器,我们想在组件卸载时把这个定时器再清理掉,这个过程就是清理副作用
说明:清除副作用的函数最常见的执行时机是在组件卸载时自动执行
import { useEffect, useState } from "react";
function Son(){
// 在初始执行一次
// 实现副作用操作逻辑
useEffect(() => {
const timer = setInterval(() => {
console.log('定时器执行中...')
},1000)
return () => {
// 在组件卸载时执行
// 清除副作用逻辑
clearInterval(timer)
}
},[])
return (
<div>
this is Son
</div>
)
}
function App() {
const [show,setShow] = useState(true)
return (
<div>
this is App
{show && <Son/>}
<button onClick={() => setShow(false)}>销毁Son组件</button>
</div>
);
}
export default App;
自定义Hook实现
概念:zidingyiHook是以use打头的函数,通过自定义Hook函数可以用来实现逻辑的封装和复用
实现步骤:
- 声明一个以use打头的函数
- 在函数体内封装可复用的逻辑
- 把组件中要用的状态或函数return出去,以对象或者数组的形式
import {useState } from "react";
// 自定义的Hook
function useToggle(){
const [show,setShow] = useState(true)
function toggle (){
setShow(!show)
}
return {
show,
toggle
}
}
function App() {
const {show,toggle} = useToggle()
return (
<div>
this is App
{show && <h2>显示与隐藏</h2>}
<button onClick={toggle}>切换</button>
</div>
);
}
export default App;
ReactHooks使用规则
使用规则
- 只能在组件中或者其他自定义Hook函数中调用
- 只能在组件的顶层调用,不能嵌套在if、for、其他函数中
import {useState } from "react";
// 错误的用法1:
// useState() 报错
function App() {
if(Math.random > 5){
// 错误的用法2:
// useState() 报错
}
return (
<div>
this is App
</div>
);
}
export default App;
Redux
什么是Redux?
Redux是React最常用的集中状态管理工具,类似于Vue中的Pinia(Vuex),可以独立于框架运行
作用:通过集中管理的方式管理应用的状态
Redux管理数据流程梳理
Redux把整个数据修改的流程分成了三个核心概念,分别是:state、action、reducer
- state - 一个对象 存放着我们管理的数据状态
- action - 一个对象 用来描述你想怎么修改数据
- reducer - 一个函数 根据action的描述生成一个新的state
配置工具
在React中使用redux,官方要求安装两个其他插件 - Redux Toolkit 和 react-redux
- Redux ToolKit (RTK) - 官方推荐编写Redux逻辑的方式,是一套工具的集合集,简化书写方式
- 简化store的配置方式
- 内置immer支持可变式状态修改
- 内置thunk更好的异步创建
- react-redux - 用来链接Redux和React组件的中间件
配置基础环境
- 使用CRA快速创建React项目
npx create-react-app react-redux
- 安装配置工具
npm i @reduxjs/toolkit react-redux
- 启动项目
npm run start
store目录结构设计
- 通常集中状态管理的部分都会单独创建一个单独的‘store’目录
- 应用通常会有很多个子store模块,所以创建一个‘modules’目录,在内部编写业务分类的子store
- store中的入口文件index.js的作用是组合modules中所有的子模块,并导出store
Redux使用步骤
使用React Toolkit创建counterStore
- initialState:仓库,store存放位置
- reducres:修改状态唯一的方法,同步方法
import { createSlice } from "@reduxjs/toolkit"
const countStore = createSlice({
name:'couter',
// 初始化state
initialState:{
count:0
},
// 修改状态的方法 同步方法 支持直接修改
reducers:{
inscrement(state){
state.count++
},
decrement(state){
state.count--
},
addToNum(state,action){
// 传递的参数都在action的payload属性中
state.count = action.payload
}
}
})
// 解构出actionCreater函数
const {inscrement,decrement,addToNum} = countStore.actions
// 获取reducer
const reducer = countStore.reducer
// 以按需导出的方式导出actionCreater
export {inscrement,decrement,addToNum}
// 已默认导出的方式导出reducer
export default reducer
为React注入store
通过react-redux
内置Provider
组件将store实例注入应用中
主要代码:
<Provider store={store}>
<App />
</Provider>
完整代码:
import React from 'react';
import ReactDOM from 'react-dom/client';
import App from './App';
import reportWebVitals from './reportWebVitals';
// 导入store
import store from './store';
import { Provider } from 'react-redux';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
// 注入store
<Provider store={store}>
<App />
</Provider>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
React组件使用store中的数据
通过钩子函数useSelector
把store中的数据映射到组件中
主要代码:
const {count} = useSelector(state => state.counter)
完整代码:
import { useSelector } from "react-redux";
function App() {
// 使用store中的数据
const {count} = useSelector(state => state.counter)
return (
<div className="App">
<span>{count}</span>
</div>
);
}
export default App;
React组件修改store中的数据
通过hook函数useDispatch
生成提交action对象的dispatch函数
主要代码:
// 1.定义reducers:
reducers:{
decrement(state){
state.count--
}
}
// 2:
const dispatch = useDispatch()
// 3:调用reducers
<button onClick={() => dispatch(decrement())}>-</button>
完整代码:
import { useDispatch, useSelector } from "react-redux";
// 导入创建action对象的方法
import { decrement, inscrement } from "./store/modules/counterStore";
function App() {
// 使用store中的数据
const {count} = useSelector(state => state.counter)
// 使用dispatch函数修改store
const dispatch = useDispatch()
return (
<div className="App">
{/* 掉用dispatch提交action对象:修改store */}
<button onClick={() => dispatch(decrement())}>-</button>
<span>{count}</span>
<button onClick={() => dispatch(inscrement())}>+</button>
</div>
);
}
export default App;
提交action传参
在reducers的同步修改方法中添加action对象参数,在调用actionCreater的时候传递参数,参数会被传递到action对象payload属性上
主要语法:
// 1.定义reducers:
reducers:{
addToNum(state,action){
// 传递的参数都在action的payload属性中
state.count = action.payload
}
}
// 2.使用reducers:
const dispatch = useDispatch()
// 3.传递参数:
<button onClick={() => dispatch(addToNum(10))}>add to 10</button>
完整代码:
import { useDispatch, useSelector } from "react-redux";
// 导入创建action对象的方法
import { addToNum } from "./store/modules/counterStore";
function App() {
// 使用store中的数据
const {count} = useSelector(state => state.counter)
// 使用dispatch函数修改store
const dispatch = useDispatch()
return (
<div className="App">
<span>{count}</span>
{/* 掉用dispatch提交action对象:修改store */}
{/* 传递参数 */}
<button onClick={() => dispatch(addToNum(10))}>add to 10</button>
<button onClick={() => dispatch(addToNum(20))}>add to 20</button>
</div>
);
}
export default App;
异步操作Store
- 创建store的写法保持不变,配置好同步修改状态的方法
- 单独封装一个函数,在函数内部return一个函数,在新函数中
- 封装异步请求获取数据
- 调用同步actionCreater传入异步数据生成一个action对象,并使用dispatch提交
- 组件中dispatch的写法保持不变
主要代码:
// 1.定义reducers:
reducers:{
setChannels(state,action){
state.channelList = action.payload
}
}
// 2.在store文件中封装异步请求(channelStore.js):
const fetchChannelList = () => {
// dispatch是一个固定参数
return async dispatch => {
const res = await axios.get('http://geek.itheima.net/v1_0/channels')
dispatch(setChannels(res.data.data.channels))
}
}
// 修改状态:
// 3.使用dispatch函数修改store
const dispatch = useDispatch()
// 4.使用useEffect触发异步请求执行:
useEffect(() => {
dispatch(fetchChannelList())
}, [dispatch])
完整代码:
import { createSlice } from "@reduxjs/toolkit";
import axios from "axios";
const channelStore = createSlice({
name:'channel',
initialState:{
channelList:[]
},
reducers:{
setChannels(state,action){
state.channelList = action.payload
}
}
})
// 异步请求 - 主要代码
const {setChannels} = channelStore.actions
const fetchChannelList = () => {
// dispatch是一个固定参数
return async dispatch => {
const res = await axios.get('http://geek.itheima.net/v1_0/channels')
dispatch(setChannels(res.data.data.channels))
}
}
// 同步代码导出setChannels,异步代码导出fetchChannelList
export {fetchChannelList}
const reducer = channelStore.reducer
export default reducer
import { useDispatch, useSelector } from "react-redux";
import { useEffect } from "react";
// 导入创建action对象的方法
import { fetchChannelList } from "./store/modules/channelStore";
function App() {
// 使用store中的数据
const {channelList} = useSelector(state => state.channel)
// 使用dispatch函数修改store
const dispatch = useDispatch()
// 使用useEffect触发异步请求执行
useEffect(() => {
dispatch(fetchChannelList())
}, [dispatch])
return (
<div className="App">
{/* 渲染异步获取的数据 */}
<ul>
{channelList.map(item => <li key={item.id}>{item.name}</li>)}
</ul>
</div>
);
}
export default App;
Redux调试工具
浏览器插件:Redux DevTools
ReactRouter
什么是前端路由
一个路径path对应一个组件component当我们在浏览器中访问一个path的时候,path对应的组件会在页面中进行渲染
安装react-router-dom
npm i react-router-dom
简单使用
- 创建路由
import Login from "../page/Login"
import Article from "../page/Article"
import {createBrowserRouter} from 'react-router-dom'
// 创建路由
const router = createBrowserRouter([
{
path: '/login',
element: <Login />
},
{
path: '/article',
element: <Article />
}
])
export default router
- 绑定路由
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
import reportWebVitals from './reportWebVitals';
import {RouterProvider} from 'react-router-dom'
// 1. 导入路由
import router from './router';
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
// 2. 绑定路由
<RouterProvider router={router}></RouterProvider>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();
路由懒加载
- 使用React内置的lazy函数进行导入
const Home = lazy(() => import("@/pages/Home"))
- 使用React内置的Suspense组件包裹路由中的element选项对应的组件
<Suspense><Home /></Suspense>
import { createBrowserRouter } from "react-router-dom";
import Layout from "@/pages/Layout";
import AuthRoute from "@/components/AuthRoute";
import { lazy, Suspense } from "react";
// 路由懒加载
// 1. lazy函数对组件进行导入
const Home = lazy(() => import("@/pages/Home"));
const router = createBrowserRouter([
{
path: "/",
element: (
<AuthRoute><Layout />
</AuthRoute>
),
children: [
{
index: true,
element: (
<Suspense><Home /></Suspense> // 2. 使用suspense组件包裹
),
},
],
},
]);
export default router;
路由导航跳转
什么是路由导航
路由系统中的多个路由之间需要进行路由跳转,并且在跳转的同时有可能需要传递参数进行通信
声明式导航
声明式导航是指通过在模块中通过<Link/>
组件描述出要跳转到哪里去,<Link/>
组件会被渲染为浏览器支持的a链接
语法:<Link to="/article">文章</Link>
- to属性:指定跳转的路由path
- 传参:字符串拼接参数,如/login?id=1001&name=bob
import {Link} from 'react-router-dom'
function Login(){
return (
<div>
这是登录页
{/* 声明式导航 */}
<Link to="/article">去文章页</Link>
</div>
)
}
export default Login
编程式导航
编程式导航是指通过useNavigate
钩子得到导航方法,然后通过调用方式以命令式的形式进行路由跳转
语法:
const navigate = useNavigate()
navigate('/article')
import {useNavigate} from 'react-router-dom'
function Login(){
const navigate = useNavigate()
return (
<div>
这是登录页
{/* 编程式导航 */}
<button onClick={() => navigate('/article')}>去文章页</button>
</div>
)
}
export default Login
路由导航传参
searchParams传参
主要代码:
// 传递:
navigate('/article?id=1001&name=jack')
// 接收:
const [params] = useSearchParams()
let id = params.get('id')
let name = params.get('name')
例如:
import {useNavigate} from 'react-router-dom'
function Login(){
const navigate = useNavigate()
return (
<div>
这是登录页
{/* searchParams传参 */}
<button onClick={() => navigate('/article?id=1001&name=jack')}>去文章页</button>
</div>
)
}
export default Login
import { useSearchParams } from "react-router-dom"
function Article(){
// 接收searchParams传参
const [params] = useSearchParams()
let id = params.get('id')
let name = params.get('name')
return (
<div>
这是文章页{id}-{name}
</div>
)
}
export default Article
params传参
注意:params传参需要在路由定义时,添加占位参数
主要代码:
// 路由中定义参数:
const router = createBrowserRouter([
{
path: '/article/:id',
element: <Article />
}
])
// 传递:
navigate('/article/1001')
// 接收:
const params = useParams()
let id = params.id
例如:
import Login from "../page/Login"
import Article from "../page/Article"
import {createBrowserRouter} from 'react-router-dom'
const router = createBrowserRouter([
{
path: '/login',
element: <Login />
},
{
path: '/article/:id',
element: <Article />
}
])
export default router
import {useNavigate} from 'react-router-dom'
function Login(){
const navigate = useNavigate()
return (
<div>
这是登录页
{/* params传参 */}
<button onClick={() => navigate('/article/1001')}>去文章页</button>
</div>
)
}
export default Login
import { useParams } from "react-router-dom"
function Article(){
// 接收params传参
const params = useParams()
let id = params.id
return (
<div>
这是文章页{id}
</div>
)
}
export default Article
嵌套路由配置
什么是嵌套路由
在一级路由中又内嵌了其他路由,这种关系就叫做嵌套路由,嵌套至一级路由内的路由又称作二级路由
实现步骤:
- 使用children属性配置路由嵌套关系
- 使用
<Outlet>
组件配置二级路由渲染位置
const router = createBrowserRouter([
{
path:'/',
element: <Layout/>,
children:[
{
path:'/board',
element: <Board/>
},
{
path: '/about',
element: <About/>
}
]
}
])
import {Outlet,Link} from 'react-router-dom'
function Layout(){
return (
<div>
这是一级路由组件Layout
<Link to="/about">去about页</Link>
<Link to="/board">去board页</Link>
{/* 配置二级路由组件渲染位置 */}
<Outlet/>
</div>
)
}
export default Layout
默认二级路由配置
场景和配置方式:当访问的是一级路由时,默认的二级路由组件可以得到渲染,只需要在二级路由的位置去掉path,设置index属性为true
下例代码解析:当访问localhost:3000时,会渲染Layout组件,同时也会渲染二级组件Board;访问localhost:3000/board时,会报404错误
const router = createBrowserRouter([
{
path:'/',
element: <Layout/>,
children:[
// 设置默认二级路由:一级路由访问时,默认二级路由组件也会渲染
{
index: true,
element: <Board/>
},
{
path: '/about',
element: <About/>
}
]
}
])
404路由配置
场景:当浏览器输入url的路径在整个路由配置中都找不到对应的path,为了用户体验,可以使用404兜底组件进行渲染
实现步骤:
- 准备一个NotFound组件
- 在路由表数组的末尾,以*号作为路由path配置路由
const router = createBrowserRouter([
...code
// 配置404页面
{
path:'*',
element: <NotFound/>
}
])
Hash和History路由模式
常用的路由模式有两种,history模式和hash模式,ReactRouter分别由createBrowerRouter(history路由)
和createHashRouter(hash路由)
函数负责创建
路由模式 | url表现 | 底层原理 | 是否需要后端支持 |
---|---|---|---|
history | url/login | history对象 + pushState事件 | 需要 |
hash | url/#/login | 监听hashChange事件 | 不需要 |
const router = createHashRouter([
{
path: '/login',
element: <Login />
},
{
path: '/article/:id',
element: <Article />
},
])
const router = createBrowserRouter([
{
path: '/login',
element: <Login />
},
{
path: '/article/:id',
element: <Article />
}
])
useReducer
- 定义一个reducer函数(根据不同的action返回不同的新状态)
- 在组件中调用useReducer,并传入reducer函数和状态的初始值
- 事件发生时,通过dispatch函数分派一个action对象(通知reducer要返回哪个新状态并渲染UI)
import { useReducer } from "react";
// 1. 定义一个reducer函数(根据不同的action返回不同的新状态)
function reducer(state, action) {
// 根据不同的action type 返回新的state
switch (action.type) {
case "INC":
return state + 1;
case "DEC":
return state - 1;
case "SET":
return action.payload;
default:
return state;
}
}
function App() {
// 2. 在组件中调用useReducer,并传入reducer函数和状态的初始值
const [state, dispatch] = useReducer(reducer, 0);
return (
<div>
this is App
{/* 3. 通过dispatch函数分派一个action对象(通知reducer要返回哪个新状态并渲染UI) */}
<button onClick={() => dispatch({ type: "DEC" })}>-</button>
{state}
<button onClick={() => dispatch({ type: "INC" })}>+</button>
<button onClick={() => dispatch({ type: "SET", payload: 100 })}>
update
</button>
</div>
);
}
export default App;
useMemo - 不常用
作用:在组件每次重新渲染的时候缓存计算的结果
用途:一般用于消耗非常大的计算
说明:使用useMemo做缓存之后可以保证只有count1依赖项发生变化时才会重新计算
语法:
useMemo(() => {
// 根据count1返回计算的结果
}, [count1])
解析:当我们点击按钮改变count1和count2时,组件App会重新渲染,故fib(count1)
会重新执行
import { useMemo, useState } from "react";
function fib(n) {
console.log("计算函数执行了");
if (n < 3) {
return 1;
}
return fib(n - 1) + fib(n - 2);
}
function App() {
const [count1, setCount1] = useState(0);
const result = fib(count1);
const [count2, setCount2] = useState(0);
console.log("组件重新渲染");
return (
<div>
this is App
<button onClick={() => setCount1(count1 + 1)}>
change count1: {count1}
</button>
<button onClick={() => setCount2(count2 + 1)}>
change count2: {count2}
</button>
{result}
</div>
);
}
export default App;
解析:当我们点击按钮改变count1和count2时,组件App会重新渲染,但由于使用useMemo
API,且依赖项为count1,所以仅在count1发生变化时执行fib函数
import { useMemo, useState } from "react";
function fib(n) {
console.log("计算函数执行了");
if (n < 3) {
return 1;
}
return fib(n - 1) + fib(n - 2);
}
function App() {
const [count1, setCount1] = useState(0);
const result = useMemo(() => {
return fib(count1);
}, [count1]);
const [count2, setCount2] = useState(0);
console.log("组件重新渲染");
return (
<div>
this is App
<button onClick={() => setCount1(count1 + 1)}>
change count1: {count1}
</button>
<button onClick={() => setCount2(count2 + 1)}>
change count2: {count2}
</button>
{result}
</div>
);
}
export default App;
React.memo
作用:运行组件在Props没有改变的情况下跳过渲染
React组件默认的渲染机制:只要父组件重新渲染子组件就重新渲染
说明:经过memo函数包裹生成的缓存组件只有在props发生变化的时候才会重新渲染
语法:
const MemoComponent = memo(function SomeComponent(props) {
// ...
})
解析:点击按钮改变count时,App组件会重新渲染,Son组件也会重新被渲染;但Son组件没有必要重新渲染,因为没有变化
import { useState } from "react";
function Son() {
console.log("son子组件重新渲染了");
return <div>this is son</div>;
}
function App() {
const [count, setCount] = useState(0);
return (
<div>
this is App
<button onClick={() => setCount(count + 1)}>+</button>
{count}
<Son />
</div>
);
}
export default App;
解析:点击按钮改变count时,App组件会重新渲染,MemoSon组件不会重新渲染,因为通过memo包裹的组件在props没有发生变化时,是不会重新渲染的
import { memo, useState } from "react";
const MemoSon = memo(function Son() {
console.log("son子组件重新渲染了");
return <div>this is son</div>;
});
function App() {
const [count, setCount] = useState(0);
return (
<div>
this is App
<button onClick={() => setCount(count + 1)}>+</button>
{count}
<MemoSon />
</div>
);
}
export default App;
React.memo - props的比较机制
机制:在使用memo缓存组件之后,React会对每一个prop使用Object.is比较新值和老值,返回true,表示没有变化
prop是简单类型:Object.is(3,3) => true 没有变化
prop引用类型(对象/数组):Object([],[])=>false 有变化,React只关心引用是否变化
注意:引用类型中,变量其实是该引用类型值在内存中的地址,所以如果地址没有变化,也就代表prop没有变化
解析:传递一个简单类型prop,点击按钮App组件会重新渲染,子组件MemoSon不会重新渲染,因为num没有发生变化
import { memo, useState } from "react";
const MemoSon = memo(function Son({ count }) {
console.log("son子组件重新渲染了");
return <div>this is son -- {count}</div>;
});
function App() {
const [count, setCount] = useState(0);
console.log("App重新渲染");
const num = 10;
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>+</button>
<MemoSon count={num} />
</div>
);
}
export default App;
解析:传递一个引用类型prop,点击按钮App组件会重新渲染,子组件MemoSon也会重新渲染,看似list变量没有发生变化,实际上在App组件重新渲染时,会在内存中新建一个[0,1,2]
值的引用地址,即list的值的引用地址发生了改变,所以导致MemoSon组件重新渲染
import { memo, useState } from "react";
const MemoSon = memo(function Son({ list }) {
console.log("son子组件重新渲染了");
return <div>this is son -- {list}</div>;
});
function App() {
const [count, setCount] = useState(0);
console.log("App重新渲染");
const list = [0, 1, 2];
return (
<div>
{count}
<button onClick={() => setCount(count + 1)}>+</button>
<MemoSon list={list} />
</div>
);
}
export default App;
useCallback
作用:使用useCallback包裹函数之后,函数可以保证在App组件重新渲染的时候保持引用稳定
语法:
const func = useCallback(() => {
// ...
}, []);
解析:点击button后App会重新渲染,当App重新渲染时,由于changeHandler是函数(引用类型),引用地址会发生变化,且作为prop传递给了Input组件,故Input组件也会重新渲染
import { memo, useState } from "react";
const Input = memo(({ onChange }) => {
console.log("子组件重新渲染了");
return <input onChange={(e) => onChange(e.target.value)} />;
});
function App() {
console.log("App重新渲染");
const [count, setCount] = useState(0);
// 当App重新渲染时,由于changeHandler函数(引用类型)引用地址会发生变化,
// 且作为prop传递给了Input组件,故Input组件也会重新渲染
const changeHandler = (value) => {
console.log(value);
};
return (
<div>
<Input onChange={changeHandler} />
<button onClick={() => setCount(count + 1)}>{count}</button>
</div>
);
}
export default App;
解决:通过useCallback包裹函数,保持引用稳定
import { memo, useCallback, useState } from "react";
const Input = memo(({ onChange }) => {
console.log("子组件重新渲染了");
return <input onChange={(e) => onChange(e.target.value)} />;
});
function App() {
console.log("App重新渲染");
const [count, setCount] = useState(0);
// 通过useCallback包裹函数保持引用稳定,在App重新渲染之后,
// changehandler引用地址保持稳定,不会发生变化
const changeHandler = useCallback((value) => {
console.log(value);
}, []);
return (
<div>
<Input onChange={changeHandler} />
<button onClick={() => setCount(count + 1)}>{count}</button>
</div>
);
}
export default App;
React - forwardRef
作用:在父组件中通过ref获取子组件内部的元素
语法:forwardRef
api中的ref是固定写法、位置
const Input = forwardRef((props, ref) => {
return <input type="text" ref={ref} />;
});
const inputRef = useRef(null);
<Input ref={inputRef} />
inputRef.current.focus()
比如:
import { forwardRef, useRef } from "react";
const Input = forwardRef((props, ref) => {
return <input type="text" ref={ref} />;
});
function App() {
const inputRef = useRef(null);
const getInput = () => {
console.log(inputRef.current.focus());
};
return (
<div>
<Input ref={inputRef} />
<button onClick={getInput}>获取input</button>
</div>
);
}
export default App;
React - useImperativeHandle
作用:通过ref调用子组件内部的方法
语法:
const Component = forwardRef((props, ref) => {
const func = () => {
// ...
};
useImperativeHandle(ref, () => {
return {
func,
};
});
return <div></div>
});
const sonRef = useRef(null);
<Component ref={sonRef} />
sonRef.current.func();
比如:
import { forwardRef, useImperativeHandle, useRef } from "react";
const Son = forwardRef((props, ref) => {
const inputRef = useRef(null);
const focusHandler = () => {
inputRef.current.focus();
};
useImperativeHandle(ref, () => {
return {
focusHandler,
};
});
return <input type="text" ref={inputRef} />;
});
function App() {
const sonRef = useRef(null);
const focusHandler = () => {
sonRef.current.focusHandler();
};
return (
<div>
<Son ref={sonRef} />
<button onClick={focusHandler}>聚焦</button>
</div>
);
}
export default App;
Class类组件(不推荐,官方停止更新,推荐使用Hooks写法)
基础结构
- 通过类属性state定义状态数据
- 通过setState方法来修改状态数据
- 通过render来写UI模板(JSX语法一致)
- 在类组件中十分依赖this关键字,如访问state、调用事件等
class Counter extends Component {
// 定义状态变量 类似于 useState()
state = {
count: 0,
};
// 事件回调
clickHandler = () => {
// 修改状态数据
this.setState({
count: this.state.count + 1,
});
};
// UI模板(JSX)
render() {
return <button onClick={this.clickHandler}>{this.state.count}</button>;
}
}
export default Counter
生命周期函数
概念:组件从创建到销毁的各个阶段自动执行的函数就是生命周期函数
- componentDidMount:组件挂载完毕自动执行,一般用于异步数据获取等
- componentWillUnmount:组件卸载时自动执行,一般用于清除副作用等,如清除定时器
import { Component, useState } from "react";
class Son extends Component {
// 挂载
componentDidMount() {
console.log("son componentDidMount");
// 设置定时器
this.timer = setInterval(() => {
console.log("son timer");
}, 1000);
}
// 卸载
componentWillUnmount() {
console.log("son componentWillUnmount");
// 卸载定时器
clearInterval(this.timer);
}
render() {
return <div>this is Son</div>;
}
}
function App() {
const [show, setShow] = useState(true);
return (
<div>
{show && <Son />}
<button onClick={() => setShow(false)}>unmount</button>
</div>
);
}
export default App;
组件通信
概念:类组件和Hooks编写的组件在组件通信的思想完全一致
- 父传子:通过prop绑定数据
- 子传父:通过prop绑定父组件中的函数,子组件调用
- 兄弟通信:状态提升,通过父组件做桥接
import { Component } from "react";
class Son extends Component {
render() {
return (
<div>
this is Son - {this.props.name}
<button onClick={() => this.props.onGetSonName("wangwu")}>
wangwu
</button>
</div>
);
}
}
class Parent extends Component {
state = {
name: "zhangsan",
};
getSonName = (sonName) => {
this.setState({
name: sonName,
});
};
render() {
return (
<div>
this is parent
<Son name={this.state.name} onGetSonName={this.getSonName} />
</div>
);
}
}
export default Parent;