赞
踩
npx create-react-app (文件名)
不用安装脚手架npm自动安装后删除
import {createRoot} from 'react-dom/client';
const container = document.getElementById('root');
//html 创建根标签
const root = createRoot(container);
root.render(<App/>);
组件默认会作为父组件的后代渲染到页面中,但有些情况下,这种方式会带来一些问题。
通过portal可以将组件渲染到页面中的指定位置。
在index.html中添加标签:
<div id='backdrop'></div>
修改Backdrop组件:
const backdropDom=document.getElementBuId('backdrop');
const Backdrop=()=>{
return ReactDom.createPortal(<div>//想要渲染的组件</div>,backdropDom)
}
如果没有类名冲突的问题,外部CSS样式表不失为是一种非常好的编写样式的方式。为了解决这个问题React中还为我们提供了一中方式,CSS Module。
我们可以将CSS Module理解为外部样式表的一种进化版,它的大部分使用方式都和外部样式表类似,不同点在于使用CSS Module后,网页中元素的类名会自动计算生成并确保唯一,所以使用CSS Module后,我们再也不用担心类名重复了!
CSS Module 在React中已经默认支持了(前提是使用了react-scripts),所以无需再引入其他多余的模块。使用CSS Module时需要遵循以下几个步骤:
xxx.module.css
。import XXX from 'xxx.module.css
。xxx.yyy
的形式来设置。代码演示:
//StyleDemo.module.css
.myDiv{
color:red;
...
}
//StyleDemo.js
import styles from './StyleDemo.module.css';
const StyleDemo = ()=>{
return (<div className={styles.myDiv}>测试</div>)
}
在React中,JSX必须有且只有一个根元素。这就导致了在有些情况下我们不得不在子元素的外部添加一个额外的父元素。
在React中已经为我们提供好了一个线程的组件帮助我们完成这个工作,这个组件可以通过React.Fragment使用。
引入React的方式,直接使用Fragment。
import React,{Fragment} from 'react;
const MyComponent = () =>{
return (
<Fragment> <div>1</div> <div>2</div> </Fragment>
)
};
export default MyComponent
在React中为我们提供了一种更加便捷的方式,直接使用<></>代替Fragment更加简单:
import React from 'react';
const MyComponent = () => {
return (
<>
<div>我是组件1</div>
<div>我是组件2</div>
</>
);
};
export default MyComponent;
在React中组件的数据通信是通过props进行的,父组件给子组件设置props,props在组件间自上向下(父传子)的逐层传递数据。但并不是所有的数据都适合这中传递方式,有些数据需要在多个组件中共同使用,props就显得麻烦。
Context为我们提供了一种在不同组件间共享数据的方式:
在外层组件中统一设置,设置后内层所有的组件都可以访问到Context中所存储的数据。Context类似JS中的全局作用域,可以将一些贡藕给你数据设置到一个同一个Context中,是所有组件都可以访问到这些数据。
const MyContext=Reat.createContext(defaultValue);```
## 实际应用
创建公共TestContext.js
```javascript
import React from 'react';
const TestContext= React.createContext({
name:'名字',age:18,sex:'未知',suaHello:()=>{alert(this.name)}
})
export default TestContext;
通过value来指定Context中的数据 该组件中的所有组件都可以听过Context 获取指定的数据。
当通过Context获取书据时回读取最近Provider
中的数据 没有Provider,则会读取默认数据。
import TestContext fomr './XXX/TestContest';
const XX =()=>{
return(
<>
<XX/> //名字 18
<TestContext.Provider value={{name:'小明',age:28}}>
<XX/> // 小明 28
<TestContext.Provider value={{name:'小红',age:38}}>
<XX/> //小红 38
</TestContext.Provider>
</TestContext.Provider>
</>
)
}
使用方式方式一 类组件和函数组件都能用:
Consumer
的标签体 需要一个回调函数,它会将context设置为回调函数的参数通过参数获取值。import TestContext from './XXX/TestContext';
const XX =()=>{
return(
<TestContext.Consumer>
{
(ctx)=>{
return<div>{ctx.name}-{ctx.age}</div>
}
}
</TestContext.Consumer>
)
}
使用方式二 函数组件用
import TestContext from './XX/TestContext';
import {useContext} from 'react';
const XX=()=>{
const ctx = useContext(TestContext);
return(
<div>
{ctx.name}-{ctx.age}
</div>
)
}
export default XX;
Too many re-renders
当我们在函数体中调用setState时,就会触发上述错误。
问题:新state值和就只想同时,不是不会触发组建的重新渲染吗
setSstate()的执行流程(函数组件)
setState() ->dispathSetData()
->会先判断 组件当时处于什么阶段
渲染阶段
如果是渲染阶段->不会检查state是否相同(渲染阶段不能调用setState())
非渲染阶段
如果非渲染阶段->会检查state是否相同
->如果值不通,则对组件重新渲染
->值相同,不会重新渲染
React组件有部分逻辑都可以直接编写到组建的函数体中,相对数组调用的filter、map等方法,像是判断某个组件是否显示等。但是有一部分逻辑如果直接写在函数体中,会影响到组建的渲染,这部分会产生“副作用”的代码,是一定不能直接卸载函数体中的。
例如:如果直接将修改state的逻辑编写到了组件之中,就会导致组件不断渲染,直至调用次数过多内存溢出。
便携React组件时,我们要极力的避免组件中出现那些会产生“副作用”的代码。同时,如果使用了React严格模式,使用了React.StrictMode标签,那么React会智能的价差你的组件是否有副作用的代码。
React不会自动替你发现发作用。React的严格模式,在开发模式下,会主动重复调用一些函数,使副作用显现。所以处于开发模式React严格模式下,这些函数会调用两次。
类组件:constructor
,render
和shouldComponentUpdate
方法,函数组件的静态方法getDerivedStateFromProps
。
函数组件: 函数组件的函数体
参数为函数的 setState
、userState
、useMemo
和userReducer
。
重复的调用会使副作用更容易凸显出来,你可以尝试着在函数组件的函数体中调用一个console.log
你会发现它会执行两次,如果你的浏览器中安装了React Developer Tools,第二次调用会显示为灰色。
为了解决副作用问题React专门提供了钩子函数useEffect()
,用来专门处理不能直接写在组件内部的代码。
userEffect(didUpdate);//以函数作为参数
useEffect(()=>{
//会产生副作用的代码
})
useEffect()
中的回调函数会在组件每次渲染完毕之后执行,这也是它和写在函数体中代码的最大的不同,函数体中的代码会在组件渲染前执行,而useEffect()
中的代码是在组件渲染后才执行,这就避免了代码的执行影响到组件渲染。
通过使用这个Hook,我设置了React组件在渲染后所要执行的操作。React会将我们传递的函数保存(我们称这个函数为effect),并且在DOM更新后执行调用它。React会确保effect每次运行时,DOM都已经更新完毕。
组件每次重新渲染effect都会执行,有一些情况,两次effect执行会互相影响。
比如:在effect中设置了一个定时器,总不能酶促effect执行都设置一个新的定时器,所以我们需要在一个effect执行前,清除掉前一个effect所带来的影响。要实现这个功能,可以在effect中将一个函数作为返回值返回。
useEffect(()=>{
//副作用代码
return()=>{
//这个函数会在下次effect执行前调用
}
})
effect返回的函数,会在下一次effect执行前调用
,我们可以在这个函数中清除掉前一次effect执行所带来的影响。
组件每次渲染effect都会执行,似乎不总是需要。因此在useEffect()
中我们可以限制effect的执行时机,在useEffect()
中可以将一个数组作为第二参数传递:
useEffect(()=>{...},[a,b])
示例中,数组中有两个变量a和吧,设置以后effect只有在变量a或b发生变化才会执行。这样即可限制effect的执行次数,也可以直接传递一个空数组,如果是空数组,那么effect只会执行一次。
在React的函数组件中,我们可以通过useState()来创建state。这种创建state的方式会给我们返回两个东西state和setState()。state用来读取数据,而setState()用来设置修改数据。但是这种方式也存在着一些不足,因为所有的修改state的方式都必须通过setState()来进行,如果遇到一些复杂度比较高的state时,这种方式似乎就变得不是那么的优雅。
为了解决复杂State
带来的不便,React
为我们提供了一个新的使用State
的方式。Reducer
横空出世,reduce单词中文意味减少,而reducer我觉得可以翻译为“当你的state的过于复杂时,你就可以使用的可以对state进行整合的工具”。当然这是个玩笑话,个人认为Reducer
可以翻译为“整合器”,它的作用就是将那些和同一个state
相关的所有函数都整合到一起,方便在组件中进行调用。
当然工具都有其使用场景,Reducer
也不例外,它只适用于那些比较复杂的state
,对于简单的state
使用Reducer
只能是徒增烦恼。但是由于初学,我们会先用一个简单的案例来对其进行演示,实际应用我们后边会以cartData
作为演示。
const [state, dispatch] = useReducer(reducer, initialArg, init); //reducer 是一个整合函数 //当前state的所有操作都应该在此函数中定义 //该函数的返回值,会成为state的新值 //initialArg 是相当于state初始值 const [state,stateDispatch] = userReducer((state,action)=>{ if(action.type=='add'){ return state+1 }else if(action.type==='sub'){ return state-1 } },1) <div>{state}</div> <button onClick={()=>stateDispatch({type:'add'})}>+</button> <button onClick={()=>stateDispatch({type:'sub'})}>-</button>
React组件会在两种情况下发生重新渲染。第一种,当组件自身的state发生变化时。第二种,当组件的父组件重新渲染时。第一种情况下的重新渲染无可厚非,state都变了,组件自然应该重新进行渲染。但是第二种情况似乎并不是总那么的必要
React为我们提供了一个方法React.memo()
。该方法是一个高阶函数,可以用来根据组件的props对组件进行缓存,当一个组件的父组件发生重新渲染,而子组件的props没有发生变化时,它会直接将缓存中的组件渲染结果返回而不是再次触发子组件的重新渲染,这样一来就大大的降低了子组件重新渲染的次数。
app.js
const App = () => { const [count, setCount] = useState(1); const clickHandler = () => { setCount(prevState => prevState + 1); }; return ( <div> <h2>App -- {count}</h2> <button onClick={clickHandler}>增加</button> <A/> </div> ); };
A.js
const A = () => { const [count, setCount] = useState(1); const clickHandler = () => { setCount(prevState => prevState + 1); }; return ( <div> <h2>组件A -- {count}</h2> <button onClick={clickHandler}>增加</button> <B/> </div> ); }; export default A;
B.js
const B = () => {
return (
<div>
<h2>组件B</h2>
</div>
);
};
export default B;
三个组件的引用关系为,A组件是App的子组件、B组件是A组件的子组件:App –> A –> B
当App组件发生重新渲染时,A和B组件都会发生重渲染。当A组件重新渲染时,B组件也会重新渲染。B组件中没有state,甚至连props都没有设置。换言之,B组件无论如何渲染,每次渲染的结果都是相同的,虽然重渲染并不会应用到真实DOM上,但很显然这种渲染是完全没有必要的。
为了减少像B组件这样组件的渲染,React为我们提供了一个方法React.memo()
。该方法是一个高阶函数,可以用来根据组件的props对组件进行缓存,当一个组件的父组件发生重新渲染,而子组件的props没有发生变化时,它会直接将缓存中的组件渲染结果返回而不是再次触发子组件的重新渲染,这样一来就大大的降低了子组件重新渲染的次数。
现在对上述案例中的B组件进行如下修改:
const B = () => {
console.log('B渲染');
return (
<div>
<h2>组件B</h2>
</div>
);
};
export default React.memo(B);
修改后的代码中,并没有直接将B组件向外导出,而是在B组件外层套了一层函数React.memo()
,这样一来,返回的B组件就增加了缓存功能,只有当B组件的props属性发生变化时,才会触发组件的重新渲染。memo只会根据props判断是否需要重新渲染,和state和context无关,state或context发生变化时,组件依然会正常的进行重新渲染
上述钩子函数存在副作用 当APP.js 传入函数给A.js 时 刷新APP.js 组件,A.js 也会随之刷新。
useCallback(callback[,arr])
//参数
//1.回调函数
//2.依赖数组
//-当依赖数组的参数发生变化时,回调函数才会重新创建
//-如果不指定依赖数组,回到函数每次都会重新创建
const clickHandler = useCallback(()=>{
//操作
},[]) //arr可以为空 如setState 不会变的需要传入空数组
警告需要传入参数
const clickHandler = useCallback(()=>{
setCount(prvState=>prvState+num)
setNum(prvState=>prvState+1)
},[num]) //需要num依赖 num是更新的 如果不传入num不会更新
封装请求钩子
APP.js
随着练习功能的增多,我们编写的React代码变得越来越复杂。像是上节课中我编写的React代码,仅仅是增加了一个加载数据的功能,我们就需要向App.js中引入了三个state和一个钩子函数:
const [data, setData] = useState([]);
const fetchData = () => {
//操作各种
};
useEffect(()=>{
fetchData();
}, []);
src/hooks/useFetch.js
import {useEffect, useState} from "react";
const useFetch = (url) => {
const [data, setData] = useState([]);
async function fetchData(){
setLoading(false);
}
}
return {data, fetchData};
};
export default useFetch;
修改后使用 APP.js
const {data:stuData, fetchData} = useFetch();
useEffect(()=>{
fetchData();
}, [])
A Predictable State Container for JS Apps
A Predictable State Container for JS Apps是Redux官方对于Redux的描述,这句话可以这样翻译“一个专为JS应用设计的可预期的状态容器”,简单来说Redux是一个可预测的状态容器。
state直译过来就是状态,使用React这么久了,对于state我们已经是非常的熟悉了。state不过就是一个变量,一个用来记录(组件)状态的变量。组件可以根据不同的状态值切换为不同的显示,比如,用户登录和没登录看到页面应该是不同的,那么用户的登录与否就应该是一个状态。再比如,数据加载与否,显示的界面也应该不同,那么数据本身就是一个状态。换句话说,状态控制了页面的如何显示。
但是需要注意的是,状态并不是React中或其他类似框架中独有的。所有的编程语言,都有状态,所有的编程语言都会根据不同的状态去执行不同的逻辑,这是一定的。所以状态是什么,状态就是一个变量,用以记录程序执行的情况。
容器当然是用来装东西的,状态容器即用来存储状态的容器。状态多了,自然需要一个东西来存储,但是容器的功能却不是仅仅能存储状态,它实则是一个状态的管理器,除了存储状态外,它还可以用来对state进行查询、修改等所有操作。(编程语言中容器几乎都是这个意思,其作用无非就是对某个东西进行增删改查)
可预测指我们在对state进行各种操作时,其结果是一定的。即以相同的顺序对state执行相同的操作会得到相同的结果。简单来说,Redux中对状态所有的操作都封装到了容器内部,外部只能通过调用容器提供的方法来操作state,而不能直接修改state。这就意味着外部对state的操作都被容器所限制,对state的操作都在容器的掌控之中,也就是可预测。
总的来说,Redux是一个稳定、安全的状态管理器。
使用Redux之前,你需要先明确一点Redux是JS应用的状态容器,它并不是只能在React使用,而是可以应用到任意的JS应用中(包括前端JS,和服务器中Node.js)。总之,凡是JS中需要管理的状态的Redux都可以胜任。
我们先来在网页中使用以下Redux,在网页中使用Redux就像使用jQuery似的,直接在网页中引入Redux的库文件即可:
<script src="https://unpkg.com/redux@4.2.0/dist/redux.js"></script>
Redux是一个状态容器,所以使用Redux必须先创建容器对象,它的所有操作都是通过容器对象来进行的,创建容器的方式有多种,我们先说一种好理解的:
Redux.createStore(reducer, [preloadedState], [enhancer])
createStore用来创建一个Redux中的容器对象,它需要三个参数:reducer、preloadedState、enhancer。
reducer是一个函数,是state操作的整合函数,每次修改state时都会触发该函数,它的返回值会成为新的state。
preloadedState就是state的初始值,可以在这里指定也可以在reducer中指定。
enhancer增强函数用来对state的功能进行扩展,暂时先不理它。
三个参数中,只有reducer是必须的,来看一个Reducer的示例:
const countReducer = (state = {count:0}, action) => {
switch (action.type){
case 'ADD':
return {count:state.count+1};
case 'SUB':
return {count:state.count-1};
default:
return state
}
};
reducer用来整合关于state的所有操作,容器修改state时会自动调用该函数,函数调用时会接收到两个参数:state和action,state表示当前的state,可以通过该state来计算新的state。state = {count:0}
这是在指定state的默认值,如果不指定,第一次调用时state的值会是undefined。也可以将该值指定为createStore()的第二个参数。action是一个普通对象,用来存储操作信息。
将reducer传递进createStore后,我们会得到一个store对象:
const store = Redux.createStore(countReducer);
store对象创建后,对state的所有操作都需要通过它来进行:
读取state:
store.getState()
修改state:
store.dispatch({type:'ADD'})
dipatch用来触发state的操作,可以将其理解为是想reducer发送任务的工具。它需要一个对象作为参数,这个对象将会成为reducer的第二个参数action,需要将操作信息设置到对象中传递给reducer。action中最重要的属性是type,type用来识别对state的不同的操作,上例中’ADD’表示增加操作,’SUB’表示减少的操作。
除了这些方法外,store还拥有一个subscribe方法,这个方法用来订阅state变化的信息。该方法需要一个回调函数作为参数,当store中存储的state发生变化时,回调函数会自动调用,我们可以在回调函数中定义state发生变化时所要触发的操作:
store.subscribe(()=>{
// store中state发生变化时触发
console.log(store.getState()) //每次变化都会获取最新的值
});
const btn01 = document.getElementById('btn01'); const btn02 = document.getElementById('btn02'); const counterSpan = document.getElementById('counter'); //默认值必填 const countReducer = (state = {count:0}, action) => { switch (action.type){ case 'ADD': return {count:state.count+1}; case 'ADD_N': return {count:state.count+action.payLoad}; default: return state } }; //默认值必填 const store = Redux.createStore(countReducer,1) const store = Redux.createStore(countReducer); store.subscribe(()=>{ counterSpan.innerText = store.getState().count; }); btn01.addEventListener('click', ()=>{ store.dispatch({type:'SUB'}); }); btn02.addEventListener('click', ()=>{ store.dispatch({type:'ADD_N',payLoad:5}); });
修改后的代码相较于第一个版本要复杂一些,同时也解决了之前代码中存在的一些问题:
通过上例也不难看出,Redux中最最核心的东西就是这个store,只要拿到了这个store对象就相当于拿到了Redux中存储的数据。在加上Redux的核心思想中有一条叫做“单一数据源”,也就是所有的state都会存储到一课对象树中,并且这个对象树会存储到一个store中。所以到了React中,组件只需获取到store即可获取到Redux中存储的所有state
npm install -S redux react-redux
或
yarn add redux react-redux
创建reducer:
const reducer = (state = { name: '孙悟空', age: 18, gender: '男', address: '花果山' }, action) => { switch (action.type) { case 'SET_NAME': return { ...state, name: action.payload }; case 'SET_AGE': return { ...state, age: action.payload }; case 'SET_ADDRESS': return { ...state, address: action.payload }; case 'SET_GENDER': return { ...state, gender: action.payload }; default : return state } };
reducer的编写和之前的案例并没有本质的区别,只是这次的数据和操作方法变得复杂了一些。以SET_NAME为例,当需要修改name属性时,dispatch需要传递一个有两个属性的action,action的type应该是字符串”SET_NAME”,payload应该是要修改的新名字,比如要将名字修改为猪八戒,则dispatch需要传递这样一个对象{type:'SET_NAME',payload:'猪八戒'}
。
创建store:
const store = createStore(reducer);
创建store和前例并无差异,传递reducer进行构建即可。
设置provider:
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Provider store={store}>
<App/>
</Provider>
);
创建store后,需要引入react-redux中提供的Provider组件,将其设置到所有组件的最外层,并且将刚刚创建的store设置为组件的store属性,只有这样才能使得Redux中的数据能被所有的组件访问到。
访问数据:
const stu = useSelector(state => state);
react-redux还为我们提供一个钩子函数useSelector,用于获取Redux中存储的数据,它需要一个回调函数作为参数,回调函数的第一个参数就是当前的state,回调函数的返回值,会作为useSelector的返回值返回,所以state => state
表示直接将整个state作为返回值返回。现在就可以通过stu来读取state中的数据了:
<p>
{stu.name} -- {stu.age} -- {stu.gender} -- {stu.address}
</p>
操作数据:
const dispatch = useDispatch();
useDispatch同样是react-redux提供的钩子函数,用来获取redux的派发器,对state的所有操作都需要通过派发器来进行。
通过派发器修改state:
dispatch({type:'SET_NAME', payload:'猪八戒'})
dispatch({type:'SET_AGE', payload:28})
dispatch({type:'SET_GENDER', payload:'女'})
dispatch({type:'SET_ADDRESS', payload:'高老庄'})
完整代码:
import ReactDOM from 'react-dom/client'; import {Provider, useDispatch, useSelector} from "react-redux"; import {createStore} from "redux"; const reducer = (state = { name: '孙悟空', age: 18, gender: '男', address: '花果山' }, action) => { switch (action.type) { case 'SET_NAME': return { ...state, name: action.payload }; case 'SET_AGE': return { ...state, age: action.payload }; case 'SET_ADDRESS': return { ...state, address: action.payload }; case 'SET_GENDER': return { ...state, gender: action.payload }; default : return state } }; const store = createStore(reducer); const App = () =>{ const stu = useSelector(state => state); const dispatch = useDispatch(); return <div> <p> {stu.name} -- {stu.age} -- {stu.gender} -- {stu.address} </p> <div> <button onClick={()=>{dispatch({type:'SET_NAME', payload:'猪八戒'})}}>改name</button> <button onClick={()=>{dispatch({type:'SET_AGE', payload:28})}}>改age</button> <button onClick={()=>{dispatch({type:'SET_GENDER', payload:'女'})}}>改gender</button> <button onClick={()=>{dispatch({type:'SET_ADDRESS', payload:'高老庄'})}}>改address</button> </div> </div> }; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <div> <Provider store={store}> <App/> </Provider> </div> );
上边的案例的写法存在一个非常严重的问题!将所有的代码都写到一个reducer中,会使得这个reducer变得无比庞大,现在只有学生和学校两个信息。如果数据在多一些,操作方法也会随之增多,reducer会越来越庞大变得难以维护。
Redux中是允许我们创建多个reducer的,所以上例中的reducer我们可以根据它的数据和功能进行拆分,拆分为两个reducer,像是这样:
const stuReducer = (state = { name: '孙悟空', age: 18, gender: '男', address: '花果山' }, action) => { switch (action.type) { case 'SET_NAME': return { ...state, name: action.payload }; case 'SET_AGE': return { ...state, age: action.payload }; case 'SET_ADDRESS': return { ...state, address: action.payload }; case 'SET_GENDER': return { ...state, gender: action.payload }; default : return state; } }; const schoolReducer = (state = { name: '花果山一小', address: '花果山大街1号' }, action) => { switch (action.type) { case 'SET_SCHOOL_NAME': return { ...state, name: action.payload }; case 'SET_SCHOOL_ADDRESS': return { ...state, address: action.payload }; default : return state; } };
修改后reducer被拆分为了stuReducer和schoolReducer,拆分后在编写每个reducer时,只需要考虑当前的state数据,不再需要对无关的数据进行复制等操作,简化了reducer的编写。于此同时将不同的功能编写到了不同的reducer中,降低了代码间的耦合,方便对代码进行维护。
拆分后,还需要使用Redux为我们提供的函数combineReducer将多个reducer进行合并,合并后才能传递进createStore来创建store。
const reducer = combineReducers({
stu:stuReducer,
school:schoolReducer
});
const store = createStore(reducer);
combineReducer需要一个对象作为参数,对象的属性名可以根据需要指定,比如我们有两种数据stu和school,属性名就命名为stu和school,stu指向stuReducer,school指向schoolReducer。读取数据时,直接通过state.stu读取学生数据,通过state.school读取学校数据。
完整代码:
import ReactDOM from 'react-dom/client'; import {Provider, useDispatch, useSelector} from "react-redux"; import {combineReducers, createStore} from "redux"; const stuReducer = (state = { name: '孙悟空', age: 18, gender: '男', address: '花果山' }, action) => { switch (action.type) { case 'SET_NAME': return { ...state, name: action.payload }; case 'SET_AGE': return { ...state, age: action.payload }; case 'SET_ADDRESS': return { ...state, address: action.payload }; case 'SET_GENDER': return { ...state, gender: action.payload }; default : return state; } }; const schoolReducer = (state = { name: '花果山一小', address: '花果山大街1号' }, action) => { switch (action.type) { case 'SET_SCHOOL_NAME': return { ...state, name: action.payload }; case 'SET_SCHOOL_ADDRESS': return { ...state, address: action.payload }; default : return state; } }; const reducer = combineReducers({ stu:stuReducer, school:schoolReducer }); const store = createStore(reducer); const App = () => { const stu = useSelector(state => state.stu); const school = useSelector(state => state.school); const dispatch = useDispatch(); return <div> <p> {stu.name} -- {stu.age} -- {stu.gender} -- {stu.address} </p> <div> <button onClick={() => { dispatch({type: 'SET_NAME', payload: '猪八戒'}); }}>改name </button> <button onClick={() => { dispatch({type: 'SET_AGE', payload: 28}); }}>改age </button> <button onClick={() => { dispatch({type: 'SET_GENDER', payload: '女'}); }}>改gender </button> <button onClick={() => { dispatch({type: 'SET_ADDRESS', payload: '高老庄'}); }}>改address </button> </div> <hr/> <p> {school.name} -- {school.address} </p> <div> <button onClick={() => { dispatch({type: 'SET_SCHOOL_NAME', payload: '高老庄小学'}); }}>改学校name </button> <button onClick={() => { dispatch({type: 'SET_SCHOOL_ADDRESS', payload: '高老庄中心大街15号'}); }}>改学校address </button> </div> </div>; }; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <div> <Provider store={store}> <App/> </Provider> </div> );
上边的案例我们一直在使用Redux核心库来使用Redux,除了Redux核心库外Redux还为我们提供了一种使用Redux的方式——Redux Toolkit。它的名字起的非常直白,Redux工具包,简称RTK。RTK可以帮助我们处理使用Redux过程中的重复性工作,简化Redux中的各种操作。
安装,无论是RTK还是Redux,在React中使用时react-redux都是必不可少,所以使用RTK依然需要安装两个包:react-redux和@reduxjs/toolkit。
npm install react-redux @reduxjs/toolkit -S
yarn add react-redux @reduxjs/toolkit
action是reducer中的第二个参数,当我们通过dispatch向reducer发送指令时需要手动创建action对象并传递。action中常见的属性有两个一个是type用来指定操作的类型,一个是payload用来指定要传递的数据。
RTK为我们提供了一个方法createAction,用来帮助我们创建action。
createAction(type, prepareAction?)
它的第一个参数为type,用来指定action中的type属性。第二个参数可选先忽略它。它的返回值是一个函数。我们可以这么调用:
conconst setName= createAction('ADD');
setName(); // {type: 'ADD', payload: undefined}
setName('猪八戒'); // {type: 'ADD', payload: '猪八戒'}
返回值的函数我们可以调用,调用该函数后会得到一个对象,这个对象有两个属性type和payload,type属性值就是我们调用createAction传递的第一个参数,上例中type就是’ADD’。而payload属性就是我们调用该函数时传递的参数。
const add = createAction('SET_NAME');
add(); // {type: 'SET_NAME', payload: undefined}
add('猪八戒'); // {type: 'SET_NAME', payload: '猪八戒'}
简单说,createAction会返回一个函数,这个函数可以用来创建固定type属性值的对象,并且这个函数的第一个参数会成为新建对象的payload属性值。
可以通过creatAction修改之前的项目:
先创建四个action函数:
const setName = createAction('SET_NAME');
const setAge = createAction('SET_AGE');
const setAddress = createAction('SET_ADDRESS');
const setGender = createAction('SET_GENDER');
修改dispatch
dispatch(setName('猪八戒'));
dispatch(setAge(28));
dispatch(setGender('女'));
dispatch(setAddress('高老庄'));
createAction返回函数所创建的对象结构是固定的{type:'xxx', payload:...}
,我们也可以通过向createAction传递第二个参数来指定payload的格式:
const add = createAction('ADD', (name, age, gender, address) => {
return {
payload:{
name,
age,
gender,
address
}
}
});
add('沙和尚', 38, '男', '流沙河'); // {"type":"ADD","payload":{"name":"沙和尚","age":38,"gender":"男","address":"流沙河"}}
该方法用来是创建reducer的工具方法。
createReducer(initialState, builderCallback)
参数:
initialState
—— state的初始值
builderCallback
—— 带有builer的回调函数,可以同builer来设置reducer的逻辑
回调函数中会传递一个builder作为参数,通过通过builder可以将action和函数进行绑定,使用时可以通过传递指定的action来触发函数的调用。
builder有一个常用的方法addCase,addCase需要两个参数,第一个参数为action,第二个参数为回调函数。action直接传递通过createAction所创建的函数即可,第二个参数是一个回调函数,回调函数类似于reducer,第一个参数为state,第二个参数为action。但又和reducer不同,该回调函数中返回的state是一个代理对象,可以直接对该对象修改,RTK会自动完成其余操作。
示例:
// 创建action const setName = createAction('setName'); // 创建reducer const stuReducer = createReducer({ name: '孙悟空', age: 18, gender: '男', address: '花果山' }, builder => { // 通过builder将action和回调函数进行绑定 builder.addCase(setName, (state, action) => { // 这里的state是代理对象,可以直接对其进行修改 state.name = action.payload; }); } ); // 配置reducer const store = configureStore({ reducer: { stu: stuReducer, school: schoolReducer } }); // 发送指令修改name属性 dispatch(setName('猪八戒'));
无论是createAction和createReducer都不是RTK中的常用方式(要是这么写代码,可能得疯)。介绍他们只是希望你能了解一下RTK的运行方式。对于我们来创建reducer时最最常用的方式是:createSlice。
createSlice是一个全自动的创建reducer切片的方法,在它的内部调用就是createAction和createReducer,之所以先介绍那两个也是这个原因。createSlice需要一个对象作为参数,对象中通过不同的属性来指定reducer的配置信息。
createSlice(configuration object)
配置对象中的属性:
initialState
—— state的初始值
name
—— reducer的名字,会作为action中type属性的前缀,不要重复
reducers
—— reducer的具体方法,需要一个对象作为参数,可以以方法的形式添加reducer,RTK会自动生成action对象。
示例:
const stuSlice= createSlice({
name:'stu',
initialState:{
name: '孙悟空',
age: 18,
gender: '男',
address: '花果山'
},
reducers:{
setName(state, action){
state.name = action.payload
}
}
});
createSlice返回的并不是一个reducer对象而是一个slice对象(切片对象)。这个对象中我们需要使用的属性现在有两个一个叫做actions,一个叫做reducer。
切片对象会根据我们对象中的reducers方法来自动创建action对象,这些action对象会存储到切片对象actions属性中:
stuSlice.actions; // {setName: ƒ}
上例中,我们仅仅指定一个reducer,所以actions中只有一个方法setName,可以通过解构赋值获取到切片中的action。
const {setName} = stuSlice.actions;
开发中可以将这些取出的action对象作为组件向外部导出,导出其他组件就可以直接导入这些action,然后即可通过action来触发reducer。
切片的reducer属性是切片根据我们传递的方法自动创建生成的reducer,需要将其作为reducer传递进configureStore的配置对象中以使其生效:
const store = configureStore({
reducer: {
stu: stuSlice.reducer,
school: schoolReducer
}
});
总的来说,使用createSlice创建切片后,切片会自动根据配置对象生成action和reducer,action需要导出给调用处,调用处可以使用action作为dispatch的参数触发state的修改。reducer需要传递给configureStore以使其在仓库中生效。
完整代码:
import ReactDOM from 'react-dom/client'; import {Provider, useDispatch, useSelector} from "react-redux"; import {configureStore, createSlice} from "@reduxjs/toolkit"; const stuSlice = createSlice({ name: 'stu', initialState: { name: '孙悟空', age: 18, gender: '男', address: '花果山' }, reducers: { setName(state, action) { state.name = action.payload; }, setAge(state, action) { state.age = action.payload; }, setGender(state, action) { state.gender = action.payload; }, setAddress(state, action) { state.gender = action.payload; } } }); const {setName, setAge, setGender, setAddress} = stuSlice.actions; const schoolSlice = createSlice({ name: 'school', initialState: { name: '花果山一小', address: '花果山大街1号' }, reducers: { setSchoolName(state, action) { state.name = action.payload; }, setSchoolAddress(state, action) { state.address = action.payload; } } }); const {setSchoolName, setSchoolAddress} = schoolSlice.actions; const store = configureStore({ reducer: { stu: stuSlice.reducer, school: schoolSlice.reducer } }); const App = () => { const stu = useSelector(state => state.stu); const school = useSelector(state => state.school); const dispatch = useDispatch(); return <div> <p> {stu.name} -- {stu.age} -- {stu.gender} -- {stu.address} </p> <div> <button onClick={() => { dispatch(setName('猪八戒')); }}>改name </button> <button onClick={() => { dispatch(setAge(28)); }}>改age </button> <button onClick={() => { dispatch(setGender('女')); }}>改gender </button> <button onClick={() => { dispatch(setAddress('高老庄')); }}>改address </button> </div> <hr/> <p> {school.name} -- {school.address} </p> <div> <button onClick={() => { dispatch(setSchoolName('高老庄中心小学')); }}>改学校name </button> <button onClick={() => { dispatch(setSchoolAddress('高老庄中心大街15号')); }}>改学校address </button> </div> </div>; };
store/index.js
import {configureStore, createSlice} from "@reduxjs/toolkit"; const stuSlice = createSlice({ name: 'stu', initialState: { name: '孙悟空', age: 18, gender: '男', address: '花果山' }, reducers: { setName(state, action) { state.name = action.payload; }, setAge(state, action) { state.age = action.payload; }, setGender(state, action) { state.gender = action.payload; }, setAddress(state, action) { state.gender = action.payload; } } }); export const {setName, setAge, setGender, setAddress} = stuSlice.actions; const schoolSlice = createSlice({ name: 'school', initialState: { name: '花果山一小', address: '花果山大街1号' }, reducers: { setSchoolName(state, action) { state.name = action.payload; }, setSchoolAddress(state, action) { state.address = action.payload; } } }); export const {setSchoolName, setSchoolAddress} = schoolSlice.actions; const store = configureStore({ reducer: { stu: stuSlice.reducer, school: schoolSlice.reducer } }); export default store
index.js
import ReactDOM from 'react-dom/client';
import App from "./App";
import {Provider} from "react-redux";
import store from "./store";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<div>
<Provider store={store}>
<App/>
</Provider>
</div>
);
XX.js
import { useDispatch, useSelector} from "react-redux"; import {setName,setAge,....} from "./store"; const App = () => { const stu = useSelector(state => state.stu); const school = useSelector(state => state.school); const dispatch = useDispatch(); return <div> <p> {stu.name} -- {stu.age} -- {stu.gender} -- {stu.address} </p> <div> <button onClick={() => { dispatch(setName('猪八戒')); }}>改name </button> <button onClick={() => { dispatch(setAge(28)); }}>改age </button> <button onClick={() => { dispatch(setGender('女')); }}>改gender </button> <button onClick={() => { dispatch(setAddress('高老庄')); }}>改address </button> </div> <hr/> <p> {school.name} -- {school.address} </p> <div> <button onClick={() => { dispatch(setSchoolName('高老庄中心小学')); }}>改学校name </button> <button onClick={() => { dispatch(setSchoolAddress('高老庄中心大街15号')); }}>改学校address </button> </div> </div>; }; export default App
srote -stuSlice.js
-school.js
-index.js
import { createSlice} from "@reduxjs/toolkit";
const stuSlice = createSlice({...})
export const {setName} =stuSlice.actions
export const {stuSlice:stuReducer}=stuSlice
import { createSlice} from "@reduxjs/toolkit";
const schoolSlice = createSlice({...})
export const {schoolSlice}=schoolSlice
export const {setName} =schoolSlice.actions
import stuReducer from './stuSlice';
import schoolSlice from './school'
const store = configureStore({
reducer: {
stu: stuReducer.reducer,
school: schoolSlice.reducer
}
});
export default store
XX.js
import {setName as schoolSetName} from './store/school'
import {setName} from './store/stuSlice'
import store from './store/index'
const {...,...} =useSelecter(state=>state)
RTK不仅帮助我们解决了state的问题,同时,它还为我们提供了RTK Query用来帮助我们处理数据加载的问题。RTK Query是一个强大的数据获取和缓存工具。在它的帮助下,Web应用中的加载变得十分简单,它使我们不再需要自己编写获取数据和缓存数据的逻辑。
Web应用中加载数据时需要处理的问题:
这些问题,RTKQ都可以帮助我们处理。首先,可以直接通过RTKQ向服务器发送请求加载数据,并且RTKQ会自动对数据进行缓存,避免重复发送不必要的请求。其次,RTKQ在发送请求时会根据请求不同的状态返回不同的值,我们可以通过这些值来监视请求发送的过程并随时中止。
RTKQ已经集成在了RTK中,如果我们已经在项目中引入了RTK则无需再引入其余的模块。如果你不想使用RTKQ给我们提供的发送请求的方式(简单封装过的fetch),你还需要引入一下你要使用的发送请求的工具。
RTKQ中将一组相关功能统一封装到一个Api对象中,比如:都是学生相关操作统一封装到StudentApi中,关于班级的相关操作封装到ClassApi中。接下来,我们尝试创建一个简单的Api,至于数据还是我们之前所熟悉的学生数据:
studentApi.js
import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/dist/query/react"; export const studentApi = createApi({ reducerPath:'studentApi', baseQuery:fetchBaseQuery({ baseUrl:'http://localhost:1337/api/' }), endpoints(build) { return { getStudents: build.query({ //查询 query() { return 'students' }, transformResponse(baseQueryReturnValue){ return baseQueryReturnValue.data }//修改 返回值格式 }), addStudents: build.mutation({ //添加 query(stu) { return { url:`students`, method:'post' body:{datastu.attributes} } }, }), editStudents: build.mutation({ //修改 query(stu) { return { url:`students/${stu.id}`, method:'put', body:{datastu.attributes} } }, }), delStudents: build.mutation({ //删除 query(id) { return { url:`students/${id}`, method:'delete' } }, }), } } }); export const {useGetStudentsQuery} = studentApi;
上例是一个比较简单的Api对象的例子,我们来分析一下,首先我们需要调用createApi()
来创建Api对象。这个方法在RTK中存在两个版本,一个位于@reduxjs/toolkit/dist/query
下,一个位于@reduxjs/toolkit/dist/query/react
下。react目录下的版本会自动生成一个钩子,方便我们使用Api。如果不要钩子,可以引入query下的版本,当然我不建议你这么做。
createApi()
需要一个配置对象作为参数,配置对象中的属性繁多,我们暂时介绍案例中用到的属性:
reducerPath
用来设置reducer的唯一标识,主要用来在创建store时指定action的type属性,如果不指定默认为api。
baseQuery
用来设置发送请求的工具,就是你是用什么发请求,RTKQ为我们提供了fetchBaseQuery作为查询工具,它对fetch进行了简单的封装,很方便,如果你不喜欢可以改用其他工具,这里暂时不做讨论。
fetchBaseQuery
简单封装过的fetch调用后会返回一个封装后的工具函数。需要一个配置对象作为参数,baseUrl表示Api请求的基本路径,指定后请求将会以该路径为基本路径。配置对象中其他属性暂不讨论。
endpoints
Api对象封装了一类功能,比如学生的增删改查,我们会统一封装到一个对象中。一类功能中的每一个具体功能我们可以称它是一个端点。endpoints用来对请求中的端点进行配置。
endpoints是一个回调函数,可以用普通方法的形式指定,也可以用箭头函数。回调函数中会收到一个build对象,使用build对象对点进行映射。回调函数的返回值是一个对象,Api对象中的所有端点都要在该对象中进行配置。
对象中属性名就是要实现的功能名,比如获取所有学生可以命名为getStudents,根据id获取学生可以命名为getStudentById。属性值要通过build对象创建,分两种情况:
查询:build.query({})
增删改:build.mutation({})
例如:
getStudents: build.query({
query() {
return 'students'
}
}),
先说query,query也需要一个配置对象作为参数(又他喵的是配置对象)。配置对象里同样有n多个属性,现在直说一个,query方法。注意不要搞混两个query,一个是build的query方法,一个是query方法配置对象中的属性,这个方法需要返回一个子路径,这个子路径将会和baseUrl拼接为一个完整的请求路径。比如:getStudets的最终请求地址是:
http://localhost:1337/api/`+`students`=`http://localhost:1337/api/students
可算是介绍完了,但是注意了这个只是最基本的配置。RTKQ功能非常强大,但是配置也比较麻烦。不过,熟了就好了。
上例中,我们创建一个Api对象studentApi,并且在对象中定义了一个getStudents方法用来查询所有的学生信息。如果我们使用react下的createApi,则其创建的Api对象中会自动生成钩子函数,钩子函数名字为useXxxQuery或useXxxMutation,中间的Xxx就是方法名,查询方法的后缀为Query,修改方法的后缀为Mutation。所以上例中,Api对象中会自动生成一个名为useGetStudentsQuery的钩子,我们可以获取并将钩子向外部暴露。
export const {useGetStudentsQuery} = studentApi;
Api对象的使用有两种方式,一种是直接使用,一种是作为store中的一个reducer使用。store是我们比较熟悉的,所以先从store入手。
import {configureStore} from "@reduxjs/toolkit";
import {studentApi} from "./studentApi";
import {setupListeners} from "@reduxjs/toolkit/query";
export const store = configureStore({
reducer:{
[studentApi.reducerPath]:studentApi.reducer
},
middleware:getDefaultMiddleware =>
getDefaultMiddleware().concat(studentApi.middleware),//中间件缓存用
});
setupListeners(XX.dispatch) //useQuery参数时用 设置之后才会生效
创建store并没有什么特别,只是注意需要添加一个中间件,这个中间件已自动生成了我们直接引入即可,中间件用来处理Api的缓存。
store创建完毕同样要设置Provider标签,这里不再展示。接下来,我们来看看如果通过studentApi发送请求。由于我们已经将studentApi中的钩子函数向外部导出了,所以我们只需通过钩子函数即可自动加载到所有的学生信息。比如,现在在App.js中加载信息可以这样编写代码:
import React from 'react'; import {useGetStudentsQuery} from './store/studentApi'; const App = () => { const {data, isFetching, isSuccess} = useGetStudentsQuery(); return ( <div> {isFetching && <p>数据正在加载...</p>} {isSuccess && data.data.map(item => <p key={item.id}> {item.attributes.name} -- {item.attributes.age} -- {item.attributes.gender} -- {item.attributes.address} </p>)} </div> ); }; export default App;
直接调用useGetStudentsQuery()它会自动向服务器发送请求加载数据,并返回一个对象。这个对象中包括了很多属性:
import React from 'react'; import {useGetStudentsQuery} from './store/studentApi'; const App = () => { const {data, isFetching, isSuccess} = useGetStudentsQuery(null,{ /* 第一个参数会传给Query, 第二个参数是一个对象,通过对象对请求进行配置 , 1.过滤筛选等操作 selectFromResult:result=> result, 2.设置轮询间隔,单位毫秒,0表示不轮询 隔一段时间发一次请求 用于时效性高的数据 pollingInterval:0, 3.设置是否跳过当前的请求 修改添加组件多功能 添加不需要传参 就需要跳过 skip:false, 4.设置是否每次都重新加载数据 默认值是false , true不用缓存 可以传数字表示有效期 refetchOnMountOrArgChange:false, 5.是否在重新获取焦点时刷新数据 需要设置setupListeners(XX.despatch) refetchOnFocus:false, 6.是否在网络重连时刷新数据 需要设置setupListeners(XX.despatch) refetchOnReconnect:false, */ }); return ( <div></div> ); }; export default App;
export const studentApi = createApi({ reducerPath:'studentApi', baseQuery:fetchBaseQuery({ baseUrl:'http://localhost:1337/api/' }), endpoints(build) { return { getStudents: build.query({ query() { return 'students' }, keepUnuseDataFor:0 //设置缓存时间,毫秒 默认60s //*时间内操作数据不会失效,超过了重新请求 }), } } }); export const {useGetStudentsQuery} = studentApi;
因为get缓存问题 可以设置标签 使标签失效可以重新获取数据
import {createApi, fetchBaseQuery} from "@reduxjs/toolkit/dist/query/react"; export const studentApi = createApi({ reducerPath:'studentApi', baseQuery:fetchBaseQuery({ baseUrl:'http://localhost:1337/api/' }), endpoints(build) { return { getStudents: build.query({ //查询 query() { return 'students' }, /* 由于改变数据 需要刷新列表所以配置id让列表刷新 providesTags:[{type:'student',id:'List'}] 待验证 可以加多个标签 providesTags:['student','All']让操作数据同时失效 All ((result,error,id)=>[{type:'student',id},'All']) */ providesTags:[{type:'student',id:'List'}] //可设置多个标签 providesTags:['student','All'] }), /* 由于失效 查询会重置所有单条查询 所以 providesTags可以传一个对象作为参数 指定某条数据失效重新加载 invalidatesTags:[{type:'student',id:id}] 但获取不到id 所以还可以传一个回调函数 providesTags:((result,error,id)=>[{type:'student',id}]), */ getStudents: build.query({ //查询单条 query(id) { return '`students/${id}`' providesTags:['student'] }), addStudents: build.mutation({ //添加 query(stu) { return { url:`students`, method:'post' body:{datastu.attributes} } }, invalidatesTags:[{type:'student',id:'List'}] //使标签失效 }), editStudents: build.mutation({ //修改 query(stu) { return { url:`students/${stu.id}`, method:'put', body:{datastu.attributes} } }, invalidatesTags:((result,error,id)=>[{type:'student',id},{type:'student',id:'List'}]) //使标签失效 }), delStudents: build.mutation({ //删除 query(id) { return { url:`students/${id}`, method:'delete' } }, invalidatesTags:((result,error,id)=>[{type:'student',id},{type:'student',id:'List'}]) //使标签失效 }), } } }); export const {useGetStudentsQuery} = studentApi;
使用React这些工具所编写的项目通常都是单页应用(SPA)。单页应用中,整个应用中只含有一个页面,React会根据不同的状态在应用中显示出不同的组件。但是我们之前所编写应用还存在着一个问题,整个应用只存在一个页面,一个请求地址,这就使得用户只能通过一个地址访问应用,当我们点击组件中的不同链接时应用的地址是不会发生变化的。这又有什么问题呢?由于应用只有一个地址,所以我们通过该地址访问应用时,总会直接跳转到应用的首页。如此一来,我们便不敢随意的刷新页面,因为一旦刷新页面便直接跳转到首页。在对页面进行分享时,也只能分享网站的首页,而不能分享指定的页面。
怎么办呢?难道我们要将一个页面拆分为多个页面吗?很明显不能这么做,这么做以后应用的跳转便脱离了React的控制,增加应用的复杂度,提高了项目维护的成本。
为了解决这个问题,我们需要引入一个新的工具React Router,React Router为我们提供一种被称为客户端路由的东西,通过客户端路由可以将URL地址和React组件进行映射,当URL地址发生变化时,它会根据设置自动的切换到指定组件。并且这种切换完全不依赖于服务器。换句话说,在用户看来浏览器的地址栏确实发生了变化,但是这一变化并不由服务器处理,而是通过客户端路由进行切换。
React Router最新版本为6,版本6和版本5之间的变化跨度比较大,我们的课程会分别讲解两个版本。
安装:
npm
npm install react-router-dom@5 -S
yarn
yarn add react-router-dom@5
import ReactDOM from "react-dom/client"; import {BrowserRouter, Link, Route, Switch} from "react-router-dom"; const Home = () => { return <div>这是首页</div>; }; const About = () => { return <div>关于我们,其实没啥可说的</div> }; const App = () => { return <div> <ul> <li> <Link to="/home">首页</Link> </li> <li> <Link to="/about">关于</Link> </li> </ul> <Switch> <Route path="/home" component={Home} /> <Route path="/about" component={About} /> </Switch> </div>; }; const root = ReactDOM.createRoot(document.getElementById('root')); root.render( <BrowserRouter> <App/> </BrowserRouter> );
react router适用于web和原生项目,我们在web项目中使用,所以需要引入的包是react-router-dom。
和Redux类似,要使得路由生效,需要使用Router组件将App组件包裹起来。这里我们选择的是BrowserRouter,除了BrowserRouter外还有其他的Router,暂时我们只介绍BrowserRouter。
案例中,BrowserRouter我们是这样使用的:
import {BrowserRouter, Link, Route, Switch} from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<BrowserRouter>
<App/>
</BrowserRouter>
);
实际开发中,也可以为BrowserRouter起一个别名Router,这样一来我们在切换Router时,只需要修改引用位置,而不需要修改其他代码,像是这样:
import {BrowserRouter as Router, Link, Route, Switch} from "react-router-dom";
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<Router>
<App/>
</Router>
);
route组件是路由的映射组件,通过该组件将url地址和React组件进行映射,映射后当url地址变为指定地址时指定的组件就会显示,否则不显示。
<Route path="/home" component={Home} />
<Route path="/about" component={About} />
上例中,路径/home
和<Home/>
组件进行了映射,路径/about
和<About/>
组件进行了映射。当访问http://localhost:3000/about
时,about组件会自动渲染显示,访问http://localhost:3000/home
时,home组件会自动渲染显示。
Route组件可以设置以下几个属性
path
用来设置要映射的路径,可以是一个字符串或字符串数组。字符串用来匹配单个路径,数组可以匹配多个路径。看一个数组的例子:
<Route path={["/about", "/hello"]}>
<About/>
</Route>
使用数组映射后,当我们访问数组中的路径时都会使组件挂载。设置路径时也可以在路径中设置参数,比如:/student/:id
其中id就是一个参数,可以动态的传递:id
的值,换句话说/student/1
或/student/2
,都会触发组件挂载。
设置动态参数后,在组件的内部可以使用useParams()
钩子来读取参数:
const Student = () => {
const {id} = useParams();
return <div>学生id:{id}</div>
};
...略...
<Route path="/student/:id">
<Student/>
</Route>
...略...
exact
路由的匹配默认并不是完整匹配,这意味着路由映射的地址是/home
时,只要我们访问的路径是以/home
开头就会触发组件的挂载,默认情况下是不会检查路由的子路径的。比如:/home/hello
、/home/abc
等都会导致home组件挂载。
exact属性用来设置路由地址是否完整匹配,它需要一个布尔值,默认为false,就像上边的情况。如果设置为true,那么只有地址和path完全一致时,组件才会挂载。
<Route path="/home" exact>
<Home/>
</Route>
这样一来只有访问/home
时,home组件才会挂载,差一个字母都不行哦!
strict
布尔值,默认值为false。false时,会匹配到以/
结尾的路径。比如:path设置为/home
默认情况下/home/
也会导致组件挂载。设置为true时,以/
结尾的路径不会被匹配。
component
设置路径匹配后需要挂载的组件。作用和Route的标签体类似。
<Route path="/home" component={Home}/>
和标签体指定组件不同,如果通过component属性指定组件,React Router会自动向组件中传递三个参数match、location和history。
match
对象,表示请求匹配的路径信息,其中包含四个属性:
location
对象,表示浏览器地址栏的信息,请求完整路径、查询字符串等,可能具有的属性:
history
对象,用来读取和操作浏览器的历史记录(页面跳转)等功能,属性:
render
render也是Route组件中的属性,和component类似,也用来指定路径匹配后需要挂载的组件。只是render需要的是一个回调函数作为参数,组件挂载时,render对应的回调函数会被调用,且函数的返回值会成为被挂载的组件。render的回调函数中会接收到一个对象作为参数,对象中包含三个属性,即match、location和history,我们可以根据需要选择是否将其传递给组件。
<Route path="/student/:id" render={routeProps => <Student {...routeProps}/>} />
children
children实际上就是组件的组件体,设置方式有两种一个是通过组件体设置,一个是通过children属性设置。它的值也有两种方式,一种直接传递组件,这样当路径匹配时组件会自动挂载。一种是传递一个回调函数,这样它和render的特点是一样的。
直接设置组件:
<Route path="/student/:id" children={<Student/>} />
<Route path="/student/:id">
<Student/>
</Route>
传递回调函数:
<Route path="/student/:id" children={routeProps => <Student {...routeProps}/>} />
<Route path="/student/:id">
{routeProps => <Student {...routeProps}/>}
</Route>
需要注意的时,当children接收到的是一个回调函数时,即使路径没有匹配组件也会被挂载到页面中(没有使用Switch标签的情况下),这一特性可以在一些特殊应用场景下发挥作用。如果不希望出现路径不匹配时组件被挂载的情况,最好选择使用render来代替。
Switch组件是Route组件的外部容器,可以将Route组件放入到Switch组件中。放入Switch组件中后,匹配路径时会自动自上向下对Route进行匹配,如果匹配到则挂载组件,并且一个Switch中只会有一个Route被挂载。如果将Route组件单独使用,那么所有的路径匹配的Route中的组件都会被挂载。
Link组件作用类似于a标签(超链接),并且Link组件在浏览器中也会被渲染为超链接。但是Link组件生成的链接点击后只会修改浏览器地址栏的url,并不会真的向服务器发送请求。这种方式有利于组件的渲染,所以在开发中应该使用Link组件而不是超链接。
除了BrowserRouter以外,react router中还为我们提供了HashRouter,它是干什么用的呢?其实很简单,当我们使用BrowserRouter时,路径会直接根据url地址进行跳转,也就是我们在使用应用时在浏览器的地址栏看到的地址就和我们正常去访问网页一样。
但是,HashRouter不是这样,使用HashRouter时,组件的跳转不再是以完整的url形式,而是通过url地址中的hash值进行跳转(url地址中#后的内容为hash值)。
BrowserRouter的地址栏
HashRouter的地址栏
为什么会有这两种Router呢?首先,你要明确我们的项目在开发完成后需要进行构建,构建后的代码需要放到服务器中,以供用户访问。服务器无非就是Nginx或Apache这些东西,服务器的主要功能是将url地址和网页进行映射。传统web项目中,每一个页面都对应一个文件,当用户访问/index.html时,服务器会自动返回根目录下的index.html。当用户访问/about.html时,服务器会返回根目录下about.html。换句话说url和文件的映射都是由服务器来完成的。
但是React项目不同,React项目所有的页面都是通过React进行渲染构建的。项目中只存在一个index.html没有那么多的页面(所以才叫单页应用)。当浏览器地址发生变化时,比如用户访问/about时,此时是不需要服务器介入的,react router会自动挂载对应的组件。
当我们将React项目部署到服务器时,如果直接访问根目录,请求会直接发送给index.html。这个页面我们是有的,所以此时不会有任何问题。用户访问页面后,点击页面后的连接切换到不同的组件也没有问题,因为页面并没有真的发生跳转,而是通过react router在内存中完成了模拟跳转。但是,当我们刷新某个路由或直接通过浏览器地址栏访问某个路由时,比如:http://localhost:3000/about,此时请求会发送给服务器,服务器会寻找名为about的资源(此时并没有经过React)。显然找不到这个资源,于是返回404。
这样一来,我们的项目只能够通过首页访问,然后点击链接跳转,刷新和直接通过路由访问都是不行的,一旦进行这些操作就会出现404。
怎么办呢?两种解决方式:
nginx.conf
中的配置信息修改如下:location / {
root html;
try_files $uri /index.html;
}
两种方式都可以解决404的问题,具体采用那种方案,需要根据你自己项目的实际情况选择。
特殊版本的Link,可以根据不同的情况设置不同的样式。
属性:
prompt组件可以在用户离开页面前弹出提示。
属性:
将请求重定向到一个新的位置,经常用来进行权限的处理。例如:当用户已经登录时则正常显示组件,用户没有登录时则跳转到登录页面。
{isLogin && <SomeAuthComponent/>}
{!isLogin && <Redirect to={"/login"}></Redirect>}
上例中,如果isLogin的值为true,表示用户已经登录,若用户登录,则挂载对应组件。若isLogin值为false,则挂载Redirect组件触发重定向,重定向会使得路径跳转到登录页面。
属性:
安装:
npm
npm install react-router-dom@6 -S
yarn
yarn add react-router-dom@6
import React from ‘react’; import ReactDOM from ‘react-dom/client’; import { BrowserRouter as Router, Link, Route, Routes } from ‘react-router-dom’; const Home = ()=>{ return <div>首页</div> }; const About = () => { return <div>关于</div> }; const App = () => { return <div>App <ul> <li> <Link to=”/”>home</Link> </li> <li> <Link to=”/about”>about</Link> </li> </ul> <Routes> <Route path=”/” element={<Home/>}/> <Route path=”/about” element={<About/>}/> </Routes> </div>; }; const root = ReactDOM.createRoot(document.getElementById(‘root’)); root.render( <Router> <App /> </Router> );
和版本5不同,6中的Route组件不能单独使用,而是必须要放到Routes组件中。简言之Routes就是一个存放Route的容器。
Route作用和版本5的一样,只是变得更简单了,没有了那么多复杂的属性,并且Route组件必须放到Routes中,当浏览器的地址发生变化时,会自动对Routes中的所有Route进行匹配,匹配到的则显示,其余Route则不再继续匹配。可以将Route当成是一个类似于if语句的东西,路径(path)匹配则其中的组件便会被渲染。
Outlet组件用来在父级路由中挂载子路由。
在版本6中Route组件是可以嵌套的,可以通过嵌套Route来构建出嵌套路由,像这样:
<Route path='/students' element={<StudentList/>}>
<Route path=':id' element={<Student/>}/>
</Route>
上例中,Route嵌套后,如果访问/students
则会挂载StudentList组件,如果访问/students/:id
则会自动在StudentList组件中对Student组件进行挂载。在StudentList组件中就可以使用Outlet来引用这些被挂载的组件。
const StudentList = () => {
return <div>
学生列表
<Outlet/>
</div>
};
和版本5的类似。
和版本5的类似。
类似于版本5中的Redirect组件,用来跳转页面。
1<BrowserRouter>用于包裹整个应用(通常为App组件) 2<HashRouter> 说明:作用与BrowserRouter一样,但<HashRouter>修改的是地址栏的hash值 router6.X和router5.X相同, 3<Routes />和<Route /> v6版本移除了先前的<Switch >,引入了新的替代者:<Routes> <Routes>包裹<Route>配合使用, <Route>相当于一个 if 语句,如果其路径与当前URL匹配,则呈现其对应的组件 <Route caseSensitive>属性用于指 定:匹配时是否区分大小写,默认false <Route>也可以嵌套使用,配合useRoutes()配置路由表,但需要通过Outlet组件来渲染其子路由 4<LINK>使用to参数来描述需要定位的页面。它的值既可是字符串,也可以是location对象(包含pathname、search、hash、与state属性)如果其值为字符串,将会被转换为location对象。 5<NavLink>与<Link>类似,且可实现导航的“高亮”效果 6<Navigate> 作用:只要<Navigate>组件被渲染,就会修改路径,切换视图 replace属性用于控制跳转模式(push或是replace,默认push) 7<Outlet> 当<Outlet>产生嵌套时,渲染其对应的后续子路由
1.useRotes() 根据路由表,动态创建<Routes>和<Route> 2.useNavigate() 作用:返回一个函数用来实现编程式导航 const navigate = useNavigate() navigate('/login',{ replace:false, state:{a:1,b:2} }) 第一种使用方式,指定具体的路径, navigate( -1) 第二种使用方式,传入数值就行前进或后退,类似于5.X中的history.go( )方法, 3.useParams() 作用:回当前匹配路由的params参数,类似于5.x中的math.paramas 4.useSearchParams() 作用:用于读取和修改当前位置的URL中的查询字符串 返回一个包含两个值的数组,内容分别为:当前search参数,更新search的函数 const [search,setSearch] = useSearchParams();const id search.get('id');用search通过get方法需要单独取出 5.useLocation() 作用:获取当前location信息,对标5.X中的路由组件的location属性 const x=useLocation();console.log(x);//{hash:"",key:"aaa",search:"?name=zs&age=18",state:{a:1,b:2} } 6.useMatch() 作用:返回当前匹配信息,对标5.X中的路由组件的match属性。 const match = useMatch( '/login/:x/:y' ) console.log(match) { params:{x:'1',y:'10'},pathname:"/LoGin/1/10",pathnameBase:"/LoGin/1/10",pattern:{path:'/login/:x/:y',caseSensitive:false,end:false} } 7.useInRouterContext处于路由的上下文当中会返回布尔值true 8.useNavigation Type() 判断路由跳转方式 作用:返回当前的导航类型(用户如何来到当前页的) 返回值:POP(浏览器刷新页面、在浏览器直接打开了这个路由组件)、PUSH、REPLACE 9.useOutlet() 用来呈现当前组件中要渲染的嵌套路由 const result = useOutlet();console.log(result) 如果嵌套路由没有挂载,则返回null;如果嵌套路由已挂载,则展示嵌套的路由对象 10.useResolvedPath() 截取路由内的路径和参数 作用:给定一个URL值,解析其中的:path\search\hash值
1.路由表统一管理路由:二级路由path后不用加/,直接写名称
2.需求如果是点击二级路由后一级路由设置成不高亮的话可以给App内标签<NavLink end>添加end属性
3.页面中to后边的路由可以不用加/,直接写地址
4.react-router5和6内都有Link和NavLink,点击后---才能变成<a />标签,6内有了一个新的Navigate,有机会渲染到页面上才做更新
关于React中的钩子函数,我们已经非常熟悉了。钩子函数的功能非常的强大,而它的使用又十分简单。关于钩子函数的使用,我们只需记住两点:
useMemo和useCallback十分相似,useCallback用来缓存函数对象,useMemo用来缓存函数的执行结果。在组件中,会有一些函数具有十分的复杂的逻辑,执行速度比较慢。闭了避免这些执行速度慢的函数返回执行,可以通过useMemo来缓存它们的执行结果,像是这样:
const result = useMemo(()=>{
return 复杂逻辑函数();
},[依赖项])
useMemo中的函数会在依赖项发生变化时执行,注意!是执行,这点和useCallback不同,useCallback是创建。执行后返回执行结果,如果依赖项不发生变化,则一直会返回上次的结果,不会再执行函数。这样一来就避免复杂逻辑的重复执行。
在React中可以通过forwardRef来指定要暴露给外部组件的ref:
const MyButton = forwardRef((props, ref) => {
return <button ref={ref}>自定义按钮</button>
});
上例中,MyButton组件将button的ref作为组件的ref向外部暴露,其他组件在使用MyButton时,就可以通过ref属性访问:
<MyButton ref={btnRef}/>
通过useImperativeHandle可以手动的指定ref要暴露的对象,比如可以修改MyButton组件如下:
const MyButton = forwardRef((props, ref) => {
useImperativeHandle(ref,()=> {
return {
name:'孙悟空'
};
});
return <button>自定义按钮</button>
});
useImperativeHandle的第二个参数是一个函数,函数的返回值会自动赋值给ref(current属性)。上例中,我们将返回值为{name:'孙悟空'}
,当然返回孙悟空没有什么意义。实际开发中,我们可以将一些操作方法定义到对象中,这样可以有效的减少组件对DOM对象的直接操作。
const MyButton = forwardRef((props, ref) => { const btnRef = useRef(); useImperativeHandle(ref,()=> { return { setDisabled(){ btnRef.current.disabled = true; } }; }); return <button ref={btnRef}>自定义按钮</button> }); const App = () => { const btnRef = useRef(); const clickHandler = () => { btnRef.current.setDisabled(); }; return <div> <MyButton ref={btnRef}/> <button onClick={clickHandler}>点击</button> </div>; };
useLayoutEffect的方法签名和useEffect一样,功能也类似。不同点在于,useLayoutEffect的执行时机要早于useEffect,它会在DOM改变后调用。在老版本的React中它和useEffect的区别比较好演示,React18中,useEffect的运行方式有所变化,所以二者区别不好演示。
useLayoutEffect使用场景不多,实际开发中,在effect中需要修改元素样式,且使用useEffect会出现闪烁现象时可以使用useLayoutEffect进行替换。
用来给自定义钩子设置标签,标签会在React开发工具中显示,用来调试自定义钩子,不常用。
useDeferredValue用来设置一个延迟的state,比如我们创建一个state,并使用useDeferredValue获取延迟值:
const [queryStr, setQueryStr] = useState('');
const deferredQueryStr = useDeferredValue(queryStr);
上边的代码中queryStr就是一个常规的state,deferredQueryStr就是queryStr的延迟值。设置延迟值后每次调用setState后都会触发两次组件的重新渲染。第一次时,deferredQueryStr的值是queryStr修改前的值,第二次才是修改后的值。换句话,延迟值相较于state来说总会慢一步更新。
延迟值可以用在这样一个场景,一个state需要在多个组件中使用。一个组件的渲染比较快,而另一个组件的渲染比较慢。这样我们可以为该state创建一个延迟值,渲染快的组件使用正常的state优先显示。渲染慢的组件使用延迟值,慢一步渲染。当然必须结合React.memo或useMemo才能真正的发挥出它的作用。
当我们在组件中修改state时,会遇到复杂一些的state,当修改这些state时,甚至会阻塞到整个应用的运行,为了降低这种state的影响,React为我们提供了useTransition,通过useTransition可以降低setState的优先级。
useTransition会返回一个数组,数组中有两个元素,第一个元素是isPending,它是一个变量用来记录transition是否在执行中。第二个元素是startTransition,它是一个函数,可以将setState在其回调函数中调用,这样setState方法会被标记为transition并不会立即执行,而是在其他优先级更高的方法执行完毕,才会执行。
除了useTransition外,React还直接为为我们提供了一个startTransition函数,在不需要使用isPending时,可以直接使用startTransition也可以达到相同的效果。
生成唯一id,使用于需要唯一id的场景,但不适用于列表的key。
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。