杨sir
2022/11/23阅读:64主题:绿意
重学React(四):应用规模化之路由
重学React(四):应用规模化之路由

前言
小羊们好!在上一篇文章中,我们通过TodoMVC
案例的升级重构掌握了Redux
全局状态管理方法。本篇中我们将继续升级TodoMVC
,引入几个关键需求:
-
增加登录页和编辑页使TodoMVC变成多页面应用 -
引入权限控制:只有登录用户才能访问待办页面 -
引入权限控制:只有管理员才能创建和删除待办
这些功能我们会在后面的教程中带大家逐步实现,顺便,我们就学会了路由库react-router
的使用。
知识点
通过本篇文章的学习,我们将掌握如下知识点:
-
如何引入react-router -
声明多个页面并灵活跳转 -
嵌套路由的使用场景 -
路由action的使用 -
通过Form简化表单逻辑 -
控制页面访问权限 -
控制按钮访问权限 -
保存登录状态 -
路由错误处理 -
...
引入路由库react-router
路由功能我们使用react-router实现,目前版本v6.x。
我们首先安装:浏览器平台安装react-router-dom
即可
yarn add react-router-dom
下面添加一个Router
:浏览器平台我们需要创建BrowserRouter
,main.jsx:
import {
RouterProvider,
} from "react-router-dom";
import router from './routes'
ReactDOM.createRoot(document.getElementById('root')).render(
<Provider store={store}>
<React.StrictMode>
<RouterProvider router={router} />
</React.StrictMode>
</Provider>
)
这里router
我们在routes/index.jsx中创建:
import {
createBrowserRouter,
} from "react-router-dom";
import App from '../App'
const router = createBrowserRouter([
{
path: "/",
element: <App></App>,
},
]);
export default router;
现在我们的应用应该可以和之前一样正常显示

规划页面路由关系
下面我们重新规划页面路由关系:
-
登录页面Login:登录完可以区分用户身份; -
应用页面App:之前的待办页面,显示列表,新增按钮,过滤按钮等; -
编辑待办EditTodo:编辑待办事项的表单页面,新增待办事项也可以复用。
它们之间的组织关系如下:可以看出/add
和/edit/:id
是/
的嵌套子路由

下面声明这些路由:
import Login from "../Login";
import EditTodo from "../EditTodo";
const router = createBrowserRouter([
{
path: "/",
element: <App></App>,
children: [
{
path: "/edit/:id",
element: <EditTodo></EditTodo>,
},
{
path: "/add",
element: <EditTodo></EditTodo>,
},
],
},
{
path: "/login",
element: <Login></Login>,
},
]);
定义一下Login和EditTodo
export default function Login() {
return (
<div>
<h1>Login</h1>
<button>登录</button>
</div>
)
}
export default function EditTodo() {
return (
<div>EditTodo</div>
)
}
然后我们输入/login
测试一下登录页

嵌套路由
想要看到EditTodo
,需要给App.jsx
添加路由出口:
import { Outlet } from "react-router-dom";
function App() {
return (
<div className="App">
{/* ... */}
{/* 嵌套路由出口 */}
<Outlet></Outlet>
</div>
);
}
然后我们输入/add
或者/edit/id
测试一下编辑页:看到EditTodo就ok了!

路由跳转
需要路由跳转的地方有两处:
-
登录页:登录成功之后跳转待办列表; -
待办列表:点击编辑或新增待办;注销登录。
登录页跳转
react-router提供useNavigate()
方式进行命令式导航,我们来看一下如何实现,Login.jsx:
import { useNavigate } from "react-router-dom";
export default function Login() {
// 使用useNavigate进行命令式导航
const navigate = useNavigate()
function onLogin() {
// 路由跳转
navigate('/')
}
return (
<div>
<button onClick={onLogin}>登录</button>
</div>
);
}

待办列表跳转
还有另一种跳转链接方式,比较适合用在待办列表中,TodoList.jsx:
{/* <span onDoubleClick={() => editTodo(todo)}>{todo.title}</span> */}
<Link to={`/edit/${todo.id}`}>{todo.title}</Link>
再配合useParams()
,即可获取参数以便后续编辑操作,EditTodo.jsx
import { useParams } from "react-router-dom"
export default function EditTodo() {
let {id} = useParams()
return (
<div>EditTodo:{id}</div>
)
}

接下来根据ID获取待办事项填入表单:这里面引入一个react-router 6.x中的新特性Form
,表单提交行为会被react-router处理,因此不会出现页面提交行为,同时可以提供一个action
来处理表单提交。我们会发现只需要为表单项设置初始值,不需要表单项受控,这样免掉很多模版代码。
import { useSelector } from "react-redux";
import { Form, useParams } from "react-router-dom";
import { selectTodos } from "./store/todoSlice";
export default function EditTodo() {
let { id } = useParams();
// 根据id获取待编辑todo
const todos = useSelector(selectTodos);
const editedTodo = todos.find((todo) => todo.id === +id);
return (
<Form method="post">
<p>
<label>
<span>name: </span>
<input
type="text"
defaultValue={editedTodo.title}
name="title" />
</label>
</p>
<p>
<label>
<span>completed: </span>
<input
type="checkbox"
defaultChecked={editedTodo.completed}
name="completed"
/>
</label>
</p>
<p>
<button type="submit">保存</button>
<button type="button">取消</button>
</p>
</Form>
);
}
下面我们完成action
编写
import { useParams, redirect } from "react-router-dom";
import { updateTodo } from "./store/todoSlice";
import store from './store'
export async function editTodoAction({ request, params }) {
// 获取表单数据
const formData = await request.formData();
const updates = Object.fromEntries(formData);
// 类型转换
updates.id = +params.id
updates.completed = !!updates.completed
// 修改数据
store.dispatch(updateTodo(updates))
// 实际场景中是请求接口
// await updateTodo(params.id, updates);
// 操作成功重定向
return redirect(`/`);
}
添加action
,routes/index.js
{
path: "/edit/:id",
element: <EditTodo></EditTodo>,
action: editTodoAction
}
测试一下效果:

最后是新增待办,我们可以复用EditTodo
,大家可以模仿更新部分做修改,不再赘述。
页面权限控制
下面我们引入页面权限控制,用户登录后才能查看App页面。
登录用户角色
我们假设有admin
和user
两种用户角色,在登录时选择,Login.jsx
import { useState } from "react";
import { useDispatch } from "react-redux";
import { useNavigate } from "react-router-dom";
import { setRole } from "./store/userSlice";
export default function Login() {
// 添加用户角色状态
const [role, setUserRole] = useState("admin");
// 修改全局中角色状态
const dispatch = useDispatch();
const onLogin = () => {
// 设置用户角色
dispatch(setRole(role))
// ..
}
return (
<div>
<h1>Login</h1>
<div>
<input
type="radio"
name="role"
value="admin"
checked={role === "admin"}
onChange={() => setUserRole("admin")}
/>
admin
<input
type="radio"
name="role"
value="user"
checked={role === "user"}
onChange={() => setUserRole("user")}
/>
user
</div>
<button onClick={onLogin}>登录</button>
</div>
);
}
保存角色状态,userSlice.js
import { createSlice } from "@reduxjs/toolkit";
const userSlice = createSlice({
name: "user",
initialState: {
role: '',
},
reducers: {
setRole: (state, { payload }) => {
state.role = payload
}
},
});
export const selectRole = (state) => state.user.role;
export const { setRole } = userSlice.actions;
export default userSlice.reducer;
注册reducer
import user from './userSlice'
const store = configureStore({
reducer: {
user
}
})
App中显示角色并能够注销
import { useDispatch, useSelector } from "react-redux";
import { selectRole, setRole } from "./store/userSlice";
function App() {
const role = useSelector(selectRole)
const dispatch = useDispatch()
const navigate = useNavigate()
function onLogout() {
// 清空角色
dispatch(setRole(''))
// 跳转登录页
navigate('/login')
}
return (
<div className="App">
{/* ... */}
<div>
你好, {role}
<button onClick={onLogout}>注销</button>
</div>
测试效果:

页面访问权限控制
如果用户没有登录就不能访问App.jsx的内容且应该重定向到登录页,下面我们看一下实现思路:
-
实现一个RequireAuth组件,能够校验用户登录态,没有登录重定向,登录则显示内容 -
将RequireAuth作为需要保护的路由组件的父组件包起来
先看看RequireAuth
:
import { useSelector } from "react-redux";
import { Navigate, useLocation } from "react-router-dom";
import { selectLogin } from "./store/userSlice";
export default function RequireAuth({ children }) {
// 检查用户是否登录
let isLogin = useSelector(selectLogin);
// 获取当前url地址
let location = useLocation();
if (!isLogin) {
// 如果未登录重定向到 /login 页面, 同时保存当前location以便登录成功之后回跳回来
return <Navigate to="/login" state={{ from: location }} replace />;
}
// 如果登录显示RequireAuth孩子内容
return children;
}
将App组件包起来,以保护它,routes/index.jsx:
const router = createBrowserRouter([
{
path: "/",
element: (
<RequireAuth>
<App></App>
</RequireAuth>
),
现在刷新页面会自动跳回登录页:

按钮权限控制
最后我们还有一个需求:只有admin角色的用户才能创建或删除待办事项。
这实际上是一个按钮权限,如果用户是admin:
-
才渲染NewTodo组件 -
才渲染删除按钮
这样就实现了按钮级别的权限控制,聪明的你应该已经有了主意!
对了!我们同样创建一个包装组件,判断用户是否拥有所需角色,创建AuthWrapper.jsx
import { useSelector } from "react-redux";
import { selectRole } from "./store/userSlice";
// 接收roles为所需角色
export default function AuthWrapper({ children, roles = [] }) {
// 获取用户角色
const role = useSelector(selectRole);
let auth = false
// 如果没有传递roles,表明没有限制
// 如果用户拥有所需角色,表明可以展示内容
if (roles.length === 0 || roles.includes(role)) {
auth = true
}
// 如果没有授权,则什么也不显示
if (!auth) {
return null
}
// 如果拥有权限显示孩子内容
return children;
}
包装一下新增组件和删除按钮:
<AuthWrapper roles={['admin']}>
<AddTodo></AddTodo>
</AuthWrapper>
<AuthWrapper roles={['admin']}>
<button
className="destroy"
onClick={() => dispatch(removeTodo(todo.id))}
>X
</button>
</AuthWrapper>
效果如下:

错误处理
react-router
默认就有错误处理,比如我们胡乱输入一个不存在的地址,然后就会显示一个404 Not Found
错误:

但是就像里面信息提示的一样,我们并未处理这个抛出的错误。实际上我们可以用一种更好的方式改善用户体验,而不是显示这个给开发者看的页面。
因此,我们提供一个错误页面给react-router,error-page.jsx:
import { useRouteError } from "react-router-dom";
export default function ErrorPage() {
const error = useRouteError();
console.error(error);
return (
<div id="error-page">
<h1>啊哦!</h1>
<p>实在抱歉,页面显示发生了点意外情况,要不咱再试试?</p>
<p>
<i>{error.statusText || error.message}</i>
</p>
</div>
);
}
然后,将错误页设置给顶层路由,routes/index.js
import ErrorPage from "../ErrorPage";
const router = createBrowserRouter([
{
path: "/",
element: <RequireAuth><App></App></RequireAuth>,
// 加在这里
errorElement: <ErrorPage></ErrorPage>,
刷新一下再看看,虽然还是不怎么样,总比刚才好了点。当然大家可以自由的创意一下!

后续更新计划
终于完成啦,掌握了好多react-router知识。但是我们现在开发显得有些刀耕火种的感觉,界面非常朴素,显然要更加专业和高效,我们还需要UI库。react社区中知名UI库众多,例如:
-
ant design -
arco design -
material design -
chakra UI
那么,我们应该如何选择?如何应用?下一篇中,我们将带大家一起学习!
写在最后
欢迎 长按图片加好友,我会第一时间和你分享 前端行业趋势,面试资源,学习路径 等等。
添加好友备注【进阶学习】拉你进羊村前端学习群,和大佬们一起学习,关注课程更新!

关注公众号后,在首页:
-
输入“面试题”获取2022最新面试题 -
输入“进群”跟500位前端大佬一起卷 -
输入“简历点评”参加直播点评活动 -
输入“宝藏卡”开启免费学习之旅 -
输入“视频教程”查看村长精品私教课
作者介绍