嵌套路由

实际应用中,界面通常由多层嵌套的组件组成。Vue Router 支持嵌套路由,让组件结构和路由结构保持一致。

为什么需要嵌套路由

组件嵌套结构

App
└── User
    ├── UserHeader
    ├── UserSidebar
    └── UserContent
        ├── UserProfile
        ├── UserPosts
        └── UserSettings

URL 嵌套结构

/user           → User 组件
/user/profile   → User + UserProfile 组件
/user/posts     → User + UserPosts 组件
/user/settings  → User + UserSettings 组件

基本用法

定义嵌套路由

const routes = [
  {
    path: '/user',
    component: User,
    children: [
      {
        path: '',
        component: UserHome
      },
      {
        path: 'profile',
        component: UserProfile
      },
      {
        path: 'posts',
        component: UserPosts
      }
    ]
  }
]

父组件模板

<template>
  <div class="user">
    <h2>用户中心</h2>
    <nav>
      <router-link to="/user">首页</router-link>
      <router-link to="/user/profile">个人资料</router-link>
      <router-link to="/user/posts">文章</router-link>
    </nav>
    <router-view></router-view>
  </div>
</template>

子路由路径

/ 开头

children: [
  { path: '/profile', component: Profile }
]

会被当作根路径,嵌套路径不会被拼接。

不以 / 开头

children: [
  { path: 'profile', component: Profile }
]

路径会被拼接:/user/profile

空路径

children: [
  { path: '', component: UserHome }
]

匹配 /user,渲染默认子组件。

多层嵌套

三层嵌套示例

const routes = [
  {
    path: '/user',
    component: User,
    children: [
      {
        path: 'settings',
        component: UserSettings,
        children: [
          {
            path: '',
            component: SettingsGeneral
          },
          {
            path: 'security',
            component: SettingsSecurity
          },
          {
            path: 'notification',
            component: SettingsNotification
          }
        ]
      }
    ]
  }
]

URL 对应关系

/user/settings           → User + UserSettings + SettingsGeneral
/user/settings/security  → User + UserSettings + SettingsSecurity
/user/settings/notification → User + UserSettings + SettingsNotification

命名路由

定义命名路由

const routes = [
  {
    path: '/user',
    name: 'user',
    component: User,
    children: [
      {
        path: 'profile',
        name: 'user-profile',
        component: UserProfile
      },
      {
        path: 'posts',
        name: 'user-posts',
        component: UserPosts
      }
    ]
  }
]

使用命名路由

<router-link :to="{ name: 'user-profile' }">个人资料</router-link>
<router-link :to="{ name: 'user-posts' }">文章</router-link>

命名视图

多个 router-view

const routes = [
  {
    path: '/user',
    component: User,
    children: [
      {
        path: 'profile',
        components: {
          default: UserProfile,
          sidebar: ProfileSidebar,
          header: ProfileHeader
        }
      }
    ]
  }
]

父组件模板

<template>
  <div class="user">
    <router-view name="header"></router-view>
    <div class="content">
      <router-view name="sidebar"></router-view>
      <router-view></router-view>
    </div>
  </div>
</template>

重定向

子路由重定向

const routes = [
  {
    path: '/user',
    component: User,
    redirect: '/user/profile',
    children: [
      { path: 'profile', component: UserProfile },
      { path: 'posts', component: UserPosts }
    ]
  }
]

相对路径重定向

children: [
  { path: '', redirect: 'profile' },
  { path: 'profile', component: UserProfile }
]

嵌套路由守卫

组件内守卫

export default {
  beforeRouteEnter(to, from, next) {
    next(vm => {
      console.log('进入子路由')
    })
  },
  beforeRouteUpdate(to, from, next) {
    console.log('子路由参数变化')
    next()
  },
  beforeRouteLeave(to, from, next) {
    if (this.hasUnsavedChanges) {
      const answer = window.confirm('确定离开?')
      if (answer) next()
      else next(false)
    } else {
      next()
    }
  }
}

实战示例

后台管理系统

const routes = [
  {
    path: '/',
    component: Layout,
    redirect: '/dashboard',
    children: [
      {
        path: 'dashboard',
        name: 'dashboard',
        component: Dashboard,
        meta: { title: '仪表盘', icon: 'dashboard' }
      },
      {
        path: 'user',
        name: 'user',
        component: UserList,
        meta: { title: '用户管理', icon: 'user' }
      },
      {
        path: 'product',
        name: 'product',
        component: ProductLayout,
        meta: { title: '商品管理', icon: 'product' },
        children: [
          {
            path: 'list',
            name: 'product-list',
            component: ProductList,
            meta: { title: '商品列表' }
          },
          {
            path: 'category',
            name: 'product-category',
            component: ProductCategory,
            meta: { title: '分类管理' }
          }
        ]
      },
      {
        path: 'order',
        name: 'order',
        component: OrderLayout,
        meta: { title: '订单管理', icon: 'order' },
        children: [
          {
            path: 'list',
            name: 'order-list',
            component: OrderList
          },
          {
            path: 'detail/:id',
            name: 'order-detail',
            component: OrderDetail,
            props: true
          }
        ]
      }
    ]
  }
]

Layout 组件

<template>
  <div class="layout">
    <aside class="sidebar">
      <el-menu :default-active="activeMenu" router>
        <el-menu-item index="/dashboard">
          <i class="icon-dashboard"></i>
          <span>仪表盘</span>
        </el-menu-item>
        <el-submenu index="product">
          <template #title>
            <i class="icon-product"></i>
            <span>商品管理</span>
          </template>
          <el-menu-item index="/product/list">商品列表</el-menu-item>
          <el-menu-item index="/product/category">分类管理</el-menu-item>
        </el-submenu>
      </el-menu>
    </aside>
    <main class="main">
      <header class="header">
        <breadcrumb></breadcrumb>
      </header>
      <div class="content">
        <router-view></router-view>
      </div>
    </main>
  </div>
</template>

<script>
export default {
  computed: {
    activeMenu() {
      return this.$route.path
    }
  }
}
</script>

面包屑组件

export default {
  computed: {
    breadcrumbs() {
      return this.$route.matched
        .filter(route => route.meta && route.meta.title)
        .map(route => ({
          title: route.meta.title,
          path: route.path
        }))
    }
  }
}

动态生成菜单

export default {
  computed: {
    menuRoutes() {
      return this.$router.options.routes
        .find(route => route.path === '/')
        .children
        .filter(route => !route.hidden)
        .map(route => ({
          path: route.path,
          title: route.meta?.title,
          icon: route.meta?.icon,
          children: route.children?.map(child => ({
            path: `${route.path}/${child.path}`,
            title: child.meta?.title
          }))
        }))
    }
  }
}

小结

嵌套路由要点:

  1. children 配置:在父路由中定义子路由
  2. router-view:父组件需要 router-view 作为子路由出口
  3. 路径拼接:子路由路径不以 / 开头会自动拼接
  4. 多层嵌套:支持任意层级的嵌套
  5. 命名视图:多个 router-view 对应多个组件