文章目录
一、React是什么?
- React 是一个声明式,高效且灵活的用于
构建用户界面
的 JavaScript 库 - 使用 React 可以将一些简短、独立的代码片段组合成复杂的
UI
界面,这些代码片段被称作组件
1. JSX
JSX是什么呢?它就是js的语法拓展。
<script type="text/babel">
const ele = (<div className="box"><h1>hi React</h1></div>)
ReactDOM.render(ele,document.querySelector('#root'))
</script>
JSX的优点:
- JSX 执行更快,因为它在编译为 JavaScript 代码后进行了优化
- 它是类型安全的,在编译过程中就能发现错误
- 使用 JSX 编写模板更加简单快速
2. JSX 原来是颗糖
JSX 是
React.createElement
的语法糖
各位看官,我们再来瞅瞅这样一段代码。
// 创建react元素
const e = React.createElement
const ele = e('div',{className: 'box'},e('h1',null,'hi React'))
ReactDOM.render(ele,document.querySelector('#root'))
您瞧见没,通过React.createElement
创建出来的跟利用JSX
创建出来的内容完全一致。可一旦,代码多了起来呢?
"use strict";
/*#__PURE__*/
React.createElement("div", null, /*#__PURE__*/React.createElement("ul", null,
/*#__PURE__*/React.createElement("li", null,
/*#__PURE__*/React.createElement("span", {
className: "info"
}, "hello world")),
/*#__PURE__*/React.createElement("li", null,
/*#__PURE__*/React.createElement("span", null, "hello world")),
/*#__PURE__*/React.createElement("li", null,
/*#__PURE__*/React.createElement("span", null, "hello world"),
/*#__PURE__*/React.createElement("div", null, "hello world"))));
是不是就复杂了起来?这么创建元素会比较复杂,成本高,没有可读性,不利于维护以及不能复用。那么,就用到了上方的JSX。
二、React18
1. 下载包
命令:
yarn add react@next react-dom@next
yarn add @babel/standalone
2. 组件
类式组件
在react 16.8.0之前,用类定义组件。基于类并继承父类React.Component组件,子类就能使用react所提供的特性
class Foo1 extends React.Component{
render() {
return <h1>这里是类式组件</h1>
}
}
ReactDOM.render(
<Foo1 />,
document.querySelector('#root')
)
注意:类式组件过于臃肿复杂,不利于代码复用
函数式组件
在react 16.8.0后 (2019年2月6日),推荐函数式编程,用函数定义组件。
新增了Hooks特性,一种无需编写类即可使用状态和其他 React 特性的方法
const App = () => {
return (
<div>
<h1>
这里是函数式组件
</h1>
</div>
)
}
const root = ReactDOM.createRoot(document.querySelector("#root"))
root.render(<App/>)
就像是官网文档写的那样,当你第一次安装并运行时,你会在控制台报一个错,React 18 不再支持 ReactDOM.render,请改用 createRoot。
请注意: 各位看官,一定要拥抱
函数式编程
,接下来当使用react-router-dom v6
版本的时候,会由巨大决策。
三、Create React App
Create React App
是学习React的一个舒适的环境,也是开始在React中创建一个新的单页应用的最佳方式。在安装create-react-app
之前,要确保是否已经安装node
。
安装yarn包管理工具
npm i yarn -g
由于淘宝镜像地址变更,于是重新配置淘宝镜像。
yarn config set registry https://registry.npmmirror.com/
上述安装完毕后,从而可以继续安装
create-react-app
安装方式yarn add global create-react-app
或者npm install -g create-react-app
然后通过命令create-react-app my-app
或者yarn create react-app my-app
创建react项目。
不过,创建完项目后,我们来看看package.json
文件,此时,项目中react
和react-dom
已经更新为18.0.0
版本。而在npmjs.com上,react18也处于最新版本。
回到项目中,我们使用yarn start
或者npm start
运行项目时,会打开一个默认的3000端口,而后,再来看看控制台有什么信息。
它出现了一个警告:
这是什么意思呢?
简单来说,就是 React 18不再支持渲染,使用createRoot代替。
那么,我们回到src/index.js
文件中,进行稍微的修改。
import React from 'react';
import {createRoot} from 'react-dom/client';
import App from './App';
const element = document.querySelector('#root')
const root = createRoot(element);
root.render(<App/>)
然后回到浏览器中,在控制台上就没有了那个警告。
四、Hook
Hook是什么?Hook 它是 React 16.8 的新增特性。它可以让你在不编写 class 的情况下使用 state 以及其他的 React 特性。
总之,Hook 是一个特殊的函数
.
1. 批处理
在说这个Hook之前,了解一下什么是批处理。
批处理是指React将多个状态更新分组到一个单独的小组,然后通过重新渲染此单独的小组来获得更好的性能。如果你在同一个点击事件中要更新两个状态,那么React 总会将它们批处理到一个渲染中。
参考资料
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
setCount(c => c + 1); // 还没有重新渲染
setFlag(f => !f); // 还没有重新渲染
// React只会在最后重新渲染一次(这就是批处理)
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
在React 18之前,我们只在React事件处理程序中批量更新。默认情况下,promise、setTimeout、本机事件处理程序或任何其他事件中的更新都没有在React中批处理。
2. 18新特性 自动批处理
从React 18的createRoot
开始,所有的更新都会自动批处理
,不管它们来自哪里。
这意味着 timeouts, promises, 本机事件处理程序或任何其他事件中的更新将以与React事件中的更新相同的方式进行批处理。我们希望这能减少渲染工作,从而提高应用程序的性能:
function App() {
const [count, setCount] = useState(0);
const [flag, setFlag] = useState(false);
function handleClick() {
fetchSomething().then(() => {
// React 18和以后会分批处理这些:
setCount(c => c + 1);
setFlag(f => !f);
// React只会在最后重新渲染一次(这就是批处理)
});
}
return (
<div>
<button onClick={handleClick}>Next</button>
<h1 style={{ color: flag ? "blue" : "black" }}>{count}</h1>
</div>
);
}
如果我们不想使用自动批处理呢?那么就需要使用ReactDOM.flushSync
handleClick = () => {
setTimeout(() => {
ReactDOM.flushSync(() => {
this.setState(({ count }) => ({ count: count + 1 }));
});
// { count: 1, flag: false }
console.log(this.state);
this.setState(({ flag }) => ({ flag: !flag }));
});
};
3. useState
// UseState.jsx
import {useState} from "react";
const UseState = () => {
const [count, setCount] = useState(0)
return (
<>
<h2>UseState--{count}</h2>
<button onClick={()=>{
setCount(count=>count+1)
}}>点击加一</button>
</>
)
}
export default UseState
在上述代码中:
- 引入 React 中的
useState
Hook。它让我们在函数组件中存储内部 state。 - 在UseState组件内部,我们通过调用
useState
Hook 声明了一个新的 state 变量。它返回一对值给到我们命名的变量上。同时,将这个变量名设置为count
,此外,这个count
拥有一个默认值为0
。第二个返回的值本身就是一个函数。它让我们可以更新 count 的值,所以我们叫它setCount
。 - 当点击加一按钮后,我们传递一个新的值给
setCount
。React 会重新渲染UseState
组件,并把最新的 count 传给它。
如果,我们想封装一个hooks,然后复用呢?那么就来封装一个自定义hook
吧。
根据规范要求,我们所自定义的hook一定也是以use
开头的。
//useCountState.js
import {useState} from "react";
export default ()=> {
const [count, setCount] = useState(0)
const handleClickCount = () => {
setCount(count=>count+1)
}
return [count, handleClickCount]
}
其中,你用到什么,你就return什么。
const UseState = () => {
const [count, handleClickCount] = useCountState()
return (
<>
<h2>UseState--{count}</h2>
<button onClick={handleClickCount}>点击加一</button>
</>
)
}
export default UseState
4. useEffect
import {useEffect, useState} from "react";
const UseEffect = () => {
const [count, setCount] = useState(0)
useEffect(()=>{
document.title = count.toString() + Math.random()
})
return (
<>
<h2>UseEffect--{count}</h2>
<button onClick={()=>{
setCount(count=>count+1)
}}>加一</button>
</>
)
}
export default UseEffect
通过上方这个案例,使用useEffect
这个 Hook,可以告诉 React 组件需要在渲染后执行某些操作。React 会保存你传递的函数,并且在执行 DOM 更新之后调用它。在这个 effect 中,我们设置了 document 的 title 属性,不过我们也可以执行数据获取或调用其他命令式的 API。
当我们使用到这个组件时,点击加一按钮,看看这个页面的标题会发生什么变化?
默认情况下,它在第一次一次点击加一操作和之后每一次更新时,都会重新渲染一次,其实从本质上来说这根本没必要多次执行。
于是,我们传第二个参数
,如果是空数组,代表只执行一次。相当于window.onload。如下图所示:
import {useEffect, useState} from "react";
const UseEffect = () => {
const [count, setCount] = useState(0)
useEffect(()=>{
document.title = count.toString() + Math.random()
},[])
return (
<>
<h2>UseEffect--{count}</h2>
<button onClick={()=>{
setCount(count=>count+1)
}}>加一</button>
</>
)
}
export default UseEffect
尽管,count
的值仍然加一,但是,title
却只渲染一次。
在其中,有一些副作用是需要清除的。例如订阅外部数据源。这种情况下,清除工作是非常重要的,可以防止引起内存泄露。
import {useEffect, useState} from "react";
const UseEffect = () => {
const [count, setCount] = useState(0)
useEffect(()=>{
const title = document.querySelector('#title')
let timer = setInterval(()=>{
setCount(count=>count+1)
},1000)
const click = () => {
console.log('我点击了')
}
title.addEventListener('click',click)
return ()=> {
// 去做清除的事情
// 为了防止内存泄漏, 先清除上一轮的effect
// 处理副作用
clearInterval(timer)
title.removeEventListener('click',click)
}
},[])
return (
<>
<h2 id='title'>UseEffect--{count}</h2>
<button >加一</button>
</>
)
}
export default UseEffect
通过useEffect来获取数据
推荐接口测试地址:https://cnodejs.org/api
import {useEffect, useState} from "react";
const UseEffect = () => {
const [list, setList] = useState([])
const [id, setId] = useState('5433d5e4e737cbe96dcef312')
const [num, setNum] = useState(0)
const [info, setInfo] = useState([])
useEffect(()=>{
const requestList = async () => {
try {
const response = await fetch('https://cnodejs.org/api/v1/topics')
const res = await response.json()
console.log(res['data'])
setList(res['data'])
} catch (err) {
if (err) throw Error
}
}
requestList().then((rs)=>{
console.log('requestList')
})
},[])
useEffect(()=>{
const requestInfo = async () => {
try {
const response = await fetch('https://cnodejs.org/api/v1/topic/' + id)
const res = await response.json()
setInfo(res['data'])
} catch (err) {
if (err) throw Error
}
}
requestInfo().then((rs)=>{
console.log('requestInfo')
})
},[id])
return (
<>
<h2 id='title'>UseEffect</h2>
<h4>{id}</h4>
<div style={{display:'flex'}}>
<ul>
{
list.map((item, index)=>{
return (
<li
key={item.id}
style={{background:index === num ? 'skyblue':'white' ,cursor:"pointer"}}
onClick={()=>{
setId(item.id)
setNum(index)
}}
>{item.id}</li>
)
})
}
</ul>
<div dangerouslySetInnerHTML={{__html: info.content}}>
</div>
</div>
</>
)
}
export default UseEffect
- 在上方代码中,利用
useState
进行初始值的设置,以及进行值的更新。 - 通过
useEffect
进行数据的渲染,当在useEffect
中的第二个参数内,传了一个id
,而这个id是useState
内的值,也可以将它理解为一个监视器,当这个id的值
发生变化,那么我就在次触发useEffect
从而进行数据的渲染。 - 在获取到
info
值时,我们不妨来看看它里面是个什么东西。 - 在其中,我所需要的是
content
里面的内容,于是,就用到了dangerouslySetInnerHTML={{__html: info.content}}
这种代码,就类似vue的v-html,将字符串转换成html代码,并渲染到页面之中。
总体演示效果:
5. useLayoutEffect
我们比较常用的是useEffect
这个hook,当然在官方文档中,当它出问题的时候再尝试使用useLayoutEffect
,那么useLayoutEffect
与useEffect
的区别是什么呢?
- 首先,
useEffect
是异步执行的,useLayoutEffect
是同步执行的。 useLayoutEffect
是当浏览器把内容渲染到页面之前执行,而useEffect
是当浏览器把内容渲染到页面之后执行。尽可能使用标准的useEffect
以避免阻塞视觉更新。
这里,我选择使用gsap库来举例
安装方式:yarn add gsap
import gsap from 'gsap'
import {useLayoutEffect, useRef} from "react";
const UseLayoutEffect = () => {
const ele = useRef(null);
useLayoutEffect(()=>{
gsap.to(ele.current,{duration:0,x:200})
},[])
return (
<>
<div
ref={ele}
style={{
width: '300px',
height: '300px',
background:'skyblue'
}}
>
</div>
</>
)
}
export default UseLayoutEffect
看起来,直接出现在页面中,而useeffect
则会闪烁一下。这里就不再展示useEffect的代码,可以自己尝试一下。
综上所述,useLayoutEffect
是渲染之前同步执行的,于是,会等到动画执行完再渲染到页面上,也就没有闪烁一下的状态。而useeffect
刚好与之相反。
6. useContext
在我们爷孙之间传值的时候,如果使用两层父子更新是不是比较麻烦。于是React
给出了一个Hook,名为useContext
。
通过 createContext
创建出来的上下文,在子组件及后代组件中可以通过 useContext
这个 Hook 获取 Provider 提供的内容。
如果要使用上下文的内容,需要通过Context.Provider
最外层包装组件,比如GoodsContent.Provider
,同时,一定要给一个value
值,指定要暴露的信息。
const GoodsContent = createContext(null)
在这行代码中,创建了一个上下文,然后createContext(defaultValue)
中的defaultValue实际上是一个默认值,在这里你定义什值都可以。如果匹配不到最新的 Provider ,那么就会使用该默认值。
import {createContext, useContext, useState} from "react";
const GoodsContent = createContext(null)
const UseContext = () => {
const [goods, setGoods] = useState({
fruits: {
apple: 'apple',
price: 14
},
meat: {
beef: 'beef',
price: 26
}
})
const setGoodsPrice = () => {
setGoods({...goods, price: goods.meat.price++})
}
return (
<>
<GoodsContent.Provider value={{...goods, setGoodsPrice}}>
<Kinds/>
</GoodsContent.Provider>
</>
)
}
const Kinds = () => {
const goods = useContext(GoodsContent)
console.log(goods)
return (
<>
<h1>Kinds</h1>
<h2>{goods.fruits.apple}--{goods.fruits.price}</h2>
<h2>{goods.meat.beef}--{goods.meat.price}</h2>
<HandleClickChangePrice/>
</>
)
}
const HandleClickChangePrice = () => {
const goods = useContext(GoodsContent)
return (
<>
<button onClick={()=>{
goods.setGoodsPrice()
}}>改变价格</button>
</>
)
}
export default UseContext
7. useReducer
在某些场景下,useReducer
会比 useState
更适用,例如 state 逻辑较复杂且包含多个子值,或者下一个 state 依赖于之前的 state 等。并且,使用 useReducer 还能给那些会触发深更新的组件做性能优化,因为你可以向子组件传递 dispatch 而不是回调函数 。
import {useReducer} from "react";
const initState = {
count: 100,
list: ['vue', 'react', 'flutter', 'electron']
}
const reducer = (state, action) => {
switch (action.type) {
case 'increment':
return {...state, count: state.count + action.step}
case 'decrement':
return {...state, count: state.count - action.step}
default:
return state
}
}
const UseReducer = () => {
const [state, dispatch] = useReducer(reducer, initState)
return (
<>
<h2>UseReducer</h2>
<button onClick={()=>{
dispatch({type: 'increment', step: 100})
}}>count+: {state.count}</button>
<button onClick={()=>{
dispatch({type: 'decrement', step: 100})
}}>count-: {state.count}</button>
<ul>
{
state.list.map((item, index)=>{
return (
<li key={index}>{item}</li>
)
})
}
</ul>
</>
)
}
export default UseReducer
const [state, dispatch] = useReducer(reducer, initState)
在这里,useReducer
接受了2个参数。
- 第一个参数:reducer函数,用来接收当前项目的state和将要触发的动作action,从而计算并返回计算出来的state。
- 第二个参数:初始化的state。返回值为最新的state和dispatch函数,也就是用来触发reducer函数的,从而进行更新值的变化。
8. useMemo
把“创建”函数和依赖项数组作为参数传入useMemo
,它仅会在某个依赖项改变时才重新计算 memoized 值
。这种优化有助于避免在每次渲染时都进行高开销的计算。
避免在渲染渲染过程中,因大量不必要的在耗时计算而导致的性能问题。
//useMemo.jsx
import Child from "./Child";
import {useMemo, useState} from "react";
const UseMemo = () => {
const [count, setCount] = useState(0)
const [value, setValue] = useState(1000)
// 避免在渲染渲染过程中 因大量不必要的在耗时计算而导致的性能问题
const cache = useMemo(()=>{
return count + 10
// 当 count 发生变化时 才会进行计算
},[count])
return (
<>
<h2>UseMemo</h2>
<h3>
count: {count}
<hr/>
value: {value}
</h3>
<button onClick={()=>{setCount(count+1)}}>count++</button>
<button onClick={()=>{setValue(value+1)}}>value++</button>
<hr/>
<Child count={cache}/>
{/*使用缓存 而不是用一次计算一次*/}
<Child count={cache}/>
<Child count={cache}/>
</>
)
}
export default UseMemo
//Child.jsx
import {memo} from "react";
const Child = (props) => {
console.log('----child -render-----')
const { count } = props
return (
<>
<h2>Child--{count}</h2>
</>
)
}
export default memo(Child)
在Child.jsx
组件中:使用到了React.memo
高阶组件。如果你的组件在相同 props 的情况下渲染相同的结果,那么就可以利用 React.memo
进行包裹,从而进行调用。如果渲染的结果是相同的,那么将跳过渲染返回最近一次的渲染结果。
const cache = useMemo(()=>{
return count + 10
// 当 count 发生变化时 才会进行计算
},[count])
因为我提供依赖项数组,于是乎useMemo
只有在count值发生变化的时候,才会进行计算。反之则会在每次渲染时都会计算新的值。
9. useCallback
useCallback
返回一个 memoized 回调函数。
把内联回调函数及依赖项数组作为参数传入 useCallback
,它将返回该回调函数的 memoized 版本,该回调函数仅在某个依赖项改变时才会更新。
来看一下没有使用useCallback
的效果。
import {useState} from "react";
const Child = ({value,change}) => {
console.log('----re-render----')
return (
<input type="text" value={value} onChange={change}/>
)
}
const UseCallback = () => {
const [v1, setV1] = useState('')
const [v2, setV2] = useState('')
const onChange1 = (e)=>{
setV1(e.target.value)
}
const onChange2 = (e)=>{
setV2(e.target.value)
}
return (
<>
<h2>UseCallback</h2>
<Child value={v1} change={onChange1}/>
<Child value={v2} change={onChange2}/>
</>
)
}
export default UseCallback
我们能清晰的看到,在未使用useCallBack
时,任何一个输入框的变化,都会导致另一个组件重新渲染。
那么,再来看看使用这个hook的效果。
import {memo, useCallback, useState} from "react";
const Child = memo(({value,change}) => {
console.log('----re-render----')
return (
<input type="text" value={value} onChange={change}/>
)
})
const UseCallback = () => {
const [v1, setV1] = useState('')
const [v2, setV2] = useState('')
const onChange1 = useCallback((e)=>{
setV1(e.target.value)
},[])
const onChange2 = useCallback((e)=>{
setV2(e.target.value)
},[])
return (
<>
<h2>UseCallback</h2>
<Child value={v1} change={onChange1}/>
<Child value={v2} change={onChange2}/>
</>
)
}
export default UseCallback
任何一个输入框的变化,
都仅会导致自己的组件重新渲染。
10. useImperativeHandle
useImperativeHandle
可以让你在使用 ref
时自定义暴露给父组件的实例值。在大多数情况下,应当避免使用 ref 这样的命令式代码。useImperativeHandle
应当与forwardRef
一起使用。
import {forwardRef, useEffect, useImperativeHandle, useRef, useState} from "react";
const ChildInput = (props,ref) => {
const inputElement = useRef()
useImperativeHandle(ref,()=>{
return {
list: ['vue3','react18','flutter'],
handleFocus() {
inputElement.current.focus()
}
}
})
console.log(props)
return (
<>
<input type="text" ref={inputElement}/>
<div>children</div>
<hr/>
<hr/>
{props.children}
</>
)
}
const NewChildInput = forwardRef(ChildInput)
const UseImperativeHandle = () => {
const newIptEle = useRef()
const [arr, setArr] = useState([])
useEffect(()=>{
console.log(newIptEle)
setArr(newIptEle.current.list)
newIptEle.current.handleFocus()
},[])
return (
<>
<h2>UseImperativeHandle</h2>
<NewChildInput ref={newIptEle}>
<ul>
{
arr.map((item, index)=>{
return (
<li key={index}>{item}-{index}</li>
)
})
}
</ul>
</NewChildInput>
</>
)
}
export default UseImperativeHandle
在上方代码中,React 会将 <NewChildInput ref={newIptEle}>
元素的 newIptEle
作为第二个参数传递给 ChildInput
函数中的渲染函数。该渲染函数会将 newIptEle
传递给 <input type="text" ref={inputElement}/>
元素。
五、React-router-dom v6
1. 说在前头
为了快速使用路由,请先确保您的环境处于:
node.js版本 >= 12.x.x
npm或者yarn包管理工具
JavaScript,React.js和React Hooks的基础知识
2. 使用步骤
2.1 引入库
首先,进入到整个项目的入口文件index.js
文件内,然后在这个文件内进行引入。
// index.js
import {
BrowserRouter as Router
} from 'react-router-dom'
同时,也需要将<App/>
包裹起来,就像这样:
// index.js
root.render(
<Router>
<App/>
</Router>
)
2.2 NavLink
<NavLink>
是<Link>
的一个特定版本,会在匹配上当前的url的时候给已经渲染的元素添加参数。<NavLink>
是<Link>
的功能是一致的,区别在于可以判断其to属性是否是当前匹配到的路由。
而<NavLink>
也相当于一个a标签,我们可以直接给这个a标签添加激活时的样式。就比如:
a.active {
color: #61dafb;
font-weight: bold;
text-decoration: none;
}
当点击<NavLink>
时,会触发激活样式,字体会发生颜色等变化。
//App.js
import {
Route,
NavLink
} from 'react-router-dom'
//...
<nav className={'top-nav'}>
{/*链接*/}
<NavLink to='/'>发现音乐</NavLink>
<NavLink to='/myMusic'>我的音乐</NavLink>
<NavLink to='/friends'>朋友</NavLink>
<NavLink to='/download'>下载客户端</NavLink>
</nav>
2.3 嵌套路由
Routes
这个新的元素是以前Switch
组件的升级版,它具有相对路由和链接、自动路由排名、嵌套路由和布局等功能。而且使用路由时,必须使用Routes将组件包裹
component
变成了element
,遇到地址时,会展示后面组件的内容,而且一定要写成组件的形式{<组件名/>}
。
而Route
必须被Routes
组件包裹。
<Routes>
<Route path='/' element={<HomePage/>}>
<Route index element={<RecommendPage/>}/>
<Route path='rank' element={<RankPage/>}/>
<Route path='songSheet' element={<SongSheetPage/>}/>
<Route path='singer' element={<SingerPage/>}/>
<Route path='newMusic' element={<NewMusicPage/>}/>
<Route path='playlist' element={<PlayListPage/>}/>
<Route path='course/:id' element={<CoursePage/>}/>
</Route>
<Route path='/myMusic' element={<MyMusicPage/>}/>
<Route path='/friends' element={<FriendsPage/>}/>
<Route path='/download' element={<DownloadPage/>}/>
<Route path='*' element={<NotFound404/>}/>
</Routes>
在Route中,index
与path
属性是不可得兼的,而且index
表示为当前路由的根。也就是在根路由下,默认显示<RecommendPage/>
组件的内容。
不过,这却不是我们想要的结果,效果如下图所示。
就算我们切换路由,但是仍没有出现剩余路由所对应的组件内容,那是因为什么呢?
是因为缺少了:Outlet
import {
NavLink,
Outlet
} from "react-router-dom";
import '../sass/homePage.scss'
const HomePage = () => {
return (
<div className="sub-header">
<nav className="sub-nav">
<NavLink to='/'>推荐</NavLink>
<NavLink to='/rank'>排行榜</NavLink>
<NavLink to='/songSheet'>歌单</NavLink>
<NavLink to='/singer'>歌手</NavLink>
<NavLink to='/newMusic'>最新音乐</NavLink>
</nav>
<div>
<Outlet/>
</div>
</div>
)
}
export default HomePage
然后,我们在<HomePage/>
组件写上Outlet
时,再来看看效果。
嵌套路由
一般需要与 Outlet
组件同时使用,此组件类似于Vue的router-view组件,告知子路由,将内容渲染至什么位置。
2.4 重定向
Navigate
代替了Redirect
组件,当在某个路径/a
下,要重定向到路径/b
时,就可以通过Navigate
组件进行重定向
import {
Navigate
} from 'react-router-dom'
const RankPage = () => {
return (
<>
<Navigate to='/download'/>
<h2>RankPage</h2>
</>
)
}
export default RankPage
当我们来到排行榜页面时,会重定向到下载页面。
2.5 useSearchParams
获取查询参数。
使用useSearchParams
(是一个hook)来访问查询参数。其用法和useState类似。
import {
useLocation,
useSearchParams
} from 'react-router-dom'
// const queryString = require('query-string');
const PlayListPage = () => {
// const search = useLocation()
// console.log(queryString.parse(search.search))
const [searchParams,setSearchParams] = useSearchParams()
console.log('id为',searchParams.get('id'))
return (
<>
<h2>PlayListPage</h2>
</>
)
}
export default PlayListPage
2.6 useParams
在组件内通过useParams
访问路径参数。
import {useLocation, useParams} from "react-router-dom";
const CoursePage = () => {
const params = useParams()
console.log(params)
console.log(params?.id)
return (
<>
<h2>CoursePage</h2>
</>
)
}
export default CoursePage
此外,我们可以在link标签内写上state。保存在 location 中的 state。
// SingerPage.jsx
import {Link} from "react-router-dom";
const SingerPage = () => {
return (
<>
<h2>SingerPage</h2>
<div style={{display:'flex',justifyContent:'space-evenly'}}>
<Link to='/course/111'>vu3</Link>
<Link to='/course/222'>react18</Link>
<Link to='/course/333' state={{id:123456,name:"i love react"}}>flutter</Link>
</div>
</>
)
}
export default SingerPage
import {useLocation, useParams} from "react-router-dom";
const CoursePage = () => {
const params = useParams()
console.log(params)
console.log(params?.id)
const {state:{name, id}} = useLocation()
console.log(name,id)
return (
<>
<h2>CoursePage</h2>
</>
)
}
export default CoursePage
2.7 useNavigate
使用useNavigate
钩子函数生成navigate
对象,代替了useHistory
,可以通过JS代码完成路由跳转。
import {
useNavigate,
Navigate
} from 'react-router-dom'
const NewMusicPage = () => {
const navigate = useNavigate()
return (
<>
<h2>NewMusicPage</h2>
<button
onClick={()=>{
navigate('/friends')
}}
>编程导航实现页面跳转push有历史记录 可以前进后退</button>
<br/>
<button
onClick={()=>{
navigate('/friends',{replace: true})
}}
>替换当前 replace</button>
</>
)
}
export default NewMusicPage
3. 模块化 useRoutes
// src/router/index.js
import HomePage from "../pages/HomePage";
import MyMusicPage from "../pages/MyMusicPage";
import FriendsPage from "../pages/FriendsPage";
import DownloadPage from "../pages/DownloadPage";
import NotFound404 from "../pages/NotFound404";
import RecommendPage from "../pages/Home/RecommendPage";
import RankPage from "../pages/Home/RankPage";
import SongSheetPage from "../pages/Home/SongSheetPage";
import SingerPage from "../pages/Home/SingerPage";
import NewMusicPage from "../pages/Home/NewMusicPage";
import PlayListPage from "../pages/Home/PlayListPage";
import CoursePage from "../pages/Home/CoursePage";
const routes = [
{
path: '/',
element: <HomePage/>,
children: [
{
path: '',
index: true,
element: <RecommendPage/>
},
{
path: 'rank',
element: <RankPage/>
},
{
path: 'songSheet',
element: <SongSheetPage/>
},
{
path: 'singer',
element: <SingerPage/>
},
{
path: 'newMusic',
element: <NewMusicPage/>
},
{
path: 'playlist',
element: <PlayListPage/>
},
{
path: 'course/:id',
element: <CoursePage/>
}
]
},
{
path: '/myMusic',
element: <MyMusicPage/>
},
{
path: '/friends',
element: <FriendsPage/>
},
{
path: '/download',
element: <DownloadPage/>
},
{
path: '*',
element: <NotFound404/>
}
]
export default routes
import {
NavLink,
useRoutes
} from 'react-router-dom'
//....
import router from "./router";
function App() {
const routersElement = useRoutes(router)
return (
<div>
<nav className={'top-nav'}>
{/*链接*/}
<NavLink to='/'>发现音乐</NavLink>
<NavLink to='/myMusic'>我的音乐</NavLink>
<NavLink to='/friends'>朋友</NavLink>
<NavLink to='/download'>下载客户端</NavLink>
</nav>
<hr/>
{routersElement}
</div>
);
}
六、Redux
Redux
是JavaScript
应用的状态容器,提供可预测的状态管理工具。
那么,我们为什么会需要这个工具?
就打个比方说,阎王看上了手下黑白无常的手下的能力,于是就找来了黑白无常并要求他们告诉他们的手下去办事。这样一来,就形成了一个上级去寻找中级,中级再去寻找低级的环。这样一来就过于麻烦,身为阎王,为什么不能直接找他们的手下办事。于是乎,就需要一个“生死薄”,通过“生死薄”来对任何人进行安排。
最终,就出现了 redux 这个管理工具。
// reducer.js
const initState = {
count: 0,
list: ['春', '夏', '秋', '冬'],
goodsInfo: {
name: '一加8pro',
price: 4888
}
}
const reducer = (state = initState, action) => {
switch (action.type) {
case 'increment':
return {...state, count: state.count + action.step}
case 'decrement':
return {...state, count: state.count - action.step}
case 'toString':
return {...state, list: state.list.toString()}
default:
return state
}
}
export default reducer
首先,创建一个reducer.js
的文件,并在其中定义一个初始的状态,以及一个 reducer
的函数。
其中两个参数分别代表为初始的状态,以及一个状态。
// action.js
// action creator 生成action
const handleIncrement = (step = 1) => {
return {
type: 'increment',
step
}
}
const handleDecrement = (step = 1) => {
return {
type: 'decrement',
step
}
}
const handleToString = () => {
return {
type: 'toString',
}
}
export {
handleIncrement,
handleDecrement,
handleToString
}
之后再通过createStore
去创建一个store
import { createStore } from "redux";
import reducer from "./reducer";
const store = createStore(reducer)
export default store
这样一来,我们就能在页面中去使用state里面的数据。
import store1 from "../1-redux/store";
import {
handleIncrement,
handleDecrement,
handleToString
} from "../1-redux/action"
const Redux1Page = () => {
console.log('store', store);
console.log('store->state', store.getState());
const {count, list, goodsInfo} = store.getState()
const {dispatch} = store
return (
<>
<h2>Redux1Page</h2>
<h2>
<b>count: {count}</b><br/>
<b>list: {list}</b><br/>
<b>goodsInfo-name: {goodsInfo.name}</b><br/>
<b>goodsInfo-price: {goodsInfo.price}</b><br/>
</h2>
<button onClick={()=>{
dispatch(handleIncrement(4))
}}>increment</button><br/>
<button onClick={()=>{
dispatch(handleDecrement(4))
}}>increment</button><br/>
<button onClick={()=>{
dispatch(handleToString())
}}>increment</button>
</>
)
}
export default Redux1Page
将store
的内容打印出来后,我们能看到store内蕴含的方法,并通过 store.getState()
获取到state
里面的数据。
通过createStore
,将reducer的内容变成一个存储这些数据的一个仓库,并把reducer
的数据全部存储在这个仓库内。
进而通过store
内的方法(派发dispatch
)调用 handleIncrement, handleDecrement, handleToString
这些方法,通过方法里面的type
以及传进去的参数step
进行数据的更新。
但此时,我们点击按钮时,并未发生点击更新的状态,那,我们该如何进行数据的更新。
我们在入口文件index.js
中,通过上方store
的方法,来注册一个监听器,实时监听数据并渲染。
store.subscribe(()=>{
root.render(
<React.StrictMode>
<Router>
<App />
</Router>
</React.StrictMode>
)
})
再回头点击操作时,发现已经可以正常使用了。