- Update yarn.lock - Add project implementation docs in docs/ - Add personal internship experience notes in 实习讲解/
27 KiB
性能优化 — 面试版
简历原话:"采用虚拟滚动和懒加载策略,路由守卫优化、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 调用
// 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,只在真正切换组织/仓库时才发请求。
优化 2:Promise.all 并行请求
// 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:条件性请求——不需要的数据就不请求
// 只有登录用户才请求角色权限
account.isLogin && promises.push(reqCatch(getRepoRole, { repoId: repoId.value }));
// 登录后才会调用的 notice 接口
// 在 useRepoInit 中:if (!isLogin) return;
为什么这样设计? 未登录用户看不到仓库的管理功能,不需要知道当前用户的角色。跳过这个请求既省时间又省后端资源。同样,组织详情里的关注/通知等接口,未登录时直接跳过。
优化 4:客户端预校验——在发请求之前就拦截无效路径
// 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 配置——权限控制不写死在守卫里
// 路由定义中声明(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:白名单短路——不该处理的页面直接跳过
// 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)内部有自己的路由和上报逻辑,主应用的守卫不应该干涉。白名单让这些页面直接跳过,避免冲突和重复调用。
优化 7:localStorage 持久化——跨会话记住用户行为
// 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(...) 动态导入,访问哪个页面才下载哪个页面的代码。
// 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),按需加载
地图数据也是懒加载的:
// 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)— 等用户"停下来"才执行
import debounce from 'lodash/debounce'; // 按需引入
// 搜索输入:用户停止输入 300ms 后才发请求
const handleSearch = debounce((keyword) => {
fetchSearchResults(keyword);
}, 300);
// 滚动加载:用户停止滚动 300ms 后才检查是否需要加载更多
const handleScroll = debounce(() => {
checkLoadMore();
}, 300);
节流(throttle)— 每隔一段时间才执行一次
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 按需引入?
// ✅ 正确:只引入 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 缓存 → 返回缓存数据给页面
核心代码(简化版)
// 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);
}
}
缓存配置
// 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 刷新时的请求去重
// 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() 只监听整个对象的替换,不监听内部属性变化。
import { shallowRef } from 'vue';
// ❌ 普通 ref:深度响应式,遍历每个属性
const tableData = ref(bigDataArray); // 1000 条数据,每条都加代理
// ✅ shallowRef:浅层响应式,只监听整体替换
const tableData = shallowRef(bigDataArray); // 不遍历,只在 .value 被替换时触发更新
项目中 30+ 处使用,主要用于:
- 大数据列表(表格数据、搜索结果)
- 恶意软件分析详情(大 JSON 对象)
- 威胁内容列表
- 表单引用(不需要响应式)
- 分页器状态(用
shallowReactive)
// 典型用法:表格数据用 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 监听元素是否进入视口,进入后才触发加载或上报。
自定义曝光指令
// 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>
滚动触发动画
// 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。
Q3:API 缓存降级系统怎么设计的?
三层设计:第一层,请求成功时把响应缓存到 IndexedDB,配置了最大缓存条数和过期时间;第二层,请求超时或报错时自动从 IndexedDB 读缓存返回给页面,用户看到旧数据而不是错误;第三层,token 过期时做请求去重,多个并发请求只发一次 token 刷新,其他请求排队等刷新完成后统一重发。
Q4:shallowRef 和 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 |