最新公告
  • 欢迎您光临起源地模板网,本站秉承服务宗旨 履行“站长”责任,销售只是起点 服务永无止境!立即加入钻石VIP
  • vue3+jsx使用递归组件实现无限级菜单

    正文概述 掘金(前端洗碗工)   2021-01-11   536

    之前我用vue2实现了一版,地址为:vue中使用递归组件实现无限级菜单 想要实现的功能就是根据路由信息自动生成对应的菜单。

    这次用vue3+jsx再实现一版,思路没有变化,但是写起来基本完全不同了,主要变化有:

    1.composition api写法与vue2中的区别

    2.使用jsx+ts

    3.router变化

    最主要的变化还是第二个,下面我会把涉及到的内容以我的理解讲出来,如果有理解更到位的大佬,欢迎指教哦。当然最基础的jsx与ts用法下面就不说了。至于jsx写法与传统vue文件写法两者的优缺点,网上的大佬已经说了很多了,主要看自己喜欢,我觉得可以不用,但不能问起来说不出(懂的·都懂)。

    vue中使用jsx

    1.安装@vue/babel-plugin-jsx

    npm run @vue/babel-plugin-jsx --save
    

    2.在项目的babel.config.js中的plugins添加,下面是在脚手架生成文件基础下添加:

    module.exports = {
      presets: ['@vue/cli-plugin-babel/preset'],
      plugins: ['@vue/babel-plugin-jsx']
    }
    

    3.创建tsx文件以及用法 使用ts写jsx的文件就是tsx文件,用tsx文件来代替我们平常写的vue文件,下面是两种文件的区别:

    .vue文件:

    <template>
      <div>{{bar}}</div>
    </template>
    
    <script lang="ts">
    import { defineComponent, ref } from 'vue'
    export default defineComponent({
      setup () {
        const bar = ref('hello')
        return {
          bar
        }
      }
    })
    </script>
    

    .tsx文件:

    import { defineComponent, ref } from 'vue'
    export default defineComponent({
     setup () {
        const bar = ref('hello')
        return () => {
          return <div>{bar.value}</div>
        }
      }
    })
    

    上面就是简单的用法区别,当setup返回一个函数时,就是返回一个render函数,这个函数返回的内容就当作组件的模板使用,下面是一些jsx在vue中基础用法:

    v-bind或props:
    <div data={data}></div> // 变量与js表达式需要用一对大括号引起来
    
    v-for:
    {
      [1,2,3].map((item) => {
        return <div key={item}>{item}<div>
      })
    }
    
    v-if:
    { flag ? <div>超人鸭<div> : null }
    
    v-on:
    const fn = () => {
      .....
    }
    <div onClick={fn}>点击</div>
    需要传递参数:
    <div onClick={() => { fn(111) }}>点击</div>
    
    class:
    <div class="a">超人鸭</div>
    

    基础的用法就是这些,还有v-model、v-show我们的插件内部已经实现了,可以直接用,具体可以看@vue/babel-plugin-jsx,插槽的用法在下面会说到。

    功能介绍以及实现思路

    上面说到,实现的功能就是根据路由信息使用递归组件实现无限级菜单,其实就是去生成菜单组件,先看看生成的菜单:

    vue3+jsx使用递归组件实现无限级菜单

    菜单其实只有两种状态:

    1.菜单目录,下面还有子菜单的,展现出来就是点击可以收缩子菜单项(上图中的用户管理、菜单1、菜单1-2)

    2.菜单项,就是没有下一级了,点击可以跳转到具体页面。

    整个菜单就是由这两种组件组成,我使用element-ui中的导航组件来实现:

    菜单目录:el-submenu

    菜单项:el-menu-item

    而菜单目录中可以任意嵌套菜单目录和菜单项,el-submenu也是可以的。

    注:vue3中用的是element-plus哦,如果对这个组件不熟悉建议先看一下文档哦,导航组件文档

    然后是路由信息 下面是在脚手架生成的 router/index.ts文件中改 routes对象而已,其他配置不动。

    export const routes: Array<RouteRecordRaw> = [
      {
        path: '/',
        name: 'LAYOUT_VIEW',
        component: Layout,
        meta: { isLogin: true, hidden: true },
        redirect: '/user'
      },
      {
        path: '/user',
        name: 'USER_MANAGE',
        component: Layout,
        meta: { title: '用户管理', icon: 'el-icon-s-tools', alwaysShow: true },
        redirect: '/user/info',
        children: [
          {
            path: 'info',
            name: 'USER_INFO',
            component: () => import('@/views/userInfo/index'),
            meta: { title: '用户信息' }
          }
        ]
      },
      {
        path: '/test1',
        name: 'TEST1',
        component: Layout,
        meta: { title: '菜单1', icon: 'el-icon-s-tools', alwaysShow: true },
        children: [
          {
            path: 'test1-1',
            name: 'TEST1-1',
            component: () => import('@/views/test.vue'),
            meta: { title: '菜单1-1' }
          },
          {
            path: 'test1-2',
            name: 'TEST1-2',
            component: () => import('@/views/test.vue'),
            meta: { title: '菜单1-2', alwaysShow: true },
            children: [
              {
                path: 'test1-2-1',
                name: 'TEST1-2-1',
                component: () => import('@/views/test.vue'),
                meta: { title: '菜单1-2-1' }
              }
            ]
          }
        ]
      },
      {
        path: '/test2',
        name: 'TEST2',
        component: Layout,
        meta: { title: '菜单2', icon: 'el-icon-s-tools', alwaysShow: false },
        children: [
          {
            path: 'test2-1',
            name: 'TEST2-1',
            component: () => import('@/views/test.vue'),
            meta: { title: '菜单2-1' }
          }
        ]
      },
      {
        path: '/login',
        name: 'LOGIN',
        component: () => import('@/views/login/index.vue'),
        meta: { isLogin: false, hidden: true }
      }
    ]
    

    这个路由信息就对应了上面生成的菜单组件。用来生成菜单组件最主要的逻辑就是根据路由有没有children属性,如果有,那就是菜单目录,对应el-submenu,如果没有,那就是菜单项,对应el-menu-item。其中layout就是一个放着**的布局组件,然后关键信息都在每个路由的meta**中:

    • title代表菜单的名称
    • icon就是图标的类名,这里用了element自带的
    • hidden就表示不在菜单中显示,比如登录路由,404页等
    • alwaysShow是一个额外的逻辑,当路由的children只有一项时,默认是直接展示菜单项的,就是不展示那种可以收缩的菜单目录,只有配置了这个alwaysShow属性后才作菜单目录渲染。当然这是我自己的逻辑,可以随意改。

    我这样写就要求每个路由信息都要有meta对象。接下来就可以拿着这个路由信息去生成组件了。

    编写组件

    文件结构:

    vue3+jsx使用递归组件实现无限级菜单

    因为element的菜单组件最外层是一个**,所以在index.tsx**中编写外部包裹的组件,sidebarItem就是真正实现递归逻辑的组件。

    先看index.tsx:

    import { defineComponent, computed } from 'vue'
    import '../style/sidebar.scss'
    import { routes } from '@/router/index' // 将在router中定义的routes引入
    import { useRoute } from 'vue-router'
    import SidebarItem from './sidebarItem'
    
    export default defineComponent({
      setup () {
        // 过滤掉第一层不显示的路由,比如登录路由
        const isShowRoutes = computed(() => {
          return routes.filter((item) => {
            return !item.meta!.hidden
          })
        })
        // 当前路由的路径,为和el-menu的高亮项对应
        const currentPath = computed(() => {
          return useRoute().path
        })
    
        return () => {
          return <div class="layout-sidebar-wrapper">
            <el-scrollbar style="height:100%">
              <el-menu default-active={currentPath.value}
                backgroundColor="#304156"
                text-color="#bfcbd9"
                unique-opened={false}
                active-text-color="#409EFF"
                collapse-transition={false}
                mode="vertical">
                {
                  isShowRoutes.value.map((route) => {
                    return <SidebarItem item={route}
                      basePath={route.path}
                      key={route.path}>
                    </SidebarItem>
                  })
                }
              </el-menu>
            </el-scrollbar>
          </div>
        }
      }
    })
    

    这代码块没高亮看着挺难受,下面把几个代码解释一下以及与vue2写法的区别 上面也说到在setup返回一个函数就是渲染函数,在里面返回组件的模板,其他逻辑没变。 使用路由,需要在vue-router中引入:

    vue2获取当前路由的信息:
    this.$route
    
    vue3:
    import { useRoute } from 'vue-router'
    const route = useRoute()
    
    vue2路由跳转:
    this.$router.push('/')
    or
    this.$router.push({
      path: '/'
    })
    
    vue3:
    import { useRouter } from 'vue-router'
    const router = useRouter()
    router.push('/')
    

    计算属性:

    vue2:
    computed: {
      currentPath() {
        return this.$route.path
      }
    }
    
    vue3:
    import { computed } from 'vue'
    const currentPath = computed(() => {
      return useRoute().path
    })
    使用需要:
    currentPath.value
    

    类型断言,ts中的一个语法,因为ts不确定我们每个路由都有meta信息,所以上面第一个计算属性中的item.meta可能为undefined,所以在后面加上一个**!**表示我们确定这个属性一定存在:

    item.meta!.hidden
    

    最后将isShowRoutes去遍历渲染 组件,就相当于 v-for组件要接收一个item也就是一个路由对象信息,一个basePath就是每一个路由信息的基础路径这两个props。这里使用tsx就有一个明显的优点,会对props作类型校验。具体可以自己去试一下。

    sidebarItem.tsx

    import { defineComponent, PropType } from 'vue'
    import { RouteRecordRaw, useRouter } from 'vue-router'
    import path from 'path'
    
    const SidebarItem = defineComponent({
      name: 'SidebarItem',
      props: {
        item: {
          type: Object as PropType<RouteRecordRaw>,
          required: true
        },
        basePath: {
          type: String,
          required: true
        }
      },
      setup (props) {
        const router = useRouter()
    
        let data: Partial<RouteRecordRaw> = { // 存储当前路由的处理后的信息
        }
    
        const resolvePath = (routePath: string): string => {
          return path.resolve(props.basePath, routePath)
        }
    
        const navigation = (path: string) => {
          router.push(path)
        }
    
        return () => {
          const handleRoute = () => {
            const { item } = props
    
            // 最后一层的情况,渲染菜单项
            if (!item.children) {
              data = { ...item, path: '' }
              return <el-menu-item onClick={() => { navigation(resolvePath(data.path!)) }} index={resolvePath(data.path!)}>
                <span>{data.meta!.title}</span>
              </el-menu-item>
            }
            
            // 把当前这一层路由中的children的hidden过滤点
            const showingChildren = item.children.filter((item) => {
              return item.meta && !item.meta.hidden
            })
            item.children = showingChildren
            
            // 如果当前路由只有一个children并且这个children没有children属性,并且没有设置alwaysShow这个属性,当菜单项渲染
            if (showingChildren.length === 1 && !showingChildren[0].children && (item.meta && !item.meta.alwaysShow)) {
              data = showingChildren[0]
              return <el-menu-item index={resolvePath(data.path!)} onClick={() => { navigation(resolvePath(data.path!)) }}>
                <span>{data.meta!.title}</span>
              </el-menu-item>
            }
    
            const slots = {
              title: () => {
                return <div>
                  {item.meta!.icon ? <i class={item.meta!.icon}></i> : null}
                  <span>{item.meta!.title ? item.meta!.title : '未定义菜单名称'}</span>
                </div>
              }
            }
            
            // 有children属性,没其他特殊情况,作菜单目录渲染,递归引用
            return <el-submenu index={resolvePath(item.path)} v-slots={slots}>
              {item.children.map((child) => {
                return <SidebarItem item={child} basePath={resolvePath(child.path)} key={child.path}></SidebarItem>
              })}
            </el-submenu>
          }
          return <div>{handleRoute()}</div>
        }
      }
    })
    
    export default SidebarItem
    

    这个组件就比较复杂了,下面从上到下把几块解释一下:

    • PropType
    vue中定义props的类型只能定义js的基本类型,对于Object、Array、Function之类的类型在ts检验中基本等于没用,所以需要PropType来定义具体的类型
    
    用法:
    item: {
      在PropType的<>填入具体的类型
      type: Object as PropType<RouteRecordRaw>,
      required: true
    }
    
    RouteRecordRaw是路由信息的类型,是vue-router自带的,把它引入就行:
    import { RouteRecordRaw } from 'vue-router'
    
    • Partial
    Partial是ts的一个语法,将传入的类型全部变成可选的,来避免一些麻烦
    
    用法:
    interface Type{
      a: string;
      b: number;
    }
    let c: Partial<Type> = {}
    此时c的类型就是:
    {
      a?: string;
      b?: number;
    }
    
    • 递归组件

    我们在vue2中使用递归组件只要声明了name属性就可以直接在template中使用,但是在tsx中,使用递归组件需要明确的定义:

    const SidebarItem = defineComponent({
      setup(){
        return () => {
          return <SidebarItem></SidebarItem>
        }
      }
    })
    最后别忘了把组件导出:
    export default SidebarItem
    
    • 插槽

    vue3+jsx使用递归组件实现无限级菜单

    上面这一块的就是插槽的引入,先看看我们原本插槽的写法,直接看element官方对el-submenu的写法:

    <el-submenu index="1">
      <template v-slot:title>
        <i class="el-icon-location"></i>
        <span>导航一</span>
      </template>
      <el-menu-item index="1-1">选项1</el-menu-item>
      <el-menu-item index="1-2">选项2</el-menu-item>
      <el-menu-item index="1-3">选项3</el-menu-item>
    </el-submenu>
    

    这就是我们之前的写法,el-submenu就是有一个叫title的具名插槽,换成tsx写法就是我上面那种,通过传入一个对象,对象里面配置各个函数,每个函数返回要插入的dom,具体可以看jsx的介绍文档,各种用法都挺详细的:

    vue3+jsx使用递归组件实现无限级菜单

    • 注意点

    上面有一个navigation函数,作用就是点击菜单项进行路由跳转,我一开始是这样写的:

    const navigation = (path: string) => {
      const router = useRouter()
      router.push(path)
    }
    

    就是将路由的声明放在方法中,但是这样router会变成undefined,需要放在setup外层声明,可能是跟执行的时机有关,setup会在相当于vue2的beforeCreate和create之间的这个时期执行,但是和路由具体的关系我还不太清楚,待我去研究一哈。

    这样整个功能就实现了,之后只需要在路由添加配置路由信息,菜单就会自动生成。欢迎指教哦。


    起源地下载网 » vue3+jsx使用递归组件实现无限级菜单

    常见问题FAQ

    免费下载或者VIP会员专享资源能否直接商用?
    本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
    提示下载完但解压或打开不了?
    最常见的情况是下载不完整: 可对比下载完压缩包的与网盘上的容量,若小于网盘提示的容量则是这个原因。这是浏览器下载的bug,建议用百度网盘软件或迅雷下载。若排除这种情况,可在对应资源底部留言,或 联络我们.。
    找不到素材资源介绍文章里的示例图片?
    对于PPT,KEY,Mockups,APP,网页模版等类型的素材,文章内用于介绍的图片通常并不包含在对应可供下载素材包内。这些相关商业图片需另外购买,且本站不负责(也没有办法)找到出处。 同样地一些字体文件也是这种情况,但部分素材会在素材包内有一份字体下载链接清单。
    模板不会安装或需要功能定制以及二次开发?
    请QQ联系我们

    发表评论

    还没有评论,快来抢沙发吧!

    如需帝国cms功能定制以及二次开发请联系我们

    联系作者

    请选择支付方式

    ×
    迅虎支付宝
    迅虎微信
    支付宝当面付
    余额支付
    ×
    微信扫码支付 0 元