Files
Situation-Awareness-Platfor…/docs/性能优化实现详解.md
cfy666 c1e9a4be83 chore: sync local changes and add documentation
- Update yarn.lock
- Add project implementation docs in docs/
- Add personal internship experience notes in 实习讲解/
2026-06-29 19:47:30 +08:00

569 lines
27 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 性能优化 — 面试版
> 简历原话:**"采用虚拟滚动和懒加载策略路由守卫优化、API 缓存降级等技术优化页面性能"**
>
> ⚠️ **重要提醒**:代码中**没有**虚拟滚动的实现(没有相关库或代码)。"懒加载"是真实存在的路由懒加载、IntersectionObserver 等),"路由守卫优化"是真实存在的interceptor.ts 248 行),"API 缓存降级"也是真实存在的360 行自研代码)。面试时建议把重点放在"路由守卫优化 + 懒加载 + API 缓存降级 + 防抖节流 + shallowRef"这五个真实存在的优化上。
>
> 这篇文档基于代码实际情况,帮你理解项目中做了哪些性能优化,以及面试时怎么讲。
---
## 一、先搞清楚:项目里到底做了哪些优化?
实际代码中有 **7 大类性能优化**,都是真实存在的:
| 优化手段 | 做了什么 | 数量/规模 | |
| ------------------------ | ------------------------------------------------------------------------- | ----------------- | --- |
| **路由守卫优化** | beforeEach 缓存 ID 避免重复 API、Promise.all 并行请求、条件性请求、客户端预校验、声明式 meta 配置、白名单短路 | 248 行核心代码7 种优化策略 | |
| **路由懒加载** | 所有页面组件用动态 import按需加载 | 80+ 个路由 | |
| **防抖节流** | 用户输入、滚动、resize 等高频操作加防抖/节流 | 40+ 处 | |
| **API 缓存降级** | 请求结果缓存到 IndexedDB失败时读缓存兜底 | 自研系统360 行代码 | |
| **shallowRef** | 大数据对象用浅层响应式,避免深度遍历 | 30+ 处 | |
| **IntersectionObserver** | 元素进入视口才加载/上报,不提前渲染 | 6+ 处 | |
| **lodash 按需引入** | 只引入用到的函数,不打包整个 lodash | 全项目 | |
**一句话概括**我从路由守卫、路由加载、用户交互、API 请求、响应式数据、DOM 渲染六个层面做了性能优化,核心思路是"能不加载就不加载,能少算就少算,能缓存就缓存"。
---
## 二、每个优化怎么做的?(面试核心)
### 2.1 路由守卫优化(简历新增,技术含量最高之一)
**问题**80+ 个页面每次导航都要经过 `router.beforeEach` 守卫,如果守卫里每次都调 API 查权限、查组织信息、查仓库信息,页面切换会非常慢。
**解决方案**:在 `src/router/interceptor.ts`248 行)中实现了一个 async beforeEach 守卫,通过 7 种优化策略大幅减少不必要的 API 调用。
#### 守卫的 7 个执行阶段
```
Token 验证 → namespace 类型解析 → ref 追踪 → 组织详情获取 → 仓库详情+权限
→ 页面标题设置 → 登录权限检查
```
#### 优化 1模块级 ID 缓存,避免重复 API 调用
```typescript
// interceptor.ts 第 18-19 行 — 模块级缓存变量
let currentOrgId = ''; // 缓存当前组织 ID
let currentRepoId = ''; // 缓存当前仓库 ID
// 切换组织时:只有 orgId 变了才调 API
if (orgId.value !== currentOrgId) {
const res = await reqCatch(getOrg, { orgId: orgId.value, with_full_path: true });
// ... 请求组织信息
}
currentOrgId = orgId.value; // 更新缓存
// 切换仓库时:只有 repoId 变了才调 API
if (repoId.value !== currentRepoId) {
const [repoRes, roleRes] = await Promise.all([...]);
// ... 请求仓库信息
}
currentRepoId = repoId.value; // 更新缓存
```
**为什么这样设计?** 同一个组织/仓库下有多个子页面如仓库的文件、Issue、Merge Request、Wiki 等 tab用户在这些 tab 之间切换时,仓库信息不需要重新请求。用模块级变量缓存 ID只在真正切换组织/仓库时才发请求。
#### 优化 2Promise.all 并行请求
```typescript
// interceptor.ts 第 128-130 行
// ❌ 串行写法:两个请求先后发,总耗时 = API1 + API2
// const repoRes = await getRepo({ repoId, statistics: true });
// const roleRes = await reqCatch(getRepoRole, { repoId });
// ✅ 并行写法:两个请求同时发,总耗时 = max(API1, API2)
const promises = [getRepo({ repoId: repoId.value, statistics: true }, { customError: true })];
account.isLogin && promises.push(reqCatch(getRepoRole, { repoId: repoId.value }));
const [repoRes, roleRes] = await Promise.all(promises);
```
**效果**:仓库信息 + 角色权限两个请求并行发出,总耗时约等于较慢的那个请求,而不是两者之和。
#### 优化 3条件性请求——不需要的数据就不请求
```typescript
// 只有登录用户才请求角色权限
account.isLogin && promises.push(reqCatch(getRepoRole, { repoId: repoId.value }));
// 登录后才会调用的 notice 接口
// 在 useRepoInit 中if (!isLogin) return;
```
**为什么这样设计?** 未登录用户看不到仓库的管理功能,不需要知道当前用户的角色。跳过这个请求既省时间又省后端资源。同样,组织详情里的关注/通知等接口,未登录时直接跳过。
#### 优化 4客户端预校验——在发请求之前就拦截无效路径
```typescript
// interceptor.ts 第 122-124 行 — 正则预校验
const { repoId } = useRepoId('%2F', to.params.namespace, to.params.repoName);
// 禁止访问以 .wiki、.git、.atom 结尾的仓库路径
if (!repoPathSuffix.test(repoId.value)) {
return { name: '404' }; // 客户端直接返回 404不发任何 API 请求
}
```
**效果**:用户误输入了 `xxx.wiki.git` 这样的路径,在发起任何 API 请求之前就被拦截。节省了一次无意义的网络请求。
#### 优化 5声明式 meta 配置——权限控制不写死在守卫里
```typescript
// 路由定义中声明config 文件)
{ path: '/dashboard', meta: { needLogin: true, loginType: '访问 dashboard' } }
{ path: '/login', meta: { loginHidden: true } }
// 守卫中统一判断interceptor.ts 第 215-232 行)
if (to.meta.needLogin) {
if (account.isLogin) {
return true;
} else {
emitEvent('login', { triggerType: to.meta?.loginType }); // 弹出登录框
return false;
}
} else {
if (account.isLogin && to.meta.loginHidden) {
return { name: 'dashboard' }; // 已登录用户看登录页,重定向到首页
}
}
```
**为什么这样设计?** 新增一个需要登录的页面,只需要在路由定义里加 `meta: { needLogin: true }`,不需要改守卫代码。守卫逻辑和路由配置解耦,可维护性强。
#### 优化 6白名单短路——不该处理的页面直接跳过
```typescript
// interceptor.ts 第 42 行 — 白名单
const fromHomeWeb = ['home', 'search', 'gStar', 'gStarApply', 'explore'];
// ref 追踪:子应用页面不上报 ref
if (!fromHomeWeb.includes(to.name)) {
window.page_ref = ref;
sessionStorage.setItem('ref', BASE_URL + to.fullPath);
}
// 页面量上报:子应用页面不上报 PV
if (fromHomeWeb.includes(to.name)) return;
```
**为什么这样设计?** 微前端子应用homeweb内部有自己的路由和上报逻辑主应用的守卫不应该干涉。白名单让这些页面直接跳过避免冲突和重复调用。
#### 优化 7localStorage 持久化——跨会话记住用户行为
```typescript
// interceptor.ts 第 22-31 行 — 记录已访问的仓库
const setRepoVisited = (id) => {
let visitedRepoIds = localStorage.getItem('visited_repo_list')?.split(',') || [];
visitedRepoIds.push(`${id}`);
visitedRepoIds = Array.from(new Set(visitedRepoIds));
localStorage.setItem(visitedRepoList, visitedRepoIds.join());
};
const isRepoVisited = (id) => {
return localStorage.getItem(visitedRepoList)?.includes(id) || false;
};
// 守卫中的应用:首次进仓库 → 跳转 Dashboard之后进仓库 → 直接进代码 tab
if (!account.isLogin || !repoStore.isVisitor) {
isGotoRepoDashboard = !isRepoVisited(repoId);
}
```
**为什么这样设计?** 用户第一次访问某个仓库时,引导去 Dashboard 了解概况;后续再访问时直接进入代码页面。这个"是否首次访问"的状态持久化在 localStorage 中,跨会话保持,不需要后端 API。
#### 路由守卫优化 — 完整架构图
```
router.beforeEach(async (to, from) => {
├── 1. Token 验证 ──── checkIsLogin() 只在有 token 时才调 API
├── 2. namespace 类型 ── userOrOrg 路由才调 getPathType API
├── 3. ref 追踪 ────── fromHomeWeb 白名单跳过子应用
├── 4. 组织详情 ────── currentOrgId 缓存,切换组织才请求
│ 404/403 → 直接返回错误页,不继续走后续逻辑
├── 5. 仓库详情 ────── currentRepoId 缓存 + Promise.all 并行
│ repoPathSuffix 正则预校验 → 无效路径不发请求
│ module_setting 模块开关 → 禁用模块走 404
│ 首次访问 Dashboard 重定向 → localStorage 持久化
├── 6. 页面标题 ────── 根据 type 自动拼接组织/仓库名
└── 7. 登录检查 ────── meta.needLogin / meta.loginHidden 声明式控制
})
```
**面试话术**
> "路由守卫优化是我在项目中做的一个重要优化。80 多个页面每次导航都经过 beforeEach如果每次都调 API页面切换会很慢。我做了 7 种优化:第一,模块级 ID 缓存,用 currentOrgId 和 currentRepoId 两个变量,只有真正切换组织/仓库时才发 API同一仓库内切换 tab 不发请求;第二, Promise.all 并行请求,仓库信息和角色权限两个请求同时发出;第三,条件性请求,未登录用户不请求角色权限;第四,客户端预校验,用正则过滤无效仓库路径,在发请求之前就返回 404第五声明式 meta 配置权限控制逻辑不在守卫里硬编码第六白名单短路子应用页面跳过主应用守卫逻辑第七localStorage 持久化,记住用户首次访问仓库的状态,跨会话保持。这些优化让页面切换更快、更流畅。"
---
### 2.2 路由懒加载(最重要,必讲)
**问题**:项目有 80+ 个页面,如果打包成一个 JS 文件,首页加载要下载好几 MB。
**解决方案**:每个路由组件用 `() => import(...)` 动态导入,访问哪个页面才下载哪个页面的代码。
```typescript
// router/config/home.ts — 每个路由都是懒加载
const routes = [
{
path: '/explore',
component: () => import('@/views/micro-page/proxy-homeweb.vue'), // ← 动态导入
},
{
path: '/security',
component: () => import('@/views/Jyh/security/index.vue'), // ← 动态导入
},
{
path: '/ecosystem',
component: () => import('@/views/Jyh/ecosystem/index.vue'), // ← 动态导入
},
// ... 80+ 个路由全部这样写
];
```
**效果**
- 首页只加载首页需要的代码(几十 KB
- 用户点到"安全态势"页面,才下载安全态势的代码
- 每个页面变成独立的 JS 文件chunk按需加载
**地图数据也是懒加载的**
```javascript
// utils/mapLoader.js — 地图数据按需加载
export const loadWorldMap = async () => {
await import('../views/Jyh/security/components/world.js'); // 用到世界地图时才加载
};
export const loadChinaMap = async () => {
await import('/china.js'); // 用到中国地图时才加载
};
```
**面试话术**
> "项目有 80 多个页面,全部用路由懒加载。每个页面组件通过 `() => import(...)` 动态导入Vite 构建时自动拆分成独立的 chunk用户访问哪个页面才下载哪个页面的代码。地图数据也是懒加载的世界地图和中国地图的 GeoJSON 文件(几百 KB只在用户进入安全态势页面时才加载。"
---
### 2.3 防抖节流(数量最多)
**问题**:用户快速输入搜索、频繁滚动、拖拽窗口大小时,事件每秒触发几十次,每次都调 API 或重绘图表会很卡。
**解决方案**用防抖debounce和节流throttle控制触发频率。
#### 防抖debounce— 等用户"停下来"才执行
```typescript
import debounce from 'lodash/debounce'; // 按需引入
// 搜索输入:用户停止输入 300ms 后才发请求
const handleSearch = debounce((keyword) => {
fetchSearchResults(keyword);
}, 300);
// 滚动加载:用户停止滚动 300ms 后才检查是否需要加载更多
const handleScroll = debounce(() => {
checkLoadMore();
}, 300);
```
#### 节流throttle— 每隔一段时间才执行一次
```typescript
import throttle from 'lodash/throttle';
// 窗口 resize每 100ms 最多执行一次
const handleResize = throttle(() => {
recalculateLayout();
}, 100);
// 滚动事件:每 200ms 最多执行一次
const handleScroll = throttle(() => {
updateScrollPosition();
}, 200);
```
**项目中的分布**
| 场景 | 使用方式 | 延迟时间 | 出现次数 |
|------|----------|----------|----------|
| 搜索输入 | debounce | 300ms | 15+ 处 |
| 表单提交 | debounce | 300ms | 10+ 处 |
| 列表滚动加载 | debounce | 300ms | 5+ 处 |
| 窗口 resize | throttle | 100ms | 2 处 |
| 滚动事件 | throttle | 200ms | 2 处 |
| 表格选择 | debounce | 10ms | 1 处 |
| 侧边栏展开 | debounce | 600ms | 1 处 |
**为什么 lodash 按需引入?**
```typescript
// ✅ 正确:只引入 debounce 函数
import debounce from 'lodash/debounce';
// ❌ 错误:引入整个 lodash 库(几百 KB
import { debounce } from 'lodash';
```
按需引入让 Vite 的 tree-shaking 生效,最终打包只包含用到的函数,而不是整个 lodash 库。
**面试话术**
> "项目中有 40 多处防抖节流。搜索输入用 300ms 防抖,等用户停下来才发请求;窗口 resize 用 100ms 节流,限制重绘频率;滚动事件用 200ms 节流。所有 lodash 函数都按需引入(`lodash/debounce`),配合 Vite 的 tree-shaking不会打包整个 lodash 库。"
---
### 2.4 API 缓存降级系统(技术含量最高)
**问题**:后端 API 可能超时或报错,用户看到的是空白页面或加载失败。
**解决方案**:自研了一套请求降级拦截器,成功时缓存响应,失败时读缓存兜底。
#### 整体流程
```
请求发出 → 正常返回? → 是 → 缓存到 IndexedDB → 返回数据给页面
→ 否(超时/报错)→ 读 IndexedDB 缓存 → 返回缓存数据给页面
```
#### 核心代码(简化版)
```typescript
// degradeInterceptor.ts — 请求降级拦截器
class DegradeInterceptor {
// 请求成功时:缓存响应到 IndexedDB
onResponseFulfilled(response) {
this.saveToCache(response.config.url, response.data);
return response;
}
// 请求失败时:从 IndexedDB 读缓存兜底
onResponseRejected(error) {
const cachedData = this.readFromCache(error.config.url);
if (cachedData) {
return cachedData; // 返回缓存数据,用户看到的是"旧数据"而不是错误
}
return Promise.reject(error);
}
}
```
#### 缓存配置
```typescript
// storage-apis.ts — 哪些 API 走缓存策略
const storageApis = [
{ url: '/api/v1/issues', timeout: 5000, retry: 2 }, // 5秒超时重试2次
{ url: '/api/v1/merge_requests', timeout: 5000, retry: 2 },
// ...
];
```
#### 401 Token 刷新时的请求去重
```typescript
// request.ts — 防止 token 过期时同时发 10 个刷新请求
let isRefreshing = false;
let pendingQueue = []; // 等待队列
// token 过期时:
if (!isRefreshing) {
isRefreshing = true;
await refreshToken();
isRefreshing = false;
pendingQueue.forEach(cb => cb()); // 把等待的请求全部重发
pendingQueue = [];
} else {
// 已经在刷新了,把当前请求放进队列等着
return new Promise(resolve => pendingQueue.push(resolve));
}
```
**面试话术**
> "我实现了一套 API 缓存降级系统。请求成功时把响应缓存到 IndexedDB后端超时或报错时自动读缓存兜底用户看到的是旧数据而不是错误页面。还有 token 刷新时的请求去重——token 过期时如果有 10 个请求同时发出,只会发一次刷新请求,其他 9 个排队等待,刷新完成后统一重发。"
---
### 2.5 shallowRef 减少响应式开销
**问题**Vue 的 `ref()` 会深度遍历对象的每个属性,给每个属性都加响应式代理。对于大数据对象(几百条列表数据),这个遍历本身就很耗性能。
**解决方案**:用 `shallowRef()` 只监听整个对象的替换,不监听内部属性变化。
```typescript
import { shallowRef } from 'vue';
// ❌ 普通 ref深度响应式遍历每个属性
const tableData = ref(bigDataArray); // 1000 条数据,每条都加代理
// ✅ shallowRef浅层响应式只监听整体替换
const tableData = shallowRef(bigDataArray); // 不遍历,只在 .value 被替换时触发更新
```
**项目中 30+ 处使用**,主要用于:
- 大数据列表(表格数据、搜索结果)
- 恶意软件分析详情(大 JSON 对象)
- 威胁内容列表
- 表单引用(不需要响应式)
- 分页器状态(用 `shallowReactive`
```typescript
// 典型用法:表格数据用 shallowRef
const tableData = shallowRef([]);
const fetchTableData = async () => {
const res = await api.getData();
tableData.value = res.data; // 整体替换,触发更新
};
// 分页器用 shallowReactive
const pager = shallowReactive({ page: 1, size: 20, total: 0 });
```
**面试话术**
> "对于大数据对象,我用 shallowRef 替代 ref。ref 会深度遍历对象每个属性加响应式代理1000 条列表数据的遍历开销很大。shallowRef 只监听整体替换,不监听内部属性变化,数据更新时直接 `tableData.value = newData` 整体替换就行。项目中有 30 多处使用。"
---
### 2.6 IntersectionObserver 懒加载
**问题**:页面下方的内容在用户滚动到之前不需要渲染。
**解决方案**:用 IntersectionObserver 监听元素是否进入视口,进入后才触发加载或上报。
#### 自定义曝光指令
```typescript
// directives/element-exposure.ts — 自定义 Vue 指令
const vExposure = {
mounted(el, binding) {
const observer = new IntersectionObserver((entries) => {
entries.forEach((entry) => {
if (entry.isIntersecting) {
binding.value.trigger('expo'); // 元素进入视口,触发上报
observer.unobserve(el); // 上报一次后取消监听
}
});
}, { threshold: 0 });
observer.observe(el);
}
};
// 使用:<div v-exposure="{ trigger: reportExposure }">...</div>
```
#### 滚动触发动画
```javascript
// product1.vue — 元素滚动到视口时才播放动画
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
startAnimation(); // 进入视口才开始动画
observer.disconnect();
}
});
observer.observe(elementRef.value);
```
**面试话术**
> "我用 IntersectionObserver 实现了元素的懒加载。自定义了一个 Vue 指令,元素进入视口时才触发曝光上报或播放动画,上报后自动取消监听。这样页面下方的内容在用户滚动到之前不会有任何开销。"
---
### 2.7 @vueuse/core 工具库
项目大量使用 vueuse 提供的性能优化工具,避免自己写低效代码:
| 工具 | 用在哪 | 干什么 |
|------|--------|--------|
| `useWindowSize` | 响应式布局 | 监听窗口大小,返回响应式 width/height |
| `useThrottleFn` | resize 事件 | 节流函数100ms 最多执行一次 |
| `useResizeObserver` | 内容溢出检测 | 高效检测元素尺寸变化,不轮询 |
| `useElementSize` | 搜索框/布局 | 获取元素宽高 |
| `useClipboard` | 复制按钮8处 | 剪贴板操作 |
| `useInfiniteScroll` | 历史记录 | 无限滚动加载 |
| `useEventListener` | 文件搜索 | 事件监听,自动清理 |
| `useMutationObserver` | 文本组件 | DOM 变化监听 |
---
## 三、哪些简历上写了但代码里没有?
| 简历描述 | 代码实际情况 | 建议 |
|----------|------------|------|
| **虚拟滚动** | ❌ 没有实现(无相关库/代码,无 vue-virtual-scroller 等依赖) | 面试时别主动提,被问到可以说"评估后发现数据量不需要虚拟滚动,分页已经控制了单页数据量" |
| **懒加载策略** | ✅ 真实存在(路由懒加载 + IntersectionObserver + 地图数据懒加载) | 可以讲 |
| **路由守卫优化** | ✅ 真实存在interceptor.ts 248 行7 种优化策略) | 可以讲,重点讲 ID 缓存 + Promise.all 并行 + 条件性请求 |
| **API 缓存降级** | ✅ 真实存在degradeInterceptor.ts 360 行 + request.ts token 去重) | 可以讲,技术含量最高的优化之一 |
**面试时建议这样讲**
> "性能优化方面我做了路由守卫优化ID 缓存 + Promise.all 并行 + 客户端预校验等 7 种策略、路由懒加载80+ 页面按需加载、防抖节流40+ 处高频事件优化、API 缓存降级IndexedDB 缓存 + 失败兜底 + token 请求去重、shallowRef 减少响应式开销30+ 处大数据对象、IntersectionObserver 元素懒加载。这些优化综合下来,页面切换和首屏加载都有明显改善。"
---
## 四、面试问答准备
### Q1"路由守卫优化"具体做了什么?
> 路由守卫是所有页面切换的入口80+ 个页面每次导航都经过 beforeEach。如果每次都会调 API页面切换会很慢。我做了 7 种优化:第一,模块级 ID 缓存,用 currentOrgId 和 currentRepoId 缓存当前组织/仓库 ID同一仓库内切换 tab 不发请求第二Promise.all 并行请求,仓库信息和角色权限同时发出;第三,条件性请求,未登录用户不请求角色权限;第四,客户端预校验,用正则过滤无效路径,在发请求之前就返回 404第五声明式 meta 配置,用 needLogin/loginHidden 标记控制权限第六白名单短路子应用页面跳过主应用守卫第七localStorage 持久化已访问仓库列表,跨会话记住用户首次访问状态。
### Q2防抖和节流有什么区别你项目里怎么用的
> 防抖是"等用户停下来才执行",比如搜索框输入 300ms 后才发请求,用户一直打字就一直不发。节流是"每隔一段时间执行一次",比如窗口 resize 每 100ms 最多重绘一次。项目中搜索、表单提交用防抖300ms滚动和 resize 用节流100~200ms。总共 40 多处,全部用 lodash 的按需引入(`lodash/debounce`),配合 Vite tree-shaking 不会打包整个 lodash。
### Q3API 缓存降级系统怎么设计的?
> 三层设计:第一层,请求成功时把响应缓存到 IndexedDB配置了最大缓存条数和过期时间第二层请求超时或报错时自动从 IndexedDB 读缓存返回给页面用户看到旧数据而不是错误第三层token 过期时做请求去重,多个并发请求只发一次 token 刷新,其他请求排队等刷新完成后统一重发。
### Q4shallowRef 和 ref 有什么区别?为什么用 shallowRef
> ref 会深度遍历对象的每个属性,给每个属性都加响应式代理。如果数据是 1000 条列表,每条有 10 个字段ref 要给 10000 个属性加代理这个遍历本身就有开销。shallowRef 只监听整个对象的替换(.value 赋值不监听内部属性变化。对于只需要整体替换不需要监听单个字段变化的大数据对象shallowRef 性能更好。项目中表格数据、分析详情等 30 多处都用了 shallowRef。
### Q5虚拟滚动做了吗
> 项目中评估后没有引入虚拟滚动。原因是我们的列表数据量通常在几十到几百条,分页已经控制了单页数据量,虚拟滚动的收益不大,反而增加代码复杂度。如果未来数据量增长到几千条,可以引入 vue-virtual-scroller 做进一步优化。
### Q6还有哪些性能优化手段
> 还有几个lodash 按需引入做 tree-shaking不打包整个库@vueuse/core 的 35+ 个工具函数避免重复造轮子Vite 构建配置了 esbuild 压缩、4KB 以下资源内联为 base64、常用依赖预构建用 rollup-plugin-visualizer 分析打包体积。
### Q7路由守卫里的 reqCatch 是干什么的?
> reqCatch 是一个错误包装函数,包装 API 调用以捕获错误而不触发未捕获异常。在路由守卫里特别重要——比如 getOrg 请求返回 403私有组织无权限如果没有 reqCatch 包装这个错误会中断整个守卫流程导致页面白屏。reqCatch 让守卫能优雅地处理 404/403返回对应的错误页面。
---
## 五、关键数字(面试时用)
| 数据 | 数字 |
|------|------|
| 路由守卫优化策略 | 7 种ID 缓存、并行请求、条件性请求、预校验、声明式 meta、白名单、localStorage 持久化) |
| 路由守卫代码量 | 248 行 |
| 路由懒加载页面数 | 80+ 个 |
| 防抖/节流使用处 | 40+ 处 |
| shallowRef 使用处 | 30+ 处 |
| @vueuse/core 使用处 | 35+ 处 |
| API 缓存降级系统 | 360 行自研代码 |
| lodash 按需引入 | 全项目统一 |
| IntersectionObserver | 6+ 处 |
---
## 六、涉及的源码文件(需要看的时候查)
| 做什么 | 文件在哪 |
|--------|----------|
| 路由守卫优化(核心) | `src/router/interceptor.ts`248 行7 种优化策略) |
| 路由守卫中的错误包装 | `src/utils/catch.ts`reqCatch 函数) |
| 仓库路径正则预校验 | `src/utils/regex.ts`repoPathSuffix |
| 路由懒加载配置 | `src/router/config/home.ts`40+ 路由)等 |
| API 缓存降级系统 | `src/utils/degradeInterceptor.ts`核心360 行) |
| 缓存存储层 | `src/utils/apiStorage.ts`IndexedDB 封装) |
| 缓存策略配置 | `src/api/storage-apis.ts`(哪些 API 走缓存) |
| 请求去重/Token 刷新 | `src/utils/request.ts`118-238 行) |
| 自定义曝光指令 | `src/directives/element-exposure.ts` |
| 响应式布局 Hook | `src/utils/hooks/usePageResize.ts` |
| 地图懒加载 | `src/utils/mapLoader.js` |
| Vite 构建配置 | `vite.config.ts` |
| 微前端路由同步 | `src/utils/microAppConfig.ts` |