React精记
1-redner注意事项
1- 挂载的位置(react容器)同一个页面可以指定多个
2- render可以调用多次,最后一次呈现的内容,会将之前的覆盖掉
3- 挂载的位置,不要被指定多次(会Warning)
4- 不要将body或html作为react容器(会Warning)
5- render呈现的内容,如果是字符串,字符串使用html标签,不会被浏览器识别
6- 可以使用连缀写法
2-jsx的基本语法
强烈建议:如果写入的JSX有嵌套,建议使用()将其包裹, root.render(<div/>),若一个则建议不要
强烈建议:将文件的名字以jsx结尾。如果将元素常量放置到外部,
注意1:在使用JSX时,标签必须要闭合 如:<a/>
注意2:在使用JSX时,标签的名字首字母不允许大写(react中大写默认以组件渲染) 注意3:在使用JSX时,不允许出现HTML没有内置标签(浏览器不支持的标签,不允许出现)
注意4:传入的JSX有且只能是一个根元素
注意5:如果有显示多个兄弟元素,可以在外围增加一个div
注意6:可以使用React提供的包裹组件,来解决一个根元素的问题
//eact.Fragment只是起到了一个包裹的作用,不会转为真实dom 可以加key属性
<></>也是一个包裹标签,是React.Fragment的语法糖。
//jsx本身也是表达式 ,所以她可以出现在{}里,{<a></a>}
//你可以在 if 语句和 for 循环的代码块中使用 JSX,将 JSX 赋值给变量,把 JSX 当作参数传入,以及从函数中返回 JSX
* {}写入的内容只支持表达式,不支持JS语法。
* js语句:不支持
* if 条件语句
* for 循环语句
* switch 选择语句
* js表达式:一个表达式会对应一个结果,表达式其实是一种特殊的JS语句
* 三元表达式
* 变量
* 常量
* fn()
3-元素渲染
React 只更新它需要更新的部分,并只会进行必要的更新来使 DOM 达到预期的状态。
1-条件渲染
root.render((
<div>{sex===1?"男":"女"}|{bol?"真":"假"}</div>
<div> {bol && "今天天气不错"} </div>
))
2-属性渲染
#数数组作为属性进行展示时是有逗号的,直接渲染是不带逗号,对象不能被展开
<p className={arr}></p>==》 <p class=“a,b”> 加了两个类名
3-样式渲染
style的属性值必须要设置为对象
<div style={{
width:"100px",
height:"200px",
background:"red"
}}></div>
4-列表渲染
一般通过map 或定义[].push
//返回JSX:箭头函数使用(),可以指定返回的值。
4-this问题
1:原生中为什么行内添加事件,回调函数中的this不指向事件源了
答:行内增加事件,是将字符串赋值过去,虽然带括号但是不会执行,当点击的时候会以js代码执行该函数,并且没有传任何参数,并且他会从window下面开始找 ,原本的this就丢失了什么都没有,后面要输出this还是看其如何调用(react默认严格模式,ES6类语法也是,箭头函数不自带this)
2:类组件中解决this指向undefined方式
1- 将函数直接设置为箭头函数
优点:方便
缺点:不能复用,如果逻辑代码较多,阅读不方便,传递参数不方便
2- 可以将箭头函数在类中进行定义
优点:可以被复用,代码多时也易于阅读
缺点:函数在实例中定义,如果组件被多次使用,会占用内存。
3-建议:可以通过bind将this进行指定(绑定)
优点:传递参数方便,可以复用
缺点:每次调用都要写bind,(自己添加)但是每次渲染都会执行 bind 方法生成一个新的函数(额外开销),并导致 子组件进行重新渲染
4-在实例属性中进行绑定
缺点:组件被多次使用,每次使用均会在内存中创建实例属性clickHandler
传递参数只能够进行一次指定,动态 指定参数不方便
优点:当你在使用函数时,不需要再次写bind
5-类组件
5.1-props与state相关
#属性不允许直接修改,若迫不得已。可拿到属性值然后改掉
//class组件又称为复杂组件 有状态组件
props:
父子组件传参通过属性:<Child username="xx" age={this.props.age} ></Child>
子组件需要接收:this.props.username
//属性是可以进行类型的限制的。
1- 引入prop-types.js,可以通过该文件对组件接收的属性进行类型限制
2- 为类组件增加静态属性 static propTypes(限制类型),static defaultProps(可以设置默认值)
state:状态可以修改,状态通过setState是 《异步修改》
方式一:this.props.setState({name:"wang"}) //参数是一个对象
方式二:this.props.setState({name:"wang"},()=>{// 当数据更新完毕且界面渲染完成之后执行该回调。})
方式三:this.props.setState((prevState,props)=>){
return {name:"xx"}
},()=>{//状态修改后执行该回调})
//在react@17的版本下 promise和定时器里 setStates是同步的
5.2-生命周期
生命周期指的是类组件。类组件从创建到销毁的过程称其为该组件的生命周期。
在组件的生命周期中,所暴露出来(在阶段中所自动执行)的函数称为钩子函数。
生命周期旧
5.1.1- 挂载阶段
#在组件挂载阶段暴露出来的钩子函数,除 render以外,均只运行一次。
实例化constructor=》在挂载之前UNSAFE_componentWillMount=》指定要挂载的内容render=> 挂载结束之后componentDidMount
····················
若父子组件的情况:
实例化constructor=》在挂载之前UNSAFE_componentWillMount=》指定要挂载的内容render=> 子组件执行上面步骤=>挂载结束之后(子组件)componentDidMount=》父组件componentDidMount
5.1.2- 更新属性阶段
UNSAFE_componentWillReceiveProps(nextProps){}//属性发生更新的时候 执行
shouldComponentUpdate(nextProps){做逻辑判断是否更新} // 当要更新属性时,执行到该钩子,this.prop 还未更新。接收的形参nextProps是即将要更新的属性
UNSAFE_componentWillUpdate(nextProps){}
render(){//到这里 属性已经更新}
componentDidUpdate(prevProps){prevProps为上一次的属性}
5.1.3- 更新状态阶段
shouldComponentUpdate(nextProps, nextState){}
UNSAFE_componentWillUpdate(nextProps, nextState){}
render(){ this.state.num = 100; //不会触发钩子而且render也不会 可强制更新
this.forceUpDate() }
componentDidUpdate(prevProps, prevState){}
5.1.4- 卸载阶段
componentWillUnmount(){}
root.unmount()
5.2- 生命周期新
<script type="text/babel">
const root = ReactDOM.createRoot(document.querySelector("#root"));
class App extends React.Component {
state = {
userName: "zhangsan"
}
render() {
return (
<div>
<button onClick={() => {
this.setState({
userName: this.state.userName + "!"
})
}}>点我</button>
<Child userName={this.state.userName}></Child>
</div>
)
}
}
class Child extends React.Component {
state = {
a: 1,
b: 2,
userName: "lisi"
}
// 作用:可以将接收到属性与当前的数据状态进行合并。
// 在什么时候执行:
// 1- 组件挂载时会运行。
// 2- 父组件执行render后会执行。
// 3- 在当前组件中执行this.setState
// 4- 强制更新
// 特点:
// 1- 组件中必须定义state
// 2- 必须要有返回值,且返回值必须是对象或null
// 3- 如果返回的对象中的属性与当前状态的属性冲突,那么会将数据状态进行覆盖
// 4- getDerivedStateFromProps是一个静态方法,该方法的this是undefined
static getDerivedStateFromProps(props) {
console.log(props, this);
return {
// a:100,
c: 3,
d: 4,
...props //返回的是this.state
}
}
render() {
// console.log(this.state);
return (
<div>
<h3>Child</h3>
<button onClick={() => {
// this.forceUpdate();// 强制更新
this.setState({
a: this.state.a + 1
})
}}>{this.state.a} </button>
</div>
)
}
// 注意:
// 1- getSnapshotBeforeUpdate需要与componentDidUpdate结合使用
// 2- getSnapshotBeforeUpdate它的返回值,会作为componentDidUpdate的第三个参数
// 3- 执行该钩子时,状态值已经发生变化
// 4- 当前的DOM还未更新。
// 在此处可以生成日志信息。
getSnapshotBeforeUpdate(prevProps, prevState) {
// prevState:状态更新之前的值
// this.state:当前状态值
console.log(prevState.a, this.state.a);// 执行该钩子时,状态值已经发生变化
console.log(document.querySelectorAll("button")[1].innerText)//当前的DOM还未更新。
return 100;
}
componentDidUpdate(prevProps, prevState, snapshot) {
console.log("更新完毕!componentDidUpdate", snapshot)
console.log(document.querySelectorAll("button")[1].innerText)////当前的DOM已更新。
}
}
root.render((
<App></App>
))
</script>
/*
使用getDerivedStateFromProps(nextProps, prevState)的原因:
旧的React中componentWillReceiveProps方法是用来判断前后两个 props 是否相同,如果不同,则将新的 props 更新到相应的 state 上去。在这个过程中我们实际上是可以访问到当前props的,这样我们可能会对this.props做一些奇奇怪怪的操作,很可能会破坏 state 数据的单一数据源,导致组件状态变得不可预测。
而在 getDerivedStateFromProps 中禁止了组件去访问 this.props,强制让开发者去比较 nextProps 与 prevState 中的值,以确保当开发者用到 getDerivedStateFromProps 这个生命周期函数时,就是在根据当前的 props 来更新组件的 state,而不是去访问this.props并做其他一些让组件自身状态变得更加不可预测的事情。
使用getSnapshotBeforeUpdate(prevProps, prevState)的原因:
在 React 开启异步渲染模式后,在执行函数时读到的 DOM 元素状态并不总是渲染时相同,这就导致在 componentDidUpdate 中使用 componentWillUpdate 中读取到的 DOM 元素状态是不安全的,因为这时的值很有可能已经失效了。
而getSnapshotBeforeUpdate 会在 render 之后被调用,也就是说在 getSnapshotBeforeUpdate 中读取到的 DOM 元素状态是可以保证与componentDidUpdate 中一致的。
*/
6-ref 获取react元素
1- ref与字符串结合使用 <h3 ref={"title"}>App</h3> , this.refs.title==>h3 (官方不推荐了)
ref与组件结合使用时,可以获得组件实例。this.refs.子组件 ,从而可以实现组件之间的通讯。但是并不是传递属性,不会在子组件里通过this.props拿到
2-ref与箭头函数结合使用
React元素的属性ref的值如果是一个箭头函数,那么不可以通过this.refs获取其真实DOM
ref的值是箭头函数的特点:
1- 当组件在挂载时,箭头函数会执行。
2- 箭头函数接收的参数即是真实的DOM.
3- 在使用时,需要将接收到的DOM作为当前实例的属性。
<h3 ref={ele => this.title = ele}>App</h3>
3-ref与React.createRef结合使用 (#推荐)
ref使用方式三:React.createRef title = createRef(); <h3 ref={this.title}>App</h3>
1- 自定义一个实例属性,其值为React.createRef()
2- 为React元素增加ref属性,其值设置为自定义的实例属性。
3- 可以通过实例属性.current得到真实DOM
7-受控组件和非受控组件
受控组件:如果在表单元素中使用value,checked那么会受其值的控制。
一旦组件受控,需要结合onChnage事件来更改value值
对于表单的两种操作:1- 设置表单值,2-收集表单数据,更改数据状态
非受控组件:defaultValue,defaultChecked
8-函数组件(重点重点重点重点)
函数组件是无状态组件 简单组件 没有生命周期钩子
8.1-props属性相关
#传递属性
1:
<Child userName={"zhangsan"} age={12}></Child> //传递属性
function Child(props) {//通过形参props对象接收属性}
2:
<BookList bookInfo={boyBook}>
<h3 style={{color:"blue"}}>男生必读</h3>//传递
</BookList>
{ props.children }//放在props下的children里了
#函数组件限制类型
函数可以直接增加属性。称为函数的静态属性
Child.propsTypes={} ,Child.defaultTypes={}
8.2-Hook
Hook 是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。* 只能在函数组件和自定义hook中使用*
1-useState函数组件中的state 状态
设置状态
1- useState是React模块提供的一个方法
2- useState返回的是一个数组,接收的参数即是初始状态值。
3- 返回的数组有两个元素,第一个元素即是状态,第二个元素是修改状态的方法。
4- 当通过setXXX修改状态之后,函数会重新执行,注意:再次执行时,状态的值为上一次的结果(useState拥有记忆性)
5- 修改状态的方法规范:以set开头,后续加上状态名,使用驼峰命名法
例:const [num, setNum] = React.useState(1); setNum(num - 1)//异步设置
#注意: 传入的参数要保证与修改的状态的引用地址不同。否则界面不会更新
2-useCallback
缓存内联回调函数
useCallback:可以将函数进行缓存处理。
useCallback函数,接收的第一个参数是内联回调函数,第二个参数是依赖性数组
如果使用了useCallback,第二个参数建议声明,如果不声明相当于useCallback无用的方法。
1-如果省略第二个参数,不会有记忆。
2-如果只是写[],代表着该函数会进行记忆(缓存)
3-如果写的[num],代表着num发生变化,不会设置num缓存(保证num是最新的状态)。
3-useMemo
缓存值
useCallback记忆的是函数,useMemo记忆的结果(将接收函数执行以后的结果进行返回)
useMemo第二个参数是数组。
1-如果将第二个参数省略,那么不会产生记忆。
2-如果将第二个参数设置为空数组,那么所得到值不会发生变化。
3-如果将第二个参数设置为[num],只有状态num发生变化,才会重新调用指定的函数,得到新的结果。
#传入useMemo的函数会在渲染期间就执行,所以不要写不在渲染期间执行的代码,如useEffect里的代码
4-useRef
获取dom
1- 引入useRef const btn = React.useRef("sdasdas"); //不写参数就是undefined
2- 将useRef函数运行的结果赋值给常量。// inputRef = useRef();
3- 将生成的常量作为操作的React元素的ref属性值 //<input ref={inputRef} type="text" />
4- 在使用时,可以通过常量名.current可以得到真实的DOM。
//useRef 返回一个可变的 ref 对象,其 .current 属性被初始化为传入的参数(initialValue)。返回的 ref 对象在组件的整个生命周期内持续存在。useRef 会在每次渲染时返回同一个 ref 对象。
5-useContext(会以redux代替)
useContext可以帮助我们解决跨组件传递数据,实现数据的共享。(类组件也可用)
//context文件下的index.js
import {createContext} from "react";
export default createContext(0);
//App.jsx里引入
import globalContext from "./context";
* 1-要通过React.createContext方法生成一个数据(对象)
* 1-1 createContext需要在src下创建一个目录context,在context中新建文件index.js进行执行。
* 1-2 createContext是一个函数,接收的数据是共享状态的初始值,返回的是一个对象
* 1-3 对象当中拥有Provider(提供者),Consumer(消费者),//可以看作一个组件
* 2-如果需要在Child组件中使用数据。
* 简单 :
child组件中 直接用创建时候设置的初始值
* import globalContext from "../context";
* <p>child:{result}</p>
* 设置到数据的传递:
* App.jsx:
* const [num,setNum] = useState(100);
* <globalContext.Provider value={{name,setName}}>//传多个值用对象
<Child userName={userName}/>
</globalContext.Provider>
接收:
import globalContext from "../context";
const {num,setNum} = useContext(globalContext);
<p>child:{num}</p> //并没有通过属性传参 所以props里没有的
······························································
<div>
<globalContext.Provider value={{ num, setNum }}>
<Child userName={userName} />
</globalContext.Provider>
</div>
6-useReducer(会以redux替代)
是useState的替代方案
useReducer也被称为状态管理器。通过useReducer可以对自身的状态进行集中式管理。
如果 state 逻辑较复杂且包含多个子值等等
reducer的使用流程:
1- 创建一个函数,该函数可以称为reducer函数,作用:可以对数据(数据状态)进行操作
2- 该函数可以接收两个参数,第一个参数是状态,第二个参数是荷载的内容.
3- 需要通过useReducer,可以获得状态以及修改状态的方法
4- useReducer接收两个参数:1-reducer函数 2- 初始状态
5- dispatch 执行时,需要传递一个对象。
/*
prevState是要更改的状态,action的值为dispatch函数传递的数据(action对象)
返回的值是最新的状态.
action对象需要设置type属性,通过type属性可以得知要对数据进行怎样的操作.*/
const reducer = function (prevState, action) {
const state = { ...prevState };//改引用地址
// 将状态更改为{num:100}
if (action.type === "jia") {
state.num += action.payload;
}
if (action.type === "jian") {
if (state.num > 1)
state.num -= 1;
}
return state;
}
function App(props) {
console.log(useReducer());// [undefined,f]
console.log(useReducer(reducer, {
num: 1
}));// [ {num: 1},f]
const [state, dispatch] = useReducer(reducer, { num: 0 });
return (
<div>
<button onClick={() => {
dispatch({
type: "jian"
});
}}>-</button>
{state.num}
<button onClick={() => {
dispatch({
type: "jia",
payload: 2
});
}}>+</button>
</div>
);
}
7-useEffect
1- useEffect可以接收一个函数(相当于componentDidMount-挂载阶段,componentDidUpdate-更新阶段)
useEffect(() => {
console.log(document.querySelector("button").innerText, "Effect"); //初次执行也是在render结束后,若状态改变即更新依然是render结束后
})
2- useEffect第二个参数是一个数组, (更新阶段)
2-1如果你省略该数组,当任何状态更新都会执行第一个参数(回调函数)
2-2如果第二个参数设置为一个空数组,那么回调函数只会在挂载完毕之后执行一次,更新阶段不会执行。(componentDidMount)
2-3如果数据内指定了状态,那么回调函数只会在指定的状态发生变化时和挂载结束时执行。componentDidMount componentDidUpdate
useEffect同组件里可以多次使用
//默认情况下,effect 会在每轮组件渲染完成后执行。这样的话,一旦 effect 的依赖发生变化,它就会被重新创建。
// 可以返回一个函数,该函数会在组件销毁前执行 类似componentWillUnMount
8-useLayoutEffect
useEffect(()=>{
console.log("useEffect",num)
const pre = Date.now();// 获取当前时间
// 延迟0.5秒
while(Date.now()-pre<500){}//0.5秒内一直循环
if(num === 0)
setNum(Math.random());
},[num])
// useLayoutEffect会阻塞浏览器的渲染。
useLayoutEffect(()=>{
console.log("useLayoutEffect",num)
const pre = Date.now();// 获取当前时间
// 延迟0.5秒
while(Date.now()-pre<500){}
if(num === 0)
setNum(Math.random());
},[num])
#其余一样 可查官网
图片引入相关(create-react-app)
1- 如果图片来自于其它服务,直接写完整地址 <img src="https://static.zoncvea56.jpeg" alt="" />
2- 如果图片来自于本项目,那么可以通过import引入图片 import vue from "./assets/img/vue.png";
3- 如果图片来自于本项目,那么也可以通过require const vue2 = require("./assets/img/vue.png");
样式引入相关(create-react-app)
import "./App.css" //根据组件名 直接引用 (若都用直接引入 如果属性重名 父会覆盖子组件的样式)
import styles from "./App.module.css"; //模块化引用,样式文件的名字需要以.module.css结尾
<p className={styles.cl}>app</p>
9-路由(重点)
一个路由就是一个映射关系(key:value) key为路由路径, value可能是function/component
路由两种模式:historyRouter hashRouter
下载:cnpm install react-router-dom
路由切换:从A路由切换至B路由,可以称其为一次路由切换。切换伴随者路由销毁和挂载
从A路由切换至B路由:首先会将A路由进行销毁,然后再将B路由进行挂载。反之亦然
#一级路由的 path 的名字前面要加斜杠,但二级路由推荐不用加斜杠,避免不必要的报错。
<Route path={"/"} element={<h3>主页</h3>}>asd </Route> //也可以直接写表达式(包括jsx)
<Route path={"/"} element={<Home />}>asd </Route> //默认只会找一次从上到下
<Route path={"*"} element={<Home />}> </Route> //所以path都不匹配 才触发
{/*当请求地址为/home,那么会重定向到path为/的路由中*/} 重定向
<Route path={"/home"} element={<Navigate to={"/"} />}></Route>
//路由导航
import {
Routes,//路由组件
Route,//直接包裹路由组件
NavLink,//导航组件 会在浏览器里转换为a标签
Navigate,// 跳转组件 重定向
} from 'react-router-dom'
会将路由导航抽离到components中,<NavLink to={"/"}>首页</NavLink>
NavLink:自带的样式 类是.active,点谁谁有这个类名
自定义样式2中方式:
className可写字符串类名,也可以写箭头函数
箭头函数: 1:该箭头函数会在界面挂载完之后执行。接收一个参数(类型是对象{isActive:true})
2:isActive:当浏览器的地址与当前NavLink的to属性相同,那么为true,否则为false
//路由切换伴随路由销毁和挂载,写在useEffect()里
//1: /#/newsList,2:/newList
BrowserRouter为history路由模式: 在打完包之后放置在生产环境以后需要单独对其进行配置。
2种路由设置模式:import { HashRouter/BrowserRouter as Router } from 'react-router-dom'
//路由嵌套
/*使用路由嵌套的流程:
1- 要在一级路由(Routes组件直接包裹的路由称为一级路由)内包裹上其它的路由(二级路由,子路由)
2- 二级路由访问的地址: 首选要访问到一级路由的path,然后才可以访问到二级路由的path
比如: /my/car ==>my是一级路由的地址标识 car是二级路由的地址标识。
3- 二级路由指定的element元素需要在一级路由element元素上进行呈现 。
####一级路由呈现二级路由的内容需要通过引入Outlet组件进行挂载位置 。
*/
#将二级路由设置为默认选中的三种方式(一级路由为my)
<Route path={""} element={<MyCollections/>}></Route>//相当于/my/“”
<Route path={"/my"} element={<MyCollections/>}></Route>// /my的时候就显示collection,无高亮
<Route index element={<MyCollections/>}></Route>
#设置二级路由地址的2方式:
1-地址相对于 /my
<Route path={"mycart"} element={<MyCart />}></Route>
2-地址相对于于站点根目录
<Route path={"/my/mycart"} element={<MyCart />}></Route>
#路由重定向的2种方法
1:
import {Navigate,// 跳转组件} from "react-router-dom"
<Route path={"/home"} element={<Navigate to={"/"}/>}></Route>
2:定义组件Redirect 通过传参的形式
<Route path={"/home"} element={<Redirect to={"/"} />}></Route>
引入useNavigate ==》const navigate = useNavigate()=》useEffect =》 navigate(to)
#路由传递参数(重点)
#1:search传递参数
//1- 刷新数据不会丢失。因为数据是在地址中(查询字符串)的 2- 不方便传递引用类型。
{/* 字符串 */}
<NavLink to={"/one/?a=1&b=2"}>search01</NavLink>
{/*对象*/}
<NavLink to={{
pathname: "/one/?a=10&b=2" //认为你的地址是/one/?a=10&b=2 认为/one是
//pathname 进行比较就不相等 单纯传递参数都行,导航的化第一或第三
}}>search02</NavLink>
{/*通过search属性指定数据*/}
<NavLink to={{
pathname: "/one",
search: "a=100&b=300"
}}>search03</NavLink>
# <按钮实现需要引入useNavigate,并组件中调用>
字符串形式 直接拼接,对象形式直接写或者分开写
{/*通过按钮来实现1*/}
<button onClick={() => {
navigate("/one?a=90&b=91")
}}>search04</button>
{/*通过按钮来实现2*/}
<button onClick={() => {
navigate({
pathname: "/one?a=8&b=9"
})
}}>search05</button>
{/*通过按钮来实现3*/}
<button onClick={() => {
navigate({
pathname: "/one",
search: "?a=88&b=9"
})
}}>search06</button>
1:search接收参数
#引入useLocation后的值没法直接用,所以要转换
1.1-引入useLocation=》组件中调用返回的是对象,并且search是字符串//search: "?a=88&b=9"
所以可用封装函数 将字符串变为对象 然后对象里拿值
1.2-通过下载querystring接收参数 然后去除问号 调用parse方法转为对象
1.3-可以通useSearchParams来接收数据。引入后调用,返回数组[{},f],取出searchParams这个对象,并用其get方法searchParams.get("a")获取值
#2-state传递参数
特点:数据没有放置在地址中,可以方便的传递引用类型。放置在state中,刷新数据不会丢失,如果重新打开一个进程数据丢失。
//通过事件 并且要用到useNavigate(“path”,{})
<button onClick={() => { navigate("/two",{state:{}})}>state2</button>
2-接收参数:useLocation.state //解构state对象时,为了放置state为空可用||{},或者设置默认值
#3- params传递参数(最多)
传参:
{/*<NavLink to={"/three/1/2"}>params</NavLink>*/}
{/*<NavLink to={"/three/1-2"}>params</NavLink>*/}
<NavLink to={"/three/1-2.html"}>params</NavLink>//.html伪类型 随便写,-/也可以改
{/*<Route path={"/three/:id/:type"} element={<Three/>}></Route>*/}
{/*<Route path={"/three/:id-:type"} element={<Three/>}></Route>*/}
<Route path={"/three/:id-:type.html"} element={<Three/>}></Route>
接收参数:
引入useParams=》{id: '1', type: '2'}
\