Kc's blog Kc's blog
首页
分类
标签
Timeline
收藏夹
关于
GitHub (opens new window)

kcqingfeng

前端小学生
首页
分类
标签
Timeline
收藏夹
关于
GitHub (opens new window)
  • 提前下班小妙招

  • node版本的切换

  • 面试

    • 面试问答
    • 面试随记
    • googleNotebooklm
    • 虚拟列表实现原理与优化方案
    • 环保相关
    • 中安科技面试题
      • 一、Vue 相关
        • 1. Vue 3 的 ref 和 reactive 有什么区别?分别在什么场景下使用?手动写一个判断数据是否为 ref 的工具函数
        • 2. Vue3 的 Composition API 与 Options API 在设计思路上有什么根本区别?在什么场景下你更倾向于使用 Composition API?请简述原因
        • 3. vuex 和 Pinia 都是状态管理库。请说明 Pinia 在 API 设计和开发体验上的三个主要优势。在什么情况下,你会认为一个项目“不需要”引入 Pinia?
        • 4. 假设你发现一个 Vue 页面在频繁更新长列表时存在明显卡顿。请列出针对性的性能优化手段,并简要说明其原理
        • 5. 你需要负责一个中后台管理系统前端的技术选型和基础框架搭建,请描述你的技术方案要点(至少涵盖:Vue 生态选型、路由/状态管理、HTTP 方案、公共代码组织、权限控制思路)
        • 6. vite 在开发阶段使用 ES Modules 实现快热更新。但当你发现项目中某个较大组件(例如复杂图表组件)的加载速度明显较慢时,可以通过哪些具体手段进行优化?
      • 二、Angular 相关
        • 1. 在 Angular 中,父组件和子组件之间有哪些通信方式?请对比它们的适用场景,另外,请解释 Angular 组件的生命周期钩子,以及在什么情况下会使用 ngOnChanges 和 ngOnInit?
        • 2. 在大型 Angular 应用中,如何设计模块结构以支持多人协作开发?请说明以下模块类型的区别和使用场景:根模块(AppModule)、核心模块(CoreModule)、共享模块(SharedModule)、功能模块(FeatureModule)
        • 3. 如果一个服务需要在多个惰性加载模块中共享,但又不是全应用单例,应该如何配置?
        • 4. ng-template 和 ng-container 区别
        • 5. angular 框架,是否有类似 vue 的 keep-alive 来实现类似“组件缓存”或“状态保持”的功能。
      • 三、前端 CSS 相关
        • 1. 元素 A 宽度为 100%,有子元素 B、C,如何让子元素 B 自适应内容或者固定宽度,C 占剩余宽度
      • 四、Vue 拦截器相关
        • 1. 请解释 Vue 生态中常见的几种拦截器类型及其核心作用
        • 2. 在 Vue Router 中,全局前置守卫 beforeEach 和组件内守卫 beforeRouteEnter 有什么区别?在什么场景下会使用组件内守卫?
        • 3. 如何防止用户在短时间内重复发送相同请求?请描述几种实现思路
      • 五、建权方面
        • 1. 请解释前端鉴权中“认证”和“授权”的区别与联系
        • 2. 用户权限数据,你一般会在何时获取,是否有特殊处理
        • 3. 对比 Cookie/Session 和 Token 两种鉴权方案,各自的适用场景是什么?
      • 补充:面试答题技巧
  • 收藏
  • 面试
kc_shen
2026-04-10
目录

中安科技面试题

# 前端面试题完整解答

# 一、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. Vue3 的 Composition API 与 Options API 在设计思路上有什么根本区别?在什么场景下你更倾向于使用 Composition API?请简述原因

# 根本区别

  • Options API:
    • 按选项类型组织代码(data/methods/computed/watch/生命周期),逻辑分散在不同选项中
    • 适合简单组件,代码结构固定、易上手,但复杂组件中同功能逻辑分散,可读性差
    • 存在this指向问题,代码复用依赖mixin(易冲突、来源不清晰)
  • Composition API:
    • 按业务逻辑组织代码,将同一功能的状态、方法、副作用集中在setup()中
    • 基于函数式编程,无this依赖,代码复用通过组合式函数(composables)实现,无命名冲突
    • 支持 TypeScript 类型推导,适合大型复杂项目

# 优先使用 Composition API 的场景

  1. 复杂业务组件:逻辑多、状态多,需要按功能拆分代码,提升可维护性
  2. 逻辑复用需求高:需要跨组件复用状态/逻辑(如表单校验、分页、权限控制),组合式函数比mixin更灵活、无冲突
  3. TypeScript 项目:Composition API 天然支持类型推导,无需额外装饰器,类型安全更友好
  4. 大型项目/多人协作:按逻辑拆分代码,便于多人并行开发、代码 review
  5. 性能优化需求:可按需引入 API,减少打包体积,同时支持更细粒度的响应式控制

# 3. vuex 和 Pinia 都是状态管理库。请说明 Pinia 在 API 设计和开发体验上的三个主要优势。在什么情况下,你会认为一个项目“不需要”引入 Pinia?

# Pinia 的三大核心优势

  1. API 极简,无冗余概念
    • 移除了 Vuex 的mutation,直接在action中修改状态(同步/异步均可),简化了状态修改流程
    • 无需嵌套模块,支持多 store 独立定义,自动模块化,避免了namespaced的繁琐
    • 支持setup组合式风格,与 Vue3 Composition API 完美适配
  2. 开发体验与类型安全
    • 天然支持 TypeScript,自动类型推导,无需额外类型声明
    • 支持 Vue DevTools 深度集成,状态追踪、时间旅行调试更友好
    • 热更新(HMR)支持,修改 store 无需重启页面
  3. 体积更小,性能更优
    • 打包体积仅约 1KB(Vuex 约 5KB),减少项目包体积
    • 无this依赖,状态访问更直接,运行时性能更优
    • 支持服务端渲染(SSR),适配 Nuxt3 等框架更简单

# 不需要引入 Pinia 的场景

  1. 小型项目/简单页面:状态仅在组件内维护,无跨组件/跨页面共享需求
  2. 状态逻辑简单:仅需父子组件通信,通过props/emit/provide/inject即可满足
  3. 轻量级项目:追求极致打包体积,避免引入额外依赖
  4. 状态复用需求低:无全局状态管理需求,组件状态独立即可

# 4. 假设你发现一个 Vue 页面在频繁更新长列表时存在明显卡顿。请列出针对性的性能优化手段,并简要说明其原理

# 核心优化手段

  1. 虚拟列表(Virtual Scrolling)
    • 原理:仅渲染可视区域内的列表项,动态计算滚动位置,复用 DOM 节点,大幅减少 DOM 渲染数量
    • 适用:万级以上长列表、高频更新场景
    • 工具:vue-virtual-scroller、vue-virtual-scroll-list
  2. 响应式优化:shallowRef / shallowReactive
    • 原理:创建浅层响应式,仅对根属性做响应式,不递归代理子属性,减少 Proxy 代理开销
    • 适用:列表数据结构固定、无需深度响应式的场景
    const list = shallowRef([]); // 仅list.value是响应式,子元素不代理
    
    1
  3. 列表项 key 优化
    • 原理:使用唯一稳定的 key(如 id),避免 Vue 用index作为 key 导致的虚拟 DOM 复用错误,减少重渲染
    • 注意:禁止用index作为 key(数据更新时会导致 DOM 错位)
  4. 防抖/节流更新
    • 原理:对高频更新的触发源(如搜索、滚动、接口轮询)做防抖/节流,限制更新频率
    • 工具:lodash.debounce/lodash.throttle,或手动实现
  5. 计算属性缓存
    • 原理:用computed缓存列表计算结果,避免每次渲染重复计算
    • 适用:列表需要过滤、排序、格式化的场景
  6. 组件拆分与懒加载
    • 原理:将列表项拆分为独立组件,用defineAsyncComponent懒加载,减少首屏渲染压力
    • 适用:列表项包含复杂逻辑/子组件的场景
  7. 使用 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
  8. 关闭 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)

# 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 模型)

  • 权限维度:
    1. 页面权限:根据用户角色动态生成路由,过滤无权限页面
    2. 按钮权限:通过v-permission指令、全局方法判断,隐藏无权限按钮
    3. 接口权限:后端校验 Token 和权限,前端仅做展示控制
  • 实现流程:
    1. 登录成功后,后端返回用户角色、权限列表
    2. 前端用 Pinia 缓存权限数据,动态生成可访问路由
    3. 路由守卫中校验路由权限,无权限跳转 403
    4. 按钮权限通过指令/方法判断,控制元素显示/隐藏
  • 特殊处理:
    • 路由刷新后,重新拉取权限数据,恢复路由状态
    • 权限变更时,动态更新路由和按钮状态

# 6. vite 在开发阶段使用 ES Modules 实现快热更新。但当你发现项目中某个较大组件(例如复杂图表组件)的加载速度明显较慢时,可以通过哪些具体手段进行优化?

# 核心优化手段

  1. 组件懒加载(动态导入)
    • 原理:将大组件拆分为独立 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
  2. 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
  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,避免阻塞主线程
  4. 缓存优化
    • 持久化缓存:vite-plugin-cache缓存组件编译结果,二次启动更快
    • 浏览器缓存:配置Cache-Control,缓存静态资源
  5. 代码分割与 Tree Shaking
    • 移除组件内未使用的代码,开启 Vite 的 Tree Shaking(默认开启)
    • 用import type仅导入类型,避免打包冗余代码
  6. 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) 懒加载导入,仅在需要时加载

# 多人协作支持方案

  1. 按功能模块拆分仓库/目录,每个开发人员负责一个功能模块
  2. 核心模块/共享模块由架构师维护,避免多人修改冲突
  3. 功能模块独立开发、独立测试,通过共享模块复用通用逻辑
  4. 路由懒加载,每个功能模块独立打包,互不影响
  5. 代码审查机制,核心模块/共享模块修改需审核,保证稳定性

# 3. 如果一个服务需要在多个惰性加载模块中共享,但又不是全应用单例,应该如何配置?

# 解决方案:forRoot()/forChild() 模式 + 模块级提供器

# 核心原理
  • Angular 服务默认是单例(在根注入器中提供),若要在懒加载模块中共享但非全局单例,需通过模块级注入器实现
  • 用forRoot()在根模块提供全局服务,forChild()在懒加载模块提供模块级服务
# 实现步骤
  1. 定义共享服务
// shared.service.ts
@Injectable()
export class SharedService {
  // 服务逻辑
}
1
2
3
4
5
  1. 封装共享模块,实现 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
  1. 根模块导入 forRoot()
// app.module.ts
@NgModule({
  imports: [SharedModule.forRoot()],
  // ...
})
export class AppModule {}
1
2
3
4
5
6
  1. 懒加载模块导入 forChild()
// feature.module.ts(懒加载)
@NgModule({
  imports: [SharedModule.forChild()],
  // ...
})
export class FeatureModule {}
1
2
3
4
5
6
# 其他方案
  • 使用providedIn: 'any':Angular 9+支持,服务在第一个加载的模块中提供,后续模块共享该实例(非全局单例,仅在加载的模块中共享)
@Injectable({ providedIn: "any" })
export class SharedService {}
1
2
  • 模块级提供器:直接在懒加载模块的providers中注册服务,每个模块独立实例
@NgModule({
  providers: [SharedService],
})
export class FeatureModule {}
1
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)

  • 原理:自定义路由复用策略,缓存已加载的组件实例,切换路由时复用

  • 实现步骤:

    1. 自定义复用策略
    @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
    1. 在根模块注册
    @NgModule({
      providers: [{ provide: RouteReuseStrategy, useClass: CustomReuseStrategy }],
    })
    export class AppModule {}
    
    1
    2
    3
    4
    1. 路由配置中标记需要缓存的路由
    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:Grid 布局(现代浏览器,代码更简洁)

.a {
  display: grid;
  width: 100%;
  grid-template-columns: auto 1fr; /* B自适应,C占剩余 */
  /* 或固定宽度:grid-template-columns: 100px 1fr; */
}
1
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

# 四、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 使用场景

  1. 组件级权限校验:仅对当前组件做权限控制,不影响全局路由
  2. 进入组件前预加载数据:在组件渲染前请求数据,避免页面闪烁
  3. 路由参数变化处理:beforeRouteUpdate 处理同路由参数变化
  4. 离开组件前确认:beforeRouteLeave 处理表单未保存确认、离开日志
  5. 路由跳转条件控制:仅在进入该组件时做特殊逻辑(如用户状态校验)

# 3. 如何防止用户在短时间内重复发送相同请求?请描述几种实现思路

# 方案 1:请求拦截器 + 取消请求(推荐,Axios 原生支持)

  • 原理:在请求拦截器中记录请求标识,重复请求时取消上一次请求
  • 实现步骤:
    1. 用Map存储请求标识和取消函数
    2. 请求发送前,检查是否存在相同请求,存在则取消
    3. 请求完成后,移除请求标识
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:防抖/节流

  • 原理:限制请求触发频率,避免高频点击
  • 适用:按钮点击、搜索输入等场景
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

# 方案 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

# 方案 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

# 方案 5:后端幂等性校验

  • 原理:前端生成唯一请求 ID(如 UUID),后端校验请求 ID,重复请求直接返回
  • 适用:关键业务(如支付、提交订单)

# 五、建权方面

# 1. 请解释前端鉴权中“认证”和“授权”的区别与联系

# 核心定义

  • 认证(Authentication):验证用户身份,确认“你是谁”
    • 核心逻辑:校验用户凭证(账号密码、Token、指纹),确认身份合法性
    • 常见方式:登录、短信验证、人脸识别
  • 授权(Authorization):验证用户权限,确认“你能做什么”
    • 核心逻辑:根据用户身份,分配对应权限(页面、按钮、接口)
    • 常见方式:RBAC(基于角色的访问控制)、ABAC(基于属性的访问控制)

# 区别与联系

维度 认证 授权
核心目标 确认身份 确认权限
执行顺序 先认证,后授权 认证通过后,才会执行授权
依赖关系 授权依赖认证(无身份则无权限) 认证是授权的前提
实现方式 登录、Token 校验 路由守卫、权限指令、接口校验
安全边界 防止非法用户访问 防止合法用户越权访问

# 联系

  • 认证是授权的基础:只有确认用户身份,才能分配对应权限
  • 授权是认证的延伸:认证通过后,需通过授权控制用户可访问的资源
  • 两者结合,实现完整的前端权限控制体系

# 2. 用户权限数据,你一般会在何时获取,是否有特殊处理

# 权限数据获取时机

  1. 登录成功后立即获取
    • 逻辑:用户登录成功,后端返回 Token,前端用 Token 请求权限接口,获取用户角色、权限列表
    • 适用:大多数中后台系统
  2. 路由跳转前全局获取(路由守卫中)
    • 逻辑:在beforeEach路由守卫中,检查权限数据是否存在,不存在则请求
    • 适用:页面刷新后,权限数据丢失,需重新获取
  3. 应用初始化时获取(App.vue 中)
    • 逻辑:应用启动时,检查 Token 是否存在,存在则请求权限数据
    • 适用:SPA 应用,首次加载时获取

# 特殊处理

  1. 权限数据持久化
    • 用localStorage/pinia-plugin-persistedstate缓存权限数据,避免重复请求
    • 注意:敏感权限数据加密存储,避免篡改
  2. 权限变更实时更新
    • 监听权限变更(如用户角色修改),动态更新权限数据和路由
  3. 路由刷新后恢复权限
    • 页面刷新后,重新请求权限数据,恢复路由状态,避免白屏
  4. 权限降级处理
    • 权限请求失败时,默认无权限,跳转登录页,避免越权访问
  5. 权限缓存有效期
    • 设置权限缓存过期时间,定期重新请求,保证权限实时性
  6. 前端权限仅做展示控制
    • 后端必须做权限校验,前端仅做 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、跨域项目
    • 高并发系统(服务端性能要求高)

# 补充:面试答题技巧

  1. 先讲原理,再讲实现,最后讲场景:每个问题按“是什么-为什么-怎么用”的逻辑回答
  2. 结合项目经验:用实际项目中的优化方案、踩坑经历举例,更有说服力
  3. 突出技术深度:不仅讲 API,还要讲底层原理(如 Proxy、虚拟 DOM、路由守卫执行顺序)
  4. 主动延伸:回答问题后,主动补充相关优化方案、最佳实践,体现技术广度

编辑 (opens new window)
上次更新: 2026/04/10, 6:04:00
环保相关

← 环保相关

最近更新
01
环保相关
04-08
02
虚拟列表实现原理与优化方案
04-07
03
googleNotebooklm
04-07
更多文章>
Theme by Vdoing | Copyright © 2019-2026 kc shen | MIT License 豫ICP备2024074563号-3
  • 跟随系统
  • 浅色模式
  • 深色模式
  • 阅读模式