React之umi中台框架

about

procomponents官方使用说明中有ProLayout 与 Umi 配合使用会有最好的效果,Umi 会把 config.ts 中的路由自动注入到配置的 layout 中,免去我们手写菜单的烦恼。,但我测试时一直没成功过。只是在umi-plugin及umi-max只可以,但layout的配置灵活性差。在纯umi + procomponents模式下,一直未实现将config.ts路由导入procomponents。

本例在纯umi + procomponents模式下,通过自定义函数loopMenuItemconfig.ts的路由转换成prolayout中的route参数值的方式实现路由与菜单的统一配置。
请添加图片描述

提示

  • 基于umi + prolayout,框架菜单由路由自动生成。虽没有reat+antd/layout组件灵活,但更实用。pc端和移动端自适应。
  • 本示例,文字说明较少,可查看注解。
  • 支持页面url方式打开时的状态保持。
  • sider时,页面底部不配置页脚。此时在sider底部配置页脚。

安装配置

# yarn create umi myapp    # 选"Simple App"
? Pick Umi App Template › - Use arrow-keys. Return to submit.
❯    Simple App
     Ant Design Pro
     Vue Simple App
# cd myapp
# yarn add @ant-design/pro-components

项目目录

# tree myapp
myapp
├── config
│   ├── config.ts      # 配置文件
│   └── router.tsx     # 路由配置文件
├── node_modules
├── package.json
├── src
│   ├── assets
│   │   ├── logo.png 
│   │   └── logo.scss
│   ├── layouts                # 默认layout配置
│   │   ├── _defaultProps.tsx  # prolayout配置
│   │   └── index.tsx          # prolayout主框架
│   ├── test                   # 页面代码
│   │   ├── 404.tsx
│   │   ├── about.tsx
│   │   ├── home.tsx
│   │   ├── login.js
│   │   ├── menu1.tsx
│   │   ├── menu2.tsx          # 有子菜单,其内容为 return <Outlet />
│   │   ├── menu21.tsx
│   │   ├── menu22.tsx
│   │   ├── menu23.tsx         # 有子菜单,其内容为 return <Outlet />
│   │   ├── menu231.tsx
│   │   ├── menu232.tsx
│   │   └── userconf.tsx
│   └── favicon.png            # 浏览器小图标
├── tsconfig.json
├── typings.d.ts
└── yarn.lock

路由配置

config/router.tsx

import React from 'react';   //此行为必须项
import { Icon } from '@iconify/react';

import {
  MailOutlined,
  AppstoreTwoTone,
  AppstoreOutlined,
  HomeOutlined,
} from '@ant-design/icons';

import { createFromIconfontCN } from '@ant-design/icons';
const IconFont = createFromIconfontCN({
  scriptUrl: [
    '//at.alicdn.com/t/font_1788044_0dwu4guekcwr.js', // icon-javascript, icon-java, icon-shoppingcart (overridden)
    '//at.alicdn.com/t/font_1788592_a5xf2bdic3u.js', // icon-shoppingcart, icon-python
  ],
});

//定义数据类型
interface LayoutRouterType {
  key?: string;
  path: string;
  name: string;
  icon?: any;
  component?: string;
  hideInMenu?: boolean;
  routes?:LayoutRouterType[];
}

//这类路由用于prolayout框架内。
const RouterListProLayout:LayoutRouterType[] = [   
  //直接采用url
  {
    key:"about",
    path:"/about",
    name:"about",
    icon: 'https://gw.alipayobjects.com/zos/antfincdn/upvrAjAPQX/Logo_Tech%252520UI.svg',
    //icon: <MailOutlined />,
    component: "@/test/about.tsx",
  },
  //采用第三方图标:iconify
  {
    key:"home",
    path:"/home",
    name:"Home",
    //icon:<HomeOutlined />,
    icon: <Icon icon="ri:mail-open-line" />, 
    component: "@/test/home.tsx",
  },
  //采用第三方图标:Iconfont
  {
    key:"menu1",
    path:"/menu1",
    name:"menu1",
    //icon:<MailOutlined />,
    icon: <IconFont type="icon-python" />,  
    component: "@/test/menu1.tsx",
  },
  //采用antd标准图标
  {
    key:"menu2",
    path:"/menu2",
    name:"sider示例",
    icon:<AppstoreOutlined />,
    component: "@/test/menu2.tsx",
    routes: [
      {
        key:"menu21",
        path:"menu21",
        name:"menu21",
        icon:<MailOutlined />,
        component: "@/test/menu21.tsx",
      },
      {
        key:"menu22",
        path:"menu22",
        name:"menu22",
        icon:<MailOutlined />,
        component: "@/test/menu22.tsx",
      },
      {
        key:"menu23",
        path:"menu23",
        name:"menu23",
        icon:<MailOutlined />,
        component: "@/test/menu23.tsx",
        routes: [
          {
            key:"menu231",
            path:"menu231",
            name:"menu231",
            icon:<MailOutlined />,
            component: "@/test/menu231.tsx",
          },
          {
            key:"menu232",
            path:"menu232",
            name:"menu232",
            icon:<MailOutlined />,
            component: "@/test/menu232.tsx",
          },
        ]
      },
    ],
  },
]

//这类路由用于prolayout框架外
const RouterListExtr = [
  { path: "/", redirect: '/about'}, 
  {
    key:"login",
    path:"/login",
    name:"login",
    component: "@/test/login.js",
    layout:false,
  },
  {
    key:"userconf",
    path:"/userconf",
    name:"userconf",
    //icon:<MailOutlined />,
    //hideInMenu:true,
    component: "@/test/userconf.tsx",
    //layout:false,
  },
  { path: "*", component: "@/test/404.tsx"}, 
]

//在umi中,路由中配置图标时采用字串方式,如
//   icon:"MailOutlined"
//而在prolayout中,菜单的图标配置采用dom对像方式,如下
//   icon:<MailOutlined />
//此函数功能就是将图标格式转换一下。
//   先配置成icon:<MailOutlined />供prolayout使用,再转换成icon:"MailOutlined"供路由配置使用。
//   若icon本身是http方式,不再转换。
//其实路由不需要图标,可以不配置,在转换过程过程中直接配置空该项即可。
function loopMenuItem(routerlist: LayoutRouterType[]):any {
  return (
    routerlist.map(
      ( { icon, routes, ...item } ) => { 
          let iconname = ''
          //const iconname = icon.type.displayName
          //console.log("icon name:",iconname)
          if (icon) {
            if ((typeof icon) === "string") {
              iconname = icon
            } else {
              iconname = icon.type.displayName
            }
          } {
            iconname = ''
          }
          //返回新item
          return ({
            ...item,
            icon: iconname,
            routes: routes && loopMenuItem(routes),
        })
       }
    )
  )
}

//将prolayout菜单配置格式转换成umi路由识别格式。
const UmiRouter = loopMenuItem(RouterListProLayout)


export {
  RouterListProLayout,   //供prolayout导航菜单使用,作为其参数routes值使用。与UmiRouter条目一一对应。
  UmiRouter,             //供umi路由配置,此类路由嵌套在prolayout框架内有菜单条目
  RouterListExtr,        //供umi路由配置,此类路由在prolayout框架没有菜单条目。显示内容可以框架内或外。
}

config/config.ts

import { defineConfig } from "umi";
import {UmiRouter,RouterListExtr} from "./router";

export default defineConfig({
  //路由配置
  routes :[
    ...UmiRouter,
    ...RouterListExtr,
  ],

  npmClient: 'yarn',
});

layouts框架

src/layouts/_defaultProps.tsx

import React from 'react';
export default {
    //title配置
    title: "DarryG",
    
    //背景图片
    /*
    bgLayoutImgList: [
        {
          src: 'https://img.alicdn.com/imgextra/i2/O1CN01O4etvp1DvpFLKfuWq_!!6000000000279-2-tps-609-606.png',
          left: 85,
          bottom: 100,
          height: '303px',
        },
        {
          src: 'https://img.alicdn.com/imgextra/i2/O1CN01O4etvp1DvpFLKfuWq_!!6000000000279-2-tps-609-606.png',
          bottom: -68,
          right: -45,
          height: '303px',
        },
        {
          src: 'https://img.alicdn.com/imgextra/i3/O1CN018NxReL1shX85Yz6Cx_!!6000000005798-2-tps-884-496.png',
          bottom: 0,
          left: 0,
          width: '331px',
        },
   ],
   */
   //跨站点导航列表: logo前面的应用列表
  appList: [
    {
      icon: 'https://gw.alipayobjects.com/zos/rmsportal/KDpgvguMpGfqaHPjicRK.svg',
      title: 'Ant Design',
      desc: '杭州市较知名的 UI 设计语言',
      url: 'https://ant.design',
    },
    {
      icon: 'https://gw.alipayobjects.com/zos/antfincdn/FLrTNDvlna/antv.png',
      title: 'AntV',
      desc: '蚂蚁集团全新一代数据可视化解决方案',
      url: 'https://antv.vision/',
      target: '_blank',
    },
    {
      icon: 'https://gw.alipayobjects.com/zos/antfincdn/upvrAjAPQX/Logo_Tech%252520UI.svg',
      title: 'Pro Components',
      desc: '专业级 UI 组件库',
      url: 'https://procomponents.ant.design/',
    },
    {
      icon: 'https://img.alicdn.com/tfs/TB1zomHwxv1gK0jSZFFXXb0sXXa-200-200.png',
      title: 'umi',
      desc: '插件化的企业级前端应用框架。',
      url: 'https://umijs.org/zh-CN/docs',
    },
    {
      icon: 'https://gw.alipayobjects.com/zos/bmw-prod/8a74c1d3-16f3-4719-be63-15e467a68a24/km0cv8vn_w500_h500.png',
      title: 'qiankun',
      desc: '可能是你见过最完善的微前端解决方案🧐',
      url: 'https://qiankun.umijs.org/',
    },
    {
      icon: 'https://gw.alipayobjects.com/zos/rmsportal/XuVpGqBFxXplzvLjJBZB.svg',
      title: '语雀',
      desc: '知识创作与分享工具',
      url: 'https://www.yuque.com/',
    },
  ],

  //通过 token 修改样式
  /*
  token: {
    colorBgAppListIconHover: 'rgba(0,0,0,0.06)',
    colorTextAppListIconHover: 'rgba(255,255,255,0.95)',
    colorTextAppListIcon: 'rgba(255,255,255,0.85)',
    sider: {
      colorBgCollapsedButton: '#fff',
      colorTextCollapsedButtonHover: 'rgba(0,0,0,0.65)',
      colorTextCollapsedButton: 'rgba(0,0,0,0.45)',
      colorMenuBackground: '#004FD9',
      colorBgMenuItemCollapsedElevated: 'rgba(0,0,0,0.85)',
      colorMenuItemDivider: 'rgba(255,255,255,0.15)',
      colorBgMenuItemHover: 'rgba(0,0,0,0.06)',
      colorBgMenuItemSelected: 'rgba(0,0,0,0.15)',
      colorTextMenuSelected: '#fff',
      colorTextMenuItemHover: 'rgba(255,255,255,0.75)',
      colorTextMenu: 'rgba(255,255,255,0.75)',
      colorTextMenuSecondary: 'rgba(255,255,255,0.65)',
      colorTextMenuTitle: 'rgba(255,255,255,0.95)',
      colorTextMenuActive: 'rgba(255,255,255,0.95)',
      colorTextSubMenuSelected: '#fff',
    },
    header: {
      colorBgHeader: '#004FD9',
      colorBgRightActionsItemHover: 'rgba(0,0,0,0.06)',
      colorTextRightActionsItem: 'rgba(255,255,255,0.65)',
      colorHeaderTitle: '#fff',
      colorBgMenuItemHover: 'rgba(0,0,0,0.06)',
      colorBgMenuItemSelected: 'rgba(0,0,0,0.15)',
      colorTextMenuSelected: '#fff',
      colorTextMenu: 'rgba(255,255,255,0.75)',
      colorTextMenuSecondary: 'rgba(255,255,255,0.65)',
      colorTextMenuActive: 'rgba(255,255,255,0.95)',
    },
  },
  */
}

src/layouts/index.tsx

import React, {useEffect, useState } from 'react';
import { useNavigate} from "react-router-dom";
import { Outlet } from 'umi'
import { useLocation } from 'react-router-dom';

import {
  Dropdown,
} from 'antd';

import {
    PageContainer,
    ProLayout,
    DefaultFooter,
    MenuDataItem,
    ProConfigProvider,
} from '@ant-design/pro-components';

import {
  LogoutOutlined,
  EditOutlined,
} from '@ant-design/icons';

import type { MenuProps } from 'antd';

import defaultProps from './_defaultProps';

//引入菜单文件(从路由提取)
import  {RouterListProLayout} from '../../config/router.tsx'

//图标动画css
import '../assets/logo.scss';

//网站图标
const logoPNG = require('../assets/logo.png');

//-------用户配置的菜单-----------------------------------------------------
const Useritems: MenuProps['items'] = [
    {
      key: 'userconf',
      icon: <EditOutlined />,
      label: '配置修改',
    },
    {
      key: 'logout',
      icon: <LogoutOutlined />,
      label: '退出登录',
    },
]

//-----主程序-----------------------------------------------------------------------
const BasicLayout: React.FC<{}> = () => {
  //跳转函数
  const navigate = useNavigate();

  //当前应用会话的位置信息
  //指定页面的完整路径,并不是url,而是页面在layout中的地址表达。
  const [pathname, setPathname] = useState('/about');

  //是否需要页面容器的状态变量。当单页面是是需要的。
  //页面容器自带面包路径功能,单页面是不需要,只有sider时需要。
  const [isPageContainer, setIsPageContainer] = useState(false);

  //------------------若直接打开url,则状态保持--------------------------------------
  const location = useLocation()
  const path = location.pathname
  useEffect(() => {
    //const pathnamenew = path.slice(10,path.length)   //原始路径中带有"/dashborad"字串,取消
    const pathnamenew = path
    if (pathnamenew.length == 0) {   //当直接打开"http://ip"时
      setPathname('/about')
      setIsPageContainer(false)
    } else {                         //当直接打开"http://ip/xxx"时
      //由于menu2有sider菜单,需面包宵。因此需识别面包宵状态。
      if (pathnamenew.slice(1,6) == "menu2") {   
        setIsPageContainer(true)
      } else {
        setIsPageContainer(false)
      }
      setPathname(pathnamenew)
    }
    },
    []   //触发条件:页面直接url方式加载或关闭时。
  );

  //------------------导航菜单单击事件--------------------------------------
  function onMenuItemRender(item:any, dom:any){
    //console.log(item)
    //console.log(dom)
    return(
      <a
        onClick={() => {
          //console.log(item)
          setPathname(item.path || '/home');
          switch (item.key) {
            case "home":
              setIsPageContainer(false)
              navigate("/home");
              break;
            case "about":
              setIsPageContainer(false)
              navigate("/about");
              break;
            case "menu1":
              setIsPageContainer(false)
              navigate("/menu1");
              break;
            case "menu2":
              setIsPageContainer(true)
              navigate("/menu2");
              break;
            case "menu21":
              setIsPageContainer(true)
              navigate("/menu2/menu21");
              break;
            case "menu22":
              setIsPageContainer(true)
              navigate("/menu2/menu22");
              break;
            case "menu23":
              setIsPageContainer(true)
              navigate("/menu2/menu23");
              break;
            case "menu231":
              setIsPageContainer(true)
              navigate("/menu2/menu23/menu231");
              break;
            case "menu232":
              setIsPageContainer(true)
              navigate("/menu2/menu23/menu232");
              break;
            case "userconf":
              setIsPageContainer(false)
              navigate("/userconf");
              break;
            }
        }}
      >
        {dom}
      </a>
    )

  }

  //------------------当前用户下接菜单--------------------------------------
  //单击用户菜单
  function onUserConf(e:any) {
    console.log('click', e);
    switch (e.key) {
      case "userconf":
        setIsPageContainer(false)
        navigate("/userconf");
        break;
      case "logout":
        setIsPageContainer(false)
        navigate("/login");
        break;
    }
  }
  //当前用户下拉菜单渲染
  const avatarPropsRender = (props:any, dom:any) => {
    //console.log(props)    //通过console口打印出props有哪些参数
    if (props.isMobile) return [];
    return (
      <Dropdown
        menu={{
          onClick: onUserConf,  //单击事件
          items: Useritems,     //下拉菜单条目
        }}
      >
        {dom}
      </Dropdown>
    )
  }

  //------------------side底部的footer--------------------------------------
  const menuFooterRender = (props:any) => {
    //console.log(props)    //通过console口打印出props有哪些参数
    if (props?.collapsed) return undefined;
    return (
      <div
        style={{
          textAlign: 'center',
          paddingBlockStart: 12,
        }}
      >
        <div>test</div>
        <div>by guo-fs.com</div>
      </div>
    );
  }

  //脚本文字
  const line1Text = <><div style={{'color':'red','display':'inline'}} >test</div></>
  const line2Text = <><div style={{'display':'inline'}} >by guo-fs.com</div></>

  //------------------渲染--------------------------------------
  return (<>
    <ProConfigProvider dark={true}>
        <ProLayout
          //导入外部配置
          {...defaultProps}

          //title配置
          //title="Darry"        

          //logo
          logo={logoPNG}      //图片路径: project_root/public/

          //title与logo渲染
          //在此可指定单击logo或title时的跳转
          headerTitleRender={(logo, title, _) => {
            const defaultDom = (
              <a href="/about">
                <img src={logoPNG} className="App-logo" />
                {title}
              </a>
            );
            if (typeof window === 'undefined') return defaultDom;
            if (document.body.clientWidth < 1400) {
              return defaultDom;
            }
            if (_.isMobile) return defaultDom;
            return (
              <>
                {defaultDom}
              </>
            );
          }}
          //单击logo或title时的的事件
          /*
          onMenuHeaderClick={(e) => {
            console.log(e)
            navigate("/");
          }}
          */

          //sider宽度
          siderWidth={216}

          //菜单布局方式
          layout="mix"            //layout 的菜单模式,side | top | mix
          splitMenus={true}       //仅当 layout="mix" 有效
          //fixSiderbar={true}    //是否固定导航

          //当前用户图示
          avatarProps={{
            src: 'https://gw.alipayobjects.com/zos/antfincdn/efFD%24IOql2/weixintupian_20170331104822.jpg',
            size: 'small',
            title: 'guofs',
            render: avatarPropsRender,
          }}

          //当前应用会话的位置信息。如果你的应用创建了自定义的 history,则需要显示指定 location 属性,
          location={{
            //pathname: '/dashborad/menu1',    //定义默认显示的菜单或页面。若定义默认页面,则子菜单无法显示
            pathname,
          }}

          //使用 IconFont 的图标配置.
          iconfontUrl="//at.alicdn.com/t/font_8d5l8fzk5b87iudi.js"

          //自定义菜单列表。
          //此功能可以实现动态路由,用来渲染访问路由
          //menuDataRender={() =>MenuItemList}
          //{...menuList}
          //route={{routes:RouterListProLayout}}
          route={{routes:RouterListProLayout}}
          
          //menuDataRender={() => loopMenuItem(route.routes)}
          //menuDataRender={() => (props.routes)}
          
          //自定义菜单项的 render 方法,单击事件
          menuItemRender={onMenuItemRender}
          

          //onPageChange={(location)=>{
          //  console.log("页面切换事件",location)
          //</ProConfigProvider>}}

          //内容区样式
          /*
          contentStyle={{
            'border': '1px dashed rgb(255, 0, 55)',
            //'height': 'calc(inherit - 200px)'
          }}
          */
          //layout 的内容模式,Fluid:自适应,Fixed:定宽 1200px
          contentWidth="Fluid"

          //脚本配置,有bug。
          // - 在contentStyle中配置height时,若页面过大,显示内容会超过页脚信息。
          // - 当内容过少时,不会沉底
          /*
          footerRender={() => (
            <DefaultFooter
              style={{ 
                     'border': '1px solid rgb(0, 0, 255)',
                     //'height': '60px',
                     //'marginTop': '-80px',
                     //'padding': '-100px',
                     //'marginBottom': '5px',
                    }}
                
              links={[
                { key: 'test', title: 'layout', href: 'www.alipay.com' },
                { key: 'test2', title: 'layout2', href: 'www.alipay.com' },
              ]}
              copyright="这是一条测试文案"
            />
          )}
          */

          //side底部的footer
          menuFooterRender={menuFooterRender}
        >
          {
            //菜单为sider时,需要PageContainer,它自带了面包路径。否则不需要。
            //菜单为sider时,页面底部不配置页脚。在sider底部配置页脚。
            isPageContainer ? 
            (
              <PageContainer
                style={{
                  //'border': '1px dashed rgb(255, 0, 55)',
                }}
              >
                  <Outlet />
              </PageContainer>
  
            )
            : (
            //采用flex方式,将页脚显示在底部。
            <div style={{
                        //'border': '1px solid rgb(255, 0, 55)',
                        'display': 'flex',
                        'flexDirection':'column',
                        'justifyContent':'space-between',
                        'height':'calc(100vh - 60px)',  //'height':'calc(100vh - 60px)'
                        'marginTop': '-40px',
                        'marginBottom': '-80px',
                        }}>
              {/* 嵌套路由显示区 */}
              <Outlet />
              {/* 页脚 */}
              <div style={{
                //'border': '1px solid rgb(255, 0, 55)',
                'display':'flex',
                //'marginBottom': '-40px',
                'height': '60px',
                'width':'100%',
                }}>
                  <div style={{
                    //'border': '1px solid rgb(255, 255, 55)',
                    'margin':'auto',
                    //'marginBottom': '5px',
                    'textAlign':'center',
                    'fontSize':'16px'
                    }}>
                      {line1Text}<br/>{line2Text}
                  </div>
              </div>
            </div>
            )
          }

        </ProLayout>
    </ProConfigProvider>
  </>)
}

export default BasicLayout

页面代码

普通页面,如下

import React from 'react';
export default () => {
    return <>
        <h2>这是 about 页面</h2>
    </>
}

菜单页面中若有子菜单,需路由嵌套,如下

import React from 'react';
import { Outlet } from "umi";

export default () => {
    return <>
        <Outlet />
    </>
}

本文在创作过程中,曾参考如下资料,感谢原始作者

  • https://cloud.tencent.com/developer/article/2317758
  • 7
    点赞
  • 12
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值