Skip to content

Different History Modes

Configure how Vue Router handles browser history and URL management for optimal user experience and SEO.

Overview

Vue Router provides different history modes to handle navigation and URL management. The choice of history mode affects how URLs appear in the browser and how your application handles routing.

HTML5 History Mode

Basic Setup

The HTML5 mode uses the browser's History API and is the recommended approach for most applications:

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

const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About },
    { path: '/user/:id', component: User }
  ]
})

Custom Base URL

If your app is deployed to a subdirectory, specify the base URL:

javascript
const router = createRouter({
  history: createWebHistory('/my-app/'),
  routes: [
    // routes...
  ]
})

Advantages

  • Clean URLs: URLs look natural (e.g., https://example.com/user/123)
  • SEO Friendly: Search engines can properly index your pages
  • Better UX: Users can share and bookmark meaningful URLs
  • Server-side Rendering: Compatible with SSR applications

Server Configuration Required

HTML5 mode requires server configuration to handle client-side routing. Without proper setup, users will get 404 errors when accessing URLs directly.

Hash Mode

Basic Setup

Hash mode uses URL fragments and doesn't require server configuration:

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

const router = createRouter({
  history: createWebHashHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About },
    { path: '/user/:id', component: User }
  ]
})

Custom Base URL

javascript
const router = createRouter({
  history: createWebHashHistory('/my-app/'),
  routes: [
    // routes...
  ]
})

Characteristics

  • URLs with Hash: URLs include a hash symbol (e.g., https://example.com/#/user/123)
  • No Server Config: Works without server-side configuration
  • SEO Limitations: Hash fragments aren't sent to servers, affecting SEO
  • Legacy Support: Works with older browsers

When to Use Hash Mode

  • Static hosting without server configuration control
  • Legacy browser support requirements
  • Quick prototyping and development
  • Electron or mobile app development

Memory Mode

Basic Setup

Memory mode doesn't interact with the browser URL and is ideal for server-side environments:

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

const router = createRouter({
  history: createMemoryHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About }
  ]
})

// For SSR, manually trigger initial navigation
router.push('/initial-route')

Use Cases

  • Server-Side Rendering (SSR): Node.js environments
  • Testing: Unit and integration tests
  • Native Apps: Electron, Capacitor, or Cordova applications
  • Embedded Contexts: When URL management isn't needed

Limitations

  • No browser history navigation (back/forward buttons)
  • URLs don't change in the browser
  • Not suitable for typical web applications

Server Configuration Examples

Apache

Create or update .htaccess file:

apache
<IfModule mod_negotiation.c>
  Options -MultiViews
</IfModule>

<IfModule mod_rewrite.c>
  RewriteEngine On
  RewriteBase /
  RewriteRule ^index\.html$ - [L]
  RewriteCond %{REQUEST_FILENAME} !-f
  RewriteCond %{REQUEST_FILENAME} !-d
  RewriteRule . /index.html [L]
</IfModule>

Alternative using FallbackResource:

apache
FallbackResource /index.html

Nginx

Add to your server configuration:

nginx
location / {
  try_files $uri $uri/ /index.html;
}

For subdirectory deployment:

nginx
location /my-app/ {
  try_files $uri $uri/ /my-app/index.html;
}

Node.js (Express)

Using connect-history-api-fallback middleware:

javascript
const express = require('express')
const history = require('connect-history-api-fallback')
const path = require('path')

const app = express()

// Apply the fallback middleware before your static middleware
app.use(history({
  // Exclude API routes
  rewrites: [
    { from: /^\/api\/.*$/, to: function(context) {
      return context.parsedUrl.pathname;
    }}
  ]
}))

// Serve static files
app.use(express.static(path.join(__dirname, 'dist')))

app.listen(3000)

Native Node.js

Basic implementation without frameworks:

javascript
const http = require('http')
const fs = require('fs')
const path = require('path')
const url = require('url')

const server = http.createServer((req, res) => {
  const parsedUrl = url.parse(req.url)
  const pathname = parsedUrl.pathname
  
  // Check if file exists
  const filePath = path.join(__dirname, 'dist', pathname)
  
  fs.access(filePath, fs.constants.F_OK, (err) => {
    if (err) {
      // File doesn't exist, serve index.html
      fs.readFile(path.join(__dirname, 'dist', 'index.html'), 'utf-8', (err, content) => {
        if (err) {
          res.writeHead(500)
          res.end('Server Error')
          return
        }
        
        res.writeHead(200, { 'Content-Type': 'text/html' })
        res.end(content)
      })
    } else {
      // Serve the requested file
      fs.readFile(filePath, (err, content) => {
        if (err) {
          res.writeHead(500)
          res.end('Server Error')
          return
        }
        
        res.writeHead(200)
        res.end(content)
      })
    }
  })
})

server.listen(3000, () => {
  console.log('Server running on http://localhost:3000')
})

IIS (Internet Information Services)

Create web.config in your root directory:

xml
<?xml version="1.0" encoding="UTF-8"?>
<configuration>
  <system.webServer>
    <rewrite>
      <rules>
        <rule name="Handle History Mode and custom 404/500" stopProcessing="true">
          <match url="(.*)" />
          <conditions logicalGrouping="MatchAll">
            <add input="{REQUEST_FILENAME}" matchType="IsFile" negate="true" />
            <add input="{REQUEST_FILENAME}" matchType="IsDirectory" negate="true" />
          </conditions>
          <action type="Rewrite" url="/" />
        </rule>
      </rules>
    </rewrite>
  </system.webServer>
</configuration>

Caddy

Caddy v2:

try_files {path} /index.html

Caddy v1:

rewrite {
    regexp .*
    to {path} /
}

Firebase Hosting

Add to firebase.json:

json
{
  "hosting": {
    "public": "dist",
    "rewrites": [
      {
        "source": "**",
        "destination": "/index.html"
      }
    ]
  }
}

Netlify

Create _redirects file in your public directory:

/*    /index.html   200

Or use netlify.toml:

toml
[[redirects]]
  from = "/*"
  to = "/index.html"
  status = 200

Vercel

Create vercel.json:

json
{
  "rewrites": [
    { "source": "/(.*)", "destination": "/index.html" }
  ]
}

AWS S3 + CloudFront

Configure CloudFront error pages:

  1. Go to CloudFront distribution settings
  2. Add custom error page for 404 errors
  3. Set response page path to /index.html
  4. Set HTTP response code to 200

Handling 404 Errors

Client-Side 404 Page

Since all routes now serve index.html, implement a catch-all route:

javascript
const router = createRouter({
  history: createWebHistory(),
  routes: [
    { path: '/', component: Home },
    { path: '/about', component: About },
    { path: '/user/:id', component: User },
    
    // Catch-all route - must be last
    { path: '/:pathMatch(.*)*', name: 'NotFound', component: NotFound }
  ]
})

404 Component Example

vue
<template>
  <div class="not-found">
    <h1>404 - Page Not Found</h1>
    <p>The page you're looking for doesn't exist.</p>
    <router-link to="/">Go Home</router-link>
  </div>
</template>

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

Server-Side 404 Handling

For Node.js applications, you can implement server-side route matching:

javascript
const express = require('express')
const { createRouter, createMemoryHistory } = require('vue-router')
const routes = require('./routes') // Your route definitions

const app = express()

app.get('*', (req, res) => {
  const router = createRouter({
    history: createMemoryHistory(),
    routes
  })
  
  router.push(req.url)
  
  router.isReady().then(() => {
    const matchedRoutes = router.currentRoute.value.matched
    
    if (matchedRoutes.length === 0) {
      res.status(404).send('Page not found')
    } else {
      // Serve your Vue app
      res.sendFile(path.join(__dirname, 'dist', 'index.html'))
    }
  })
})

Best Practices

1. Choose the Right Mode

javascript
// ✅ Production web app with server control
const router = createRouter({
  history: createWebHistory(),
  routes
})

// ✅ Static hosting or legacy support
const router = createRouter({
  history: createWebHashHistory(),
  routes
})

// ✅ SSR or testing
const router = createRouter({
  history: createMemoryHistory(),
  routes
})

2. Environment-Based Configuration

javascript
const router = createRouter({
  history: process.env.NODE_ENV === 'production' 
    ? createWebHistory('/my-app/')
    : createWebHistory(),
  routes
})

3. Graceful Fallbacks

javascript
// Check for History API support
const supportsHistory = 'pushState' in window.history

const router = createRouter({
  history: supportsHistory 
    ? createWebHistory() 
    : createWebHashHistory(),
  routes
})

4. Base URL Configuration

javascript
// Use environment variables for base URL
const router = createRouter({
  history: createWebHistory(process.env.VUE_APP_BASE_URL || '/'),
  routes
})

Common Issues and Solutions

1. 404 on Direct Access

Problem: Users get 404 when accessing URLs directly

Solution: Configure server fallback to index.html

2. Assets Not Loading

Problem: CSS/JS assets return 404 in subdirectory deployments

Solution: Configure correct base URL and asset paths

javascript
// vite.config.js
export default {
  base: '/my-app/',
  // ...
}

// Vue Router
const router = createRouter({
  history: createWebHistory('/my-app/'),
  routes
})

3. API Routes Conflict

Problem: API routes are caught by history fallback

Solution: Exclude API routes from fallback

javascript
// Express with connect-history-api-fallback
app.use(history({
  rewrites: [
    { from: /^\/api\/.*$/, to: function(context) {
      return context.parsedUrl.pathname;
    }}
  ]
}))

4. Hash Mode SEO Issues

Problem: Search engines can't index hash routes

Solution: Use HTML5 mode with proper server configuration, or implement prerendering

Testing Different Modes

Unit Testing

javascript
import { createRouter, createMemoryHistory } from 'vue-router'
import { mount } from '@vue/test-utils'

describe('Router', () => {
  it('navigates correctly', async () => {
    const router = createRouter({
      history: createMemoryHistory(),
      routes: [
        { path: '/', component: Home },
        { path: '/about', component: About }
      ]
    })
    
    await router.push('/about')
    expect(router.currentRoute.value.path).toBe('/about')
  })
})

E2E Testing

javascript
// Cypress example
describe('Navigation', () => {
  it('handles direct URL access', () => {
    cy.visit('/user/123')
    cy.contains('User Profile')
    cy.url().should('include', '/user/123')
  })
  
  it('maintains URL on refresh', () => {
    cy.visit('/about')
    cy.reload()
    cy.url().should('include', '/about')
  })
})

Migration Guide

From Hash to HTML5 Mode

  1. Update router configuration:
javascript
// Before
const router = createRouter({
  history: createWebHashHistory(),
  routes
})

// After
const router = createRouter({
  history: createWebHistory(),
  routes
})
  1. Configure server fallback
  2. Update any hardcoded hash URLs
  3. Test all routes work with direct access

From Vue 2 to Vue 3

javascript
// Vue 2
const router = new VueRouter({
  mode: 'history',
  routes
})

// Vue 3
const router = createRouter({
  history: createWebHistory(),
  routes
})

Next Steps

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