Skip to content

RouteMeta Interface | Vue Router

RouteMeta is an interface that defines the structure for custom metadata associated with route records. It allows you to attach arbitrary data to routes for various purposes like authentication, layout configuration, and feature flags. 🏷️

📋 Interface Definition

typescript
interface RouteMeta {
  // Custom properties can be added here
  [key: string]: any
}

🎯 Purpose and Usage

The RouteMeta interface provides a type-safe way to attach custom metadata to your route configurations. This metadata can be used for route guards, layout selection, breadcrumb generation, and other route-specific behaviors.

Key Use Cases:

  • Authentication - Mark routes that require login
  • Authorization - Specify required permissions or roles
  • Layout Configuration - Choose different layouts for different routes
  • SEO Optimization - Add meta tags and page titles
  • Feature Flags - Enable/disable features per route

🔍 Property Structure

Extensible Interface

The RouteMeta interface is designed to be extended with your application-specific properties. By default, it accepts any string-keyed properties.

typescript
// Basic usage with custom properties
const routeMeta: RouteMeta = {
  requiresAuth: true,
  pageTitle: "User Profile",
  breadcrumb: "Profile"
}

Type-Safe Extension

For better TypeScript support, you can extend the interface with your specific properties:

typescript
// Extend RouteMeta in your application
declare module 'vue-router' {
  interface RouteMeta {
    requiresAuth?: boolean
    requiredRoles?: string[]
    pageTitle?: string
    breadcrumb?: string
    layout?: 'default' | 'admin' | 'auth'
    noFooter?: boolean
    // Add your custom properties here
  }
}

💡 Practical Usage Examples

Basic Route Configuration

typescript
import type { RouteRecordRaw } from 'vue-router'

const routes: RouteRecordRaw[] = [
  {
    path: '/',
    component: Home,
    meta: {
      pageTitle: 'Home Page',
      requiresAuth: false
    }
  },
  {
    path: '/admin',
    component: AdminDashboard,
    meta: {
      requiresAuth: true,
      requiredRoles: ['admin'],
      layout: 'admin',
      pageTitle: 'Admin Dashboard'
    }
  },
  {
    path: '/user/:id',
    component: UserProfile,
    meta: {
      requiresAuth: true,
      breadcrumb: 'User Profile',
      pageTitle: (route) => `Profile - ${route.params.id}`
    }
  }
]
typescript
// Authentication guard
router.beforeEach((to, from) => {
  // Check if route requires authentication
  if (to.meta.requiresAuth && !isAuthenticated()) {
    // Redirect to login
    return {
      path: '/login',
      query: { redirect: to.fullPath }
    }
  }
  
  // Check role-based access
  const requiredRoles = to.meta.requiredRoles as string[] | undefined
  if (requiredRoles && requiredRoles.length > 0) {
    const userRoles = getCurrentUserRoles()
    const hasRequiredRole = requiredRoles.some(role => 
      userRoles.includes(role)
    )
    
    if (!hasRequiredRole) {
      // Show access denied
      return '/access-denied'
    }
  }
})

Dynamic Page Titles

typescript
// Set page title based on route meta
router.afterEach((to) => {
  let pageTitle = 'My App'
  
  if (to.meta.pageTitle) {
    if (typeof to.meta.pageTitle === 'function') {
      pageTitle = to.meta.pageTitle(to)
    } else {
      pageTitle = to.meta.pageTitle as string
    }
  }
  
  document.title = pageTitle
})

🔧 Advanced Patterns

Meta-Based Layout System

typescript
class LayoutManager {
  private layouts: Map<string, Component> = new Map()
  
  constructor() {
    this.setupLayouts()
  }
  
  private setupLayouts() {
    this.layouts.set('default', DefaultLayout)
    this.layouts.set('admin', AdminLayout)
    this.layouts.set('auth', AuthLayout)
    this.layouts.set('minimal', MinimalLayout)
  }
  
  getLayoutForRoute(route: RouteLocationNormalized): Component {
    const layoutName = route.meta.layout as string || 'default'
    return this.layouts.get(layoutName) || DefaultLayout
  }
  
  // App component usage
  setupAppLayout() {
    const route = useRoute()
    const currentLayout = computed(() => 
      this.getLayoutForRoute(route)
    )
    
    return { currentLayout }
  }
}
typescript
class BreadcrumbService {
  generateBreadcrumbs(route: RouteLocationNormalized): Breadcrumb[] {
    return route.matched.map((record, index) => {
      const isLast = index === route.matched.length - 1
      
      // Get breadcrumb text from meta or fallback
      let text: string
      if (record.meta?.breadcrumb) {
        text = typeof record.meta.breadcrumb === 'function'
          ? record.meta.breadcrumb(route)
          : record.meta.breadcrumb
      } else {
        text = String(record.name || this.formatPath(record.path))
      }
      
      return {
        text,
        to: isLast ? undefined : this.getRouteForRecord(record, route),
        active: isLast
      }
    })
  }
  
  private formatPath(path: string): string {
    return path.split('/')
      .filter(segment => segment)
      .map(segment => segment.charAt(0).toUpperCase() + segment.slice(1))
      .join(' > ')
  }
}

Feature Flag System

typescript
class FeatureFlagManager {
  private enabledFeatures: Set<string> = new Set()
  
  constructor() {
    this.loadEnabledFeatures()
  }
  
  isFeatureEnabled(route: RouteLocationNormalized, feature: string): boolean {
    // Check route-specific feature flags
    const routeFeatures = route.meta.features as string[] | undefined
    if (routeFeatures?.includes(feature)) {
      return true
    }
    
    // Check global feature flags
    return this.enabledFeatures.has(feature)
  }
  
  shouldShowComponent(route: RouteLocationNormalized, component: string): boolean {
    const hiddenComponents = route.meta.hiddenComponents as string[] | undefined
    return !hiddenComponents?.includes(component)
  }
}

🚨 Important Considerations

Meta Property Safety

typescript
// ✅ Safe: Check property existence and type
if (to.meta.requiresAuth) {
  // TypeScript knows requiresAuth is truthy
  if (!isAuthenticated()) {
    // Handle authentication
  }
}

// ❌ Unsafe: Direct access without checking
const roles = to.meta.requiredRoles // Could be undefined

// ✅ Better: Type checking
const roles = to.meta.requiredRoles as string[] | undefined
if (roles && roles.length > 0) {
  // Safe to use roles
}

Meta Inheritance

typescript
// Meta properties are inherited in nested routes
const routes = [
  {
    path: '/admin',
    meta: { requiresAuth: true, layout: 'admin' },
    children: [
      {
        path: 'users',
        component: UserManagement
        // Inherits requiresAuth and layout from parent
      },
      {
        path: 'settings',
        component: AdminSettings,
        meta: { 
          ...// Inherits parent meta
          requiresAdmin: true // Additional property
        }
      }
    ]
  }
]

📚 Best Practices

Consistent Meta Property Definitions

typescript
// Define a standard set of meta properties for your app
interface AppRouteMeta {
  // Authentication & Authorization
  requiresAuth?: boolean
  requiredRoles?: string[]
  permissions?: string[]
  
  // SEO & Page Information
  pageTitle?: string | ((route: RouteLocationNormalized) => string)
  metaDescription?: string
  canonicalUrl?: string
  
  // Layout & UI
  layout?: string
  hideHeader?: boolean
  hideFooter?: boolean
  fullWidth?: boolean
  
  // Business Logic
  features?: string[]
  analyticsCategory?: string
  breadcrumb?: string | ((route: RouteLocationNormalized) => string)
}

// Extend Vue Router's RouteMeta
declare module 'vue-router' {
  interface RouteMeta extends AppRouteMeta {}
}

Meta Validation

typescript
class MetaValidator {
  static validateRouteMeta(meta: RouteMeta): ValidationResult {
    const errors: string[] = []
    
    // Validate requiresAuth
    if (meta.requiresAuth !== undefined && typeof meta.requiresAuth !== 'boolean') {
      errors.push('requiresAuth must be a boolean')
    }
    
    // Validate requiredRoles
    if (meta.requiredRoles && !Array.isArray(meta.requiredRoles)) {
      errors.push('requiredRoles must be an array')
    }
    
    // Validate pageTitle
    if (meta.pageTitle && typeof meta.pageTitle !== 'string' && 
        typeof meta.pageTitle !== 'function') {
      errors.push('pageTitle must be string or function')
    }
    
    return { isValid: errors.length === 0, errors }
  }
  
  static validateRouteConfig(route: RouteRecordRaw): ValidationResult {
    if (!route.meta) {
      return { isValid: true, errors: [] }
    }
    
    return this.validateRouteMeta(route.meta)
  }
}

Meta Utilities

typescript
class MetaUtilities {
  // Merge meta from parent and child routes
  static mergeMeta(parentMeta: RouteMeta | undefined, childMeta: RouteMeta | undefined): RouteMeta {
    return {
      ...parentMeta,
      ...childMeta,
      // Special handling for arrays
      requiredRoles: [
        ...(parentMeta?.requiredRoles as string[] || []),
        ...(childMeta?.requiredRoles as string[] || [])
      ],
      features: [
        ...(parentMeta?.features as string[] || []),
        ...(childMeta?.features as string[] || [])
      ]
    }
  }
  
  // Check if route has specific meta property
  static hasMeta(route: RouteLocationNormalized, property: string): boolean {
    return property in route.meta
  }
  
  // Get meta value with fallback
  static getMetaValue<T>(
    route: RouteLocationNormalized, 
    property: string, 
    defaultValue: T
  ): T {
    const value = route.meta[property]
    return value !== undefined ? value as T : defaultValue
  }
}

💡 Pro Tip: Extend the RouteMeta interface with your application-specific properties for full TypeScript support. This ensures type safety when accessing meta properties throughout your application.

🚀 Vue Router - 让前端路由变得简单而强大 | 构建现代化单页应用的最佳选择