问题背景

Vue 3 + Vite 项目部署到 Nginx 子路径 /qk/ 下。本地 npm run dev 一切正常,侧边栏正常显示;但 npm run build 打包上传到宝塔服务器后,侧边栏凭空消失,页面只剩主内容区。

环境

  • Vue 3 + Vue Router 5 + Vite 8
  • Element Plus 2.14
  • Nginx(宝塔面板)
  • 部署路径:/qk/(非根目录)

现象

环境表现
npm run dev侧边栏正常显示,导航正常
生产部署侧边栏完全消失,主内容区直接裸渲染

浏览器无任何 JS 报错,所有 JS/CSS 资源均返回 200。

排查过程

第一阶段:确认不是常见问题

先排除最基础的几个原因:

1. 资源加载 — Network 面板确认所有 JS/CSS 返回 200,无 404。

2. Nginx 配置 — 确认 try_files 配置正确:

location ^~ /qk {
    try_files $uri $uri/ /qk/index.html;
}

3. Vite base 和 Router base — 确认两处配置一致:

// vite.config.ts
export default defineConfig({
  base: '/qk/',
})

// src/router/index.ts
const router = createRouter({
  history: createWebHistory(import.meta.env.BASE_URL),
  routes,
})

这些都没问题。import.meta.env.BASE_URL 在构建时会被替换为 /qk/,和 vite.config.ts 中的 base 保持一致。

第二阶段:检查 DOM

在浏览器 Console 中检查侧边栏相关的 DOM 元素:

document.querySelector('.el-aside')    // null
document.querySelector('.el-menu')     // null
document.querySelector('.layout-container')  // null

侧边栏相关的 DOM 元素完全不存在,不是 CSS 隐藏的问题,而是根本没有渲染。

image-20260206181203768
image-20260206181203768

同时检查 #app 的内容:

document.querySelector('#app').innerHTML.substring(0, 200)
// 输出:<div class="dashboard"><h2 class="welcome-title">欢迎您...

Home 组件的内容直接渲染在 #app 里,没有经过 MainLayout 的包裹。这说明 MainLayout 组件没有被渲染

第三阶段:确认构建产物正确

对比本地构建产物和服务器上的文件:

# 本地文件 SHA1
shasum < dist/assets/index-CkDFcajS.js
# aca210159051a4f72f68479f947d1df1d1641c96

# 服务器文件 SHA1
curl -s https://gyqaq.fun/qk/assets/index-CkDFcajS.js | shasum
# aca210159051a4f72f68479f947d1df1d1641c96

SHA1 完全一致,排除部署问题。

在构建产物中搜索 MainLayout 的模板内容:

grep -o 'layout-container' dist/assets/index-CkDFcajS.js  # 存在
grep -o '轻客管家' dist/assets/index-CkDFcajS.js           # 存在

MainLayout 的代码确实在构建产物中,没有被 tree-shaking 掉。

第四阶段:检查路由匹配

在浏览器 Console 中检查 Vue Router 的运行时状态:

const app = document.querySelector('#app').__vue_app__
const router = app.config.globalProperties.$router
const route = router.currentRoute.value

console.log('路径:', route.path)
// 输出:/home

console.log('匹配的路由:', route.matched.map(r => ({ path: r.path, name: r.name })))
// 输出:
// [{ path: '/', name: 'main' },        ← MainLayout 
//  { path: '/home', name: 'home' }]    ← Home

路由匹配完全正确,MainLayout 和 Home 都被匹配到了。

image-20260206181203768
image-20260206181203768

第五阶段:定位根因

继续检查 MainLayout 在路由记录中的组件引用:

const parent = route.matched[0]
const comp = parent.components?.default

console.log('组件类型:', typeof comp)    // undefined ❗
console.log('有 render:', !!comp?.render)  // false
console.log('有 setup:', !!comp?.setup)    // false
image-20260206181203768
image-20260206181203768

MainLayout 在路由记录中的 componentundefined

路由匹配正确,但组件引用丢失了。这说明组件在路由注册时还没有被初始化。

第六阶段:发现循环依赖

逐个检查 MainLayout 的 import 链:

src/router/index.ts
  └─ import MainLayout from '@/layout/MainLayout.vue'
       └─ import { uploadImage } from '@/api/upload'
            └─ import { api } from '@/api/http'
                 └─ import router from '@/router'   ← 回到了起点!

http.ts 静态导入了 router,形成循环依赖。

image-20260206181203768
image-20260206181203768

为什么开发模式正常?

Vite 开发服务器使用原生 ES Module,每个文件是独立的模块请求。浏览器在解析循环依赖时,会正确处理已初始化的部分:

浏览器加载 router → 遇到 MainLayout import → 加载 MainLayout
→ 遇到 http import → 加载 http → 遇到 router import(已在加载中)
→ 返回 router 的已初始化部分 → 完成加载链

由于 ES Module 的 live binding 特性,各模块在完成初始化后都能拿到正确的引用。

为什么生产构建出问题?

Vite 生产构建使用 Rollup 将所有模块打包为单个 JS 文件。在单文件中,变量按声明顺序初始化。循环依赖导致:

// 打包后的简化示意
var R8;  // MainLayout,尚未赋值
var k8 = [{ component: R8 }];  // 路由定义,此时 R8 = undefined
// ... 几百行之后 ...
R8 = MainLayoutComponent;  // MainLayout 在这里才被赋值

k8[0].component 在创建时捕获的是 R8 当时的值(undefined),后续对 R8 的赋值不会更新 k8[0].component。这就是组件引用丢失的原因。

解决方案

移除 http.ts 中对 router 的静态导入,将 401 跳转改为 window.location

// src/api/http.ts

// 删除这一行 
// import router from '@/router'

// 在 401 处理中(第 65 行附近)
if (error.response?.status === 401) {
  userStore.logout()
  // router.push('/login')  
  window.location.href = import.meta.env.BASE_URL + 'login'  
  return Promise.reject(error)
}

window.location.href 会触发页面整体跳转,对于 401 未授权场景这是合理的——用户需要重新登录,全量刷新反而更干净。

根因总结

循环依赖:router → MainLayout → upload → http → router

生产构建(单文件打包)中,循环依赖导致 MainLayout 在路由注册时尚未初始化
→ component 属性为 undefined
→ MainLayout 不渲染
→ 侧边栏消失

经验教训

  1. 警惕循环依赖:模块 A 导入模块 B,模块 B 又间接导入模块 A,这种循环在开发模式(ES Module 原生加载)下可能完全正常,但在生产构建(打包为单文件)下会暴露问题。
  2. 排查思路:当"开发正常、生产异常"时,除了常见的 base 路径、Nginx 配置问题,还要考虑打包工具对模块初始化顺序的影响。
  3. 工具层的 import 要慎重:像 axios 拦截器这类工具层代码,尽量避免静态导入路由实例。用 window.location、动态 import() 或延迟获取引用来解耦。