338 lines
11 KiB
Markdown
338 lines
11 KiB
Markdown
# 问题排查记录:权限平台默认值与开发服务 Network 地址
|
||
|
||
## 记录时间
|
||
|
||
- 2026-04
|
||
|
||
## 背景
|
||
|
||
本次排查涉及四个独立但连续出现的问题:
|
||
|
||
1. 页面初始化时,`this.permissionsSysOriginPlatforms[0]` 偶发拿不到,导致默认平台没有赋值,后续 `renderData(true)` 不执行。
|
||
2. 本地执行 `npm run dev` 后,控制台一直显示 `Network: unavailable`,但同机器的其他项目可以正常显示类似 `http://192.168.0.115:5173/` 的局域网访问地址。
|
||
3. `src/views/user/user-table/index.vue` 页面刷新后,搜索栏中的系统默认选项为空,列表也没有自动刷出;只有再次手动点击搜索后,默认系统和列表才恢复正常。
|
||
4. 除首页外,进入其他页面后刷新浏览器会出现页面空白。
|
||
|
||
---
|
||
|
||
## 一、权限平台默认值偶发为空
|
||
|
||
### 现象
|
||
|
||
页面中存在如下初始化逻辑:
|
||
|
||
```js
|
||
const querySystem = this.permissionsSysOriginPlatforms[0];
|
||
if (!querySystem) {
|
||
return;
|
||
}
|
||
that.listQuery.sysOrigins = [querySystem.value];
|
||
that.renderData(true);
|
||
```
|
||
|
||
当 `this.permissionsSysOriginPlatforms[0]` 为空时,页面会直接返回,默认查询条件与首屏数据都不会初始化。
|
||
|
||
### 结论
|
||
|
||
`permissionsSysOriginPlatforms` 不是接口直接返回的平台数组,而是:
|
||
|
||
1. 按钮权限接口先返回权限别名列表。
|
||
2. 前端再用本地写死的 `sysOriginPlatforms` 常量进行过滤。
|
||
3. 过滤结果再写入 Vuex,页面最终通过 getter 读取。
|
||
|
||
也就是说:
|
||
|
||
- 平台候选项来源于前端常量。
|
||
- 当前用户能拿到哪些平台,取决于按钮权限接口返回的权限别名。
|
||
|
||
### 关键代码链路
|
||
|
||
- 页面读取位置:`src/views/user/user-table/index.vue`
|
||
- getter 映射位置:`src/store/getters.js`
|
||
- Vuex 组装位置:`src/store/modules/user.js`
|
||
- 平台常量定义:`src/constant/origin.js`
|
||
- 按钮权限接口:`src/api/ops-system.js`
|
||
|
||
### 根因
|
||
|
||
根因是时序问题,不是页面本地代码本身写错:
|
||
|
||
1. 路由守卫中会调用 `store.dispatch('user/buttonPermissions')`。
|
||
2. 但原逻辑没有 `await` 这个异步请求。
|
||
3. 页面已经进入并执行 `created()` 时,权限数据可能还没回填到 Vuex。
|
||
4. 于是 `this.permissionsSysOriginPlatforms[0]` 偶发为空。
|
||
|
||
另外还有一个补充前提:
|
||
|
||
- 当前前端常量里只有 `ATYOU` 一个系统平台,并且依赖权限 `query:atyou`。
|
||
- 如果接口没有返回这个权限,过滤后数组本身也会为空。
|
||
|
||
### 已做处理
|
||
|
||
已在 `src/permission.js` 中将:
|
||
|
||
```js
|
||
store.dispatch('user/buttonPermissions')
|
||
```
|
||
|
||
修改为:
|
||
|
||
```js
|
||
await store.dispatch('user/buttonPermissions')
|
||
```
|
||
|
||
### 处理结果
|
||
|
||
调整后,路由放行前会先等待按钮权限加载完成,页面 `created()` 再读取 `permissionsSysOriginPlatforms` 时,不会再因为异步竞争导致首项为空。
|
||
|
||
### 后续建议
|
||
|
||
- 若后续仍出现空数组,需要优先检查接口返回是否包含 `query:atyou`。
|
||
- 如果页面还需要进一步增强健壮性,可以在页面侧增加 watcher 或兜底默认逻辑,但当前首先应保证路由阶段权限已完成回填。
|
||
|
||
---
|
||
|
||
## 三、`user-table` 刷新后系统默认项为空
|
||
|
||
### 现象
|
||
|
||
在 `src/views/user/user-table/index.vue` 页面中:
|
||
|
||
- 首次进入时有概率搜索栏中的“系统”默认值为空。
|
||
- 刷新页面后更容易复现。
|
||
- 首屏列表没有自动加载。
|
||
- 用户手动点击一次搜索后,系统默认选项和列表又会恢复正常。
|
||
|
||
### 根因
|
||
|
||
这个问题本质上还是权限平台数据的异步时序问题,但触发点更具体:
|
||
|
||
1. 页面在 `created()` 中直接读取 `this.permissionsSysOriginPlatforms[0]`。
|
||
2. 如果刷新时权限平台数据尚未回填,`querySystem` 就为空。
|
||
3. 原逻辑在判空后会直接 `return`,导致:
|
||
- `listQuery.sysOrigins` 没有默认值。
|
||
- `renderData(true)` 不执行。
|
||
- 页面其余初始化流程也被提前中断。
|
||
4. 等到权限数据后续回填后,页面没有额外 watcher 继续补初始化,所以只能依赖用户再次手动搜索。
|
||
|
||
### 已做处理
|
||
|
||
本次针对这个页面做了两层修复:
|
||
|
||
1. 在 `src/permission.js` 中继续保留并确认:
|
||
|
||
```js
|
||
await store.dispatch('user/buttonPermissions')
|
||
```
|
||
|
||
确保路由放行前优先等待按钮权限加载完成。
|
||
|
||
2. 在 `src/views/user/user-table/index.vue` 中增加页面级兜底:
|
||
|
||
- 新增 `hasInitializedSysOrigin` 标记,避免重复初始化。
|
||
- 监听 `permissionsSysOriginPlatforms`。
|
||
- 当权限平台列表第一次变为有值时:
|
||
- 自动取第一项写入 `listQuery.sysOrigins`
|
||
- 自动执行 `renderData(true)`
|
||
|
||
3. 去掉 `created()` 中“拿不到系统平台就直接 return”的初始化方式,避免首屏逻辑因为一次空值被永久跳过。
|
||
|
||
### 处理结果
|
||
|
||
调整后,即使刷新页面时 `permissionsSysOriginPlatforms` 比页面创建更晚回填:
|
||
|
||
- 搜索栏系统默认值也会在权限到达后自动补上。
|
||
- 首屏列表会自动触发加载。
|
||
- 不再需要用户手动点击搜索来恢复页面状态。
|
||
|
||
### 涉及文件
|
||
|
||
- `src/permission.js`
|
||
- `src/views/user/user-table/index.vue`
|
||
|
||
---
|
||
|
||
## 四、非首页刷新后页面白屏
|
||
|
||
### 现象
|
||
|
||
- 首页通常可以正常刷新。
|
||
- 进入其他菜单页后,直接刷新浏览器会出现页面空白。
|
||
- 这类问题在二级、三级动态路由页面上更容易复现。
|
||
|
||
### 根因
|
||
|
||
问题出在刷新时的动态路由注入时序:
|
||
|
||
1. 刷新页面后,应用会重新进入 `src/permission.js` 中的路由守卫。
|
||
2. 当前项目的很多页面并不是一开始就存在于静态路由中,而是依赖 `getMenus()` 返回菜单后再通过 `router.addRoutes()` 动态注入。
|
||
3. 原逻辑中虽然调用了 `getMenus()`,但没有等待菜单加载和动态路由注入真正完成,就提前执行了 `next()`。
|
||
4. 当用户刷新的是非首页深层路由时,当前目标路由可能还没注册到 router 中,于是会出现匹配失败,最终表现为空白页。
|
||
|
||
首页之所以通常不受影响,是因为首页对应的是默认静态路由,不依赖后续动态注入。
|
||
|
||
### 已做处理
|
||
|
||
已将 `src/permission.js` 中刷新后的初始化流程改为严格串行:
|
||
|
||
```js
|
||
await store.dispatch('user/getInfo')
|
||
const grantRouters = await store.dispatch('user/getMenus')
|
||
if (grantRouters && grantRouters.length > 0) {
|
||
router.addRoutes(grantRouters)
|
||
}
|
||
store.dispatch('user/getUserDashboard')
|
||
await store.dispatch('user/buttonPermissions')
|
||
next({ ...to, replace: true })
|
||
```
|
||
|
||
### 修复要点
|
||
|
||
这次修复包含两个关键点:
|
||
|
||
1. 先等待 `getMenus()` 返回,再执行 `router.addRoutes(grantRouters)`。
|
||
2. 动态路由注入完成后,不再直接 `next()`,而是使用:
|
||
|
||
```js
|
||
next({ ...to, replace: true })
|
||
```
|
||
|
||
这样会用当前目标地址重新进入一次匹配流程,确保刚注入的动态路由能够正确命中当前页面。
|
||
|
||
### 处理结果
|
||
|
||
调整后:
|
||
|
||
- 刷新非首页页面时,不会再因为动态路由尚未注册而白屏。
|
||
- 当前页面会在动态路由注入完成后重新匹配并正常进入。
|
||
|
||
### 涉及文件
|
||
|
||
- `src/permission.js`
|
||
|
||
---
|
||
|
||
## 二、开发服务 `Network: unavailable`
|
||
|
||
### 现象
|
||
|
||
执行 `npm run dev` 后输出类似:
|
||
|
||
```txt
|
||
App running at:
|
||
- Local: http://localhost:9528
|
||
- Network: unavailable
|
||
```
|
||
|
||
但同一台机器上的其他项目可以正常显示局域网访问地址。
|
||
|
||
### 初步排查结论
|
||
|
||
这不是项目无法启动,也不是没有局域网 IP,而是 Vue CLI 选错了网卡。
|
||
|
||
通过系统网络信息确认:
|
||
|
||
- 当前机器存在可用私网地址:`192.168.0.115`
|
||
- 同时还存在一个虚拟网卡地址:`26.26.26.1`
|
||
|
||
### 根因
|
||
|
||
项目使用的是较老版本的 `@vue/cli-service 3.6.0`。
|
||
|
||
该版本在生成 `Network` 地址时,会在内部调用 `address.ip()` 取一个 IPv4 地址,然后只接受以下私网地址范围:
|
||
|
||
- `10.x.x.x`
|
||
- `172.16.x.x` 到 `172.31.x.x`
|
||
- `192.168.x.x`
|
||
|
||
如果拿到的不是私网地址,就会直接将 `Network` 显示为 `unavailable`。
|
||
|
||
本次机器环境中,Vue CLI 优先拿到的是虚拟网卡 `26.26.26.1`,由于它不属于私网范围,因此被直接丢弃,最终控制台显示 `Network: unavailable`。
|
||
|
||
### 关键代码位置
|
||
|
||
- 输出逻辑:`node_modules/@vue/cli-service/lib/commands/serve.js`
|
||
- 局域网地址选择逻辑:`node_modules/@vue/cli-service/lib/util/prepareURLs.js`
|
||
|
||
### 已尝试处理
|
||
|
||
#### 第一步
|
||
|
||
先在 `vue.config.js` 中增加:
|
||
|
||
```js
|
||
host: '0.0.0.0'
|
||
```
|
||
|
||
目的:
|
||
|
||
- 让 dev server 监听所有网卡,而不是只监听本机回环地址。
|
||
|
||
结果:
|
||
|
||
- 监听范围已放开。
|
||
- 但 `Network` 仍然显示 `unavailable`。
|
||
- 说明问题不在监听地址,而在 Vue CLI 的网卡识别逻辑。
|
||
|
||
#### 第二步
|
||
|
||
在 `vue.config.js` 中新增自定义 LAN IP 选择逻辑,替代 Vue CLI 默认的“随便取一个 IPv4”的方式。
|
||
|
||
处理思路:
|
||
|
||
1. 使用 `os.networkInterfaces()` 枚举网卡。
|
||
2. 只保留私网 IPv4。
|
||
3. 优先选择 `WLAN / Wi-Fi / Ethernet / 以太网` 这类常见真实网卡。
|
||
4. 排除 `TAP / VPN / Virtual / VMware / Hyper-V / Docker / vEthernet` 等虚拟网卡。
|
||
5. 将最终结果写入 `devServer.public`。
|
||
|
||
### 当前配置结果
|
||
|
||
`vue.config.js` 已调整为:
|
||
|
||
- `host: '0.0.0.0'`
|
||
- `public: lanIp ? \`${lanIp}:${port}\` : undefined`
|
||
|
||
并增加了自动识别私网地址的辅助函数。
|
||
|
||
### 处理结果
|
||
|
||
此修改的目的不是单纯让服务“能访问”,而是:
|
||
|
||
1. 明确让服务监听所有网卡。
|
||
2. 明确告诉旧版 Vue CLI 应该向外公布哪个局域网地址。
|
||
3. 避免其误选虚拟网卡后把 Network 判成 unavailable。
|
||
|
||
### 备用方案
|
||
|
||
如果后续本机 IP 变化,或当前网络环境特殊,也可以临时手动指定:
|
||
|
||
```powershell
|
||
$env:DEV_SERVER_PUBLIC_IP='192.168.0.115'
|
||
npm run dev
|
||
```
|
||
|
||
或:
|
||
|
||
```powershell
|
||
npm run dev -- --public 192.168.0.115:9528
|
||
```
|
||
|
||
---
|
||
|
||
## 本次涉及的改动文件
|
||
|
||
- `src/permission.js`
|
||
- `src/views/user/user-table/index.vue`
|
||
- `vue.config.js`
|
||
- `docs/问题排查记录-权限平台与开发服务地址.md`
|
||
|
||
## 本次经验总结
|
||
|
||
1. 页面里出现“偶发空值”时,优先检查数据来源是不是异步回填,而不是只盯页面局部逻辑。
|
||
2. `permissionsSysOriginPlatforms` 这类字段要区分“候选项来源”和“最终权限结果来源”。
|
||
3. 老版本 Vue CLI 对网卡选择并不稳定,机器上存在 VPN、TAP、虚拟网卡时,`Network: unavailable` 往往是地址识别问题,不代表 dev server 本身不可用。
|
||
4. 对局域网访问有明确需求的项目,最好显式配置 `devServer.host` 与 `devServer.public`,不要完全依赖工具链自动推断。
|
||
5. 对首屏依赖异步权限数据的页面,不应只在 `created()` 中做一次性读取;更稳妥的方式是配合 watcher 或显式初始化状态,在权限到达后自动补齐默认值与首屏请求。
|
||
6. 涉及动态路由的后台项目,刷新页面时必须保证“菜单加载 -> 路由注入 -> 再放行当前路由”是串行完成的,否则深层页面极易出现刷新白屏。
|