杨sir

V1

2022/11/23阅读:11主题:绿意

重学React(四):应用规模化之路由

重学React(四):应用规模化之路由

image-20221107103643254
image-20221107103643254

前言

小羊们好!在上一篇文章中,我们通过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了!

测试EditTodo
测试EditTodo

路由跳转

需要路由跳转的地方有两处:

  • 登录页:登录成功之后跳转待办列表;
  • 待办列表:点击编辑或新增待办;注销登录。

登录页跳转

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
}

测试一下效果:

使用Form简化模版代码
使用Form简化模版代码

最后是新增待办,我们可以复用EditTodo,大家可以模仿更新部分做修改,不再赘述。

页面权限控制

下面我们引入页面权限控制,用户登录后才能查看App页面。

登录用户角色

我们假设有adminuser两种用户角色,在登录时选择,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位前端大佬一起卷
  • 输入“简历点评”参加直播点评活动
  • 输入“宝藏卡”开启免费学习之旅
  • 输入“视频教程”查看村长精品私教课

分类:

前端

标签:

React.js

作者介绍

杨sir
V1