24. 前端常见问题排查速查
前端手册排错FAQ大约 13 分钟约 3770 字
前端几乎所有"莫名其妙"的问题都能归到四个根因:public/config.js 没生效、api-services 契约没重新生成、后端路由/前端组件路径不匹配、缓存(LocalStorage/pinia)里有脏数据。按「现象 → 原因 → 解决」对照排查。
| 现象 | 原因 | 解决 |
|---|
| 所有接口 401,页面反复跳登录 | utils/request.ts 与 utils/axios-utils.ts 的响应拦截器在 status === 401 或 serve.code === 401 或响应头 access-token === 'invalid_token' 时调 clearAccessTokens() 并 window.location.reload() | 看后端 JwtHandler:Token 过期 / 进黑名单 / TokenVersion 不一致 / 租户过期 / 账号停用,五种都会 401。先 Local.remove('access-token') 重新登录,再按后端 FAQ 第 1 节排查 |
| 登录成功但接口仍 401 | Token 存在 Local 的 access-token,api-services 走 axios-utils.ts 的拦截器,读的是 Local.get(accessTokenKey) ?? Session.get('token');两套实例(service 与 axiosInstance)各读各的 | 统一用 getAPI(XxxApi)(走 axiosInstance);老的 useBaseApi 走 service,混用会出现一边有 Token 一边没有 |
| 请求没带 Token | axios-utils.ts 只在 accessToken 存在时设置 Authorization;Authorization 头被网关/CORS 拦掉 | 后端 CORS 要放行 Authorization 与 X-Authorization(刷新 Token 用) |
| 提示"网络超时"/"网络连接错误" | service 超时 50 秒(request.ts),axiosInstance 默认 10 分钟;错误提示由 showNetworkErrorThrottled 节流 2 秒弹一次 | 后端慢就优化接口;需要自己处理错误时给请求加 customCatch 配置,避免拦截器重复弹窗 |
| 后端返回业务错误但页面没提示 | axios-utils.ts 里 serve.code !== 200 会 ElMessage.error;除非请求带 customCatch | 检查后端是否返回了统一结果结构 {code,message,result};[NonUnify] 接口不走这套 |
| 一堆请求同时失败弹一屏红条 | showNetworkErrorThrottled 只节流 2 秒 | 用 suppressNetworkErrorFor(ms) 在路由守卫里手动压住后续错误 |
| 现象 | 原因 | 解决 |
|---|
getAPI(SysXxxApi).apiSysXxxYyyPost is not a function | 契约没重新生成。src/api-services/** 是 swagger-codegen 产物,后端改接口后前端不会自动同步 | 启动后端 → cd Web && pnpm buildApi(走 script/build-api.js → api_build/build.bat) |
| 生成时报 Java 相关错误 | `api_build/build.bat | sh依赖java -jar swagger-codegen-cli.jar,需要 Java 8+ 与 JAVA_HOME` |
| 生成的地址不对 | build.bat 里默认 set apiUrl=http://localhost:5005/swagger/Default/swagger.json,加参数可切分组(document / dataApproval / dingTalk …) | 后端端口不是 5005 就改 build.bat,或用 api_build/build_api.sh 交互式选地址 |
| 方法名对不上 | 命名规律是 api + 控制器名 + 方法名 + HttpMethod,如 SysConfigApi.apiSysConfigPagePost | 用 /api/sysCommon/allSysApiList 拿真实路由再对照 |
| 某个插件的接口没有 | 插件契约在独立目录(api-services/document、goView、aiChat 等),主契约 system 不含它们 | 针对插件目录单独执行契约生成 |
| 生成后 TS 报错一片 | 模型(models)与页面手写的类型冲突 | 不要手改 src/api-services/**(会被覆盖),业务类型放 src/types |
同步契约的标准动作:
# 1. 确认后端 Swagger 可访问
curl -s http://localhost:5005/swagger/All%20Groups/swagger.json | head -c 200
# 2. 生成主契约(Windows)
cd Web && pnpm buildApi
# 3. 生成插件契约
cd Web/api_build && build.bat document
# 4. 确认产物时间已更新
ls -l src/api-services/system/api.ts
| 现象 | 原因 | 解决 |
|---|
| 打开页面白屏,控制台无报错 | router/backEnd.ts 的 dynamicImport 匹配不到组件:matchKeys.length === 1 才返回组件,> 1 直接返回 false,0 返回 undefined | 检查菜单的 Component 字段:/system/user/index 必须唯一对应 src/views/system/user/index.vue;同名 index.vue 出现在多处会导致匹配到多个 |
菜单点了报 No match found for location with path 'xxx' | 没有把 notFoundAndNoPower 挂上,或路由没 addRoute | setFilterRouteEnd() 会自动追加 404/401;确认 initBackEndControlRoutes 执行了(Session.get('token') 为空会直接 return false) |
| 刷新后路由丢失 | 后端路由模式依赖 Session.get('token'),刷新时 Session 为空 | main.ts 里每次启动都会重新 initBackEndControlRoutes;确认 token 存在 |
| 菜单能显示但页面 404 | 菜单 Path 与前端实际路由不一致,或 IsIframe/IsLink 配错 | 菜单管理里核对 Path(如 /platform/tenant)与 Component(如 /system/tenant/index) |
| 页面空白且报组件加载失败 | 组件文件不存在或路径大小写错(Linux 区分大小写) | 确认 src/views 下对应文件存在;Windows 开发能跑,Linux 部署 404 就是大小写问题 |
| 循环跳转登录页 | 401 后 clearAccessTokens() 会 window.location.reload(),Token 又无效 → 死循环 | 清 LocalStorage(access-token、x-access-token)后重新登录 |
检查路由最快的办法:
// 浏览器控制台
JSON.parse(sessionStorage.getItem('token') ?? 'null'); // token 还在不在
// 查看已注册路由
// 在 router/index.ts 或任意页面里:
// console.log(router.getRoutes().map(r => r.path))
| 现象 | 原因 | 解决 |
|---|
v-auth="sysUser/add" 按钮不显示 | authDirective 取 useUserInfo().userInfos.authApiList,值来自登录接口返回的 apis 字段(stores/userInfo.ts 的 authApiList: d.apis)。权限码不在列表里就被 removeChild | 角色管理 → 授权菜单里勾上该按钮权限;改完清缓存重新登录 |
| 超管看得到,普通用户看不到 | 后端 CheckAuthorizeAsync 对超管直接放行,apis 里可能不含该码但能调通 | 用普通账号验证;权限码必须与后端真实路由完全一致(大小写不敏感但字符串要一致) |
v-auth 在 v-if 之后不生效 | 指令只在 mounted 执行一次,元素被 v-if 重建后不会重新判断 | 需要动态显隐就用 authFunction.ts 里的函数式判断,或确保元素不被销毁重建 |
v-auths / v-auth-all 行为搞混 | v-auth:满足一个即显示;v-auths:[a,b] 有任一匹配即显示;v-auth-all:全部匹配才显示 | 按语义选指令 |
| 按钮被移除导致布局错 | 指令用 el.parentNode.removeChild(el),父节点为 null(如元素已被卸载)会报错 | 不要挂在会被条件渲染的根节点上 |
| 现象 | 原因 | 解决 |
|---|
| 列不显示 | ①columns 里 field 拼错;②useVxeTable 的 id 不固定导致本地列缓存(customConfig.storage,依赖 id)错乱;③列被用户在列设置里隐藏了 | 固定 id;清 localStorage 里该表格的列状态;核对 field |
| 导出按钮点了没反应 / 导出的文件打不开 | 导出依赖 hooks/setupVXETableHook.ts 里 VxeUITable.use(VxeUIPluginExportXLSX, { ExcelJS: ExcelJS }) | 确认 main.ts 里 .use(setupVXETable) 已注册;确认 exceljs 与 @vxe-ui/plugin-export-xlsx 已安装 |
| 导出只有当前页 | exportConfig.modes 默认是 ['current','selected','all'],remote: false 表示前端导出当前数据 | 要导全部就把数据全量拉下来,或后端给导出接口(SysLogOpService.ExportLogOp 就是服务端导出) |
| 表格不发起查询 | useVxeTable 只在没传 data 时生成 proxyConfig;给了 data 就是本地数据模式 | 远程分页不要传 data,传 proxyConfig.ajax.query |
| 分页/排序偏好不生效 | useGridPageParam(id, init) 必须在 useVxeTable 之前调用,因为 pagerConfig/sortConfig 在 setup 阶段就取值 | 调整调用顺序;id 变化会让本地偏好失效 |
| 虚拟滚动没开 | setupVXETableHook 已全局开启:virtualYConfig: { enabled: true, gt: 50 }、virtualXConfig: { enabled: true, gt: 30 }(行数 > 50 才触发) | 数据量小于阈值不触发属正常;想强制开启在 extras 里覆盖 |
| 表格渲染卡 | 一次加载几千行且没走分页 | 用后端分页;或把 pagerConfig.pageSize 调小(默认 50) |
| 列宽/列序改了不保存 | customConfig.storage 需要表格有 id | 给 useVxeTable 传稳定 id |
| 现象 | 原因 | 解决 |
|---|
pnpm install 报软链/权限错误(Windows) | pnpm 用硬链接 + 符号链接组织 node_modules,Windows 下部分目录(OneDrive、无开发者模式、非 NTFS)不支持 | ①开启 Windows 开发者模式;②项目放在 NTFS 本地盘,不要放 OneDrive/网络盘;③pnpm install --config.node-linker=hoisted 退回扁平结构 |
pnpm install 报 allowBuilds 相关阻塞 | pnpm-workspace.yaml 里配置了 allowBuilds(@parcel/watcher、es5-ext、node-pty 等需要编译原生模块) | 按提示允许构建,或安装 VS Build Tools / Python |
| 构建报 SCSS 弃用警告刷屏 | Sass 新版本对 legacy-js-api 等弃用告警 | vite.config.ts 已配置 silenceDeprecations: ['legacy-js-api','global-builtin','fs-importer-cwd','import'];升级 sass 后新增的告警补进这个数组 |
构建报 SCSS @import 循环/找不到 | src/theme/index.scss 按顺序 @import(element-plus → app → dark → vxeTable …),顺序错了变量会未定义 | 新样式文件在 index.scss 末尾 @import;注意 @use 与 @import 不要混用 |
| 构建 OOM(内存溢出) | package.json 的 build 脚本是 node --max-old-space-size=8192 ./node_modules/vite/bin/vite build | 机器内存不足就调大 swap 或降低 max-old-space-size 分包构建 |
| 构建很慢 | visualizer({ open: false }) 每次都会产出 stats.html;compression 做 gzip + brotli 双压缩 | 出问题时临时注释 visualizer;CI 上关掉 brotli |
报 chunk size warning | chunkSizeWarningLimit: 2000(KB) | 只是提示;真正要治就做 manualChunks 拆分(vite.config.ts 已按包名拆 vue-vendor / vendor) |
| 依赖版本漂移 | 锁文件是 pnpm-lock.yaml,packageManager: pnpm@12.14.0 | 用 corepack 固定 pnpm 版本,别用 npm/yarn 装 |
| 现象 | 原因 | 解决 |
|---|
| 开发时接口 404 / 连不上后端 | vite.config.ts 的 server.proxy 把 ^/api、^/[Uu]pload、^/[Ss]se 代理到 env.VITE_API_URL;.env.development 里默认是 http://localhost:5005 | 后端没起或端口不对就改 .env.development 的 VITE_API_URL |
| 上传 / SSE 请求代理不到 | 只代理了 /api、/upload、/sse 三类前缀 | 新增前缀要在 vite.config.ts 的 proxy 里补,并重启 dev server |
public/config.js 改了不生效 | 该文件是 vite.config.ts 在启动时用 fs.writeFileSync 从 loadEnv 结果生成的,改了会被下次启动覆盖 | 改 .env / .env.development / .env.production,别直接改 public/config.js |
部署到子目录后 config.js 404 | index.html 里写死 <script src="/config.js"></script>(绝对路径) | 部署到子路径时把这里改成相对路径,或用 nginx 把 /config.js 指到实际位置 |
| 生产环境接口打成相对路径 / 跨域 | axios-utils.ts 里 basePath: import.meta.env.DEV ? '' : window.__env__.VITE_API_URL——开发走代理(空),生产用绝对地址 | 生产必须配 VITE_API_URL;同域部署可置空 |
| 页面一直显示 loading | index.html 里的 .loading-wrapper 要等 NextLoading.done() 才隐藏;initBackEndControlRoutes 抛错就会卡住 | 看控制台报错;Session.get('token') 为空会导致提前 return 但 loading 未关闭 |
| 系统更新提示一直弹 | webUpdateNotice 插件按 build_timestamp 检测,public/config.js 里的 VITE_BUILD_TIMESTAMP 每次构建都变 | 属正常;不想提示就在 vite.config.ts 里禁用该插件 |
| 热更新(HMR)失效 | server.hmr: true、allowedHosts: true;通过 IP/域名访问或代理后 HMR 的 WebSocket 连不上 | 用 localhost 访问;反向代理要放行 WebSocket 升级头 |
改了 .env 不生效 | Vite 只在启动时 loadEnv | 重启 dev server;生产要重新构建 |
| 现象 | 原因 | 解决 |
|---|
| 暗色模式局部样式错乱 | 暗色靠 [data-theme='dark'] 选择器(src/theme/dark.scss)覆盖 CSS 变量,组件里写死颜色的地方覆盖不到 | 组件内统一用 var(--next-bg-main)、var(--el-*) 等变量,别写死 #fff |
| 切换主题时有闪烁 | themeTransition.scss 做了过渡动画,大规模重绘会闪 | 属预期;卡顿明显就减少一次性切换的 DOM 规模 |
| 第三方组件在暗色下很突兀 | 第三方库(如 wangEditor、monaco)不带 data-theme 适配 | 在 src/theme/dark.scss 里针对该组件补覆盖规则 |
| 图标不显示 | 图标有三套:font-awesome、remix-icon、iconfont,都在 theme/index.scss 里 @import | 确认前缀(fa- / ri- / iconfont)与 svgIcon 组件识别规则一致;VITE_PUBLIC_PATH 不对会导致字体 404 |
| 字体/图片 404 | VITE_PUBLIC_PATH 决定 build.base | 子目录部署要设成 /子目录/;svgIcon 组件会判断 window.__env__.VITE_PUBLIC_PATH |
| 现象 | 原因 | 解决 |
|---|
| 改了主题配置刷新后复原 | 主题配置存在 LocalStorage(Local.get('themeConfig')) | 用页面上的设置入口改,不要直接改 DOM;换浏览器/无痕窗口配置不互通 |
| 字典改了前端下拉还是旧的 | 字典在 initBackEndControlRoutes 里通过 useUserInfo().setDictList() 一次性加载 | 改完字典重新登录;清 sys_dict: 前缀的后端缓存后再拉 |
| 常量改了不生效 | 同上,setConstList() 只在路由初始化时拉一次 | 重新登录;后端清 sys_const: 前缀缓存 |
| 页面切换后表单数据还在 | KeepAlive:layout/routerView/parent.vue 用 <keep-alive :include="getKeepAliveNames">,缓存名单来自 stores/keepAliveNames 的 cachedViews | 菜单的 IsKeepAlive 关掉;或在 onActivated 里重置表单 |
| KeepAlive 名单不对 | router/index.ts 里只有 newArr[0].meta.isKeepAlive && v.meta.isKeepAlive 同时为真才缓存——顶级路由关了,下面全部不缓存 | 检查顶级路由与子路由的 meta.isKeepAlive |
| 标签页(tagsView)状态异常 | stores/tagsViewRoutes 与 Local.get('tagsViewList') | 清除 tagsViewList 后刷新 |
| 换账号后还看到上个人的数据 | pinia store 未重置 + LocalStorage 残留 | 退出登录走 clearAccessTokens()(会 Session.clear() 并刷新页面);检查是否漏清自定义 store |
| 长列表滚动位置丢失 | virtualYConfig.scrollToTopOnChange: true,数据源变更会回到顶部 | 需要保持位置就覆盖该配置 |
1. 看浏览器控制台:先看红色报错,再看 Network 里那条请求的 URL / 状态码 / 响应体
2. 确认运行时配置:控制台执行 window.__env__ ,核对 VITE_API_URL / VITE_PUBLIC_PATH
3. 确认 Token:LocalStorage 里 access-token、x-access-token 是否存在
4. 确认契约:后端 Swagger 里有没有这个方法 → 前端 api-services 里有没有 → 方法名对不对
5. 确认路由:菜单 Path / Component 与 src/views 下的文件是否一一对应
6. 确认权限:userInfo.authApiList 里有没有这个权限码
7. 清缓存重试:LocalStorage + SessionStorage 全清,重新登录
8. 最小化复现:换超管账号、换浏览器无痕窗口,排除缓存与权限干扰
一键自检(浏览器控制台):
// 1. 运行时配置
console.log(window.__env__);
// 2. Token 是否存在
console.log({ at: localStorage.getItem('access-token')?.slice(0, 20), rt: !!localStorage.getItem('x-access-token') });
// 3. 用户权限码数量(需页面已加载 pinia)
// import { useUserInfo } from '/@/stores/userInfo'
// console.log(useUserInfo().userInfos.authApiList.length)
// 4. 已注册路由里有没有目标页面
// console.log(router.getRoutes().map(r => r.path).filter(p => p.includes('user')))
清理命令:
# 清依赖重装(Windows pnpm 软链问题通用解法)
cd Web
pnpm store prune
Remove-Item -Recurse -Force node_modules, .vite, dist -ErrorAction SilentlyContinue
pnpm install --config.node-linker=hoisted
# 只清构建缓存
Remove-Item -Recurse -Force node_modules/.vite, dist -ErrorAction SilentlyContinue
- 别手改
src/api-services/**:那是 swagger-codegen 产物,下次 pnpm buildApi 会整体覆盖。要扩展就写在 src/api/** 里。 - 别手改
public/config.js:它由 vite.config.ts 每次启动生成,改 .env* 才是正解。 index.html 里 /config.js 是绝对路径:子目录部署必改。- 两套 axios 实例并存:
utils/request.ts 的 service(老 useBaseApi 用)和 utils/axios-utils.ts 的 axiosInstance(getAPI 用),Token 处理逻辑略有差异,新代码统一用 getAPI。 useGridPageParam 必须在 useVxeTable 之前调用,否则分页/排序偏好在 setup 阶段取不到。- 表格
id 必须固定:列宽列序的本地缓存用 id 做 key,用了动态值缓存就失效。 authApiList 来自登录接口,改完后端权限必须重新登录(不是刷新)才能看到。- Windows 上 pnpm 问题先怀疑目录位置:OneDrive / 网络盘 / 未开开发者模式是三大元凶。
- Linux 部署 404 先怀疑大小写:
import.meta.glob 匹配区分大小写,Windows 开发不会暴露。 dynamicImport 匹配到多个文件时返回 false:菜单 Component 务必指向唯一文件,别用会被多处匹配的模糊路径。
前端排障的固定顺序:window.__env__ → Token → Network → 契约 → 路由 → 权限 → 清缓存。其中"契约不同步"和"public/config.js 未生效"占了一半以上,前者跑 pnpm buildApi,后者改 .env 而不是改生成物。