V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
爱意满满的作品展示区。
zhangfg
V2EX  ›  分享创造

腿夹腿,带你用 react 撸后台,系列三(布局和鉴权篇)(最近有一大波更新)

  •  
  •   zhangfg · 50 天前 · 1157 次点击
    这是一个创建于 50 天前的主题,其中的信息可能已经有所发展或是发生改变。

    Github 地址 | 文档 | 在线预览 | 主题版在线预览

    react-antd-console 是一个后台管理系统的前端解决方案,封装了后台管理系统必要功能(如登录、鉴权、菜单、面包屑、标签页等),帮助开发人员专注于业务快速开发。项目基于 React 18Ant design 5ViteTypeScript 等新版本。对于使用到的各项技术,会被持续更新至最新版本。可放心用于生产环境

    为了方便大家更好的掌握和使用本项目,推出系列文章:

    • 腿夹腿,带你用 react 撸后台,系列一( Vite 篇)
    • 腿夹腿,带你用 react 撸后台,系列二(项目骨架篇)
    • 腿夹腿,带你用 react 撸后台,系列三(布局和鉴权篇)

    如果你喜欢这个项目或认为对你有用,欢迎使用体验和 Star

    1. 概述

    上篇我们提到使用一个配置 routesConfig,可以生成 reactRoutesroutes,利用 reactRoutes 可生成路由,利用 routes 可生成菜单/面包屑等数据。本篇再来看看,路由、布局、菜单、权限、鉴权等相互是怎么发生化学反应的

    2. 布局组件 ConsoleLayout

    react-router 中,利用 nested-routes 特性,父组件可作为布局组件。如果在布局组件中使用 <Outlet />,那么当页面 url 匹配到路由配置的 path时,就会渲染对应的路由配置的 component 组件,而布局组件中的其他元素会一直保持不变,并且不会重新渲染

    // src/router/config/index.tsx
    
    {
      path: '/',
      component: () => import('@/layouts/ConsoleLayout'), // 使用 import() 作代码分割
      flatten: true, // flatten 可以将子路由的菜单层级提升到本级,在渲染菜单时有用
      children: [
        {
          path: 'index',
          component: () => import('@/pages/home'),
          name: '首页',
          permission: 'homeIndex',
          icon: <SvgIcon name="home" />,
        }
      ],
    }
    
    // src/layouts/ConsoleLayout/index.tsx
    
    import { Outlet } from 'react-router-dom';
    
    const ConsoleLayout = () => {
      return (
        <div className="console-layout">
          <div className="console-layout__left-side">
            <SideMenu />
          </div>
          <div className="console-layout__right-side">
            <Header />
            <div className="console-layout__right-side-main">
              <Outlet />
            </div>
            <Footer />
          </div>
        </div>
      );
    };
    

    再稍微写点样式,就可以写好整体的布局结构了

    3. 保持登录状态和获取权限数据

    话分两头。在登录时, token 被保存在了 localstorage,这样刷新页面后,程序仍然可以读取到保存的 token,因此保持了登录状态。另一方面,只要进入页面就会调用一次用户基本信息接口,其返回了代表用户路由访问权限的数据 permissions。再把后端的数据映射为前端的数据,保存在全局数据中。为方便查询,使用 Set 数据结构

    // src/models/withAuth/permissions.ts
    
    function formatPermissions(permissions: string[]) {
      const set = new Set(permissions);
      return {
        homeIndex: set.has('home:index'), // 每一个权限和每一个路由配置,一一对应
      };
    }
    
    export default formatPermissions;
    

    4. 菜单组件 <SideMenu />

    antd 有 菜单组件 Menu: Menu 组件可以根据 items 等 api 生成菜单。可以利用权限数据 permissions 和路由配置 routesConfig,生成 items。没有权限的路由,将不包含在 items

    5. 鉴权

    前面我们搭建好了基本的布局结构,但这还不够。还需要在每次切换路由(需要鉴权的路由)的时候,检查登录状态和是否有当前路由访问权限。具体表现为:

    • 每次路由切换,都会检查是否有本地 token ?
      • 若无本地 token ,则跳转登录页;
      • 若有本地 token ,则检查是否已请求基础数据?
        • 若未请求基础数据,则请求之;
        • 若已请求基础数据,则根据基础数据检查是否具备当前路由的访问权限?
          • 若无权限,则去 403 页。
          • 若有权限,则渲染路由 <Outlet />
    withAuthModel.init = () => {
      // 检查 token
      const { accessToken } = lsGetToken({ bellwether: true }) ?? {};
      if (!accessToken) {
        this.setState({
          hasToken: false,
          loading: false,
        });
        history.push(`/login`);
        return;
      }
      this.setState({ hasToken: true });
    
      // 检查基础数据
      try {
        if (!this.state.hasRequestedBaseInfo) {
          await this.requestBaseInfo();
        }
      } catch (err) {
        console.log(err);
      }
      this.setState({ loading: false });
      
      // 检查当前路由的访问权限
      const routePath = router.getRoutePath(window.location.pathname);
      const route = router.flattenRoutes.get(routePath);
      if (route?.permission && !this.state.permissions[route.permission]) {
        history.push('/no-access'); 
      }
    }
    

    上述逻辑如果在 vue 中,可以利用 vue-router 的导航守卫功能封装。在 react 可以通过 useEffect 监听 location.pathname 封装。再把这些逻辑放到一个高阶组件 withAuth 中,用 withAuthConsoleLayout 布局组件包裹

    const withAuth = (Component) => {
      const Auth = (props) => {
        const loading = useModel(withAuthModel, 'loading');
        const location = useLocation();
    
        useEffect(() => {
          (async function() {
            await withAuthModel.init();
          })();
        }, [location.pathname]);
    
        if (loading) {
          return <Loading />;
        }
    
        return (
          <Component {...props} />
        ); 
      };
    
      return Auth;
    };
    
    const WithAuthConsoleLayout = withAuth(() => {
      return <ConsoleLayout />;
    });
    

    也可以编写自定义的布局组件,然后用 withAuth 高阶组件包裹,以达到鉴权效果

    通过以上一顿操作,我们不仅实现了功能,还将配置数据布局/菜单样式鉴权等逻辑解耦。

    6. 系列文章

    • 腿夹腿,带你用 react 撸后台,系列一( Vite 篇)
    • 腿夹腿,带你用 react 撸后台,系列二(项目骨架篇)
    • 腿夹腿,带你用 react 撸后台,系列三(布局和鉴权篇)

    如果你喜欢这个项目或认为对你有用,欢迎使用体验和 Star

    Github 地址 | 文档 | 在线预览 | 主题版在线预览

    2 条回复    2024-11-12 16:47:18 +08:00
    Rehtt
        1
    Rehtt  
       48 天前 via Android
    之前是手摸手,现在是腿夹腿了🤣
    zhangfg
        2
    zhangfg  
    OP
       44 天前
    @Rehtt 只要内容够硬,姿势都不是问题😂
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   3204 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 20ms · UTC 12:06 · PVG 20:06 · LAX 04:06 · JFK 07:06
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.