Skip to content

Navigation Guards

Control and protect your routes with powerful navigation guards that can intercept, validate, and redirect navigation attempts.

Introduction

Navigation guards are functions that allow you to control route navigation by intercepting route changes. They can be used for authentication, authorization, data validation, and other navigation-related logic.

Types of Navigation Guards

Vue Router provides several types of navigation guards:

  • Global Guards: Apply to all routes
  • Per-Route Guards: Apply to specific routes
  • In-Component Guards: Defined within components

Global Before Guards

Basic Usage

Register global before guards using router.beforeEach():

javascript
import { createRouter, createWebHistory } from 'vue-router'

const router = createRouter({
  history: createWebHistory(),
  routes: [
    // your routes
  ]
})

router.beforeEach((to, from) => {
  // Guard logic here
  console.log('Navigating from', from.path, 'to', to.path)
})

Authentication Example

javascript
router.beforeEach(async (to, from) => {
  // Check if route requires authentication
  if (to.meta.requiresAuth && !isAuthenticated()) {
    // Redirect to login page
    return { name: 'Login', query: { redirect: to.fullPath } }
  }
})

// Route configuration with meta field
const routes = [
  {
    path: '/dashboard',
    name: 'Dashboard',
    component: Dashboard,
    meta: { requiresAuth: true }
  },
  {
    path: '/login',
    name: 'Login',
    component: Login
  }
]

Return Values

Guards can return different values to control navigation:

javascript
router.beforeEach((to, from) => {
  // Allow navigation (default)
  return true
  // or simply return nothing/undefined
  
  // Cancel navigation
  return false
  
  // Redirect to different route
  return '/login'
  // or
  return { name: 'Login' }
  // or
  return { path: '/login', query: { redirect: to.fullPath } }
})

Async Guards

javascript
router.beforeEach(async (to, from) => {
  try {
    // Async operation
    const user = await getCurrentUser()
    
    if (to.meta.requiresAuth && !user) {
      return { name: 'Login' }
    }
    
    // Check user permissions
    if (to.meta.requiresRole && !user.roles.includes(to.meta.requiresRole)) {
      return { name: 'Unauthorized' }
    }
  } catch (error) {
    console.error('Authentication check failed:', error)
    return { name: 'Error' }
  }
})

Error Handling

javascript
router.beforeEach(async (to, from) => {
  try {
    await validateRoute(to)
  } catch (error) {
    // Throwing an error cancels navigation and triggers router.onError()
    throw new Error(`Navigation failed: ${error.message}`)
  }
})

// Handle navigation errors
router.onError((error) => {
  console.error('Navigation error:', error)
  // Show error message to user
})

Global Resolve Guards

Execute logic right before navigation is confirmed:

javascript
router.beforeResolve(async (to) => {
  // Perfect place for data fetching or final checks
  if (to.meta.requiresCamera) {
    try {
      await requestCameraPermission()
    } catch (error) {
      if (error instanceof NotAllowedError) {
        return false // Cancel navigation
      }
      throw error // Unexpected error
    }
  }
})

Data Fetching Example

javascript
router.beforeResolve(async (to) => {
  // Fetch critical data before entering route
  if (to.meta.preloadData) {
    try {
      const data = await fetchRouteData(to.params)
      // Store data in route meta or global store
      to.meta.data = data
    } catch (error) {
      // Handle data loading errors
      return { name: 'Error', params: { error: error.message } }
    }
  }
})

Global After Hooks

Execute logic after navigation is confirmed:

javascript
router.afterEach((to, from, failure) => {
  // Navigation completed successfully
  if (!failure) {
    // Analytics tracking
    trackPageView(to.fullPath)
    
    // Update page title
    document.title = to.meta.title || 'My App'
    
    // Accessibility announcements
    announcePageChange(to.meta.title)
  }
})

Analytics and Tracking

javascript
router.afterEach((to, from, failure) => {
  if (!failure) {
    // Google Analytics
    gtag('config', 'GA_MEASUREMENT_ID', {
      page_path: to.fullPath
    })
    
    // Custom analytics
    analytics.track('Page View', {
      path: to.fullPath,
      title: to.meta.title,
      referrer: from.fullPath
    })
  }
})

Global Injections in Guards

Since Vue 3.3, you can use dependency injection in guards:

javascript
// main.js
const app = createApp(App)
app.provide('analytics', analyticsService)
app.provide('auth', authService)

// router.js
router.beforeEach((to, from) => {
  const auth = inject('auth')
  const analytics = inject('analytics')
  
  // Use injected services
  if (to.meta.requiresAuth && !auth.isAuthenticated()) {
    return { name: 'Login' }
  }
})

Pinia Store Integration

javascript
import { useAuthStore } from '@/stores/auth'

router.beforeEach((to, from) => {
  const authStore = useAuthStore()
  
  if (to.meta.requiresAuth && !authStore.isAuthenticated) {
    return { name: 'Login' }
  }
})

Per-Route Guards

Basic Per-Route Guard

javascript
const routes = [
  {
    path: '/admin',
    component: AdminPanel,
    beforeEnter: (to, from) => {
      // Only admins can access
      if (!isAdmin()) {
        return { name: 'Unauthorized' }
      }
    }
  }
]

Multiple Guards

javascript
// Reusable guard functions
function requireAuth(to, from) {
  if (!isAuthenticated()) {
    return { name: 'Login', query: { redirect: to.fullPath } }
  }
}

function requireAdmin(to, from) {
  if (!isAdmin()) {
    return { name: 'Unauthorized' }
  }
}

function logAccess(to, from) {
  console.log(`Accessing ${to.path}`)
}

const routes = [
  {
    path: '/admin',
    component: AdminPanel,
    beforeEnter: [requireAuth, requireAdmin, logAccess]
  },
  {
    path: '/profile',
    component: UserProfile,
    beforeEnter: [requireAuth, logAccess]
  }
]

Conditional Guards

javascript
function createRoleGuard(requiredRole) {
  return (to, from) => {
    const user = getCurrentUser()
    if (!user || !user.roles.includes(requiredRole)) {
      return { name: 'Unauthorized' }
    }
  }
}

const routes = [
  {
    path: '/admin',
    component: AdminPanel,
    beforeEnter: createRoleGuard('admin')
  },
  {
    path: '/moderator',
    component: ModeratorPanel,
    beforeEnter: createRoleGuard('moderator')
  }
]

Nested Route Guards

javascript
const routes = [
  {
    path: '/user',
    component: UserLayout,
    beforeEnter: requireAuth, // Applies to all child routes
    children: [
      {
        path: 'profile',
        component: UserProfile
        // Inherits parent's beforeEnter
      },
      {
        path: 'settings',
        component: UserSettings,
        beforeEnter: (to, from) => {
          // Additional guard for settings
          if (!canEditSettings()) {
            return false
          }
        }
      }
    ]
  }
]

In-Component Guards

Options API

vue
<script>
export default {
  name: 'UserProfile',
  
  beforeRouteEnter(to, from) {
    // Called before component is created
    // No access to `this`
    console.log('Entering user profile')
    
    // Can access instance via callback
    return (vm) => {
      vm.loadUserData(to.params.id)
    }
  },
  
  beforeRouteUpdate(to, from) {
    // Called when route changes but component is reused
    // Has access to `this`
    this.loadUserData(to.params.id)
  },
  
  beforeRouteLeave(to, from) {
    // Called when leaving the route
    // Has access to `this`
    if (this.hasUnsavedChanges) {
      const answer = window.confirm(
        'You have unsaved changes. Are you sure you want to leave?'
      )
      if (!answer) return false
    }
  },
  
  methods: {
    loadUserData(userId) {
      // Load user data
    }
  }
}
</script>

Composition API

vue
<script setup>
import { ref, onMounted } from 'vue'
import { onBeforeRouteUpdate, onBeforeRouteLeave } from 'vue-router'

const userData = ref(null)
const hasUnsavedChanges = ref(false)

// Load initial data
onMounted(() => {
  loadUserData()
})

// Handle route updates
onBeforeRouteUpdate(async (to, from) => {
  if (to.params.id !== from.params.id) {
    await loadUserData(to.params.id)
  }
})

// Handle route leave
onBeforeRouteLeave((to, from) => {
  if (hasUnsavedChanges.value) {
    const answer = window.confirm(
      'You have unsaved changes. Are you sure you want to leave?'
    )
    if (!answer) return false
  }
})

async function loadUserData(userId = route.params.id) {
  try {
    userData.value = await fetchUser(userId)
  } catch (error) {
    console.error('Failed to load user data:', error)
  }
}
</script>

Advanced Component Guards

vue
<script setup>
import { ref, computed } from 'vue'
import { onBeforeRouteLeave } from 'vue-router'
import { useConfirmDialog } from '@/composables/useConfirmDialog'

const formData = ref({})
const originalData = ref({})
const { confirm } = useConfirmDialog()

const hasChanges = computed(() => {
  return JSON.stringify(formData.value) !== JSON.stringify(originalData.value)
})

onBeforeRouteLeave(async (to, from) => {
  if (hasChanges.value) {
    const shouldLeave = await confirm({
      title: 'Unsaved Changes',
      message: 'You have unsaved changes. Do you want to save before leaving?',
      actions: ['Save & Leave', 'Leave without Saving', 'Cancel']
    })
    
    if (shouldLeave === 'Save & Leave') {
      await saveForm()
      return true
    } else if (shouldLeave === 'Leave without Saving') {
      return true
    } else {
      return false // Cancel navigation
    }
  }
})
</script>

Understanding the complete navigation flow:

1. Navigation triggered
2. Call beforeRouteLeave guards in deactivated components
3. Call global beforeEach guards
4. Call beforeRouteUpdate guards in reused components
5. Call beforeEnter in route configs
6. Resolve async route components
7. Call beforeRouteEnter in activated components
8. Call global beforeResolve guards
9. Navigation is confirmed
10. Call global afterEach hooks
11. DOM updates triggered
12. Call callbacks passed to next in beforeRouteEnter guards

Flow Visualization

javascript
// Example demonstrating the flow
router.beforeEach((to, from) => {
  console.log('1. Global beforeEach')
})

router.beforeResolve((to, from) => {
  console.log('2. Global beforeResolve')
})

router.afterEach((to, from) => {
  console.log('3. Global afterEach')
})

// In component
export default {
  beforeRouteEnter(to, from) {
    console.log('Component beforeRouteEnter')
  },
  
  beforeRouteUpdate(to, from) {
    console.log('Component beforeRouteUpdate')
  },
  
  beforeRouteLeave(to, from) {
    console.log('Component beforeRouteLeave')
  }
}

Advanced Patterns

Conditional Navigation

javascript
router.beforeEach(async (to, from) => {
  // Skip guards for certain routes
  if (to.meta.skipGuards) {
    return true
  }
  
  // Different logic based on route type
  if (to.meta.type === 'public') {
    return true
  } else if (to.meta.type === 'protected') {
    return await checkAuthentication()
  } else if (to.meta.type === 'admin') {
    return await checkAdminAccess()
  }
})

Progressive Enhancement

javascript
router.beforeEach(async (to, from) => {
  // Basic check first
  if (!isAuthenticated()) {
    return { name: 'Login' }
  }
  
  // Progressive checks
  if (to.meta.requiresVerification && !isEmailVerified()) {
    return { name: 'VerifyEmail' }
  }
  
  if (to.meta.requiresSubscription && !hasActiveSubscription()) {
    return { name: 'Subscribe' }
  }
  
  if (to.meta.requiresRole && !hasRequiredRole(to.meta.requiresRole)) {
    return { name: 'Unauthorized' }
  }
})

Guard Composition

javascript
// Composable guards
function useAuthGuard() {
  return (to, from) => {
    if (!isAuthenticated()) {
      return { name: 'Login', query: { redirect: to.fullPath } }
    }
  }
}

function useRoleGuard(role) {
  return (to, from) => {
    if (!hasRole(role)) {
      return { name: 'Unauthorized' }
    }
  }
}

function useSubscriptionGuard() {
  return (to, from) => {
    if (!hasActiveSubscription()) {
      return { name: 'Subscribe' }
    }
  }
}

// Combine guards
const routes = [
  {
    path: '/admin',
    component: AdminPanel,
    beforeEnter: [
      useAuthGuard(),
      useRoleGuard('admin'),
      useSubscriptionGuard()
    ]
  }
]

Best Practices

1. Keep Guards Simple

javascript
// ✅ Good: Simple and focused
router.beforeEach((to, from) => {
  if (to.meta.requiresAuth && !isAuthenticated()) {
    return { name: 'Login' }
  }
})

// ❌ Avoid: Complex logic in guards
router.beforeEach(async (to, from) => {
  // Too much logic here
  const user = await getUser()
  const permissions = await getPermissions(user.id)
  const subscription = await getSubscription(user.id)
  // ... more complex logic
})

2. Use Meta Fields

javascript
// ✅ Good: Use meta fields for configuration
const routes = [
  {
    path: '/admin',
    component: AdminPanel,
    meta: {
      requiresAuth: true,
      requiresRole: 'admin',
      title: 'Admin Panel'
    }
  }
]

router.beforeEach((to, from) => {
  if (to.meta.requiresAuth && !isAuthenticated()) {
    return { name: 'Login' }
  }
  
  if (to.meta.requiresRole && !hasRole(to.meta.requiresRole)) {
    return { name: 'Unauthorized' }
  }
})

3. Handle Errors Gracefully

javascript
router.beforeEach(async (to, from) => {
  try {
    await validateAccess(to)
  } catch (error) {
    console.error('Guard error:', error)
    
    // Provide fallback behavior
    if (error.code === 'NETWORK_ERROR') {
      // Allow navigation but show warning
      showNetworkWarning()
      return true
    } else {
      // Block navigation for other errors
      return { name: 'Error' }
    }
  }
})

4. Provide User Feedback

javascript
router.beforeEach((to, from) => {
  if (to.meta.requiresAuth && !isAuthenticated()) {
    // Show helpful message
    showMessage('Please log in to access this page', 'warning')
    return { name: 'Login', query: { redirect: to.fullPath } }
  }
})

Testing Navigation Guards

Unit Testing

javascript
import { createRouter, createMemoryHistory } from 'vue-router'
import { beforeEach } from '@/router/guards'

describe('Navigation Guards', () => {
  let router
  
  beforeEach(() => {
    router = createRouter({
      history: createMemoryHistory(),
      routes: [
        { path: '/', component: Home },
        { path: '/login', component: Login },
        { path: '/admin', component: Admin, meta: { requiresAuth: true } }
      ]
    })
    
    router.beforeEach(beforeEach)
  })
  
  it('redirects to login when not authenticated', async () => {
    // Mock authentication state
    jest.spyOn(auth, 'isAuthenticated').mockReturnValue(false)
    
    await router.push('/admin')
    
    expect(router.currentRoute.value.name).toBe('Login')
    expect(router.currentRoute.value.query.redirect).toBe('/admin')
  })
  
  it('allows access when authenticated', async () => {
    jest.spyOn(auth, 'isAuthenticated').mockReturnValue(true)
    
    await router.push('/admin')
    
    expect(router.currentRoute.value.path).toBe('/admin')
  })
})

Integration Testing

javascript
// Cypress example
describe('Navigation Guards', () => {
  it('redirects unauthenticated users to login', () => {
    cy.visit('/admin')
    cy.url().should('include', '/login')
    cy.contains('Please log in')
  })
  
  it('allows authenticated users to access protected routes', () => {
    cy.login('user@example.com', 'password')
    cy.visit('/admin')
    cy.url().should('include', '/admin')
    cy.contains('Admin Panel')
  })
})

Common Pitfalls

1. Infinite Redirects

javascript
// ❌ Bad: Can cause infinite redirect
router.beforeEach((to, from) => {
  if (!isAuthenticated()) {
    return { name: 'Login' } // Always redirects, even from Login page
  }
})

// ✅ Good: Check current route
router.beforeEach((to, from) => {
  if (!isAuthenticated() && to.name !== 'Login') {
    return { name: 'Login' }
  }
})

2. Async Guard Issues

javascript
// ❌ Bad: Not handling async properly
router.beforeEach((to, from) => {
  checkAuth().then(isAuth => {
    if (!isAuth) return { name: 'Login' }
  })
  // Guard returns undefined immediately
})

// ✅ Good: Proper async handling
router.beforeEach(async (to, from) => {
  const isAuth = await checkAuth()
  if (!isAuth) return { name: 'Login' }
})

3. Memory Leaks

javascript
// ❌ Bad: Creating new listeners on each guard
router.beforeEach((to, from) => {
  window.addEventListener('beforeunload', handleUnload)
})

// ✅ Good: Proper cleanup
router.beforeEach((to, from) => {
  // Clean up previous listeners
  window.removeEventListener('beforeunload', handleUnload)
  
  if (to.meta.requiresUnloadWarning) {
    window.addEventListener('beforeunload', handleUnload)
  }
})

Next Steps

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