# 性能优化 — 面试版 > 简历原话:**"采用虚拟滚动和懒加载策略,路由守卫优化、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,只在真正切换组织/仓库时才发请求。 #### 优化 2:Promise.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)内部有自己的路由和上报逻辑,主应用的守卫不应该干涉。白名单让这些页面直接跳过,避免冲突和重复调用。 #### 优化 7:localStorage 持久化——跨会话记住用户行为 ```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); } }; // 使用:
...
``` #### 滚动触发动画 ```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。 ### 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` |