You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue3+Vite中router.push()后URL变更但组件未渲染求助

Vue3 + Vue-Router路由跳转后URL变更但组件不渲染问题排查与解决

问题概述

使用Vue3(3.2.45)、Vite(4.0.0)搭配vue-router(^4.1.6)开发项目时,调用router.push()后URL有时会更新,但对应组件未渲染。已尝试给<router-view>添加:key属性,问题仍存在。

依赖版本

vue: ^3.2.45
vue-router: ^4.1.6
vite: ^4.0.0

项目配置代码

vite.config.ts

import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'
import VueI18nPlugin from '@intlify/unplugin-vue-i18n/vite'
import path from 'path'

// https://vitejs.dev/config/
export default defineConfig({
  envDir: './env',
  plugins: [
    vue(),
    VueI18nPlugin({
      include: path.resolve(__dirname, './src/locales/**')
    })
  ],
  build: {
    sourcemap: false
  },
  resolve: {
    alias: {
      '@': path.resolve(__dirname, './src')
    }
  },
  optimizeDeps: {
    exclude: ['flatpickr/dist/types']
  }
})

router.ts

import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
import { CheckAuthType, RouteName } from '../types/enums/system'

import loggedInRoutes from './LoggedIn/loggedInRoutes'

const Error404 = () => import('../views/Error/Error404.vue')
const Login = () => import('../views/Auth/Login.vue')
const RegisteredHome = () => import('../views/Home/RegisteredHome.vue')

import { SessionManager } from '../managers'

const routes: Array<RouteRecordRaw> = [
  {
    path: '/:pathMatch(.*)*',
    name: RouteName.error404,
    component: Error404
  },
  {
    path: '/',
    redirect: (to) => {
      console.log('TEST ROOT REDIRECT TO: ', to.name)

      return { name: RouteName.login }
    }
  },
  {
    name: RouteName.login,
    path: '/login',
    component: Login,
    beforeEnter: (to, from, next) => {
      const sessionExist = SessionManager.sessionExist()

      console.log(`TEST BEFORE ENTER LOGIN TO: ${to.name as string} FROM: ${from.name as string} SESSION: ${sessionExist}`)
      if (sessionExist) {
        next({
          name: RouteName.dashboard
        })
      } else {
        next()
      }
    }
  },
  {
    path: '/dsh',
    component: RegisteredHome,
    props: {
      main: true
    },
    meta: {
      requireAuth: true
    },
    name: RouteName.registeredHome,
    children: loggedInRoutes
  }
]
const router = createRouter({
  history: createWebHistory(),
  routes,
  scrollBehavior(to, from, savedPosition) {
    return savedPosition || { left: 0, top: 0 }
  }
})

router.beforeEach((to, from, next) => {
  const sessionExist = SessionManager.sessionExist()
  console.log(`TEST BEFORE EACH TO: ${to.name as string} FROM: ${from.name as string} SESSION: ${sessionExist}`)

  if (to.matched.some((record) => record.meta.requireAuth)) {
    if (!sessionExist) {
      next({
        name: RouteName.login
      })
    } else {
      if (to.meta.authKey) {
        if (Array.isArray(to.meta.authKey) && to.meta.authKey.length) {
          let checkCount = 0

          to.meta.authKey.forEach((key) => {
            if (SessionManager.checkAuth(key)) {
              checkCount++
            }
          })

          if (to.meta.checkAuthType === CheckAuthType.EVERY) {
            if (checkCount === to.meta.authKey.length) {
              next()
            } else {
              next({
                name: RouteName.unauthorized
              })
            }
          } else {
            if (checkCount > 0) {
              next()
            } else {
              next({
                name: RouteName.unauthorized
              })
            }
          }
        } else if (SessionManager.checkAuth(to.meta.authKey as string)) {
          next()
        } else {
          next({
            name: RouteName.unauthorized
          })
        }
      } else {
        next()
      }
    }
  } else {
    next()
  }
})

router.afterEach((to, from, failure) => {
  if (failure) {
    console.log('ROUTE ISSUE!', failure)
  }
})

export default router

main.ts

import { createApp } from 'vue'
import router from './router'

import App from './App.vue'

import '../src/assets/css/app.css'
import Toast from 'vue-toastification'
import { LocalizationManager } from './managers'
import { vMaska } from 'maska'
import moment from 'moment-timezone'
import { BreadCrumbItem } from './types/classes/system'
import 'vue-router'
import { CheckAuthType } from './types/enums/system'
import 'leaflet'
declare module 'vue-router' {
  interface RouteMeta {
    authKey?: Array<string> | string
    checkAuthType?: CheckAuthType
    activeMenu?: string
    breadCrumb?: Array<BreadCrumbItem>
  }
}

declare module 'leaflet' {
  interface MarkerOptions {
    locationIndex?: number
  }
}

moment.tz.setDefault('Europe/Istanbul')

const app = createApp(App).use(router)

app.use(Toast)
app.use(LocalizationManager.i18n.value)
app.directive('maska', vMaska)
app.mount('#app')

App.vue

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

<script setup lang="ts">
import { EventManager, LocalizationManager, SessionManager } from './managers'
import { useRoute, useRouter } from 'vue-router'
import LoadingOverlay from './components/LoadingOverlay.vue'
import { watch, onMounted } from 'vue'
import { EventName, RouteName } from './types/enums/system'
import { useI18n } from 'vue-i18n'

const route = useRoute()
const router = useRouter()
const { t } = useI18n()
LocalizationManager.changeTitle()

const logoutAndGotoMain = () => {
  router.push({ name: RouteName.login })
}

const changeTitle = () => {
  const projectName = import.meta.env.VITE_APP_PROJECT_NAME
  let pageName = route.name
  let temp = projectName
  if (pageName) {
    temp += ' | ' + t('PageNames.' + pageName.toString())
  }
  document.title = temp
}

watch(
  () => route.name,
  () => {
    changeTitle()
  }
)

if (import.meta.env.VITE_APP_DEBUG_SCREEN as boolean) {
  document.documentElement.classList.add('debug-screens')
}

onMounted(() => {
  EventManager.emitter.on(EventName.logoutAndGotoMain, logoutAndGotoMain)
  EventManager.emitter.on(EventName.changeTitle, changeTitle)
  if (SessionManager.sessionToken) {
    SessionManager.refreshSession()
  }
})
</script>

RegisteredHome.vue

<template>
  <SideMenu>
    <div class="pt-6">
      <router-view name="loggedIn"></router-view>
    </div>
  </SideMenu>
</template>

<script setup lang="ts">
import SideMenu from '@/layout/SideMenu.vue'
</script>

LoggedInRoutes.ts

import { RouteRecordRaw } from 'vue-router'
import { RouteName } from '@/types/enums/system'

import DashboardRoutes from './DashboardRoutes'
import SystemRoutes from './System'

const Unauthorized = () => import('@/views/Error/Unauthorized.vue')

const loggedInRoutes: Array<RouteRecordRaw> = [
  {
    path: '/unauthorized',
    name: RouteName.unauthorized,
    components: {
      loggedIn: Unauthorized
    },
    meta: {
      breadCrumb: [
        {
          routeName: RouteName.unauthorized,
          active: true
        }
      ]
    }
  },
  ...DashboardRoutes,
  ...SystemRoutes,
]

export default loggedInRoutes

排查与解决方法

1. 给命名视图添加:key属性

项目使用了命名视图(RegisteredHome.vue中的<router-view name="loggedIn">),但仅给根<router-view>设置了:key,命名视图未配置。Vue-Router的命名视图需要单独设置:key来强制组件刷新。

修改RegisteredHome.vue:

<template>
  <SideMenu>
    <div class="pt-6">
      <router-view name="loggedIn" :key="$route.fullPath" />
    </div>
  </SideMenu>
</template>

同时确保根<router-view>使用$route.fullPath作为:key(比$route.name更可靠,支持同路由名不同参数的场景):
修改App.vue:

<template>
  <router-view :key="$route.fullPath" />
  <LoadingOverlay />
</template>

2. 修正子路由路径配置

LoggedInRoutes.ts中unauthorized路由的路径为/unauthorized,这会使其脱离RegisteredHome(路径/dsh)的嵌套,成为独立路由。如果需要嵌套,子路由路径应去掉开头的斜杠:

{
  path: 'unauthorized', // 改为相对路径,对应/dsh/unauthorized
  name: RouteName.unauthorized,
  components: {
    loggedIn: Unauthorized
  },
  meta: {
    breadCrumb: [
      {
        routeName: RouteName.unauthorized,
        active: true
      }
    ]
  }
},

若需/unauthorized作为独立路由,需将其移到router.ts的根路由数组中,而非作为RegisteredHome的子路由。

3. 检查路由守卫逻辑

  • 确保beforeEach和beforeEnter中next()仅调用一次,避免多次跳转导致路由状态异常。
  • 查看router.afterEach中的failure日志,确认是否存在路由跳转失败的情况,日志会直接提示失败原因。

4. 验证异步组件加载

给异步组件添加错误捕获,排查是否存在组件加载失败的情况:

const Unauthorized = () => import('@/views/Error/Unauthorized.vue').catch(err => {
  console.error('加载Unauthorized组件失败:', err)
  return import('@/views/Error/Error404.vue')
})

5. 排查响应式丢失问题

检查组件中是否存在未用ref/reactive声明的响应式数据,这类数据不会在路由变化时触发组件更新。

验证步骤

  1. 打开浏览器控制台,查看ROUTE ISSUE!日志,确认是否有路由跳转失败。
  2. 在路由守卫中添加更详细的日志,追踪to/from路由的参数、名称变化,确认跳转逻辑符合预期。
  3. 临时注释路由守卫代码,测试基础跳转是否正常,逐步缩小问题范围。

内容的提问来源于stack exchange,提问作者Taha Yerlikaya

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.16 12:36:56