本次会议为一场模拟面试,旨在为候选人张同学进行面试辅导。面试官围绕候选人背景、项目经验和通用技术问题进行了深入提问,并对面试表现和简历提出了优化建议。
小结
- 自我介绍与基本情况
候选人从事前端开发约6年,主要负责TOB项目的重构与维护,具备独立负责项目从0到1搭建的经验。
面试官指出其自我介绍需优化,使其更精炼、专业,并强调了学历背景的重要性。
- 通用能力与职业规划
离职原因:因项目组瘦身,非因个人原因离职。
加班看法:明确区分了"项目紧急"和"非必要"的加班,前者愿意接受,后者则不太能接受。
技术提升:通过查阅资料、项目复盘和学习新技术(如CSDN、B站)来提升自己。
未来规划:希望在前端技术上深耕,并逐步探索AI开发、后端及运维方向,未来考虑向售前或管理岗转型。
- 项目经验与技术细节
XSQ园区服务系统重构:该项目为老旧信创系统升级,采用Vue3进行PC端和H5端重构,解决了浏览器兼容性和H5端金融适配等问题。
"FNBK"项目:候选人详细介绍了该项目中V2版本的选择原因(稳定性、安全性、团队熟悉度)、组件封装、用户隐私保护(HTTPS、RSA加密)、图片上传压缩、红娘后台的权限审批流程等。
"NGSJ"项目:候选人阐述了路由权限控制、图表响应式适配、Vuex全局状态管理、图表数据缓存、用户行为埋点、图表配置保存与恢复等技术实现。
- 面试技巧与心态
面试官建议:
面试时需站在面试官的角度思考问题,避免过度展开细节。
对公司了解需深入,包括其业务范围、产品线,而不仅是表面的产品。
对于"代码评审"等问题,需明确其目的(如保证代码质量、安全性和团队协作),而非仅描述工具使用细节。
待办
- 模拟面试后续安排
张同学需在下周一下午再次进行模拟面试,届时将重点辅导"WH"项目的技术细节。
- 简历优化
张同学需先调整"WH"项目的技术栈描述,修正其为uni-app而非原生小程序的错误。
张同学需将简历中的"专升本"专业修改为"计算机科学与技术"。
将根据优化后的简历内容,进行排版和润色,形成最终版本。
- 投递准备
张同学可在下周开始正式投递简历。
1、请简单做个自我介绍
自我介绍:
面试官您好,我叫 张XX,来自山东XX,毕业于山东XX大学,有6年的前端开发经验,我参与过多个YH内部系统的重构与开发,有PC端、小程序和H5端的开发经验,熟悉的技术栈是 Vue2、Vue3,熟练掌握 Composition API、Vite 等现代前端技术,能够独立完成从需求分析到页面开发、联调上线的全流程,具备完整的项目交付能力。以上是我的自我介绍,谢谢。
2、你为什么从上一家公司离职?
因为是YH的项目,现在YH的项目已经到了收尾阶段,后期就没有那么多工作要做了,所以公司现在的话是裁员的一个状态。
标准版回答(首选,稳妥)
我上一家主要负责YH内部相关项目的开发,之前历时两年的核心重构项目已经全面落地收尾。现阶段整体业务进入维稳阶段,日常工作量大幅缩减,不需要这么多研发人力。公司结合业务现状做了团队人员优化、缩编瘦身,我们整个项目组进行了合理精简,所以我这边被动离开,寻求新的发展机会。
精简版(面试语速快 / 简短回答用)
上家公司YH类核心项目全部收尾完成,业务进入维护期,工作量锐减。公司统一做团队缩编优化,项目组整体裁员精简,因此被动离职,希望找业务更饱和、长期稳定的岗位。
避坑要点(切记)
- 不提 降薪、薪资不满、吐槽行情、抱怨压榨,全程客观中立;
- 不说公司经营差、倒闭、制度问题,只说「项目收尾、业务收缩、人员优化」;
- 定位是 被动裁员 / 团队缩编,不是主动跑路、能力不行被开;
- 全程情绪平稳,不消极,落地落脚在「想找稳定长期发展平台」。
3、你上一家公司加班情况怎么样?
我不太喜欢公司强制性的加班,比如说咱项目确实是赶进度或者是紧急上线,这种情况加班很正常,我是愿意去加班的,如果是非必要的这种加班,比如说项目也不急,公司天天催,天天让我们在公司待着加班又没有活干,这种加班我可能不太接受。
你说我们之前也加,然后项目紧急上线的时候都加,没有不加的。
我上一家是YH外包项目,整体节奏完全跟随YH方安排。
日常业务平稳、项目进度正常的情况下,都是朝九晚六正常上下班,基本没有额外加班。
只有遇到 版本迭代、项目紧急上线、临时需求加急这类关键节点,才会统一安排加班,而且加班都是提前报备审批,合理安排,不会盲目消耗。
关于加班我一直是理性看待的:
目前行业大环境下,项目关键期的临时性加班我完全理解、也能接受。毕竟为了保障项目顺利交付、按时上线,这种必要的工作投入是应该的,我之前也多次配合过项目上线的阶段性加班,完全没问题。
但我比较抵触 无意义的 强制性内卷加班。
如果项目进度正常、没有紧急需求,只是单纯要求固定加班、耗时长、无效坐班,这种形式化加班我是不太认可的。
简单来说:必要的 阶段性 加班 我全力配合,拒绝 无意义、无产出 的 无效 强制加班,这样也能保证日常工作效率,合理平衡工作节奏。
4、你平时是通过什么方式提升自己技术的?
平时我主要通过 项目实战 + 复盘沉淀 + 主动拓展学习三块来提升技术。
第一,在日常项目开发里,遇到难点和疑难问题时,会先独立排查、查阅官方文档和技术博客去解决;问题处理完之后,我会及时做复盘总结,把解决方案、踩坑点整理下来,避免后续重复踩坑,巩固实操能力。
第二,会持续关注前端生态的新技术和行业动态,通过技术社区、优质技术博主、公开课这类渠道,了解框架更新、工程化、性能优化这类前沿方向,保持技术不落后。
第三,也会针对性做专项学习,比如数据可视化、项目性能优化、工程化配置这类业务常用能力,结合实际业务场景去落地实践,把学到的内容用到项目里,做到学以致用,稳步提升整体技术深度。
5、你目前的薪资和期望薪资是多少?
面试标准回答
HR:你目前薪资大概多少?
我目前综合月薪 ??k,是固定薪资,不含额外绩效。
HR:那你的期望薪资是多少?
结合我之前的项目经验,包括数据可视化、权限系统开发、YH类外包项目落地经验,还有目前市场行情,我的 期望薪资是 ??k。
我也了解现在行业整体薪资涨幅区间,能够接受根据公司定级、岗位具体工作内容,进行合理沟通调整,核心希望薪资和个人能力、工作内容相匹配。
补充关键要点(帮你避坑)
- 原薪 ??k,期望 ??k,涨幅约 26.7%,刚好卡在优秀求职者 20%-30% 的合理区间,完全不夸张;
- 外包岗普遍涨幅 10%-15%,对应薪资:1?.25k,你的期望留有谈判空间,就算往下压,也能谈到理想价位;
- 不咬死价格、加上「可合理沟通」,不会让 HR 反感,既体现自信,又不僵硬。
6、你对自己未来3~5年有什么规划吗?
未来 3-5 年我有清晰的阶段性规划。
短期来看,优先深耕前端核心技术栈,结合业务场景把数据可视化、工程化、性能优化这些实用能力做深做透,夯实业务开发功底,更好适配项目需求。
中期会顺势结合行业趋势,熟练落地 AI 辅助开发,用好工具提升开发效率、减少重复工作,同时主动补充后端基础知识,了解接口逻辑、基础架构,提升和后端的协作效率,慢慢往 前后端 协同的方向发展。
长期希望逐步拓展技术边界,了解基础运维和项目部署相关内容,具备全流程项目落地的认知,慢慢向全栈方向靠拢,成长为能独立负责模块、吃透业务、综合能力更强的开发人员,和公司长期稳定共同发展。
我再给你压缩一版精简短句版,面试语速快的时候用:
未来三到五年,我先深耕前端核心技术,夯实业务开发能力;其次结合行业趋势,熟练运用 AI 提升开发效率,补充后端基础知识,加强跨部门协作;长远会了解部署运维相关内容,逐步向全栈方向发展,提升综合能力,稳定长期发展。
7、在项目优先级排期上面你是怎么安排的?
第一、根据项目经理的整体排期,因为项目经理的话,他会去做一些优先级的一个排序。
第二、我会先去评估每个任务的一个紧急程度,会根据这个项目任务的紧急程度和一个依赖关系,去做一个自己的排期
第三、由易到难。
在任务和项目优先级安排上,我会分两方面去处理。
首先,严格遵从团队 和 项目经理 的 整体排期,结合业务上线节点、测试要求、甲方需求这些核心因素,优先推进紧急上线、有时间节点要求的核心任务,保证关键项目按时交付。
其次,面对并行的多项工作,我会自主做好梳理评估。结合任务的 紧急程度、业务影响、模块依赖关系合理划分顺序。日常工作中,我会优先处理简单、见效快的需求,高效产出、保障整体进度;
对于技术难度高、逻辑复杂的开发任务,我会利用个人业余时间提前研究技术方案、梳理难点,做好前期准备,不占用工作时间,保证日常开发节奏稳定,高效平衡各项工作。
8、你认为一个好的前端工程师应该具备什么样的素质?
我认为一名优秀的前端工程师,首先要 技术基础扎实,熟练掌握核心技术栈,同时遵守代码规范、注重代码质量,保证项目可维护性和团队协作效率。
其次要有 较强的责任心,对待开发任务认真严谨,重视功能稳定性,确保需求高质量落地。
前端直接面向用户,一定要 注重 用户体验,在页面性能、交互流畅度、兼容性等细节上多做优化,提升整体使用感受。
另外具备良好的 沟通 协作能力也很关键,和后端、测试、产品高效对接,减少联调沟通成本,推进项目顺利进行。
最后要有 持续学习的意识,前端技术更新迭代很快,需要主动跟进新技术、新趋势,不断补齐自身能力,适配不同业务场景的开发需求。
1、技术扎实;2、责任心;3、用户体验;4、善于沟通、持续学习。
9、你是怎么应对项目中的紧急 bug ?
遇到线上或项目紧急 bug,我会第一时间 优先停下手头非核心工作,集中精力处理问题。
首先快速复现问题,确认 bug 影响范围、是否影响核心功能与用户使用;
然后快速分层排查,从接口请求、数据返回、代码逻辑、样式兼容性等维度定位根因,高效修复;
修复完成后,在对应环境反复自测、联动测试同事快速复核,确保问题彻底解决、不产生次生问题;
问题处理完毕后,我会简单复盘原因,避免同类问题重复出现,保障项目稳定运行。
极简口述版(面试语速快用)
面对紧急 bug,我会立刻优先处理,先快速 复现问题 、确认影响范围,再快速 排查定位 问题根源,高效完成修复并 复测验证。事后做好复盘总结,规避同类问题再次发生。
10、你来之前的话对我们公司有做过了解吗?让你比较心动的点在哪?
问题 1:你来之前,对我们公司有做过了解吗?
标准回答
有的,我在面试前专门了解过咱们公司。
知道公司主要深耕金融YH类外包项目,长期合作各大YH,业务体系稳定,项目流程规范,团队管理也比较成熟。
同时岗位业务方向和我之前做的YH类前端项目经验高度匹配,整体工作模式、业务场景我都比较熟悉。
问题 2:那你觉得我们公司最吸引你的点是什么?
标准回答
第一,业务方向很契合。
主打YH金融项目,和我过往的项目经验、技术栈完全对口,上手快,能快速投入工作,不用重新适应陌生行业。
第二,公司平台稳定、流程正规。
金融类项目对代码规范、项目交付、协作流程要求很高,在这样的环境里工作,能倒逼自己提升专业能力,稳步成长。
第三,发展匹配我的规划。
我想长期深耕业务前端,稳定沉淀技术和项目经验,咱们公司的项目体量和业务场景,很适合长期稳定发展,这也是我比较心动的地方。
补充小技巧(面试加分)
- 不说空话、不夸大海口,紧扣 YH外包、业务匹配、稳定成长三个核心;
- 不用讲复杂产品,只聊 行业属性、项目类型、工作氛围、个人发展;
- 暗示自己:稳定、好上手、能长期干、适配金融项目,正是外包公司想要的人选。
11、你在工作中遇到最大的挑战是什么?你是怎么克服这些问题的?
我工作中遇到最大的挑战,是接手过一个YH老旧存量项目的改版优化。
这个项目技术栈非常老旧,采用 JSP 混合开发、前后端没有分离,整体代码耦合度很高,早期还用了 jQuery 技术,代码杂乱、没有规范,初期阅读和理解成本特别大。
一开始最大的难点是方案不明确,不确定是在原有旧代码上做修补迭代,还是整体重构。我先花时间完整梳理了旧项目的全部业务逻辑与代码结构,梳理清楚整体业务流程后,主动和项目经理沟通评估。结合后期维护成本、迭代效率和长期使用性综合判断,最终确定 整体重构的方案。
确定方案后,我使用 Vue 脚手架从零搭建轻量化项目框架,按需引入依赖,保证项目精简、无冗余,适配项目业务体量。
开发中期遇到了明显的浏览器兼容问题,项目在主流浏览器可以正常运行,但在 IE 浏览器出现白屏、功能异常的情况。
我逐行排查代码、缺少的兼容配置、缺失的兼容插件,逐步补充适配方案,不断完善框架底层配置,解决了兼容性问题。
通过这次经历,我不仅提升了 老旧项目改造、兼容适配、独立方案评估的能力,也养成了项目前期提前考虑环境兼容、底层架构规划的习惯,避免后期返工,提升整体开发效率。
12、你是怎么看待代码评审的?
代码评审是团队协作开发里 必不可少 的 核心流程,价值和意义非常关键,我主要从这几点理解:
把控代码质量相当于代码阶段的质检,避免个人开发中写出不规范、冗余、逻辑混乱的代码,统一团队编码风格,减少烂代码、垃圾代码堆积,降低后期维护成本。
规避线上风险提前发现隐藏 bug、逻辑漏洞、安全隐患和性能问题,很多自测发现不了的隐性问题,通过交叉评审能提前拦截,从源头减少线上故障,保障项目稳定运行。
统一团队规范约束每个人的开发习惯,配合 Eslint、Prettier 等工具 + 人工评审,让团队代码风格、业务写法保持统一,新人上手更快,多人协作冲突更少。
团队技术沉淀与成长评审过程也是互相学习的过程,既能发现自身开发盲区,也能借鉴同事优秀的写法、设计思路,整体提升团队整体技术水平。
便于后期维护迭代规范、高质量、逻辑清晰的代码,后续迭代、bug 修复、接手开发时成本更低,让项目能长期健康维护,适配长期业务迭代。
精简版(短答,适合快速应答)
我认为代码评审是团队开发中必要的质检环节。一方面能统一编码规范、把控代码质量,提前拦截 bug、安全和性能隐患,减少线上问题;另一方面可以促进团队互相学习,降低项目维护成本,保障项目长期稳定迭代。
13、你能接受出差吗?
标准版(首选,通用所有公司)
我 完全可以接受出差。
之前有过长时间驻场开发的经验,适应异地办公、短期外派的节奏,不管是短期临时出差,还是阶段性驻场都没问题。
个人适应能力比较强,也能配合项目节奏、公司安排,只要差旅流程、费用正常报销,出差对我来说没有任何顾虑,会保质保量完成异地工作。
精简短答(面试官快速追问、不想多说时用)
可以接受出差,我有 驻场 和 异地办公经验,适应力强,能完全配合项目和公司的出差安排。
加分优化版(体现态度 + 稳定性)
我能够接受合理范围内的出差安排。
过往有驻场工作经历,熟悉异地协作、现场对接的工作模式,不排斥外出办公。
工作上一切以项目进度和公司需求为先,合理的出差调配都可以配合,不会因为出差影响工作落地。
补充避雷提醒
- 不要说 **"我很喜欢出差、想天天出差"**,太极端,面试官会担心你不稳定、不想深耕开发;
- 不要说 **"尽量不要出差、偶尔可以"**,容易直接扣分;
- 只表态 服从安排、有经验、无顾虑,简洁得体,刚好踩中面试得分点。
14、简单介绍一下XSQ园区服务系统重构与国产化升级项目
该项目是 老旧存量园区内部办公系统的整体重构 + 信创国产化升级,服务园区内部多机构员工日常办公、后勤维护等核心场景。
原系统上线年限久,技术栈老旧,依赖国外技术体系,包括 WAS 中间件、Oracle 数据库、Windows 服务器,存在安全漏洞多、维护成本高、架构混乱、体验卡顿等问题,且业务入口分散、多套登录地址,使用不便。
我负责 前端整体重构开发,技术栈采用 Vue3 全新开发,同时兼顾 PC 端与 H5 移动端适配。项目统一业务登录入口、优化整体业务架构,剥离国外老旧技术组件,适配国产化环境改造。
改造完成后,统一了园区 6 家机构的日常办公使用,不仅修复原有安全隐患、降低后期维护成本,同时提升系统运行性能、优化操作体验,全面满足单位国产化信创改造要求与长期稳定办公需求。
15、信创环境下遇到了哪些兼容性问题?是怎么解决的?
在信创国产化改造项目中,主要遇到 浏览器兼容、样式布局、系统环境、第三方依赖、字体图标、文件预览这几类核心兼容问题,具体问题 + 落地解决方案如下:
1、信创浏览器接口缓存 & 请求兼容问题
- 问题:国产浏览器(麒麟、统信配套自研浏览器、360 信创版等)请求机制特殊,存在接口强缓存、协商缓存不失效问题,后台数据更新后,前端页面数据无法实时同步,接口重复请求、参数解析异常。
- 解决方案:
① 在 Axios 请求拦截器统一配置 时间戳随机参数、禁用请求缓存;
② 统一配置请求头
Cache-Control: no-cache,强制浏览器不缓存接口;③ 针对 get 请求统一处理参数序列化,适配国产浏览器解析规则。
2、页面样式与布局兼容问题
- 问题:国产浏览器内核老旧,对 CSS3 新属性、Flex / Grid 布局、圆角阴影、弹性样式适配差;ES6 + 高级语法不识别,导致页面错乱、样式丢失、布局塌陷。
- 解决方案:
① 引入
PostCSS、Autoprefixer自动补全 css 前缀,统一适配多内核浏览器;② 配置 Babel,将 ES6+、箭头函数、解构赋值等高阶语法 降级编译为 ES5;
③ 摒弃小众 css 语法,使用稳妥的传统布局方案,特殊样式做降级兜底处理。
3、操作系统 & 端侧适配兼容问题
- 问题:信创环境多为麒麟、统信 Linux 系统,搭配国产硬件,同时兼顾 PC 端 + H5 移动端;移动端(尤其 IOS、老旧安卓)存在点击延迟、适配错乱、分辨率兼容问题。
- 解决方案:
① 采用 响应式布局 + rem/vw 适配方案,统一多分辨率设备展示;
② 封装通用兼容方法,修复移动端点击 300ms 延迟、事件冒泡兼容问题;
③ 针对 Linux 国产系统做单独样式兜底,规避系统内核渲染差异。
4、第三方组件 & 插件兼容性问题
- 问题:项目中 Echarts 图表、富文本、弹窗、日历、文件上传等第三方组件,大多基于 Chrome 内核开发,在信创浏览器出现渲染失败、功能失效、弹窗错位。
- 解决方案:
① 选用国产化适配成熟的替代组件,或降级使用稳定低版本依赖;
② 对自定义弹窗、遮罩层做层级、定位兼容处理;
③ 图表关闭高级渲染模式,使用基础 Canvas 渲染,适配老旧内核。
5、字体、图标及特殊功能兼容
- 问题:国产系统缺少常规网页字体、Iconfont 图标乱码、特殊弹窗、打印导出功能无法正常使用。
- 解决方案:
① 引入通用 web 安全字体,规避系统字体缺失问题;
② 图标改用 svg 格式替代字体图标,提升跨环境兼容性;
③ 打印、导出等功能做浏览器判断,信创环境提供替代操作方案。
16、项目中你是如何封装可复用组件的?
在项目中,我主要基于 Element Plus 基础组件 进行 二次封装,沉淀全局可复用业务组件,提升开发效率、统一项目 UI 规范。
统一基础样式与规范对常用的 按钮、弹窗、表单、搜索栏、卡片、表格 等组件统一封装,内置全局统一的尺寸、配色、圆角、间距、样式风格,避免每个页面重复写样式,保证全站 UI 视觉统一。
通过 Props 实现灵活配置 公共基础属性、固定样式内置默认值; 个性化需求通过 props 传参 暴露配置项,比如尺寸、主题色、文案、显隐、禁用状态、自定义样式等,不同页面按需传入参数,实现差异化使用。
插槽 + 事件扩展灵活性 使用 具名插槽、默认插槽,支持自定义内容; 同时封装统一自定义事件,覆盖确认、取消、关闭、刷新等常用操作,兼顾通用性和业务定制性。
整合通用业务逻辑把高频重复逻辑内置到组件内部,比如弹窗关闭重置表单、表格默认分页、权限按钮控制、loading 加载状态、空数据兜底等,减少页面冗余代码。
全局注册,开箱即用封装完成后在全局统一注册,页面直接引入使用,不用重复引入原生组件,降低维护成本,后期如果需要改整体样式或逻辑,只需要修改封装组件一处,全局生效。
17、事物申请模块中的表单校验是如何设计的?
事务申请模块的表单校验,是基于 二次封装的全局表单组件统一设计实现的。
规则统一配置 提前为每个表单项绑定独立校验规则,包含 非空必填校验、手机号 / 身份证 / 邮箱等正则格式校验、长度限制、内容格式校验等,所有校验规则集中配置,统一管理。
实时 + 提交双重校验 输入框失焦时触发实时校验,即时提示错误;点击提交按钮时,一次性全局遍历所有表单规则,批量校验全部表单项。
错误反馈交互处理 校验不通过时,表单项标红高亮,展示红色错误提示文案;同时自动滚动定位到 第一个校验失败的表单项,提升用户操作体验。
提交行为限制 提交按钮做了 Loading 加载状态 + 防抖节流处理,防止重复频繁提交;只有所有表单校验规则全部通过,才会放行、调用新增 / 提交接口。
后续流程闭环接口请求成功后,弹出成功提示,自动跳转至事务申请列表页;接口异常则给出错误提示,保留用户已填内容,方便修改重新提交。
用户进入 事务申请 表单页面,填写 相关信息,比如 申请 事由、时间、附件 等。在用户提交前,我会在前端做 完整的 表单校验,包括 必填项 校验、格式 校验、长度 限制、逻辑校验等,校验不通过则给出明确提示,阻止提交。
校验通过后,前端调用 提交申请 接口,把表单数据传给后端。接口返回成功后,前端给出提交成功的 提示,并自动 跳转到 我的申请 列表页面。
18、您如何实现"我的专区"中的待办事项提醒?
答案:从后端获取待办列表(如待审批申请),使用红点和数字角标展示数量。点击可跳转到详情。通过 轮询 或 WebSocket 实时更新。
我的专区待办事项提醒,整体采用 后端驱动、前端渲染展示的方案实现。
- 数据来源:所有待办数据由后端统一生成,比如用户提交事务申请后,系统会自动给对应审批人生成待办任务。
- 前端展示:页面初始化时,前端调用 待办列表接口 ,拉取当前登录用户的全部待办数据;同时通过 数字角标、红点标记展示待办总数、未处理数量,起到强提醒作用。
- 交互跳转:点击单条待办 或 处理按钮,直接路由跳转至对应审批详情页,进行审核操作。
- 实时更新:采用 轮询 + WebSocket结合的方式,实时监听新增待办、已办结任务状态变更,自动刷新待办数量和列表,保证提醒数据实时同步。
- 状态联动:审批完成后,后端更新任务状态,前端自动过滤已完成事项,减少无效提醒。
对于 审批人 ,系统会通过 轮询 或 WebSocket 推送待办 通知,审批人 点击进入 审批 详情页 ,做出 "通过" 或 "驳回" 操作 ,前端再次发起 审批请求 -> 最终,申请人会收到最终审批结果的通知。
19、你是如何处理 Token 过期问题的?
我在项目中基于二次封装的 Axios 统一处理 Token 过期问题。在 响应拦截器 中全局拦截接口返回状态码,当识别到 401、Token 失效 / 过期、登录失效 等状态时:
- 立即 清空本地存储的 Token、用户信息等缓存数据;
- 弹出友好提示,告知用户登录已过期,请重新登录;
- 强制跳转至登录页面,重新授权登录;
- 同时增加异常拦截,防止重复弹窗、重复跳转,优化用户体验。
极简口述短版(面试快速回答)
通过 Axios 响应拦截器统一捕获 401 状态码,判断 Token 过期后,清空 本地登录缓存,提示登录失效,自动跳转到登录页重新登录。
20、如何优化移动端加载性能问题?
在移动端项目中,我从 代码、资源、渲染、网络、打包多个维度做加载性能优化:
路由与代码优化 采用 路由懒加载 、按路由拆分打包,减少首屏加载资源体积,避免一次性加载所有页面代码,提升首屏渲染速度。
图片资源优化 对项目图片统一压缩,替换为 WebP 轻量化格式,同时配合后端二次压缩;合理使用图片懒加载,不在可视区的图片延迟加载,减少初始请求压力。
长列表性能优化 针对大数据长列表,和后端对接做 分页加载 ,上拉触底分页请求;海量列表场景使用 虚拟滚动,只渲染可视区域 DOM,避免 DOM 过多造成页面卡顿、滑动卡顿。
网络与请求优化 接口合理 节流防抖,避免频繁请求;项目开启 Gzip 压缩,减小静态资源传输体积,加快资源加载速度。
组件与缓存优化 合理使用
keep-alive缓存常用页面和组件,减少重复渲染、重复请求;非响应式数据不放入data,直接 挂载在实例上,减少 Vue 响应式监听开销,降低内存消耗。用户体验优化全局配置 Loading 加载状态,请求期间展示加载动画,避免白屏;优化静态资源加载顺序,关键资源优先加载,提升用户感知体验。
21、您为什么选择原生小程序开发而不是uni-app?
答案:原生小程序性能更好,调试工具完善,且项目 不需要跨多端。团队也熟悉原生语法,开发效率更高。
面试标准回答:为什么芜花律师管理平台选用微信原生小程序,而非 uni-app
性能体验更优原生小程序直接依托微信底层引擎运行,无 uni-app 框架层、编译层额外开销,页面渲染、数据更新、滑动交互更流畅;律师管理平台包含表单、列表、权限弹窗、数据查询等高频操作,原生性能更稳定,避免多端框架兼容带来的卡顿、加载延迟问题。
项目无跨端需求 本项目是 专属微信端的内部业务小程序 ,只需要服务微信生态用户,不需要打包安卓、iOS App、支付宝小程序等多端能力,uni-app 跨端的核心优势完全用不上,没必要引入 多余框架。
原生能力兼容性更强、调用更稳定 微信原生 API 对小程序专属能力支持更底层、更全面,比如 微信登录、授权、定位、拍照上传、消息推送、本地存储、业务权限校验等官方能力,原生调用更直接,减少框架封装 带来的兼容 bug、适配异常,业务稳定性更高。
调试与开发更贴合业务场景 全程使用微信开发者工具原生调试,报错精准、日志清晰、源码直观;搭配 HBuilderX 辅助编码只是提升编写效率,底层代码仍是微信原生小程序语法,并非 uni-app 编译代码,排查问题、线上问题复盘更高效。
团队技术栈匹配,开发维护效率更高团队熟悉微信小程序原生语法、WXML / WXSS、小程序生命周期及原生工程规范,无需额外学习 uni-app 语法、跨端适配规则、条件编译等额外成本;项目轻量化、业务逻辑聚焦,原生开发更简洁,后期迭代、维护、二次优化成本更低。
项目轻量化,避免框架冗余uni-app 内置大量跨端兼容代码、公共组件与依赖包,会增加项目体积;原生小程序按需编码、轻量化打包,包体积更小,小程序启动速度更快,符合轻量化业务系统的开发要求。
我帮你把这段压缩成 精简面试口述版,方便直接背:
芜花律师管理平台只针对微信单端 使用,不需要多端 打包能力,所以没必要用 uni-app。原生小程序依托微信底层引擎,没有框架多层编译开销 ,页面交互和加载性能更好;同时原生对微信各类授权、上传、推送等官方 API 支持 更稳定,减少兼容问题。加上团队熟悉原生小程序语法,搭配微信开发者工具调试更精准,开发和后期维护效率更高,项目也更轻量化。
22、您如何搭建小程序开发环境?
一、项目真实技术栈(最终版)
技术栈:uni-app + uView UI + 微信小程序端
不是原生小程序!不是原生小程序!不是原生小程序!
之前写的 "原生" 全部作废,统一改成 uni-app。
二、面试标准答案(你直接背)
问:你是如何搭建小程序开发环境的?
答:
我使用 HBuilderX 作为开发工具,直接新建 uni-app 项目,选择默认模板,运行到微信小程序端。
开发时搭配 微信开发者工具 进行联调预览。
项目中通过 npm 安装 uView UI 组件库,并在 main.js 中引入使用,完成基础开发环境搭建。
问:为什么用 uni-app,不用原生小程序?
答:
因为项目需要支持 微信小程序,未来可能扩展其他端,uni-app 一套代码多端发布,开发效率更高。
而且 uni-app 基于 Vue 语法,写法更简洁,搭配 uView UI 能快速完成表单、列表、弹窗等业务组件。
问:你这个项目是从 0 到 1 开发的吗?
答:
不是从零开发,是 接手已有的二手项目 ,在原有项目基础上进行 功能迭代、页面修复、接口对接、优化体验。
三、项目描述里必须改的内容(我直接给你最终版)
项目名称:芜花律师管理平台小程序
技术栈:
uni-app + Vue2 + uView UI + 微信小程序 + 微信开发者工具 + HBuilderX
项目描述:
该项目是律师事务所内部管理平台,主要用于案件管理、客户信息、流程审批、数据查询等日常办公功能。我 接手维护已有项目,负责页面开发、接口联调、UI 还原、问题修复及功能迭代。
我的职责:
- 基于 uni-app + uView 开发各类业务页面
- 使用 wx.request / uni.request 对接后端接口
- 完成表单验证、列表展示、权限控制、图片上传等功能
- 微信开发者工具调试、兼容小程序环境
- 修复线上问题、优化页面加载速度
23、FNBK项目为什么选择Vue2而不是Vue3?
我们YH内部的「FNBK」项目最终选用 Vue2 而非 Vue3,主要是结合 行业现状、系统稳定性、团队技术栈三方面综合决策:
金融行业稳定性优先 YH 类业务对系统稳定 性、线上风险把控要求极高。Vue2 生态成熟、落地周期长,经过大量金融项目长期验证 ,兼容性、线上安全漏洞风险更低,更适配行内风控与稳定要求。
团队技术栈统一,降低开发维护成本 行内前端团队长期深耕 Vue2 技术栈,整体熟练度更高、业务积累丰富。统一技术栈 能保证开发效率,同时后期迭代、Bug 修复、新人接手维护成本更低。
Vue3 当时生态未完全成熟 项目立项阶段,Vue3 还处于快速迭代阶段,部分配套插件、UI 库、老旧业务组件适配不完善,存在隐性兼容问题 和 未知 Bug。金融项目不能冒版本迭代带来的线上风险,因此优先选用更稳妥的 Vue2。
24、项目中你封装了哪些组件?
在这个项目里,我基于业务高频复用场景,独立二次封装了多个通用业务组件,提升整体开发效率:
- 全局 Dialog 弹窗组件统一弹窗样式、标题、底部按钮、关闭逻辑,支持自定义内容与回调,减少重复代码。
- 通用表单组件整合输入框、选择器等表单项,内置表单校验规则、必填校验、错误提示,统一表单提交与重置逻辑。
- 图片上传组件基于 Vant Uploader 二次封装,限制上传数量、大小、格式,自带预览、删除、上传 loading 及失败兜底处理。
- 无限滚动列表组件基于 van-list 封装,统一分页加载、空数据、加载中、无更多、异常兜底等状态,适配项目全部列表页面。
- Echarts 图表公共组件统一初始化、自适应窗口、防抖重绘、销毁释放实例,支持传入配置项快速生成图表,避免内存泄漏。
极简口述版(面试快速回答)
我在项目中封装了大量可复用通用组件,包括自定义弹窗 、带校验的通用表单 、二次封装的图片上传 、无限滚动列表 ,还有 Echarts 公共图表组件,统一了业务样式 和 交互逻辑,提升了页面开发效率。
25、在项目中你是怎么处理用户隐私保护的?
在项目中我从 传输、展示、本地存储、接口权限多维度落地用户隐私与敏感数据保护:
- 传输层安全 全程采用 HTTPS 协议传输数据,杜绝明文 http 请求,防止数据在网络传输过程中 被抓包、窃取 或 篡改。
- 敏感数据加密 手机号、证件号、登录密码等核心隐私信息,前后端采用 RSA 非对称加密处理,前端加密后再提交,后端解密解析,保障关键数据安全。
- 脱敏展示处理 后端返回敏感字段统一做脱敏处理,手机号、身份证、邮箱等进行星号打码展示,前端不展示完整原始信息。
- 禁止本地缓存敏感信息严格控制本地存储,用户隐私、账号密码类敏感数据,不存入 localStorage、sessionStorage 和 Cookie,避免本地数据泄露风险。
- 额外补充(加分项) 页面退出、账号切换时,主动清空表单缓存 与 临时数据;同时接口做好权限控制,避免越权查询他人隐私数据。
简洁口述版
项目里主要从三方面保护用户隐私:
第一,全站使用 HTTPS 加密传输,关键敏感数据通过 RSA 非对称加密前后端交互;
第二,手机号、证件等信息全部脱敏打码展示;
第三,敏感数据不做本地持久化存储,防止本地信息泄露,全方位保障用户数据安全。
26、你是如何实现图片上传压缩的?
我们项目基于 Vant Uploader 组件做二次封装实现图片上传,前端做限制 + 轻量压缩,后端二次压缩兜底:
- 前端先做基础校验,限制图片大小、格式,比如单张不超过 5M,只允许 JPG、PNG 常规格式,拦截不合格文件,减少无效请求。
- 利用浏览器**
canvas** 绘图能力,对原图进行 前端本地压缩,修改分辨率 和 画质比例,在不影响展示清晰度的前提下,减小图片体积,加快上传速度、节省带宽。- 压缩完成后转为 Base64 或 Blob 格式传给后端。
- 后端接收图片后,再做 二次压缩 + 画质优化,统一图片尺寸,做存储格式化处理,同时规避大图占用服务器存储的问题,形成双层压缩保障。
极简口述版
我在 Vant 上传组件基础上自行封装,前端先校验图片大小和格式,再通过 Canvas 实现本地画质压缩、降低体积;上传到服务端后,后端再进行二次压缩处理,双重优化,提升上传效率并节约服务器存储。
27、如何实现红娘后台的权限控制?
一、核心业务流程(完全匹配你的需求)
- 用户登录 :后端返回
用户ID、角色、权限标识、红娘审核状态- 前端页面判断 :
- 普通用户(未申请 / 申请中 / 被拒绝)→ 显示【申请成为红娘】按钮
- 审核通过的红娘 → 显示【红娘中心管理】
- 申请流程:普通用户填写资料 + 申请理由 → 提交 → 生成审核单
- 管理员流程:收到待审核提醒 → 进入审核列表 → 查看资料 → 同意 / 拒绝(拒绝必填理由)→ 审批完成
- 状态流转:未申请 → 审核中 → 审核通过 / 审核拒绝
二、数据库设计(必须表结构)
1. 用户表(user)
存储基础信息 + 角色 + 红娘状态
sqlCREATE TABLE `user` ( id BIGINT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(50) NOT NULL COMMENT '账号', nickname VARCHAR(50) COMMENT '昵称', phone VARCHAR(20) COMMENT '手机号', -- 核心角色字段 role VARCHAR(20) DEFAULT 'USER' COMMENT '角色:ADMIN-管理员,USER-普通用户,MATCHMAKER-红娘', -- 红娘审核状态(关键) matchmaker_status VARCHAR(20) DEFAULT 'NONE' COMMENT 'NONE-未申请,APPLYING-申请中,PASS-通过,REJECT-拒绝', reject_reason TEXT COMMENT '拒绝理由', create_time DATETIME DEFAULT NOW(), update_time DATETIME DEFAULT NOW() );2. 红娘申请表(matchmaker_apply)
存储用户提交的申请资料
sqlCREATE TABLE `matchmaker_apply` ( id BIGINT PRIMARY KEY AUTO_INCREMENT, user_id BIGINT NOT NULL COMMENT '申请人ID', name VARCHAR(50) NOT NULL COMMENT '真实姓名', age INT COMMENT '年龄', gender VARCHAR(10) COMMENT '性别', wechat VARCHAR(50) COMMENT '微信号', address VARCHAR(255) COMMENT '所在地区', apply_reason TEXT NOT NULL COMMENT '申请理由', status VARCHAR(20) DEFAULT 'APPLYING' COMMENT 'APPLYING-待审核,PASS-通过,REJECT-拒绝', audit_user_id BIGINT COMMENT '审核管理员ID', audit_remark TEXT COMMENT '审核拒绝理由', create_time DATETIME DEFAULT NOW(), update_time DATETIME DEFAULT NOW() );
三、后端接口设计(核心 5 个接口)
1. 登录接口(关键:返回角色 + 权限 + 红娘状态)
返回体示例(前端判断按钮显示全靠它):
javascript{ "code": 200, "data": { "token": "xxxx", "userId": 1001, "role": "USER", // 角色:ADMIN/USER/MATCHMAKER "permissions": [], // 权限标识列表 "matchmakerStatus": "NONE" // NONE/APPLYING/PASS/REJECT } }2. 提交红娘申请接口
- 权限:仅 普通用户可调用
- 逻辑:校验状态 → 新增申请单 → 用户状态改为
APPLYING- 不可重复提交
3. 管理员 - 获取待审核列表
- 权限:仅 管理员
- 返回:所有
APPLYING状态的申请单4. 管理员 - 审核红娘申请(核心)
入参:申请 ID、审核状态、拒绝理由(拒绝时必填)
逻辑:
- 校验管理员权限
- 同意:更新申请单状态 → 更新用户
role=MATCHMAKER、matchmakerStatus=PASS- 拒绝:更新申请单 + 填写拒绝理由 → 更新用户
matchmakerStatus=REJECT+ 记录拒绝理由5. 红娘中心 - 获取我的牵线人员
- 权限:仅 红娘
- 逻辑:根据当前红娘 ID 查询其管理的用户
四、前端权限控制逻辑(最关键)
1. 【我的页面】按钮显示判断
javascript// 从登录信息中取 const { role, matchmakerStatus } = userInfo // 规则: if (role === 'MATCHMAKER') { // 红娘 → 显示【红娘中心管理】 return <Button onClick={() => navigate('/matchmaker/home')}>红娘中心管理</Button> } else { // 普通用户 → 显示【申请成为红娘】 return <Button onClick={() => navigate('/matchmaker/apply')}>申请成为红娘</Button> }2. 页面路由权限拦截
/matchmaker/apply(申请页)→ 仅 普通用户可进/matchmaker/home(红娘中心)→ 仅 红娘可进/admin/matchmaker-audit(审核页)→ 仅 管理员可进3. 申请提交页面
表单字段:
- 真实姓名、年龄、性别、微信、地区、申请理由(必填)
- 提交后状态变为:审核中
五、完整状态流转图
sql未申请(NONE) ↓ 提交申请 审核中(APPLYING) ↓ 管理员审核 ├── 同意 → 红娘(PASS + 角色MATCHMAKER) └── 拒绝 → 已拒绝(REJECT + 拒绝理由)
六、权限控制核心规则(总结)
角色控制
- 管理员:审核红娘申请
- 红娘:进入红娘中心、管理牵线人员
- 普通用户:查看个人中心、提交红娘申请
状态控制
- 只有
matchmakerStatus=NONE/REJECT才能重新申请- 审核中不可重复提交
- 拒绝后可查看拒绝理由并重新提交申请
按钮 / 菜单 / 路由全部受控 前端不依赖写死的路径,全部根据 角色 + 状态动态渲染。
七、管理员审核提醒方案
- 后台申请提交后:
- 新增一条 系统消息 :
用户xxx提交了红娘申请,请及时审核- 管理员登录后:
- 右上角显示小红点 / 待办数量
- 点击直接进入审核列表
总结
我给你的是 可直接落地开发的完整方案:
- 表结构:用户表 + 红娘申请表(包含所有状态和字段)
- 接口:登录、申请、审核、列表、红娘中心
- 前端:按钮显示逻辑 + 路由权限 + 表单
- 状态流:未申请 → 审核中 → 通过 / 拒绝
- 权限:基于 角色 + 状态双重控制,安全且符合业务
28、你是如何实现黑名单功能的?
我们当前FNBK社交项目,目前暂未开发用户黑名单拉黑功能。
现有陌生人聊天限制规则为:双方未互相关注时,陌生人仅可发送有限条数消息,超出后无法继续发起会话,以此规避陌生用户过度骚扰的问题。
但针对 已产生会话、聊天过程中言语反感、不想继续沟通的精准拉黑场景,目前确实存在功能缺失,用户无法主动拉黑指定联系人、屏蔽对方消息。
这也是我们项目后续迭代的优化点,后续会规划接入 黑名单功能:支持用户一键拉黑指定用户,拉黑后双向屏蔽会话、禁止对方发消息、禁止查看个人资料,从根本上解决用户主动不想沟通、被打扰的需求,完善产品体验。
29、你是如何实现拖拽式的问卷创建?
我们项目里的 拖拽式问卷创建 ,是基于 Vue.Draggable 这个拖拽组件实现的。
整体分为 左侧题型面板、中间编辑区域、右侧配置面板三部分:
左侧题型容器 我把它配置成 可向外拖拽、但不允许往内部拖入的纯源头容器,只用来存放 单选、多选、填空、评分 等 标准题型。
中间问卷编辑区这是核心拖拽区域,支持:
- 从左侧把题型 拖入,自动生成对应题目组件
- 在内部 自由上下拖拽排序
- 支持复制、删除、清空 等操作
拖拽事件处理 我通过 Draggable 提供的生命周期函数,比如
onStart:记录拖拽开始信息onEnd:拖拽结束后获取拖拽项、位置、目标容器,拿到这些数据后,动态往问卷列表里 push 对应题目结构,实时更新视图。右侧属性配置面板 点击中间区域任意题目,右侧会 自动切换显示该题型的配置项,比如修改题目标题、添加 / 删除选项、设置是否必填、默认值等,配置后实时同步到题目数据里。
数据结构整个问卷最终会生成一个标准 JSON 数组,包含所有题目类型、标题、选项、配置,方便 后端保存 和 后续渲染 答卷页面。
精简版(15 秒快速回答)
我们使用 Vue.Draggable 实现拖拽问卷。左侧是 题型面板,配置为只能向外拖拽;中间是 编辑区,支持拖入题目、自由排序;通过拖拽结束事件获取数据,自动生成题目;点击题目后,右侧出现 配置面板,可修改标题、选项、是否必填等;最终生成结构化 JSON 用于保存 和 渲染。
30、如何实现富文本的编辑?
我们项目中富文本编辑,采用 wangEditor开源编辑器实现。
首先安装并引入插件,在项目中进行 二次封装,统一管理编辑器 实例 与 配置。
原生编辑器部分功能不会默认展示,比如 图片上传、附件上传等,需要手动在配置项中开启对应菜单、注册上传能力。
同时自定义配置上传请求地址、请求头、文件大小、格式校验等规则,对接后端文件上传接口,实现图片、附件的本地上传与回显。
编辑器基础编辑能力直接开箱即用,支持文本加粗、字体大小、文字颜色、段落排版、列表、引用等常规富文本操作。
编辑完成后直接获取编辑器 HTML 内容进行提交存储,编辑回显时再把 HTML 内容赋值给编辑器,完成数据 双向渲染,满足业务图文编辑需求。
精简短句版(面试快速口述)
项目基于 wangEditor 实现富文本编辑,对组件进行二次封装。手动配置开启图片、附件上传菜单,对接后端上传接口,自定义文件校验规则。同时支持文字样式、排版等基础编辑功能,通过 HTML 格式存取内容,实现内容编辑与回显。
31、你们是如何实现首页数据大屏的?
我们项目首页数据大屏,整体基于 ECharts + 自适应布局 开发实现。
首先由产品和 UI 敲定整体布局、模块划分以及视觉交互效果,采用 上下左右分区 + 中间核心内容的经典大屏布局结构。
布局层面采用弹性布局搭配适配方案,做了 窗口缩放自适应处理,浏览器放大、缩小不会出现内容挤压、遮挡、布局错乱的问题,保证核心区域展示稳定。
图表部分全部使用 ECharts 实现:
左上角为数据统计卡片,展示问卷总量、各类核心数字指标;
右上角使用 环形饼图,可视化展示已答、未答问卷的占比,同时配置了加载动画、旋转动效提升大屏视觉效果;
左下角统计已发布问卷总数;
右下角通过柱状图 / 排行榜形式,展示各部门问卷填写数据排名。
中间核心区域为业务列表,展示已答、未答问卷事项,支持分类切换查看。
同时对图表样式、配色、文字大小、图例、提示框做了统一定制化优化,适配大屏展示风格,所有统计数据通过接口请求后端实时获取,渲染到页面,完成整体数据可视化大屏需求。
精简口述版(简短高效,面试快速回答)
首页数据大屏采用 ECharts 做数据可视化,结合弹性自适应布局。
页面分为四大统计区块和中间业务列表区,左右上下分别放置指标卡片、环形占比图、发布数量、部门填写排行榜。
单独处理了浏览器缩放适配,避免布局错乱;自定义 ECharts 样式和动画,接口拉取后端实时统计数据,实现问卷相关数据可视化展示。
32、如何实现问卷答案暂存的?
目前我们项目 暂未开发问卷答案自动暂存功能。
现有逻辑为:用户填写完成点击提交时,前端先做全局校验,如果存在必填题目未作答,会弹出提示拦截提交,保证完整作答后才能提交成功。
如果后续需要迭代实现 问卷答案暂存,整体有两套落地方案,前后端结合实现:
手动 / 主动暂存增加「保存草稿」按钮,用户主动点击后,前端收集当前已填写的所有题目答案,调用后端暂存接口,将答题数据存入答题草稿表,关联用户 ID、问卷 ID,记录作答进度。
定时自动暂存通过定时器监听用户输入、选项切换等操作,间隔一段时间自动调用保存接口,实时同步作答内容到后端,持续保存答题进度。
双重兜底防丢失 服务端持久化存储为主,同时搭配前端
localStorage本地缓存做兜底,避免断网、刷新页面、意外关闭页面导致答案丢失。下次用户再次进入同一份问卷时,前端先请求后端草稿数据,有暂存记录就自动回显题目答案,继续上次作答进度,实现断点续填。
精简口述短版
当前项目没有做问卷答案暂存,仅在提交时校验必填项,未完成则拦截提交。
后续如需实现,可以通过 手动保存 或 定时自动保存,把作答数据提交后端存入草稿表,同时搭配本地缓存兜底;再次进入页面回显草稿数据,实现答题进度 暂存 与 续填。
33、你们是如何实现答题时长统计的?
目前项目中 暂时没有开发答题时长统计功能。
如果要实现该功能,整体采用 前端触发、后端计算的方案来做:
用户从问卷列表进入答题页面时,在请求问卷详情接口的同时,前端携带 开始标识 ,后端记录 答题开始时间。
当用户完成所有题目、点击提交问卷时,在提交接口中,后端自动记录 答题结束时间。
后端通过 结束时间 减去 开始时间,自动计算出本次完整 答题时长,持久化存入 答题记录表单。
后续在需要展示数据的页面,后端直接返回答题时长字段,前端直接渲染展示即可。
同时也可以做前端兜底方案,页面初始化开启计时器,实时记录本地答题时长,用于页面实时展示,最终以后端统计时间为准,保证数据准确。
精简口述版
当前项目未做答题时长统计。
落地思路是:进入答题页后端记录开始时间,提交问卷时记录结束时间,由后端计算时间差并保存;前端直接展示后端返回的答题时长数据,保证统计数据真实可靠。
34、你们有问卷打印功能吗?
目前项目 没有开发问卷在线打印功能。
但实现了 答题数据导出能力,管理员可以将用户的问卷作答记录、填写情况,一键导出为 Excel 文件,方便线下查看、汇总统计与归档使用。
如果后续需要新增打印功能,可以借助 浏览器原生
window.print()方法,定制打印样式,隐藏无关布局,只展示问卷题目与内容,快速实现页面打印需求。精简口述版
项目暂无问卷打印功能,现有替代方案是支持导出答题数据为 Excel,满足数据汇总查看需求。如需后续迭代,可调用 浏览器原生打印 API 快速实现 打印功能。
35、你们如何实现答题路径分析?
项目中 没有单独做复杂的答题路径分析 ,主要通过 后端状态字段 + 前端路由拦截来控制用户的访问路径和答题权限。
首先,用户点击问卷或通过链接进入答题页时,会先请求后端问卷详情接口。
后端会根据用户账号、问卷作答记录,返回对应的状态标识:未作答、已作答、无权限等。
未答状态 正常渲染问卷题目,允许用户正常填写、提交答案。
已答状态 后端返回 已作答标识 ,前端拿到状态后做判断,自动重定向到 提示 页面,展示「您已答过无需作答」文案,同时提供返回列表按钮,防止重复答题。
无权限 / 未登录 校验 Token 登录令牌,未登录或身份非法时,直接拦截路由,重定向到登录页,杜绝非法访问。
整体通过 接口状态校验 + 前端路由控制,限制用户访问路径,规范答题流程,保证一份问卷每人仅能作答一次,数据不会重复冗余。
精简口述版
我们没有单独开发答题路径分析模块,依靠后端返回的问卷状态,结合前端路由拦截实现管控。进入答题页先请求接口,未答正常展示问卷;已答则跳转提示页禁止重复作答;未登录无 Token 直接重定向登录页,以此规范用户答题访问路径。
36、NGSJZT你是如何实现路由鉴权的?
我直接给你一套 可直接落地、企业级、数据中台通用的路由鉴权方案,完全匹配你说的场景:
- 后端返回 权限路由列表
- 前端 动态加载路由
- 无 token → 重定向登录页
- 无权限 → 重定向 404
- 普通用户 / 管理员按权限访问页面
这套方案是 Vue3 + Vue Router + Pinia 最标准的实现,React 逻辑完全一样,只是 API 不同。
一、整体设计思路(核心逻辑)
- 登录成功 :后端返回
token+ 当前用户可访问的 路由权限列表- 前端存储:把 token 存在 localStorage,权限路由存在 Pinia
- 路由守卫 :全局监听路由跳转,做 3 层判断:
- 有无 token → 无 → 跳登录
- 有无权限路由 → 无 → 从后端拉取
- 跳转目标是否在权限路由里 → 无 → 跳 404
- 动态添加路由 :只把用户有权限 的路由挂载 到 router里
- 菜单渲染:根据权限路由列表动态生成侧边栏
二、完整代码实现(可直接复制)
1. 路由基础结构(router/index.js)
javascriptimport { createRouter, createWebHistory } from 'vue-router' import useUserStore from '@/store/user' // 固定路由(所有人都能访问,不需要权限) const constantRoutes = [ { path: '/login', name: 'Login', component: () => import('@/views/login/index.vue'), meta: { hidden: true } }, { path: '/404', name: '404', component: () => import('@/views/error/404.vue'), meta: { hidden: true } }, { path: '/', redirect: '/dashboard' // 默认首页 } ] // 需要权限的路由(基础模板,真正的从后端返回) export const asyncRoutes = [ { path: '/dashboard', name: 'Dashboard', component: () => import('@/views/dashboard/index.vue'), meta: { title: '数据概览', icon: 'dashboard' } }, { path: '/data-manage', name: 'DataManage', component: () => import('@/views/data-manage/index.vue'), meta: { title: '数据管理', icon: 'database' } }, { path: '/system', name: 'System', component: () => import('@/views/system/index.vue'), meta: { title: '系统管理', icon: 'setting', roles: ['admin'] } // 仅管理员 } ] const router = createRouter({ history: createWebHistory(), routes: constantRoutes }) // 重置路由(退出登录用) export function resetRouter() { const newRouter = createRouter({ history: createWebHistory(), routes: constantRoutes }) router.matcher = newRouter.matcher } export default router
2. Pinia 状态管理(store/user.js)
负责:存 token、存权限路由、拉取权限、动态添加路由
javascriptimport { defineStore } from 'pinia' import router, { asyncRoutes, resetRouter } from '@/router' import { getAuthRouters } from '@/api/user' // 后端接口:获取权限路由 const useUserStore = defineStore('user', { state: () => ({ token: localStorage.getItem('token') || '', routes: [], // 最终展示的路由(权限过滤后) addRoutes: [] // 动态添加的路由 }), actions: { // 设置 token setToken(token) { this.token = token localStorage.setItem('token', token) }, // 退出登录 logout() { this.token = '' this.routes = [] this.addRoutes = [] localStorage.removeItem('token') resetRouter() }, // 后端返回的权限路由 → 过滤 + 生成可访问路由 async generateRoutes() { // 1. 从后端拉取当前用户权限列表 const res = await getAuthRouters() const permissionRoutes = res.data // 例如:['Dashboard', 'DataManage'] // 2. 过滤出用户有权限的异步路由(需要权限的路由,res.data) const accessedRoutes = filterAsyncRoutes(asyncRoutes, permissionRoutes) // 3. 保存到 store this.addRoutes = accessedRoutes this.routes = constantRoutes.concat(accessedRoutes) // 4. 动态添加到路由系统 accessedRoutes.forEach(route => { router.addRoute(route) }) // 5. 最后加 404 兜底 router.addRoute({ path: '/:pathMatch(.*)*', redirect: '/404', meta: { hidden: true } }) return accessedRoutes } } }) // 工具函数:根据后端权限过滤路由(需要权限的路由,例如:['Dashboard', 'DataManage']) function filterAsyncRoutes(routes, permissionRoutes) { const res = [] routes.forEach(route => { const tmp = { ...route } // { ...单个路由对象 } ✅ 正常浅拷贝 // 如果后端返回的权限包含当前路由 name → 保留(['Dashboard', 'DataManage', 'System']) if (permissionRoutes.includes(tmp.name)) { // 没有子路由的路由:tmp.children → undefined // 有子路由的路由:tmp.children → 递归过滤后的子路由数组 if (tmp.children) { tmp.children = filterAsyncRoutes(tmp.children, permissionRoutes) // 关键:如果子路由过滤后为空,就不保留这个父路由 if (tmp.children.length === 0) return } res.push(tmp) } }) return res } export default useUserStore
3. 全局路由守卫(permission.js)
最核心的鉴权逻辑,判断跳转、token、权限
javascriptimport router from './router' import useUserStore from '@/store/user' // 白名单:不需要登录就能访问 const whiteList = ['/login'] router.beforeEach(async (to, from, next) => { const userStore = useUserStore() const hasToken = userStore.token // 1. 判断有没有 token if (hasToken) { if (to.path === '/login') { // 已登录,访问登录页 → 重定向到首页 next('/dashboard') } else { // 判断是否已经获取过权限路由(动态添加的路由) const hasRoutes = userStore.addRoutes && userStore.addRoutes.length > 0 if (hasRoutes) { next() // 有权限 → 放行 } else { try { // 没拉取权限 → 拉取并动态添加路由 await userStore.generateRoutes() next({ ...to, replace: true }) // 确保路由加载完成 } catch (error) { // 异常:token 过期 / 异常 → 退出重登 await userStore.logout() next(`/login?redirect=${to.path}`) } } } } else { // 2. 无 token if (whiteList.includes(to.path)) { next() // 白名单放行 } else { // 其他页面 → 重定向到登录 next(`/login?redirect=${to.path}`) } } })
4. 入口文件挂载守卫(main.js)
import './permission'; // 关键:挂载路由鉴权守卫
javascriptimport { createApp } from 'vue' import App from './App.vue' import router from './router' import store from './store' // 关键:挂载路由鉴权守卫 import './permission' const app = createApp(App) app.use(store).use(router).mount('#app')
三、后端返回格式示例(你直接按这个给前端)
javascript{ "code": 200, "data": [ "Dashboard", "DataManage" // "System" 管理员才会返回 ] }规则 :返回的是路由的
name字段,前端根据 name 做匹配。
四、最终效果(完全满足你的需求)
- 未登录 / 无 token 访问任何页面 → 自动跳转到
/login- 已登录,但访问无权限页面 自动重定向到
/404- 普通用户只能看到并访问:数据概览、数据管理
- 管理员能看到并访问:数据概览、数据管理、系统管理
- 路由完全动态 没有权限的路由 根本不会挂载,安全性极高
五、这套方案的优势(数据中台必备)
- 安全:无权限路由不挂载,无法通过 URL 强行访问
- 灵活:后端完全控制权限,前端无需改代码
- 标准:企业中台最通用的权限架构
- 可扩展:支持按钮级权限、多级路由、路由守卫
总结
- 核心靠 路由守卫做全局鉴权判断
- 靠 动态添加路由实现权限控制
- 靠 Store统一管理权限与 token
- 无 token → 登录页;无权限 → 404 页
- 权限完全由 后端返回的路由名称列表控制
37、如何实现图表的响应式适配?
一、面试标准回答(直接背)
我们项目中对 ECharts 做了二次封装图表通用组件,统一管理基础配置与自适应逻辑。
- 尺寸自适应 在
mounted挂载完成后,监听浏览器窗口resize事件,统一调用 ECharts 实例的resize()方法 ,实现窗口缩放、浏览器大小变化时图表自动适配容器。- 数据响应更新 通过
watch深度监听图表option配置变化,搭配nextTick确保 DOM 更新完成后,再执行setOption刷新图表,保证数据、样式动态变更实时生效。- 销毁优化 在组件
unmounted生命周期中,移除resize监听、销毁 ECharts 实例,防止内存泄漏、页面卡顿。
二、精简版技术细节(面试官追问用)
- 监听全局
window.resize,节流处理避免频繁触发,提升性能;- 容器采用 百分比 / 弹性布局,不给固定宽高,让图表容器先自适应;
- 深度监听 option,区分静态配置 和 动态业务数据变更,按需刷新;
- 页面销毁时解绑事件 +
dispose销毁实例,优化中台多页面切换性能。
三、Vue3 完整极简代码(和你话术完全对应)
javascript<template> <div class="chart-box" ref="chartRef"></div> </template> <script setup> import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue' import * as echarts from 'echarts' const chartRef = ref(null) let myChart = null // 外部传入的图表配置 const props = defineProps(['option']) // 1. 监听option变化,更新图表 watch(() => props.option, () => { nextTick(() => { myChart?.setOption(props.option) }) }, { deep: true }) // 2. 挂载:初始化图表 + 绑定resize onMounted(() => { myChart = echarts.init(chartRef.value) myChart.setOption(props.option) // 窗口缩放自适应 window.addEventListener('resize', handleResize) }) // 图表resize const handleResize = () => { myChart?.resize() } // 3. 销毁:解绑事件 + 释放实例,防内存泄漏 onUnmounted(() => { window.removeEventListener('resize', handleResize) myChart?.dispose() myChart = null }) </script> <style scoped> .chart-box { width: 100%; height: 100%; } </style>
四、补充加分点(面试官加分项)
- 增加 节流函数 包裹
resize,防止频繁执行造成页面卡顿;- 适配侧边栏收缩 / 展开:侧边栏变化时,手动调用一次
resize;- 多图表页面统一封装全局 resize 管理,避免全局绑定多个监听。
38、你是如何使用Pinia全局管理状态的?
一、目录结构(和你完全一致)
bashsrc/ └── stores/ # 独立存放所有 Pinia 状态 ├── home.ts # 首页模块状态 ├── list.ts # 列表页模块状态 └── common.ts # 全局公共状态(核心)二、核心思想
- 按业务模块拆分:一个页面 / 功能对应一个 store,互不污染
- 全局状态收敛 :所有全局共享数据只放在
common.ts- 统一出口:组件只从对应 store 取数据,不分散管理
三、具体代码实现(可直接复制使用)
1. 公共状态:
stores/common.ts(你说的 token、loading、用户信息)
javascriptimport { defineStore } from 'pinia' // 定义公共 store,id 唯一 export const useCommonStore = defineStore('common', { // 状态:全局共享数据 state: () => ({ token: '', // 登录令牌 userInfo: { // 用户信息 name: '', gender: '', dept: '', jobNumber: '', phone: '' }, loading: false // 全局加载状态 }), // 修改状态的方法(同步) actions: { // 设置 token setToken(token: string) { this.token = token }, // 设置用户信息 setUserInfo(info: any) { this.userInfo = { ...this.userInfo, ...info } }, // 控制 loading setLoading(status: boolean) { this.loading = status }, // 退出登录清空 logout() { this.token = '' this.userInfo = {} as any } }, // 计算属性:简化数据获取 getters: { // 判断是否登录 isLogin: (state) => !!state.token } })2. 业务模块:
stores/home.ts
javascriptimport { defineStore } from 'pinia' export const useHomeStore = defineStore('home', { state: () => ({ bannerList: [], // 首页轮播图 hotList: [] // 首页热门数据 }), actions: { setBanner(list: any[]) { this.bannerList = list } } })3. 列表模块:
stores/list.ts
javascriptimport { defineStore } from 'pinia' export const useListStore = defineStore('list', { state: () => ({ tableData: [], // 列表数据 page: 1, // 页码 total: 0 // 总条数 }), actions: { setTableData(data: any[]) { this.tableData = data } } })
四、在 Vue 组件中使用(最常用方式)
1. 读取 / 修改全局公共状态
javascript<script setup lang="ts"> // 引入公共 store import { useCommonStore } from '@/stores/common' const commonStore = useCommonStore() // 直接使用状态 console.log(commonStore.token) console.log(commonStore.userInfo.name) // 调用方法修改 commonStore.setToken('xxxx-xxxx') commonStore.setLoading(true) </script> <template> <!-- 模板直接使用 --> <div v-if="commonStore.loading">加载中...</div> <div>欢迎:{{ commonStore.userInfo.name }}</div> </template>2. 使用业务模块状态
javascript<script setup> import { useHomeStore } from '@/stores/home' const homeStore = useHomeStore() // 获取首页数据 console.log(homeStore.bannerList) </script>
五、我常用的进阶优化(和你的结构兼容)
1. 状态持久化(刷新页面不丢失 token、用户信息)
安装插件:
bashnpm i pinia-plugin-persistedstate在
common.ts开启持久化:
javascriptexport const useCommonStore = defineStore('common', { // ...其他代码 persist: true // 自动存入 localStorage })2. 解构状态(保持响应式)
javascript// 正确写法:解构后依然响应式 import { storeToRefs } from 'pinia' const { token, userInfo } = storeToRefs(commonStore)3. 统一注册 Pinia(main.ts)
javascriptimport { createPinia } from 'pinia' import piniaPluginPersistedstate from 'pinia-plugin-persistedstate' const pinia = createPinia() pinia.use(piniaPluginPersistedstate) app.use(pinia)
六、和你的思路完全对齐的总结
我就是 严格按照你的方式使用 Pinia:
- 独立
stores目录:统一管理全局状态- 模块化拆分 :
home.ts/list.ts/common.ts- 公共状态收敛 :
token、loading、用户信息全部放在common.ts- 组件按需引入:用哪个模块就引入哪个 store
这套方案 简洁、清晰、易维护,是中大型 Vue3 项目的标准用法!
总结
- 目录结构:独立
stores+ 业务模块拆分,和你的设计完全一致- 公共状态:
common.ts管理token、用户信息、loading等全局数据- 使用方式:组件引入 → 读取 / 修改 → 响应式更新
- 工程化:支持持久化、模块化、可维护性极强
39、如何实现图表数据缓存?
一、先搞懂三种缓存的本质区别
| 方案 | 缓存层级 | 适用场景 | 优缺点 |
|---|---|---|---|
| keep-alive | 组件实例层 | 路由 / 组件切换,不想重复渲染 | 优点:实现简单;缺点:只缓存组件状态,不解决请求重复发、数据过期问题 |
| Pinia | 全局状态层 | 多组件共享的图表数据 | 优点:可控性强;缺点:需要手动写缓存 key、过期逻辑,重复造轮子 |
| Vue Query | 请求 / 数据层 | 服务器状态(图表、列表这类 API 数据) | 优点:内置缓存、过期、后台刷新;缺点:需要学习 API,但代码量最少 |
二、三种方案的具体实现(图表数据场景)
1. keep-alive 实现:组件级缓存(最简单,但不解决请求优化)
适用场景:从列表页跳转到图表页,返回时不想重新加载组件和数据
javascript<!-- App.vue / 路由出口 --> <router-view v-slot="{ Component }"> <keep-alive include="ChartPage"> <component :is="Component" /> </keep-alive> </router-view> <!-- ChartPage.vue --> <script setup> import { onActivated, onDeactivated } from 'vue' import { useChartStore } from '@/stores/chart' const chartStore = useChartStore() // 只有组件首次挂载时请求数据 onMounted(() => { if (!chartStore.chartData) { chartStore.fetchChartData() } }) // 组件被激活时(从缓存恢复)可以做数据刷新判断 onActivated(() => { // 比如超过5分钟再刷新一次数据 if (Date.now() - chartStore.lastFetchTime > 5 * 60 * 1000) { chartStore.fetchChartData() } }) </script>⚠️ 注意:keep-alive 只缓存 组件实例,如果不配合状态管理,每次进入组件还是会重新执行请求。
2. Pinia 实现:手动写缓存逻辑(你说的接口 + key 方式)
适用场景:不想引入新库,自己可控的缓存方案
javascript// stores/chart.ts import { defineStore } from 'pinia' import { fetchChartApi } from '@/api/chart' export const useChartStore = defineStore('chart', { state: () => ({ chartCache: new Map<string, { data: any; expire: number }>(), // key: 接口+参数拼接 cacheTTL: 5 * 60 * 1000, // 5分钟过期 loading: false }), actions: { async getChartData(params: { type: string; timeRange: string }) { // 1. 生成唯一key(接口地址+参数) const cacheKey = `/api/chart?${new URLSearchParams(params).toString()}` // 2. 检查缓存是否有效 const cached = this.chartCache.get(cacheKey) if (cached && Date.now() < cached.expire) { return cached.data // 直接返回缓存,不发请求 } // 3. 缓存无效,发起请求 this.loading = true try { const res = await fetchChartApi(params) // 4. 更新缓存 this.chartCache.set(cacheKey, { data: res.data, expire: Date.now() + this.cacheTTL }) return res.data } finally { this.loading = false } }, // 手动清除缓存(比如用户主动刷新) clearChartCache() { this.chartCache.clear() } } })组件中使用:
javascript<script setup> import { useChartStore } from '@/stores/chart' const chartStore = useChartStore() const loadData = async () => { const data = await chartStore.getChartData({ type: 'sales', timeRange: '7d' }) // 渲染图表 } loadData() </script>
3. Vue Query 实现:开箱即用的服务器状态缓存(最推荐)
适用场景:所有从 API 获取的图表数据,完全解决你提到的所有痛点
第一步:安装
bashnpm install @tanstack/vue-query第二步:全局配置(main.ts)
javascriptimport { VueQueryPlugin } from '@tanstack/vue-query' const queryClient = new QueryClient({ defaultOptions: { queries: { staleTime: 5 * 60 * 1000, // 数据5分钟内为"新鲜",不重复请求 cacheTime: 30 * 60 * 1000, // 缓存保留30分钟 refetchOnWindowFocus: false, // 窗口聚焦时不自动刷新(可选) retry: 1 // 失败重试1次 } } }) app.use(VueQueryPlugin, { queryClient })第三步:图表数据缓存(组件内直接用)
javascript<script setup> import { useQuery } from '@tanstack/vue-query' import { fetchChartApi } from '@/api/chart' // 1. 定义请求函数 const fetchChartData = async (params) => { const res = await fetchChartApi(params) return res.data } // 2. 使用useQuery,自动缓存、过期管理 const { data, isLoading, error } = useQuery({ // queryKey 作为缓存key,自动去重相同请求 queryKey: ['chart', 'sales', { type: 'sales', timeRange: '7d' }], queryFn: () => fetchChartData({ type: 'sales', timeRange: '7d' }), // 可以单独配置这个查询的缓存策略 staleTime: 10 * 60 * 1000, // 这个图表数据10分钟内不刷新 cacheTime: 60 * 60 * 1000 // 缓存保留1小时 }) </script> <template> <div v-if="isLoading">加载中...</div> <div v-if="error">加载失败</div> <ECharts :option="data" /> </template>关键特性说明(解决你说的所有痛点)
- 自动缓存 :相同
queryKey的请求,会直接从缓存拿数据,不发请求- 后台刷新:即使数据显示缓存,也会在后台自动请求更新(stale-while-revalidate 策略)
- 请求去重:短时间内多个组件请求同一个图表,只会发一次请求
- 过期控制 :
staleTime控制数据多久算过期,cacheTime控制缓存多久清理
三、三种方案怎么选?(给你一个决策树)
- 如果只是想简单缓存组件状态,不想处理请求优化 → 用
keep-alive- 如果不想引入新库,想自己掌控缓存逻辑 → 用
Pinia手动实现(但要自己写 key、过期、刷新逻辑)- 如果想彻底解决服务器数据的缓存、同步、过期问题 → 用
Vue Query(最适合你现在的 Vue3 项目)
四、推荐的组合拳(性能最优)
实际项目中可以组合使用:
- keep-alive 缓存图表组件实例(避免重复渲染)
- Vue Query 管理图表的 API 数据缓存(避免重复请求、处理过期)
- (可选)Pinia 只存纯客户端状态(如当前选中的时间范围、图表类型)
40、你是如何实现用户行为埋点的?
封装公共 track () → 路由守卫自动上报 → 按钮 / 图片手动上报
我实现用户行为埋点的完整方案
一、整体思路(和你说的完全一致)
- 封装一个公共工具函数
track()------ 统一处理埋点数据、发送请求- 页面访问埋点 ------ 在 路由守卫 里自动调用
track()- 点击 / 交互埋点 ------ 在按钮、图片、卡片等地方 手动调用
track()- 统一数据格式 ------ 事件名、用户 ID、页面、时间、参数
二、直接给你可复制的完整代码
1. 新建埋点工具:
utils/track.ts
javascript// 埋点公共方法 export function track(eventName: string, params = {}) { try { // 1. 基础公共信息(所有埋点都带) const baseInfo = { event: eventName, // 事件名:page_view / click_btn / click_img userId: localStorage.getItem('token') || '', // 用户ID pagePath: location.pathname,// 当前页面路径 pageTitle: document.title, // 页面标题 timestamp: Date.now(), // 时间戳 userAgent: navigator.userAgent // 设备信息 } // 2. 合并自定义参数 const data = { ...baseInfo, ...params } // 3. 上报埋点(sendBeacon 页面关闭也能上报) if (navigator.sendBeacon) { // 异步上报,不阻塞页面 navigator.sendBeacon('/api/track', JSON.stringify(data)) } else { // 降级方案 fetch('/api/track', { method: 'POST', body: JSON.stringify(data), keepalive: true }) } } catch (err) { console.log('埋点上报失败', err) } }
2. 路由守卫自动上报页面访问(你说的核心)
router/index.ts
javascriptimport { track } from '@/utils/track' router.beforeEach((to, from, next) => { // 页面访问埋点:自动上报 track('page_view', { page: to.path, pageName: to.meta.title || '' }) next() })效果:进入任何页面,自动上报埋点,不用手动写!
3. 按钮 / 图片 / 卡片点击埋点(手动调用)
任意 Vue 组件:
javascript<template> <!-- 按钮点击 --> <button @click="handleClick">提交</button> <!-- 图片点击 --> <img @click="trackImg" src="xxx.png" /> </template> <script setup> import { track } from '@/utils/track' // 按钮点击 const handleClick = () => { track('click_button', { buttonName: '提交', page: '用户中心' }) } // 图片点击 const trackImg = () => { track('click_image', { imageName: '轮播图1', position: '首页顶部' }) } </script>
三、我在企业项目里的埋点规范(你可以直接用)
1. 事件名统一规范
page_view→ 页面访问click_button→ 按钮点击click_image→ 图片点击click_link→ 链接点击search→ 搜索submit_form→ 表单提交stay_time→ 页面停留时长2. 上报方式为什么用 sendBeacon?
- 页面关闭、刷新也能上报
- 不阻塞页面
- 不会丢失埋点(比 fetch /axios 更稳)
3. 埋点数据包含什么?
- 用户 ID
- 事件类型
- 页面路径
- 点击内容 / 位置
- 时间戳
- 设备信息
- 自定义参数
四、总结(和你说的一模一样)
我实现埋点就是 3 步:
- 封装公共 track () 函数 统一上报
- 路由守卫自动上报页面
- 组件里手动触发点击 / 交互埋点
这就是 Vue2/Vue3/React 通用的标准埋点方案。
41、你如何实现自定义图表配置(如颜色、类型)?
答案:提供配置面板 ,用户可修改图表类型(柱状图/折线图)、颜色等,配置保存到后端,下次加载恢复。
我如何实现自定义图表配置(标准回答 + 实现)
核心思路(和你说的完全一致)
- 提供可视化配置面板:让用户选择图表类型(柱状图 / 折线图)、自定义颜色
- 图表类型 :使用 Tab 切换,通过
v-model绑定当前类型- 颜色配置 :使用颜色选择器,双向绑定,通过
setOption实时更新图表- 父子组件通信:配置面板(父)→ 图表组件(子)传递颜色、类型参数
- 配置持久化:将用户自定义配置保存到后端,下次进入页面自动恢复
完整实现逻辑(可直接背、直接用)
1. 数据结构(存储用户图表配置)
javascriptconst chartConfig = reactive({ type: 'bar', // 图表类型 bar / line colors: [ // 自定义颜色组 '#1890ff', '#2fc25b', '#facc14', '#223273' ], title: '自定义图表' })
2. 图表类型 Tab 切换(v-model 绑定)
javascript<el-tabs v-model="chartConfig.type"> <el-tab label="柱状图" name="bar" /> <el-tab label="折线图" name="line" /> </el-tabs>切换时,自动更新图表类型。
3. 颜色配置(双向绑定 + setOption 更新)
javascript<el-color-picker v-for="(color, index) in chartConfig.colors" v-model="chartConfig.colors[index]" @change="updateChart" />颜色改变后,调用
chart.setOption()实时生效。
4. 父子组件传参(配置 → 图表组件)
javascript<!-- 父组件 --> <ChartComponent :type="chartConfig.type" :colors="chartConfig.colors" /> <!-- 子组件接收 props --> const props = defineProps({ type: String, colors: Array })
5. 图表实时更新(核心:setOption)
javascriptfunction updateChart() { chart.setOption({ xAxis: { ... }, yAxis: { ... }, series: [{ type: props.type, // 动态类型 itemStyle: { color: (params) => props.colors[params.dataIndex] // 动态颜色 } }] }) } // 监听配置变化,自动更新图表 watch(() => [props.type, props.colors], updateChart, { deep: true })
6. 保存 + 恢复配置
javascript// 保存到后端 const saveConfig = () => { axios.post('/api/chart/save', chartConfig) } // 加载时从后端恢复 onMounted(async () => { const res = await axios.get('/api/chart/config') Object.assign(chartConfig, res.data) })
最终标准答案(你可以直接复述)
问:您如何实现自定义图表配置(如颜色、类型)?
答:
- 提供可视化配置面板,支持用户切换图表类型(柱状图 / 折线图)、自定义颜色。
- 图表类型通过 Tab 切换 ,使用
v-model双向绑定实现实时切换。- 颜色通过颜色选择器 进行双向绑定,修改后通过 ECharts 的
setOption方法更新图表样式。- 使用 父子组件传参,将颜色、类型等配置传递给图表组件,实现图表实时更新。
- 将用户自定义配置 保存到后端,下次加载页面时自动恢复,保持用户配置持久化。
42、你是如何实现权限控制的菜单渲染?
核心思路(和你说的一模一样)
- 登录成功后 ,从 后端接口获取当前用户的权限菜单列表(路由 + 菜单数据)。
- 前端保存权限菜单到 Pinia 全局状态中。
- 路由守卫中判断权限,动态生成可访问路由。
- 侧边栏 循环渲染 Pinia 中的权限菜单,实现 根据权限动态显示菜单。
完整实现步骤(可直接背)
1. 登录获取权限菜单
用户登录成功后,调用接口获取 后端返回的权限路由 / 菜单数据。
javascript// 登录请求 const res = await loginAPI(data) // 从返回结果中拿到:用户信息 + 权限菜单列表 const { token, menus } = res.data2. 存入 Pinia 全局状态
把后端返回的菜单存入 Pinia,供全局组件使用。
javascript// stores/permission.ts const usePermissionStore = defineStore('permission', { state: () => ({ menuList: [] // 存储后端返回的权限菜单 }), actions: { setMenuList(menus) { this.menuList = menus } } })3. 动态生成路由(路由守卫中处理)
在路由守卫里,根据权限菜单动态添加路由,控制页面访问权限。
javascriptrouter.beforeEach((to, from, next) => { if (token) { // 已登录 if (permissionStore.menuList.length === 0) { // 重新获取菜单 → 动态添加路由 const menus = await getMenuListAPI() permissionStore.setMenuList(menus) // 动态添加路由 menus.forEach(route => { router.addRoute(route) }) next({ ...to, replace: true }) } else { next() } } else { // 未登录,跳转登录 next('/login') } })4. 侧边栏循环渲染菜单
侧边栏组件直接从 Pinia 中读取权限菜单,循环渲染。
javascript<template> <el-menu> <el-menu-item v-for="menu in menuList" :key="menu.path" :index="menu.path" > {{ menu.name }} </el-menu-item> </el-menu> </template> <script setup> const permissionStore = usePermissionStore() const menuList = permissionStore.menuList // 权限菜单 </script>
最终标准答案(面试直接说)
问:你如何实现权限控制的菜单渲染?
答:
- 用户登录成功后 ,从 后端接口获取当前用户的权限菜单列表。
- 将权限菜单 保存到 Pinia 全局状态中统一管理。
- 在 路由守卫 里,根据权限菜单 动态添加可访问路由,控制页面访问权限。
- 侧边栏组件 直接循环渲染 Pinia 中的权限菜单 ,实现 根据用户权限动态显示菜单。
43、您如何处理浏览器缓存导致数据陈旧?
答案:请求头添加 Cache-Control: no-cache, 或者使用时间戳参数。用户手动刷新时强制请求新数据。
标准答案(面试直接背)
我会通过 三种方式处理浏览器缓存导致的数据陈旧问题:
- 在请求头中添加
Cache-Control: no-cache,强制浏览器不使用缓存,每次都请求最新数据。- 在请求 URL 后拼接时间戳参数 (
?t=时间戳),让每次请求地址都不一样,绕过浏览器缓存。- 用户手动刷新页面时,强制重新请求最新数据,确保数据最新。
这些方案通常会 统一封装在 axios 的请求拦截器里,全局生效,不用每个接口单独处理。
完整实现(axios 二次封装里直接用)
1. 请求头加缓存控制(最标准)
config.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate'
javascript// axios 封装文件 import axios from 'axios' const service = axios.create({ baseURL: '/api' }) // 请求拦截器 service.interceptors.request.use(config => { // 👇 核心:禁用浏览器缓存 config.headers['Cache-Control'] = 'no-cache, no-store, must-revalidate' config.headers['Pragma'] = 'no-cache' // 兼容旧浏览器 return config }) export default service2. URL 加时间戳(你说的方案)
javascriptservice.interceptors.request.use(config => { // 👇 拼接时间戳,每次请求唯一,避免缓存 if (config.method === 'get') { config.params = { ...config.params, _t: Date.now() // 时间戳 } } return config })
最终一句话总结(最精炼)
问:您如何处理浏览器缓存导致数据陈旧?
答:
我会在 二次封装的 axios 请求拦截器 中,统一添加 请求头
Cache-Control: no-cache,或者给请求 URL 拼接时间戳参数 ,强制浏览器每次都获取最新数据,避免缓存导致数据陈旧;同时支持用户 手动刷新时强制拉取新数据。
44、您如何处理图表在 v-show 隐藏后尺寸错乱?
答案:使用 ResizeObserver 监听容器尺寸变化,或者使用 nextTick 在显示后调用 resize。
标准答案(面试直接说)
当图表容器使用
v-show隐藏后再显示时,容器宽高会变为 0,导致 ECharts 无法正确计算尺寸,出现错乱。解决方案是:在容器显示后,使用 nextTick 等待 DOM 更新,然后调用 chart.resize () 重新计算尺寸;也可以使用 ResizeObserver 监听容器变化自动修复。
正确实现代码(最稳、最常用)
javascript<script setup> import { ref, nextTick } from 'vue' import * as echarts from 'echarts' const chartRef = ref(null) const showChart = ref(true) let chart = null // 初始化图表 onMounted(() => { chart = echarts.init(chartRef.value) chart.setOption(option) }) // 切换显示/隐藏后 → 修复尺寸 const toggleShow = async () => { showChart.value = !showChart.value if (showChart.value) { // 👇 关键:DOM 显示后,nextTick 再执行 resize await nextTick() chart.resize() } } </script> <template> <div v-show="showChart" ref="chartRef" style="width:100%;height:300px"></div> <button @click="toggleShow">切换显示</button> </template>
你说的 clear () 为什么不是最优解?
你说的:隐藏时调用 echarts.clear ()
这个方法 能解决错乱,但不是最佳实践,原因:
clear()是 清空图表内容,不是修复尺寸- 再次显示需要 重新 setOption,浪费性能
- 只是 "清空",不是 "自适应"
**真正修复尺寸的是:resize ()**它才是让图表重新读取容器宽高的方法。
最标准、最专业的答案(背这个)
问:您如何处理图表在 v-show 隐藏后尺寸错乱?
答:
v-show 隐藏时图表容器宽高会变为 0,导致 ECharts 尺寸计算异常。我会在 图表显示后,使用 nextTick 等待 DOM 渲染完成,再调用 chart.resize () 让图表重新计算宽高,从而恢复正常尺寸。也可以使用 ResizeObserver 监听容器尺寸变化,自动触发 resize。
总结
- 你的方向 完全正确:知道隐藏显示会出问题,需要主动处理
- 正确 API 不是
clear(),而是 resize()- 标准方案:nextTick + chart.resize()
简历完善调整:













