【问题标题】:Vue router - how to have multiple components loaded on the same route path based on user role?Vue路由器-如何根据用户角色在同一路由路径上加载多个组件?
【发布时间】:2019-12-16 16:22:58
【问题描述】:

我有应用程序,用户可以在其中以不同的角色登录,例如。 sellerbuyeradmin。 对于每个用户,我想在同一路径上显示仪表板页面,例如。 http://localhost:8080/dashboard 但是,每个用户将在不同的 vue 组件中定义不同的仪表板,例如。 SellerDashboardBuyerDashboardAdminDashboard

所以基本上,当用户打开http://localhost:8080/dashboard 时,vue 应用程序应该根据用户角色(我存储在 vuex 中)加载不同的组件。同样,我想将它用于其他路线。例如,当用户进入个人资料页面时,http://localhost:8080/profile 应用程序应该根据登录的用户显示不同的个人资料组件。

所以我想为所有用户角色设置相同的路由,而不是为每个用户角色设置不同的路由,例如。我不希望用户角色包含在 url 中,如下所示:http://localhost:8080/admin/profilehttp://localhost:8080/seller/profile 等...

如何用 vue 路由器实现这个场景?

我尝试使用子路由和每个路由保护 beforeEnter 的组合来解析基于用户角色的路由。这是一个代码示例:

router.js 中:

import Vue from 'vue'
import VueRouter from 'vue-router'
import Home from '../views/Home.vue'
import store from '@/store'

Vue.use(VueRouter)

const routes = [
  {
    path: '/',
    name: 'home',
    component: Home,

    beforeEnter: (to, from, next) => {
      next({ name: store.state.userRole })
    },

    children: [
      {
        path: '',
        name: 'admin',
        component: () => import('@/components/Admin/AdminDashboard')
      },
      {
        path: '',
        name: 'seller',
        component: () => import('@/components/Seller/SellerDashboard')
      },
      {
        path: '',
        name: 'buyer',
        component: () => import('@/components/Buyer/BuyerDashboard')
      }
    ]
  },
]

const router = new VueRouter({
  mode: 'history',
  base: process.env.BASE_URL,
  routes
})

export default router

store.js 中:

import Vue from 'vue'
import Vuex from 'vuex'

Vue.use(Vuex)

export default new Vuex.Store({
  state: {
    userRole: 'seller' // can also be 'buyer' or 'admin'
  }
})

App.vue 包含顶级路由的父路由视图,例如。将/ 映射到Home 组件,将/about 映射到About 组件:

<template>
  <router-view/>
</template>

<script>
export default {
  name: 'App',
}
</script>

Home.vue 包含嵌套的router-view 用于不同用户的基于角色的组件:

<template>
  <div class="home fill-height" style="background: #ddd;">
    <h1>Home.vue</h1>
    <!-- nested router-view where user specific component should be rendered -->
    <router-view style="background: #eee" />
  </div>
</template>

<script>
export default {
  name: 'home'
}
</script>

但这不起作用,因为当我在beforeEnter 中调用next({ name: store.state.userRole }) 时,我在浏览器控制台中收到Maximum call stack size exceeded 异常。例外是:

vue-router.esm.js?8c4f:2079 RangeError: Maximum call stack size exceeded
    at VueRouter.match (vue-router.esm.js?8c4f:2689)
    at HTML5History.transitionTo (vue-router.esm.js?8c4f:2033)
    at HTML5History.push (vue-router.esm.js?8c4f:2365)
    at eval (vue-router.esm.js?8c4f:2135)
    at beforeEnter (index.js?a18c:41)
    at iterator (vue-router.esm.js?8c4f:2120)
    at step (vue-router.esm.js?8c4f:1846)
    at runQueue (vue-router.esm.js?8c4f:1854)
    at HTML5History.confirmTransition (vue-router.esm.js?8c4f:2147)
    at HTML5History.transitionTo (vue-router.esm.js?8c4f:2034)

因此没有渲染任何内容。

有什么办法可以解决这个问题吗?

【问题讨论】:

    标签: javascript vue.js vuex vue-router


    【解决方案1】:

    您可能想尝试一下这个解决方案:

    <template>
      <component :is="compName">
    </template>
    data: () {
     return {
          role: 'seller' //insert role here - maybe on `created()` or wherever
     }
    },
    components: {
     seller: () => import('/components/seller'),
     admin: () => import('/components/admin'),
     buyer: () => import('/components/buyer'),
    }
    

    或者,如果您更喜欢整洁一点(结果相同):

    <template>
      <component :is="loadComp">
    </template>
    data: () => ({compName: 'seller'}),
    computed: {
     loadComp () {
      const compName = this.compName
      return () => import(`/components/${compName}`)
     }
    }
    

    这将使您可以使用动态组件,而无需预先导入所有 cmp,而是每次只使用需要的一个。

    【讨论】:

    • 第一个示例中的compName 来自哪里?
    【解决方案2】:

    一种方法是使用dynamic component。您可以有一个子路由,其组件也是非特定的(例如DashboardComponent):

    router.js

    const routes = [
      {
        path: '/',
        name: 'home',
        children: [
          {
            path: '',
            name: 'dashboard',
            component: () => import('@/components/Dashboard')
          }
        ]
      }
    ]
    

    components/Dashboard.vue

    <template>
      <!-- wherever your component goes in the layout -->
      <component :is="dashboardComponent"></component>
    </template>
    
    <script>
    import AdminDashboard from '@/components/Admin/AdminDashboard'
    import SellerDashboard from '@/components/Seller/SellerDashboard'
    import BuyerDashboard from '@/components/Buyer/BuyerDashboard'
    
    const RoleDashboardMapping = {
      admin: AdminDashboard,
      seller: SellerDashboard,
      buyer: BuyerDashboard
    }
    
    export default {
      data () {
        return {
          dashboardComponent: RoleDashboardMapping[this.$store.state.userRole]
        }
      }
    }
    </script>
    

    【讨论】:

    • 感谢您的回答,但它可能不是我需要的。理想情况下,我想使用 webpack import() 来动态获取当前用户角色所需的应用程序块。例如。如果用户以管理员身份登录,我不想为其他角色加载组件。在您的情况下,我将静态导入 Dashboard.vue 中的所有组件,从而将它们交付给所有用户,无论角色如何。第二件事是我希望将这个逻辑包含在路由器中,而不是分散在多个通用组件中,这些组件仅用作其角色特定版本的容器。
    • 单个子路由并将name设置为store.state.userRole,然后根据该值动态构建import路径呢?
    • 动态构建 import() 路径将不起作用,因为 import() 仅适用于构建时预先定义的静态字符串。这是 webpack 知道应该捆绑哪些 js 文件等所必需的......但我现在尝试了以防万一,并确认当我尝试动态加载路由组件时出现运行时异常vue-router.esm.js?8c4f:2079 Error: Cannot find module '@/components/Seller/SellerProfile' :(
    • 啊,明白了。对不起,我的答案不是你要找的,这只是我的第一个想法。我对 Vue 比较陌生(但在其他前端框架方面有一些经验)。
    • @mlst 出于好奇(我目前无法测试)如果您在beforeEnter 中添加一个检查,例如if (to.name !== store.state.userRole) { next({ name: store.state.userRole }) } ...然后是else { next() }
    【解决方案3】:

    此类代码仅检索给定角色的组件代码:

    import Vue from "vue";
    import VueRouter from "vue-router";
    import Home from "../views/Home.vue";
    import store from "../store";
    
    Vue.use(VueRouter);
    
    const routes = [
      {
        path: "/",
        name: "home",
        component: () => {
          switch (store.state.userRole) {
            case "admin":
              return import("../components/AdminDashboard");
            case "buyer":
              return import("../components/BuyerDashboard");
            case "seller":
              return import("../components/SellerDashboard");
            default:
              return Home;
          }
        }
      }
    ];
    
    const router = new VueRouter({
      mode: "history",
      base: process.env.BASE_URL,
      routes
    });
    
    export default router;
    

    【讨论】:

    • 请注意,如果您的应用支持注销/登录而不刷新整页,则此方法将不起作用。因为Router只会调用component函数一次,然后缓存并重用从import返回的Promise解析的组件
    【解决方案4】:

    您遇到Maximum call stack size exceeded 异常,因为next({ name: store.state.userRole }) 将触发另一个重定向并再次调用beforeEnter,从而导致无限循环。 要解决这个问题,可以查看to参数,如果已经设置,可以调用next()确认导航,不会导致重定向。见以下代码:

    beforeEnter: (to, from, next) => {
      // Helper to inspect the params.
      console.log("to", to, "from", from)
    
      // this is just an example, in your case, you may need
      // to verify the value of `to.name` is not 'home' etc. 
      if (to.name) { 
        next();
      } else {
        next({ name: store.state.userRole })
      }
    },
    

    【讨论】:

      【解决方案5】:

      我遇到了同样的问题(我将 Meteor JS 与 Vue JS 一起使用),我找到了使用 render 函数 在同一路径上加载不同组件的方法。所以,在你的情况下应该是:

      import Vue from "vue";
      import VueRouter from "vue-router";
      import Home from "../views/Home.vue";
      import AdminDashboard from "../components/AdminDashboard";
      import BuyerDashboard from "../components/BuyerDashboard";
      import SellerDashboard from "../components/SellerDashboard";
      import store from "../store";
      
      Vue.use(VueRouter);
      
      const routes = [
        {
          path: "/",
          name: "home",
          component: {
            render: (h) => {
                switch (store.state.userRole) {
                   case "admin":
                     return h(AdminDashboard);
                   case "buyer":
                     return h(BuyerDashboard);
                   case "seller":
                     return h(SellerDashboard);
                   default:
                     return h(Home);
                 }
             }
          }
         }
      ];
      
      const router = new VueRouter({
        mode: "history",
        base: process.env.BASE_URL,
        routes
      });
      
      export default router;
      

      请注意,此solution 也有效,但仅适用于第一次,如果您再次进入该路由,它将保留最后加载的组件(您需要重新加载页面)。因此,使用 render 函数时,它总是会加载新组件。

      【讨论】:

        【解决方案6】:

        Vue 路由器 4 (Vue 3)

        如果您使用的是 Vue Router 4(仅适用于 Vue 3),另一种解决方案是使用 dynamic routing

        这项新功能允许我们即时删除/添加路线。

        // router.js
        import { createRouter, createWebHistory } from 'vue-router'
        import store from "../store";
        import Home from "../views/Home.vue";
        import About from "../views/About.vue";
        
        // all routes independent of user role
        const staticRoutes = [
          {
            path: "/",
            name: "home",
            component: Home,
          },
          {
            path: "/about",
            name: "about",
            component: About,
          },
        ]
        
        const getRoutesForRole = (role) => {
          // imlementation can vary - see the rest of the answer
        }
        
        // routes used at app initialization
        const initialRoutes = [...staticRoutes, ...getRoutesForRole(store.state.userRole)]
        
        const router = createRouter({
          history: createWebHistory(),
          routes: initialRoutes,
        })
        
        export default router
        
        export const updateRoutesForRole = () {
          // implementation can vary - see the rest of the answer
        }
        

        如何生成动态路由 - getRoutesForRole

        当然,实施取决于许多因素 - 你有多少路线(以及角色)可能是最重要的。

        只有 2 或 3 个路由(并且角色不多),使用静态定义就可以了:

        const routesPerRole = {
            "admin": [
              {
                path: "/dashboard",
                name: "dashboard",
                component: () => import("../components/AdminDashboard.vue")
              }, // more routes follow....
            ],
            "seller": [
              {
                path: "/dashboard",
                name: "dashboard",
                component: () => import("../components/SellerDashboard.vue")
              }, // more routes follow....
            ],
            "buyer": [
              {
                path: "/dashboard",
                name: "dashboard",
                component: () => import("../components/BuyerDashboard.vue")
              }, // more routes follow....
            ],
          }
        
        const getRoutesForRole = (role) => {
          if(!role) return []
        
          return routesPerRole[role]  
        }
        

        如果您有许多路线和/或许多角色,您可能想要更通用的东西。首先,我们需要一些好的命名约定 - 例如,假设我们将在这样的目录结构中组织我们的组件:@/components/${role}/${componentName}.vue

        然后我们可以使用 Webpacks dynamic import

        const routeTemplates = [
          {
            path: "/dashboard",
            name: "dashboard",
            component: 'Dashboard'
          },
        ]
        
        const getRoutesForRole = (role) => {
          if(!role) return []
        
          const routesForRole = routeTemplates.map(route => ({
            ...route,
            component: () => import(`@/components/${role}/${route.component}.vue`)
          }))
          
          return routesForRole 
        }
        

        请注意,感谢 import() 在 Webpack 中使用动态表达式的方式,这将使 Webpack 为 @/components 文件夹中的每个组件创建新的 JS 块,这可能不是您想要的。

        简单的解决方法是将“角色相关”组件移动到它自己的子文件夹中,因此不要使用@/components/admin/....,只需使用@/components/perRoleComponents/admin/....

        import(`@/components/perRoleComponents/${role}/${route.component}.vue`)
        

        其他解决方案是为每个角色使用不同的import() 语句。这也将允许我们使用 Webpacks "magic comments",例如强制 Webpack 将每个角色的所有组件打包到单个 js 块中:

        const routeTemplates = [
          {
            path: "/dashboard",
            name: "dashboard",
            component: 'Dashboard'
          },
        ]
        
        const getComponentLoader = (role, componentName) => {
          switch(role) {
            "admin": return () => import(
               /* webpackChunkName: "admin-components" */
               /* webpackMode: "lazy-once" */
               `@/components/admin/${componentName}.vue`)
        
            "seller": return () => import(
               /* webpackChunkName: "seller-components" */
               /* webpackMode: "lazy-once" */
               `@/components/seller/${componentName}.vue`)
        
            "buyer": return () => import(
               /* webpackChunkName: "buyer-components" */
               /* webpackMode: "lazy-once" */
               `@/components/buyer/${componentName}.vue`)
          }
        }
        
        const getRoutesForRole = (role) => {
          if(!role) return []
        
          const routesForRole = routeTemplates.map(route => ({
            ...route,
            component: getComponentLoader(role, route.component)
          }))
          
          return routesForRole 
        }
        

        如何更新路线 - updateRoutesForRole()

        最简单的情况是每个角色都有相同的路由集并且只想使用不同的组件。在这种情况下,当角色改变时切换路由我们可以使用addRoute

        向路由器添加新的路由记录。如果路由有 name 并且已经存在具有相同路由的路由,则首先将其删除。

        export const updateRoutesForRole = () {
          const role = store.state.userRole
          const routesForRole = getRoutesForRole(role)
         
          routesForRole.forEach(r => router.addRoute(r))
        }
        

        对于并非所有角色都可以使用所有路由的更复杂的场景,必须首先使用 removeRoute 函数删除先前的路由(对于先前的活动角色 - 如果有)。我们的getRoutesForRole() 也必须不同。一种解决方案是使用route meta fields

        const routeTemplates = [
          {
            path: "/dashboard",
            name: "dashboard",
            component: 'Dashboard',
            meta: { forRoles: ['admin', 'seller'] }
          },
        ]
        
        const getRoutesForRole = (role) => {
          if(!role) return []
        
          const routesForRole = routeTemplates
          .filter(route => route.meta?.forRoles?.includes(role))
          .map(route => ({
            ...route,
            component: () => import(`@/components/${role}/${route.component}.vue`)
          }))
          
          return routesForRole 
        }
        
        export const updateRoutesForRole = () {
          const role = store.state.userRole
         
          // delete previous 1st
          router.getRoutes()
            .filter(route => route.meta?.forRoles)
            .forEach(route => router.removeRoute(route.name))
        
          const routesForRole = getRoutesForRole(role)
         
          routesForRole.forEach(r => router.addRoute(r))
        }
        

        路由器 v3(适用于 Vue 2)

        请注意,Router v3(及更早版本)在设计时从未考虑到动态路由。没有removeRoute() 功能。有一个addRoute(),所以上面描述的一些场景可能是可能的但是它目前(路由器 v3.5.3)does not work as described in the documentation

        【讨论】:

          【解决方案7】:

          解决此问题的一种方法是为三种类型的用户创建三个单独的组件DashboardForAdminDashBoardForSellerDashBoardForBuyer

          然后使用 mixin.js

          export default {
              data: function () {
                  return {
                    userType : "buyer"; // replace this with a function that returns "seller", "buyer", or "admin"
                  }
              }
          }
          

          创建一个Vue组件DashboardContainer根据mixin返回值渲染正确的仪表板组件

              <template>
                  <div>
                      <div v-if="userType === 'admin'">
                          <DashboardForAdmin />
                      </div>
                      <div v-else-if="userType === 'buyer'">
                          <DashboardForBuyer />
                      </div>
                      <div v-else>
                          <DashboardForSeller />
                      </div>
                  </div>
              </template>
          
              <script>
                  import mixin from '@/mixin.js';
          
                  import DashboardForAdmin from '@/components/DashboardForAdmin.vue';
                  import DashBoardForSeller from '@/components/DashBoardForSeller.vue';
                  import DashBoardForBuyer from '@/components/DashBoardForBuyer.vue';
          
                  export default {
                      mixins: [mixin],
                      components: {
                          DashboardForAdmin, DashBoardForSeller, DashBoardForBuyer 
                      },
                  };
              </script>
          

          现在您可以为DashboardContainer 添加一条路线

          【讨论】:

            猜你喜欢
            • 2020-08-16
            • 2019-12-10
            • 2018-12-25
            • 1970-01-01
            • 1970-01-01
            • 2021-07-12
            • 2019-10-14
            • 2018-07-11
            • 2018-10-11
            相关资源
            最近更新 更多