文章目录
ReactRouter
前置
在一开始前端开发都是单页应用,也就是只有一个html文件。后来主流的开发模式变成了通过路由进行页面切换。这样做的优势就是:避免整体页面刷新 用户体验变好。缺点就是:前端负责事情变多了 开发的难度变大。
路由的本质是什么?
路由的概念来源于后端 : 一个路径表示匹配一个服务器资源,例如:
- /a.html -> a对应的文件资源
- /b.html -> b对应的文件资源
共同的思想: 一对一的关系
前端的路由: 一个路径path对应唯一的一个组件comonent 当我们访问一个path 自动把path对应的组件进行渲染
javascript
const routes = [
{
path:'/home',
component: Home
},
{
path:'/about',
component: About
},
{
path:'/article',
component: Article
}
]
基本使用
首先安装依赖:
bash
yarn add react-router-dom@6
我们以一个小案例为例:
需求: 准备俩个按钮,点击不同按钮切换不同组件内容的显示
实现步骤:
- 导入必要的路由router内置组件
- 准备俩个React组件
- 按照路由的规则进行路由配置
javascript
// 引入必要的内置组件
import { BrowserRouter, Routes, Route, Link } from 'react-router-dom'
// 准备俩个路由组件
const Home = () => <div>this is home</div>
const About = () => <div>this is about</div>
function App() {
return (
<div className="App">
{/* 按照规则配置路由,是一个非hash模式的路由 */}
<BrowserRouter>
{/* 指定跳转的组件,to用来配置路由地址 */}
<Link to="/">首页</Link>
<Link to="/about">关于</Link>
<Routes>
<Route path="/" element={<Home />}></Route>
<Route path="/about" element={<About />}></Route>
</Routes>
</BrowserRouter>
</div>
)
}
export default App
核心内置组件说明
BrowerRouter组件
作用: 包裹整个应用,一个React应用只需要使用一次
Hash路由和history路由是两种前端路由的实现方式,它们的区别主要有以下几点:
- Hash路由是一种把前端路由的路径用井号 # 拼接在真实 URL 后面的模式。当井号 # 后面的路径发生变化时,浏览器并不会向服务器发送请求,而是根据 hash 值的变化来更新页面内容。
- History路由是一种利用 HTML5 的 history API 来实现的路由模式。它可以通过 pushState 和 replaceState 方法来修改浏览器的历史记录,从而改变 URL 的显示,同时不会触发页面的刷新。
- Hash路由相比于 history 路由,有以下几个缺点:
- Hash路由的 URL 较丑,有一个多余的 # 符号。
- Hash路由原本是用来做页面定位的,如果用来做路由的话,原来的锚点功能就不能用了。
- Hash路由的传参是基于 URL 的,如果要传递复杂的数据,会有体积的限制,而 history 路由不仅可以在 URL 里放参数,还可以将数据存放在一个特定的对象中。
- Hash路由设置的新值必须与原来不一样才会触发记录添加到栈中,而 history 路由可以设置与当前 URL 一模一样的新 URL,这样也会把记录添加到栈中。
Link组件
作用: 用于指定导航链接,完成声明式的路由跳转 类似于 <router-link/>
这里to属性用于指定路由地址,表示要跳转到哪里去,Link组件最终会被渲染为原生的a链接
Routes组件
作用: 提供一个路由出口,组件内部会存在多个内置的Route组件,满足条件的路由会被渲染到组件内部
什么是路由出口?
路由出口是一个用于在页面中显示路由组件的标签,它可以让你在不同的位置展示不同的内容,根据路由的变化而变化。路由出口有以下几个特点:
- 你可以在一个页面中使用多个路由出口,只要给它们不同的名字,就可以实现复杂的布局效果。
- 你可以在路由出口中嵌套其他的路由出口,以实现多级的路由导航。
- 你可以在路由出口中使用路由守卫,以实现对路由的控制和拦截。
Route组件
作用: 用于定义路由路径和渲染组件的对应关系 [element:因为react体系内把组件叫做react element]
其中path属性用来指定匹配的路径地址,element属性指定要渲染的组件,图中配置的意思为: 当url上访问的地址为 /about 时,当前路由发生匹配,对应的About组件渲染
编程式导航
声明式 【 Link to】 vs 编程式 【调用路由方法进行路由跳转】
概念: 通过js编程的方式进行路由页面跳转,比如说从首页跳转到关于页
实现步骤:
- 导入一个 useNavigate 钩子函数
- 执行
useNavigate 函数
得到跳转函数
- 在事件中执行跳转函数完成路由跳转
javascript
// 导入useNavigate函数
import { useNavigate } from 'react-router-dom'
const Home = () => {
// 执行函数
const navigate = useNavigate()
return (
<div>
Home
<button onClick={ ()=> navigate('/about') }> 跳转关于页 </button>
</div>
)
}
export default Home
注: 如果在跳转时不想添加历史记录,可以添加额外参数replace 为true
javascript
navigate('/about', { replace: true } )
路由传参
场景:跳转路由的同时,有时候要需要传递参数
searchParams传参
路由传参
路由取参
params传参
路由传参
在指定路由的时候,要先占个位!
路由取参
嵌套路由
场景:在我们做的很多的管理后台系统中,通常我们都会设计一个Layout组件,在它内部实现嵌套路由
实现步骤:
- App.js中定义嵌套路由声明
javascript
<Routes>
<Route path="/" element={<Layout/>}>
<Route path="board" element={ <Board/> } />
<Route path="article" element={ <Article/> } />
</Route>
{ /* 省略部分 */ }
</Routes>
- Layout组件内部通过
<Outlet/>
指定二级路由出口
javascript
import { Outlet } from 'react-router-dom'
const Layout = () => {
return (
<div>
layout
{ /* 二级路由的path等于 一级path + 二级path */ }
<Link to="/board">board</Link>
<Link to="/article">article</Link>
{ /* 二级路由出口 */ }
<Outlet/>
</div>
)
}
export default Layout
默认二级路由
场景: 应用首次渲染完毕就需要显示的二级路由
实现步骤:
- 给默认二级路由标记index属性
- 把原本的路径path属性去掉
代码实现:
javascript
<Routes>
<Route path="/" element={<Layout/>}>
<Route index element={ <Board/> } />
<Route path="article" element={ <Article/> } />
</Route>
</Routes>
javascript
import { Outlet } from 'react-router-dom'
const Layout = () => {
return (
<div>
layout
{ /* 默认二级不再具有自己的路径 */ }
<Link to="/">board</Link>
<Link to="/article">article</Link>
{ /* 二级路由出口 */ }
<Outlet/>
</div>
)
}
404路由配置
场景:当url的路径在整个路由配置中都找不到对应的path,使用404兜底组件进行渲染
首先我们准备一个NotFound组件
javascript
const NotFound = () => {
return <div>this is NotFound</div>
}
export default NotFound
然后将这个组件添加到声明当中的,作为兜底方法
javascript
<BrowserRouter>
<Routes>
<Route path="/" element={<Layout />}>
<Route index element={<Board />} />
<Route path="article" element={<Article />} />
</Route>
<Route path="*" element={<NotFound />}></Route>
</Routes>
</BrowserRouter>
集中式路由配置
场景: 当我们需要路由权限控制点时候, 对路由数组做一些权限的筛选过滤,所谓的集中式路由配置就是用一个数组统一把所有的路由对应关系写好,替换本来的Routes组件
javascript
import { BrowserRouter, Routes, Route, useRoutes } from 'react-router-dom'
import Layout from './pages/Layout'
import Board from './pages/Board'
import Article from './pages/Article'
import NotFound from './pages/NotFound'
// 1. 准备一个路由数组 数组中定义所有的路由对应关系
const routesList = [
{
path: '/',
element: <Layout />,
children: [
{
element: <Board />,
index: true, // index设置为true 变成默认的二级路由
},
{
path: 'article',
element: <Article />,
},
],
},
// 增加n个路由对应关系
{
path: '*',
element: <NotFound />,
},
]
// 2. 使用useRoutes方法传入routesList生成Routes组件
function WrapperRoutes() {
let element = useRoutes(routesList)
return element
}
function App() {
return (
<div className="App">
<BrowserRouter>
{/* 3. 替换之前的Routes组件 */}
<WrapperRoutes />
</BrowserRouter>
</div>
)
}
export default App
Mobx
什么是Mobx
一个可以和React良好配合的集中状态管理工具,和Redux解决的问题相似,都可以独立组件进行集中状态管理
mobx和react的关系,相当于vuex和vue
同类工具还有:
- redux
- dva
- recoil
优势:
- 简单:编写无模板的极简代码精准描述你的意图
- 轻松实现最优渲染:依赖自动追踪,实现最小渲染优化
- 架构自由:可移植, 可测试 无特殊心智负担
环境配置
Mobx是一个独立的响应式的库,可以独立于任何UI框架存在,但是通常大家习惯把它和React进行绑定使用,用Mobx来做响应式数据建模,React作为UI视图框架渲染内容,我们环境的配置需要三个部分
- 一个create-react-app创建好的React项目环境
- mobx框架本身
- 一个用来链接mobx和React的中间件
bash
# 安装mobx和中间件工具 mobx-react-lite 只能函数组件中使用
$ yarn add mobx mobx-react-lite
基础使用
需求: 使用mobx实现一个计数器的案例
首先我们初始化mobx:
一般我们mobx的代码会写在store文件夹中:
初始化步骤
- 定义数据状态state
- 在构造器中实现数据响应式处理 makeAutoObservble
- 定义修改数据的函数action
- 实例化store并导出
javascript
import { makeAutoObservable } from 'mobx'
class CounterStore {
count = 0 // 定义数据
constructor() {
makeAutoObservable(this) // 响应式处理
}
// 定义修改数据的方法
addCount = () => {
this.count++
}
}
const counter = new CounterStore()
export default counter
然后React使用store:
实现步骤
- 在组件中导入counterStore实例对象
- 在组件中使用storeStore实例对象中的数据
- 通过事件调用修改数据的方法修改store中的数据
- 让组件响应数据变化
javascript
// 导入counterStore
import counterStore from './store'
// 导入中间件连接mobx、react 完成响应式变化
import { observer } from 'mobx-react-lite'
function App() {
return (
<div className="App">
<button onClick={() => counterStore.addCount()}>
{counterStore.count}
</button>
</div>
)
}
// 包裹组件让视图响应数据变化
export default observer(App)
在原来我们的数据是react来管理的,所以数据的变化会引起模板的重新渲染,而现在我们将状态交给mobx来管理,并且改变状态的方法也是mobx中提供的,所以我们需要observer方法来包裹App跟组件,让视图响应数据变化。
observer函数*
observer函数是一个高阶组件(Higher-Order Component,HOC),它可以接收一个React组件作为参数,并返回一个新的React组件,这个新的组件会自动订阅Mobx中的可观察数据,并在数据变化时重新渲染。
observer函数应该包裹那些需要响应Mobx中的数据变化的组件,通常是那些展示数据或者处理用户交互的组件。
我们可能会有一种猜想,我们直接使用observer函数包裹最外层的组件App是不是就可以了?这样当App组件重新渲染的时候,也会把子组件连带着一起重新渲染了?
这种想法大错特错!
App组件重新渲染了,子组件也不一定会重新渲染,这取决于子组件是否接收了来自App组件的props,以及这些props是否发生了变化。子组件只有在它的props或者state发生变化时,才会重新渲染 。如果子组件只是依赖于Mobx中的数据,而不是App组件传递的props,那么它就不会因为App组件的重新渲染而重新渲染,除非它也使用了observer函数来订阅Mobx中的数据。子组件包含在App组件中,只是表示它是App组件的子节点,它并不会自动继承App组件的props或者响应App组件的更新。你可以把这个过程想象成一个树形结构,每个组件都是一个节点,每个节点都有自己的数据和渲染逻辑,只有当节点的数据发生变化时,它才会重新渲染自己和它的子节点。
如果你想让子组件跟随App组件的更新而更新,你可以有以下几种方法:
- 在App组件中使用observer函数,并且把Mobx中的数据通过props传递给子组件,这样子组件就会根据props的变化而重新渲染。
- 在子组件中也使用observer函数,并且直接从Mobx中获取数据,这样子组件就会根据Mobx中的数据变化而重新渲染。
- 在App组件中使用forceUpdate方法,强制App组件和它的子组件重新渲染,但这种方法不推荐使用,因为它会破坏React的优化机制。
计算属性(衍生状态)
概念: 有一些状态根据现有的状态计算(衍生)得到,我们把这种状态叫做计算属性, 看下面的例子
实现步骤
- 声明一个存在的数据
- 通过get关键词 定义计算属性
- 在 makeAutoObservable 方法中标记计算属性(其实标记不标记都可以,只是为了可读性)
javascript
import { computed, makeAutoObservable } from 'mobx'
class CounterStore {
list = [1, 2, 3, 4, 5, 6]
constructor() {
makeAutoObservable(this, {
filterList: computed
})
}
// 修改原数组
changeList = () => {
this.list.push(7, 8, 9)
}
// 定义计算属性
get filterList () {
return this.list.filter(item => item > 4)
}
}
const counter = new CounterStore()
export default counter
get使用来表明getter方法的关键字
javascript
// 导入counterStore
import counterStore from './store'
// 导入observer方法
import { observer } from 'mobx-react-lite'
function App() {
return (
<div className="App">
{/* 原数组 */}
{JSON.stringify(counterStore.list)}
{/* 计算属性 */}
{JSON.stringify(counterStore.filterList)}
<button onClick={() => counterStore.changeList()}>change list</button>
</div>
)
}
// 包裹组件让视图响应数据变化
export default observer(App)
异步数据处理
实现步骤:
- 在mobx中编写异步请求方法 获取数据 存入state中
- 组件中通过 useEffect + 空依赖 触发action函数的执行
javascript
// 异步的获取
import { makeAutoObservable } from 'mobx'
import axios from 'axios'
class ChannelStore {
channelList = []
constructor() {
makeAutoObservable(this)
}
// 只要调用这个方法 就可以从后端拿到数据并且存入channelList
setChannelList = async () => {
const res = await axios.get('http://geek.itheima.net/v1_0/channels')
this.channelList = res.data.data.channels
}
}
const channlStore = new ChannelStore()
export default channlStore
javascript
import { useEffect } from 'react'
import { useStore } from './store'
import { observer } from 'mobx-react-lite'
function App() {
const { channlStore } = useStore()
// 1. 使用数据渲染组件
// 2. 触发action函数发送异步请求
useEffect(() => {
channlStore.setChannelList()
}, [])
return (
<ul>
{channlStore.channelList.map((item) => (
<li key={item.id}>{item.name}</li>
))}
</ul>
)
}
// 让组件可以响应数据的变化[也就是数据一变组件重新渲染]
export default observer(App)
模块化
场景: 一个项目有很多的业务模块,我们不能把所有的代码都写到一起,这样不好维护,提了提供可维护性,需要引入模块化机制
实现步骤
- 拆分模块js文件,每个模块中定义自己独立的state/action
- 在store/index.js中导入拆分之后的模块,进行模块组合
- 利用React的context的机制导出统一的useStore方法,给业务组件使用。(当然也可以直接导出,使用context导出的好处就是调试+依赖注入)
store/taskStore.js:
javascript
import { makeAutoObservable } from 'mobx'
class TaskStore {
taskList = []
constructor() {
makeAutoObservable(this)
}
addTask () {
this.taskList.push('vue', 'react')
}
}
const task = new TaskStore()
export default task
store/counterStore.js:
javascript
import { makeAutoObservable } from 'mobx'
class CounterStore {
count = 0
list = [1, 2, 3, 4, 5, 6]
constructor() {
makeAutoObservable(this)
}
addCount = () => {
this.count++
}
changeList = () => {
this.list.push(7, 8, 9)
}
get filterList () {
return this.list.filter(item => item > 4)
}
}
const counter = new CounterStore()
export default counter
组合模块导出统一方法:
index.js
javascript
import React from 'react'
import counter from './counterStore'
import task from './taskStore'
class RootStore {
constructor() {
this.counterStore = counter
this.taskStore = task
}
}
const rootStore = new RootStore()
// context机制的数据查找链 Provider如果找不到 就找createContext方法执行时传入的参数
const context = React.createContext(rootStore)
const useStore = () => React.useContext(context)
// useStore() => rootStore { counterStore, taskStore }
export { useStore }
这个地方直接导出rootstore也是可以的。
接下来我们就来使用:
javascript
import { observer } from 'mobx-react-lite'
// 导入方法
import { useStore } from './store'
function App() {
// 得到store
const store = useStore()
//这个地方我们可以直接解构赋值,想用哪一个就解构哪一个,例如:
//const {counterStore} = useStore()
return (
<div className="App">
<button onClick={() => store.counterStore.addCount()}>
{store.counterStore.count}
</button>
</div>
)
}
// 包裹组件让视图响应数据变化
export default observer(App)
多组件数据共享
目标:当数据发生变化,所有用到数据的组件都会得到同步的组件的更新
实现步骤:在Foo组件和Bar组件中分别使用store中的数据,然后在app组件中进行数据修改,查看Foo组件和Bar组件是否得到更新
Bar.js
javascript
// 用taskStore中的taskList数据
import { useStore } from './store'
import { observer } from 'mobx-react-lite'
const Bar = () => {
const { taskStore } = useStore()
return (
<ul>
{taskStore.taskList.map((item) => (
<li>{item}</li>
))}
</ul>
)
}
export default observer(Son)
Foo.js
javascript
// 用taskStore中的taskList数据
import { useStore } from './store'
import { observer } from 'mobx-react-lite'
const Bar = () => {
const { taskStore } = useStore()
return (
<ul>
{taskStore.taskList.map((item) => (
<li>{item}</li>
))}
</ul>
)
}
export default observer(Son)
App.js
javascript
import Bar from './Bar'
import Foo from './Foo'
import { useStore } from './store'
function App() {
const { taskStore } = useStore()
return (
<div className="App">
<Bar />
<button onClick={() => taskStore.setTaskList('angular')}>
修改taskStore
</button>
</div>
)
}
export default App
Mobx和React职责划分
Mobx和React的职责划分就是,Mobx负责管理应用的状态,React负责渲染应用的界面。你应该把那些需要跨组件共享或者响应变化的数据交给Mobx维护,比如用户的信息、购物车的内容、主题的设置等。你应该把那些只和组件自身相关或者不需要响应变化的数据交给React维护,比如表单的输入、组件的展开状态、动画的进度等。
- 这里的业务状态数据就类似于从后端获得的数据,其余的我们都可以当作UI的临时状态。