中安科技面试题
# 前端面试题完整解答
# 一、Vue 相关
# 1. Vue 3 的 ref 和 reactive 有什么区别?分别在什么场景下使用?手动写一个判断数据是否为 ref 的工具函数
# 核心区别
| 特性 | ref | reactive |
|---|---|---|
| 响应式原理 | 基于 Proxy 包装原始值/对象,本质是把值包裹在 .value 属性中 | 直接对对象/数组创建 Proxy 代理,无 .value 包裹 |
| 支持类型 | 支持所有类型(原始值、对象、数组、函数) | 仅支持对象/数组/Map/Set 等引用类型,不支持原始值 |
| 访问方式 | 需通过 .value 访问(模板中自动解包,无需 .value) | 直接访问属性,无需额外操作 |
| 解构/展开 | 解构会丢失响应式,需用 toRefs/toRef 保持 | 解构会丢失响应式,需用 toRefs 保持 |
| 重新赋值 | 可直接替换整个值(ref.value = newVal),响应式不丢失 | 不可直接替换整个对象(会断开 Proxy 连接),只能修改属性 |
# 适用场景
- ref:
- 定义原始类型响应式数据(
string/number/boolean) - 定义需要整体替换的响应式对象(如分页数据、表单数据)
- 跨组件/组合式函数中传递响应式引用
- 模板中需要直接绑定原始值的场景
- 定义原始类型响应式数据(
- reactive:
- 定义复杂对象/数组的响应式状态(如表单对象、列表数据)
- 希望直接操作对象属性、无需
.value的场景 - 状态逻辑集中在单个对象内的场景
# 判断 ref 的工具函数
import { isRef } from "vue";
// 手动实现(原理:ref实例有__v_isRef标记)
function isRef(value) {
return !!(value && value.__v_isRef === true);
}
1
2
3
4
5
6
2
3
4
5
6
# 2. Vue3 的 Composition API 与 Options API 在设计思路上有什么根本区别?在什么场景下你更倾向于使用 Composition API?请简述原因
# 根本区别
- Options API:
- 按选项类型组织代码(
data/methods/computed/watch/生命周期),逻辑分散在不同选项中 - 适合简单组件,代码结构固定、易上手,但复杂组件中同功能逻辑分散,可读性差
- 存在
this指向问题,代码复用依赖mixin(易冲突、来源不清晰)
- 按选项类型组织代码(
- Composition API:
- 按业务逻辑组织代码,将同一功能的状态、方法、副作用集中在
setup()中 - 基于函数式编程,无
this依赖,代码复用通过组合式函数(composables)实现,无命名冲突 - 支持 TypeScript 类型推导,适合大型复杂项目
- 按业务逻辑组织代码,将同一功能的状态、方法、副作用集中在
# 优先使用 Composition API 的场景
- 复杂业务组件:逻辑多、状态多,需要按功能拆分代码,提升可维护性
- 逻辑复用需求高:需要跨组件复用状态/逻辑(如表单校验、分页、权限控制),组合式函数比
mixin更灵活、无冲突 - TypeScript 项目:Composition API 天然支持类型推导,无需额外装饰器,类型安全更友好
- 大型项目/多人协作:按逻辑拆分代码,便于多人并行开发、代码 review
- 性能优化需求:可按需引入 API,减少打包体积,同时支持更细粒度的响应式控制
# 3. vuex 和 Pinia 都是状态管理库。请说明 Pinia 在 API 设计和开发体验上的三个主要优势。在什么情况下,你会认为一个项目“不需要”引入 Pinia?
# Pinia 的三大核心优势
- API 极简,无冗余概念
- 移除了 Vuex 的
mutation,直接在action中修改状态(同步/异步均可),简化了状态修改流程 - 无需嵌套模块,支持多 store 独立定义,自动模块化,避免了
namespaced的繁琐 - 支持
setup组合式风格,与 Vue3 Composition API 完美适配
- 移除了 Vuex 的
- 开发体验与类型安全
- 天然支持 TypeScript,自动类型推导,无需额外类型声明
- 支持 Vue DevTools 深度集成,状态追踪、时间旅行调试更友好
- 热更新(HMR)支持,修改 store 无需重启页面
- 体积更小,性能更优
- 打包体积仅约 1KB(Vuex 约 5KB),减少项目包体积
- 无
this依赖,状态访问更直接,运行时性能更优 - 支持服务端渲染(SSR),适配 Nuxt3 等框架更简单
# 不需要引入 Pinia 的场景
- 小型项目/简单页面:状态仅在组件内维护,无跨组件/跨页面共享需求
- 状态逻辑简单:仅需父子组件通信,通过
props/emit/provide/inject即可满足 - 轻量级项目:追求极致打包体积,避免引入额外依赖
- 状态复用需求低:无全局状态管理需求,组件状态独立即可
# 4. 假设你发现一个 Vue 页面在频繁更新长列表时存在明显卡顿。请列出针对性的性能优化手段,并简要说明其原理
# 核心优化手段
- 虚拟列表(Virtual Scrolling)
- 原理:仅渲染可视区域内的列表项,动态计算滚动位置,复用 DOM 节点,大幅减少 DOM 渲染数量
- 适用:万级以上长列表、高频更新场景
- 工具:
vue-virtual-scroller、vue-virtual-scroll-list
- 响应式优化:shallowRef / shallowReactive
- 原理:创建浅层响应式,仅对根属性做响应式,不递归代理子属性,减少 Proxy 代理开销
- 适用:列表数据结构固定、无需深度响应式的场景
const list = shallowRef([]); // 仅list.value是响应式,子元素不代理1 - 列表项 key 优化
- 原理:使用唯一稳定的 key(如 id),避免 Vue 用
index作为 key 导致的虚拟 DOM 复用错误,减少重渲染 - 注意:禁止用
index作为 key(数据更新时会导致 DOM 错位)
- 原理:使用唯一稳定的 key(如 id),避免 Vue 用
- 防抖/节流更新
- 原理:对高频更新的触发源(如搜索、滚动、接口轮询)做防抖/节流,限制更新频率
- 工具:
lodash.debounce/lodash.throttle,或手动实现
- 计算属性缓存
- 原理:用
computed缓存列表计算结果,避免每次渲染重复计算 - 适用:列表需要过滤、排序、格式化的场景
- 原理:用
- 组件拆分与懒加载
- 原理:将列表项拆分为独立组件,用
defineAsyncComponent懒加载,减少首屏渲染压力 - 适用:列表项包含复杂逻辑/子组件的场景
- 原理:将列表项拆分为独立组件,用
- 使用 v-memo(Vue3.2+)
- 原理:缓存组件/元素的虚拟 DOM,仅当依赖项变化时才重渲染
<div v-for="item in list" :key="item.id" v-memo="[item.id, item.updateTime]"> {{ item.content }} </div>1
2
3 - 关闭 devtools 与生产模式优化
- 原理:生产环境下 Vue 会移除响应式调试代码,减少运行时开销;关闭 devtools 可避免额外监听
# 5. 你需要负责一个中后台管理系统前端的技术选型和基础框架搭建,请描述你的技术方案要点(至少涵盖:Vue 生态选型、路由/状态管理、HTTP 方案、公共代码组织、权限控制思路)
# 1. Vue 生态选型
- 核心框架:Vue 3 + TypeScript(类型安全、Composition API、性能更优)
- 构建工具:Vite(热更新快、打包体积小、支持 ES Module)
- UI 组件库:Element Plus(中后台生态完善、组件丰富、适配 Vue3)
- 样式方案:SCSS + CSS Modules(样式隔离、变量复用、主题定制)
- 图标库:Iconify(按需引入、多图标库支持)
# 2. 路由/状态管理
- 路由:Vue Router 4
- 配置:按模块拆分路由(
admin/user/dashboard),支持动态路由(权限控制) - 路由守卫:全局
beforeEach做权限校验、登录拦截、页面加载进度 - 懒加载:
() => import('@/views/xxx.vue'),减少首屏包体积
- 配置:按模块拆分路由(
- 状态管理:Pinia
- 按业务拆分 store(
user/app/permission/tagsView) - 状态持久化:
pinia-plugin-persistedstate,缓存用户信息、主题配置 - 组合式函数封装:复用状态逻辑(如
useTable/useForm)
- 按业务拆分 store(
# 3. HTTP 方案
- 请求库:Axios
- 封装统一请求实例:配置基础 URL、超时时间、请求/响应拦截器
- 拦截器功能:
- 请求拦截:添加 Token、请求头、加载动画
- 响应拦截:统一处理错误码(401/403/500)、数据格式化、取消重复请求
- 取消请求:用
AbortController实现,避免重复请求 - 类型封装:基于 TypeScript 封装请求函数,自动推导响应数据类型
# 4. 公共代码组织
- 目录结构:
src/ ├── api/ # 接口请求(按模块拆分) ├── assets/ # 静态资源(图片、样式、图标) ├── components/ # 全局公共组件(Table/Form/Modal) ├── composables/ # 组合式函数(useTable/useForm/usePermission) ├── directives/ # 自定义指令(v-permission/v-copy) ├── layout/ # 全局布局(Header/Sidebar/TagsView) ├── router/ # 路由配置 ├── store/ # Pinia状态管理 ├── styles/ # 全局样式(变量、重置、主题) ├── utils/ # 工具函数(request/format/auth/storage) └── views/ # 页面组件(按模块拆分)1
2
3
4
5
6
7
8
9
10
11
12 - 工具函数封装:统一封装日期格式化、权限判断、本地存储、防抖节流等通用逻辑
- 全局组件注册:自动注册
components/下的全局组件,无需手动引入 - 自定义指令:封装权限指令
v-permission、复制指令v-copy等
# 5. 权限控制思路(RBAC 模型)
- 权限维度:
- 页面权限:根据用户角色动态生成路由,过滤无权限页面
- 按钮权限:通过
v-permission指令、全局方法判断,隐藏无权限按钮 - 接口权限:后端校验 Token 和权限,前端仅做展示控制
- 实现流程:
- 登录成功后,后端返回用户角色、权限列表
- 前端用 Pinia 缓存权限数据,动态生成可访问路由
- 路由守卫中校验路由权限,无权限跳转 403
- 按钮权限通过指令/方法判断,控制元素显示/隐藏
- 特殊处理:
- 路由刷新后,重新拉取权限数据,恢复路由状态
- 权限变更时,动态更新路由和按钮状态
# 6. vite 在开发阶段使用 ES Modules 实现快热更新。但当你发现项目中某个较大组件(例如复杂图表组件)的加载速度明显较慢时,可以通过哪些具体手段进行优化?
# 核心优化手段
- 组件懒加载(动态导入)
- 原理:将大组件拆分为独立 chunk,按需加载,避免首屏阻塞
// 方式1:defineAsyncComponent const ChartComponent = defineAsyncComponent(() => import('./ChartComponent.vue')) // 方式2:Suspense + 动态导入 <Suspense> <template #default> <ChartComponent /> </template> <template #fallback> <Loading /> </template> </Suspense>1
2
3
4
5
6
7
8
9
10
11 - Vite 构建优化
- 分包优化:
vite.config.ts中配置build.rollupOptions,将大组件/第三方库单独分包
export default defineConfig({ build: { rollupOptions: { output: { manualChunks: { "chart-lib": ["echarts"], // 图表库单独分包 "big-component": ["@/components/ChartComponent.vue"], }, }, }, }, });1
2
3
4
5
6
7
8
9
10
11
12- 预构建优化:
optimizeDeps预构建大依赖,减少首次加载时间
optimizeDeps: { include: ["echarts", "xlsx"]; // 预构建大依赖 }1
2
3 - 分包优化:
- 组件内部优化
- 按需引入第三方库:如 echarts 仅引入需要的图表类型,避免全量引入
// 全量引入(不推荐) import * as echarts from "echarts"; // 按需引入(推荐) import * as echarts from "echarts/core"; import { BarChart } from "echarts/charts"; import { CanvasRenderer } from "echarts/renderers"; echarts.use([BarChart, CanvasRenderer]);1
2
3
4
5
6
7- 虚拟列表/分页加载:大列表用虚拟列表,减少 DOM 渲染
- 防抖/节流:图表渲染、数据更新做防抖,避免高频重绘
- Web Worker:复杂计算(如图表数据处理)放到 Web Worker,避免阻塞主线程
- 缓存优化
- 持久化缓存:
vite-plugin-cache缓存组件编译结果,二次启动更快 - 浏览器缓存:配置
Cache-Control,缓存静态资源
- 持久化缓存:
- 代码分割与 Tree Shaking
- 移除组件内未使用的代码,开启 Vite 的 Tree Shaking(默认开启)
- 用
import type仅导入类型,避免打包冗余代码
- SSR/静态站点生成
- 对大组件做服务端渲染(SSR),提前渲染 HTML,提升首屏速度
- 用
vite-plugin-ssr实现 SSR,适配复杂组件
# 二、Angular 相关
# 1. 在 Angular 中,父组件和子组件之间有哪些通信方式?请对比它们的适用场景,另外,请解释 Angular 组件的生命周期钩子,以及在什么情况下会使用 ngOnChanges 和 ngOnInit?
# 父子组件通信方式
| 通信方式 | 实现方式 | 适用场景 |
|---|---|---|
@Input() / @Output() | 父组件通过@Input()传值,子组件通过@Output()触发事件 | 父子组件直接通信,简单场景,数据单向流动 |
| 服务(Service) | 共享服务中定义可观察对象(Observable),组件订阅 | 跨层级组件通信、兄弟组件通信、全局状态共享 |
@ViewChild() / @ViewChildren() | 父组件获取子组件实例,直接调用方法/访问属性 | 父组件需要主动操作子组件(如调用子组件方法) |
@ContentChild() / @ContentChildren() | 获取投影内容的组件实例 | 父组件需要操作子组件的投影内容 |
| 路由参数 | 通过路由params/queryParams传值 | 路由跳转时传递数据 |
NgRx/状态管理 | 全局状态管理,组件通过store共享数据 | 大型项目、复杂状态共享 |
# Angular 组件生命周期钩子
| 钩子 | 触发时机 | 核心作用 |
|---|---|---|
ngOnChanges() | 输入属性(@Input())变化时触发(首次初始化+后续变化) | 响应输入属性变化,执行初始化/更新逻辑 |
ngOnInit() | 组件初始化完成后触发(仅执行 1 次) | 组件初始化逻辑(如请求数据、初始化状态) |
ngDoCheck() | 每次变更检测时触发 | 自定义变更检测逻辑(慎用,性能开销大) |
ngAfterContentInit() | 投影内容初始化完成后触发(仅执行 1 次) | 操作投影内容 |
ngAfterContentChecked() | 投影内容变更检测后触发 | 校验投影内容 |
ngAfterViewInit() | 组件视图初始化完成后触发(仅执行 1 次) | 操作 DOM、初始化第三方库(如 echarts) |
ngAfterViewChecked() | 组件视图变更检测后触发 | 校验视图状态 |
ngOnDestroy() | 组件销毁前触发 | 清理资源(取消订阅、移除事件监听) |
# ngOnChanges vs ngOnInit 使用场景
ngOnInit:- 仅在组件首次初始化时执行 1 次
- 适用:组件初始化逻辑(如请求接口、初始化变量、订阅服务)
- 注意:此时
@Input()属性已完成初始化,但后续变化不会触发
ngOnChanges:- 当
@Input()属性发生变化时触发(首次初始化+后续更新) - 适用:响应输入属性变化(如根据父组件传值更新子组件状态、重新渲染视图)
- 注意:仅对基本类型变化有效,引用类型变化需用
ngDoCheck手动检测
- 当
# 2. 在大型 Angular 应用中,如何设计模块结构以支持多人协作开发?请说明以下模块类型的区别和使用场景:根模块(AppModule)、核心模块(CoreModule)、共享模块(SharedModule)、功能模块(FeatureModule)
# 模块结构设计原则
- 按业务/功能拆分模块,每个模块对应一个业务域,多人并行开发互不干扰
- 遵循单一职责,模块内仅包含相关组件/服务/指令
- 懒加载功能模块,提升首屏加载速度
- 核心模块仅导入一次,共享模块全局复用
# 各模块区别与场景
| 模块类型 | 核心作用 | 包含内容 | 适用场景 | 导入规则 |
|---|---|---|---|---|
| 根模块(AppModule) | 应用入口模块,启动整个应用 | 根组件(AppComponent)、全局服务、核心模块、共享模块、根路由 | 应用启动入口,仅 1 个 | 仅导入核心模块、共享模块、根路由,不导入功能模块 |
| 核心模块(CoreModule) | 全局核心服务/组件,仅在根模块导入 | 全局服务(AuthService/HttpService)、全局组件(Header/Sidebar)、拦截器、守卫 | 全局单例服务、全局布局组件 | 仅在 AppModule 导入一次,禁止在其他模块导入 |
| 共享模块(SharedModule) | 跨模块复用的组件/指令/管道 | 通用组件(Table/Form/Modal)、指令(v-permission)、管道(dateFormat)、第三方模块(Angular Material) | 多个功能模块复用的通用逻辑 | 可在任意功能模块导入,按需复用 |
| 功能模块(FeatureModule) | 按业务拆分的独立模块,支持懒加载 | 业务组件、路由、服务、状态管理 | 大型应用的业务模块(如 UserModule/OrderModule) | 懒加载导入,仅在需要时加载 |
# 多人协作支持方案
- 按功能模块拆分仓库/目录,每个开发人员负责一个功能模块
- 核心模块/共享模块由架构师维护,避免多人修改冲突
- 功能模块独立开发、独立测试,通过共享模块复用通用逻辑
- 路由懒加载,每个功能模块独立打包,互不影响
- 代码审查机制,核心模块/共享模块修改需审核,保证稳定性
# 3. 如果一个服务需要在多个惰性加载模块中共享,但又不是全应用单例,应该如何配置?
# 解决方案:forRoot()/forChild() 模式 + 模块级提供器
# 核心原理
- Angular 服务默认是单例(在根注入器中提供),若要在懒加载模块中共享但非全局单例,需通过模块级注入器实现
- 用
forRoot()在根模块提供全局服务,forChild()在懒加载模块提供模块级服务
# 实现步骤
- 定义共享服务
// shared.service.ts
@Injectable()
export class SharedService {
// 服务逻辑
}
1
2
3
4
5
2
3
4
5
- 封装共享模块,实现 forRoot/forChild
// shared.module.ts
@NgModule({
// 共享组件/指令/管道
})
export class SharedModule {
// 根模块调用:提供全局单例服务
static forRoot(): ModuleWithProviders<SharedModule> {
return {
ngModule: SharedModule,
providers: [SharedService], // 根注入器提供,全局单例
};
}
// 懒加载模块调用:提供模块级服务(每个模块独立实例)
static forChild(): ModuleWithProviders<SharedModule> {
return {
ngModule: SharedModule,
providers: [SharedService], // 模块注入器提供,每个模块独立实例
};
}
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
- 根模块导入 forRoot()
// app.module.ts
@NgModule({
imports: [SharedModule.forRoot()],
// ...
})
export class AppModule {}
1
2
3
4
5
6
2
3
4
5
6
- 懒加载模块导入 forChild()
// feature.module.ts(懒加载)
@NgModule({
imports: [SharedModule.forChild()],
// ...
})
export class FeatureModule {}
1
2
3
4
5
6
2
3
4
5
6
# 其他方案
- 使用
providedIn: 'any':Angular 9+支持,服务在第一个加载的模块中提供,后续模块共享该实例(非全局单例,仅在加载的模块中共享)
@Injectable({ providedIn: "any" })
export class SharedService {}
1
2
2
- 模块级提供器:直接在懒加载模块的
providers中注册服务,每个模块独立实例
@NgModule({
providers: [SharedService],
})
export class FeatureModule {}
1
2
3
4
2
3
4
# 4. ng-template 和 ng-container 区别
| 特性 | ng-template | ng-container |
|---|---|---|
| 本质 | Angular 模板语法,不会直接渲染 DOM,仅作为模板容器 | Angular 结构指令容器,不会渲染额外 DOM 节点 |
| 作用 | 定义可复用的模板片段,通过*ngIf/*ngFor/ngTemplateOutlet渲染 | 包裹多个元素,避免额外 DOM 节点,配合结构指令使用 |
| 渲染结果 | 仅在被引用时渲染内容,否则不渲染 | 仅渲染内部内容,不生成自身节点 |
| 适用场景 | 条件渲染、模板复用、动态组件 | 多元素条件渲染、避免 DOM 嵌套、结构指令包裹 |
| 示例 | <ng-template #tpl>...</ng-template> | <ng-container *ngIf="show">...</ng-container> |
# 核心区别总结
ng-template是模板定义,不渲染,需手动引用;ng-container是容器,不渲染,用于包裹元素ng-container常配合*ngIf/*ngFor使用,避免额外div节点;ng-template用于定义可复用模板
# 5. angular 框架,是否有类似 vue 的 keep-alive 来实现类似“组件缓存”或“状态保持”的功能。
Angular没有内置的keep-alive组件,但可通过以下方案实现组件缓存/状态保持:
# 方案 1:路由复用策略(RouteReuseStrategy)
原理:自定义路由复用策略,缓存已加载的组件实例,切换路由时复用
实现步骤:
- 自定义复用策略
@Injectable() export class CustomReuseStrategy implements RouteReuseStrategy { private cachedRoutes = new Map<string, DetachedRouteHandle>(); // 决定是否复用路由 shouldReuseRoute( future: ActivatedRouteSnapshot, curr: ActivatedRouteSnapshot, ): boolean { return future.routeConfig === curr.routeConfig; } // 决定是否存储路由 shouldDetach(route: ActivatedRouteSnapshot): boolean { // 配置需要缓存的路由 return route.data?.reuse === true; } // 存储路由 store(route: ActivatedRouteSnapshot, handle: DetachedRouteHandle): void { this.cachedRoutes.set(route.routeConfig?.path || "", handle); } // 决定是否恢复路由 shouldAttach(route: ActivatedRouteSnapshot): boolean { return this.cachedRoutes.has(route.routeConfig?.path || ""); } // 恢复路由 retrieve(route: ActivatedRouteSnapshot): DetachedRouteHandle | null { return this.cachedRoutes.get(route.routeConfig?.path || "") || null; } }1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33- 在根模块注册
@NgModule({ providers: [{ provide: RouteReuseStrategy, useClass: CustomReuseStrategy }], }) export class AppModule {}1
2
3
4- 路由配置中标记需要缓存的路由
const routes: Routes = [ { path: "list", component: ListComponent, data: { reuse: true } }, ];1
2
3
# 方案 2:手动缓存组件状态
- 原理:用服务缓存组件的状态(如表单数据、滚动位置),组件销毁时保存,初始化时恢复
- 适用:简单场景,无需路由复用
# 方案 3:第三方库
- 如
ngx-keep-alive、angular-component-cacher,封装了路由复用逻辑,简化使用
# 三、前端 CSS 相关
# 1. 元素 A 宽度为 100%,有子元素 B、C,如何让子元素 B 自适应内容或者固定宽度,C 占剩余宽度
# 方案 1:Flex 布局(推荐,兼容性好)
.a {
display: flex;
width: 100%;
}
.b {
/* 自适应内容 */
flex: 0 0 auto;
/* 或固定宽度 */
/* flex: 0 0 100px; */
}
.c {
flex: 1; /* 占剩余宽度 */
overflow: hidden; /* 防止内容溢出 */
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
2
3
4
5
6
7
8
9
10
11
12
13
14
# 方案 2:Grid 布局(现代浏览器,代码更简洁)
.a {
display: grid;
width: 100%;
grid-template-columns: auto 1fr; /* B自适应,C占剩余 */
/* 或固定宽度:grid-template-columns: 100px 1fr; */
}
1
2
3
4
5
6
2
3
4
5
6
# 方案 3:Float + BFC(兼容旧浏览器,不推荐)
.a {
width: 100%;
overflow: hidden; /* 清除浮动 */
}
.b {
float: left;
/* 自适应内容 */
/* 或固定宽度:width: 100px; */
}
.c {
overflow: hidden; /* 触发BFC,占剩余宽度 */
}
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 四、Vue 拦截器相关
# 1. 请解释 Vue 生态中常见的几种拦截器类型及其核心作用
# 1. Axios 请求/响应拦截器
- 核心作用:统一处理 HTTP 请求/响应,实现全局配置
- 请求拦截器:在请求发送前执行,可添加 Token、修改请求头、显示加载动画、取消重复请求
- 响应拦截器:在响应返回后执行,可统一处理错误码(401/403/500)、格式化响应数据、隐藏加载动画
- 适用场景:全局请求统一处理、权限校验、错误处理
# 2. Vue Router 路由守卫(拦截器)
- 核心作用:拦截路由跳转,实现权限控制、页面加载逻辑
- 全局守卫:
beforeEach/beforeResolve/afterEach,拦截所有路由跳转 - 路由独享守卫:
beforeEnter,仅拦截特定路由 - 组件内守卫:
beforeRouteEnter/beforeRouteUpdate/beforeRouteLeave,拦截组件内路由
- 全局守卫:
- 适用场景:登录拦截、权限校验、页面加载进度、路由跳转日志
# 3. Pinia/Vuex 状态拦截器(订阅器)
- 核心作用:监听状态变化,实现状态持久化、日志记录
store.$subscribe:监听状态变化,可缓存状态到本地存储store.$onAction:监听 action 执行,可记录日志、错误处理
- 适用场景:状态持久化、状态变更日志、全局状态校验
# 4. Vue 生命周期拦截器(自定义指令/插件)
- 核心作用:拦截组件生命周期,实现全局逻辑
- 自定义指令:
bind/inserted/update等钩子,拦截元素渲染 - 插件:
Vue.use()注册全局拦截,拦截组件初始化
- 自定义指令:
- 适用场景:全局权限指令、组件埋点、性能监控
# 2. 在 Vue Router 中,全局前置守卫 beforeEach 和组件内守卫 beforeRouteEnter 有什么区别?在什么场景下会使用组件内守卫?
# 核心区别
| 特性 | 全局前置守卫 beforeEach | 组件内守卫 beforeRouteEnter |
|---|---|---|
| 作用范围 | 拦截所有路由跳转 | 仅拦截当前组件的路由进入 |
| 执行时机 | 路由跳转前,全局统一执行 | 组件渲染前,仅在进入该组件时执行 |
this 指向 | 无this,通过next()控制 | 无法直接访问this,需通过next(vm => {})获取组件实例 |
| 适用场景 | 全局权限校验、登录拦截、页面加载进度 | 组件级权限校验、进入组件前的逻辑(如数据预加载) |
| 执行顺序 | 先执行全局守卫,再执行组件内守卫 | 全局守卫之后,组件渲染之前 |
# 组件内守卫 beforeRouteEnter 使用场景
- 组件级权限校验:仅对当前组件做权限控制,不影响全局路由
- 进入组件前预加载数据:在组件渲染前请求数据,避免页面闪烁
- 路由参数变化处理:
beforeRouteUpdate处理同路由参数变化 - 离开组件前确认:
beforeRouteLeave处理表单未保存确认、离开日志 - 路由跳转条件控制:仅在进入该组件时做特殊逻辑(如用户状态校验)
# 3. 如何防止用户在短时间内重复发送相同请求?请描述几种实现思路
# 方案 1:请求拦截器 + 取消请求(推荐,Axios 原生支持)
- 原理:在请求拦截器中记录请求标识,重复请求时取消上一次请求
- 实现步骤:
- 用
Map存储请求标识和取消函数 - 请求发送前,检查是否存在相同请求,存在则取消
- 请求完成后,移除请求标识
- 用
const pendingRequests = new Map();
// 请求拦截器
axios.interceptors.request.use((config) => {
// 生成请求唯一标识(url + method + 参数)
const key = `${config.url}&${config.method}&${JSON.stringify(
config.params || config.data,
)}`;
if (pendingRequests.has(key)) {
// 取消上一次请求
pendingRequests.get(key)();
}
// 存储取消函数
const controller = new AbortController();
config.signal = controller.signal;
pendingRequests.set(key, () => controller.abort());
return config;
});
// 响应拦截器
axios.interceptors.response.use(
(res) => {
// 请求完成,移除标识
const key = `${res.config.url}&${res.config.method}&${JSON.stringify(
res.config.params || res.config.data,
)}`;
pendingRequests.delete(key);
return res;
},
(err) => {
// 错误时移除标识
const key = `${err.config.url}&${err.config.method}&${JSON.stringify(
err.config.params || err.config.data,
)}`;
pendingRequests.delete(key);
return Promise.reject(err);
},
);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
# 方案 2:防抖/节流
- 原理:限制请求触发频率,避免高频点击
- 适用:按钮点击、搜索输入等场景
import { debounce } from "lodash";
// 防抖:最后一次点击后延迟执行
const handleSubmit = debounce(() => {
axios.post("/api/submit", data);
}, 500);
// 节流:固定时间内仅执行一次
const handleScroll = throttle(() => {
axios.get("/api/data");
}, 1000);
1
2
3
4
5
6
7
8
9
10
11
2
3
4
5
6
7
8
9
10
11
# 方案 3:按钮禁用状态
- 原理:请求发送时禁用按钮,请求完成后恢复
<template>
<button :disabled="isLoading" @click="handleSubmit">提交</button>
</template>
<script setup>
const isLoading = ref(false);
const handleSubmit = async () => {
isLoading.value = true;
try {
await axios.post("/api/submit", data);
} finally {
isLoading.value = false;
}
};
</script>
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
2
3
4
5
6
7
8
9
10
11
12
13
14
15
# 方案 4:全局请求锁
- 原理:用全局变量控制请求并发,同一时间仅允许一个相同请求
const requestLock = new Map();
const sendRequest = async (url, data) => {
const key = `${url}&${JSON.stringify(data)}`;
if (requestLock.get(key)) return;
requestLock.set(key, true);
try {
return await axios.post(url, data);
} finally {
requestLock.delete(key);
}
};
1
2
3
4
5
6
7
8
9
10
11
12
2
3
4
5
6
7
8
9
10
11
12
# 方案 5:后端幂等性校验
- 原理:前端生成唯一请求 ID(如 UUID),后端校验请求 ID,重复请求直接返回
- 适用:关键业务(如支付、提交订单)
# 五、建权方面
# 1. 请解释前端鉴权中“认证”和“授权”的区别与联系
# 核心定义
- 认证(Authentication):验证用户身份,确认“你是谁”
- 核心逻辑:校验用户凭证(账号密码、Token、指纹),确认身份合法性
- 常见方式:登录、短信验证、人脸识别
- 授权(Authorization):验证用户权限,确认“你能做什么”
- 核心逻辑:根据用户身份,分配对应权限(页面、按钮、接口)
- 常见方式:RBAC(基于角色的访问控制)、ABAC(基于属性的访问控制)
# 区别与联系
| 维度 | 认证 | 授权 |
|---|---|---|
| 核心目标 | 确认身份 | 确认权限 |
| 执行顺序 | 先认证,后授权 | 认证通过后,才会执行授权 |
| 依赖关系 | 授权依赖认证(无身份则无权限) | 认证是授权的前提 |
| 实现方式 | 登录、Token 校验 | 路由守卫、权限指令、接口校验 |
| 安全边界 | 防止非法用户访问 | 防止合法用户越权访问 |
# 联系
- 认证是授权的基础:只有确认用户身份,才能分配对应权限
- 授权是认证的延伸:认证通过后,需通过授权控制用户可访问的资源
- 两者结合,实现完整的前端权限控制体系
# 2. 用户权限数据,你一般会在何时获取,是否有特殊处理
# 权限数据获取时机
- 登录成功后立即获取
- 逻辑:用户登录成功,后端返回 Token,前端用 Token 请求权限接口,获取用户角色、权限列表
- 适用:大多数中后台系统
- 路由跳转前全局获取(路由守卫中)
- 逻辑:在
beforeEach路由守卫中,检查权限数据是否存在,不存在则请求 - 适用:页面刷新后,权限数据丢失,需重新获取
- 逻辑:在
- 应用初始化时获取(App.vue 中)
- 逻辑:应用启动时,检查 Token 是否存在,存在则请求权限数据
- 适用:SPA 应用,首次加载时获取
# 特殊处理
- 权限数据持久化
- 用
localStorage/pinia-plugin-persistedstate缓存权限数据,避免重复请求 - 注意:敏感权限数据加密存储,避免篡改
- 用
- 权限变更实时更新
- 监听权限变更(如用户角色修改),动态更新权限数据和路由
- 路由刷新后恢复权限
- 页面刷新后,重新请求权限数据,恢复路由状态,避免白屏
- 权限降级处理
- 权限请求失败时,默认无权限,跳转登录页,避免越权访问
- 权限缓存有效期
- 设置权限缓存过期时间,定期重新请求,保证权限实时性
- 前端权限仅做展示控制
- 后端必须做权限校验,前端仅做 UI 隐藏,避免越权访问
# 3. 对比 Cookie/Session 和 Token 两种鉴权方案,各自的适用场景是什么?
# 核心对比
| 特性 | Cookie/Session | Token(JWT) |
|---|---|---|
| 存储位置 | 服务端存储 Session,客户端存储 Cookie | 客户端存储 Token(localStorage/Cookie),服务端无存储 |
| 状态性 | 有状态(服务端维护 Session) | 无状态(服务端无需存储,Token 自包含信息) |
| 扩展性 | 差(多服务器需 Session 共享,如 Redis) | 好(无状态,支持分布式、微服务) |
| 安全性 | 依赖 Cookie,易受 CSRF 攻击,需配置HttpOnly/Secure | 可避免 CSRF,需防 XSS(Token 存储在 localStorage 易被窃取) |
| 性能 | 服务端需查询 Session,高并发下性能差 | 服务端仅需验证 Token 签名,性能高 |
| 跨域支持 | 受同源策略限制,跨域需配置 Cookie | 天然支持跨域,Token 放在请求头中 |
| 有效期 | Session 可配置过期时间,服务端可控 | Token 可配置过期时间,客户端可控,支持刷新 Token |
| 适用场景 | 传统服务端渲染(SSR)、单体应用、内部系统 | 前后端分离、SPA/小程序、分布式/微服务、移动端 |
# 适用场景
- Cookie/Session:
- 传统服务端渲染项目(如 Java Web、PHP)
- 单体应用、内部管理系统(无分布式需求)
- 对 CSRF 防护要求高、服务端可控性强的场景
- Token(JWT):
- 前后端分离项目、SPA/小程序
- 分布式/微服务架构(无状态,无需 Session 共享)
- 移动端 App、跨域项目
- 高并发系统(服务端性能要求高)
# 补充:面试答题技巧
- 先讲原理,再讲实现,最后讲场景:每个问题按“是什么-为什么-怎么用”的逻辑回答
- 结合项目经验:用实际项目中的优化方案、踩坑经历举例,更有说服力
- 突出技术深度:不仅讲 API,还要讲底层原理(如 Proxy、虚拟 DOM、路由守卫执行顺序)
- 主动延伸:回答问题后,主动补充相关优化方案、最佳实践,体现技术广度
编辑 (opens new window)
上次更新: 2026/04/10, 6:04:00