Skip to content

前端工程师-曹林超 ​

# 面试口述稿 · 前端工程师

适用岗位:Web 前端工程师 / 高级前端(中后台 + 跨端 + 可视化方向) 用法:先背「自我介绍」和「项目主线」,再按项目准备追问预案。技术八股见 前端笔记 里的 JavaScript / Vue / 浏览器相关章节。


一、自我介绍(60-90 秒,背熟) ​

面试官你好,我是曹林超,5 年前端开发经验,现在在一家做新能源充电的公司做前端负责人。

我是 2021 年入行的,早期在中控文旅做 ERP 后台、H5 和微信小程序,组件化和工程化的底子是在那段时间打下来的。2023 年去了北京聚点互动做医疗方向,主要是 React 18 中后台加 Taro 跨端小程序,还做过在线问诊的聊天模块,那块用虚拟滚动加 SSE 做了万级消息渲染和流式输出。

2024 年 5 月入职现在这家公司做充电业务,前端这块整体是我负责的:C 端多端充电应用、运营 SaaS 后台、商户端小程序,还有一个 Three.js 的 3D 运营大屏。技术上主要做了三件事:一是把存量的原生小程序渐进式重构成 uni-app,一套代码覆盖微信、支付宝和 APP;二是搭 SaaS 后台的复杂表格、动态表单和 RBAC 权限体系;三是 3D 大屏的性能优化,保证它能长时间稳定运行不卡顿。

我比较擅长中后台和可视化这两块,React 和 Vue 两条线都熟,比较习惯从需求评审、技术方案一直推进到上线。

口述要点:一句话定位「中后台 + 跨端 + 可视化」三条线,然后用时间顺序串起三段经历。


二、职业主线 ​

Q:这几年技术栈是怎么变化的? ​

分三个阶段:第一阶段做 ERP 和商城,主要是 Vue2 + uni-app,偏业务组件沉淀;第二阶段做医疗中后台,转到 React 18 + TypeScript,开始接触数据看板、虚拟滚动、SSE 这些偏性能的场景;第三阶段就是现在,Vue3 + React 双栈并行,SaaS 后台用 Vue3 + Ant Design Vue,3D 大屏用 React 18 + Three.js。

Q:Vue 和 React 你更熟哪个?两个都用的原因是什么? ​

两个都在生产项目里用过,不存在只写过 demo 的情况。选型主要看场景:SaaS 后台和跨端用 Vue 系,因为 Element/Ant Design Vue 的表格和表单生态更成熟,团队上手也快;大屏用 React,主要是 Three.js 和 D3.js 在 React 生态里的封装例子多,我自己也更喜欢 Hooks 里做副作用管理。跨端小程序两边都用过,uni-app 是 Vue 系,Taro 是 React 系。


三、项目一:充电跨端应用(uni-app 重构)|2024.05 — 至今 ​

Q:为什么要把原生小程序重构成 uni-app?(必问) ​

核心是成本问题。我们最初只有一个原生微信小程序,后来要接支付宝小程序、还要出 APP,如果继续维护原生,等于三套代码各写一遍,业务迭代要同步三次,长期一定崩。

uni-app 是当时权衡下来最合适的:一是 Vue 语法团队本来就熟,迁移成本低;二是能同时编译到小程序、APP、H5;三是有成熟的条件编译机制,各端差异不用靠 if-else 硬写。

但存量代码已经有大量业务逻辑,所以没推倒重来,做的是渐进式重构。

Q:渐进式重构具体怎么落地的? ​

三步走:

第一步是抽公共层,先把请求拦截、登录授权、定位、扫码、支付这些跨端高频能力抽成独立模块,保证新老代码都能调;

第二步是按页面迁移,一个页面一个页面迁,迁完一个验收一个,没迁完的老页面继续留在原生里跑;

第三步是收口切换,等页面基本迁完、验证稳定之后再统一切到 uni-app 构建发布。

关键是整个过程业务需求是并行迭代的,不能让重构把业务停掉,所以每一步都要保证线上可用。

Q:各端差异怎么抹平的? ​

分三种处理方式:

  • 能力差异:统一封装一层,比如登录授权,微信是 wx.login、支付宝是 my.getAuthCode,内部用条件编译区分,对外暴露同一个方法;
  • 样式差异:主要是单位和小程序特有的样式限制,统一转 rpx、把不支持的 CSS 特性做降级;
  • 生命周期差异:小程序页面生命周期和 APP 不一样,我们在公共 composable 里做了统一抽象,业务页面只写一份逻辑。

原则是「差异收敛在基础层,业务层不感知平台」。

Q:首屏和弱网怎么优化的? ​

主要是三块:

  • 分包加载:把商城这类低频模块拆到分包,主包只留扫码充电的主链路,主包体积降下来首屏就快;
  • 页面懒加载 + 按需引入:组件不一次性全注册,用到才加载;
  • 接口层优化:高频页面的请求做了防抖和缓存,弱网下减少无效请求,同时给扫码链路加了兜底的加载态和重试。

扫码充电是我们最高频的入口,用户很多时候是在地下车库信号很差的环境下用,所以这条链路是重点优化的对象。

Q:支付这块踩过什么坑? ​

主要是回调时序和异常兜底。支付的坑在于前端拿到支付成功不等于订单已到账,渠道的回调是异步的,可能会晚于前端返回。

所以我们的做法是:前端支付完成后不直接改本地状态,而是去轮询查订单真实状态,查到了再刷新 UI;同时后端有回调兜底,超时未支付会关单。另外提现这块做了异常兜底,比如渠道返回处理中,前端展示「处理中」而不是直接报失败,避免用户重复提交。

Q:多端打包、审核、发布有什么要注意的? ​

小程序审核有几个固定的坑:类目和隐私协议要一次配好、涉及支付的必须有对应资质、预发布环境要单独配域名白名单。我们的做法是把各端的构建配置和发布流程写成文档,出一版走一遍清单,避免每次都漏。版本这块做了灰度发布能力,出问题可以回退。


四、项目二:运营 SaaS 管理后台|2024.05 — 至今 ​

Q:项目整体是怎么搭的? ​

基于 Vue3 + TypeScript + Ant Design Vue + Vite + Pinia。工程上用 Turbo Monorepo 管理,因为后台、商户端、公共组件库要共享代码,Monorepo 能让公共组件和工具函数只维护一份。图表用 ECharts,地图用高德。

Q:复杂表格怎么做的?(高频) ​

后台的订单、财务这些列表,字段多、数据量大、还要求列定制和导出,所以选了 VxeTable 做底座。做了几层封装:

  • 列配置化:把列定义抽成配置数组,支持动态显隐、列宽记忆、排序筛选,用户自己配的列偏好会持久化;
  • 大数据量处理:走服务端分页 + 虚拟滚动,不做一次性全量渲染,否则几千行直接卡死;
  • 导出:大数据量导出走异步任务,后端生成文件后前端拿下载地址,避免前端一次性把几十万行拉到内存里;
  • 单元格自定义渲染:状态、金额、操作列这些用插槽做业务化的展示。

这样业务侧接入一个新列表,只需要写列配置,不用重复写表格逻辑。

Q:动态表单怎么设计的? ​

后台有大量结构类似的表单,每个都手写一遍成本很高。所以做了一套配置化的表单方案:

用配置描述字段的类型、校验规则、联动条件,渲染层根据配置动态生成。支持多组件类型(输入、选择、日期、上传、级联)、联动校验(A 字段变了 B 字段的规则要跟着变)、动态显隐(比如选了某种结算方式才显示某个字段)。

效果是常规表单的开发从「写一个页面」变成「写一份配置」。

Q:RBAC 权限前端怎么做? ​

前后端一体的方案。前端拿到菜单树之后做三级控制:

  • 路由级:没有权限的路由不注册,用户访问不到,动态路由在拿到权限数据之后 addRoute 挂载;
  • 按钮级:封装自定义指令,按权限点控制按钮的显示;
  • 数据级:比如场站合伙人只能看自己场站的订单,这块是后端在查询时拼数据范围,前端不做过滤。

这里有个原则我会强调:权限一定在后端兜底,前端做权限只是体验优化,不能当作安全边界。

Q:看板图表怎么做的? ​

用 ECharts 做经营数据、订单趋势、场站运营这些看板。做了两件事:一是把常用图表封装成统一组件,只传数据和配置,不用每个页面重新写 option;二是图表实例做了统一管理和销毁,组件卸载时 dispose,避免页面反复切换导致 ECharts 实例泄漏、内存越来越高。


五、项目三:3D 运营可视化大屏|2025.04 — 至今 ​

Q:为什么要用 Three.js,ECharts 不够用吗? ​

需求是全国场站地图 + 飞线动画 + 热力 + 视角切换。ECharts 能做 2D 地图和简单飞线,但要做带纵深的 3D 地图、区域挤出和相机视角动画就吃力了。所以地图主体用 Three.js + D3.js 的地理数据来做,指标图表还是 ECharts,两者联动。

Q:3D 性能优化具体做了什么?(重点,能体现深度) ​

大屏最怕的是跑久了越来越卡,所以优化分「渲染量」和「渲染频率」两条线:

减少渲染量:

  • 几何体合并:几千个场站如果每个都是一个独立 mesh,draw call 会爆炸。做法是把相同材质的几何体合并成一个 BufferGeometry,用实例化/合批把 draw call 从几千降到个位数;
  • 视锥体剔除:相机外的对象不参与渲染;
  • 按需渲染:不是无限 requestAnimationFrame 空转,静态场景只在数据变化或交互时渲染,动画结束后就停。

控制资源:

  • 纹理和几何体在场景切换时做 dispose,不然显存只增不减;
  • 图表和 3D 的场景对象都做了统一的生命周期管理。

另外大屏是 7×24 挂着跑的,所以还加了异常兜底,WebGL 上下文丢失这类情况能自动恢复,不能白屏。

Q:地图和图表怎么联动? ​

用一个中心化的状态层管当前选中的区域(省/市),地图点击区域之后更新状态,图表监听到状态变化就带着区域参数重新请求数据。反过来图表上的条件变化也一样。这样避免了组件之间互相直接调用,联动逻辑好维护。

Q:大屏的动画怎么做的? ​

用 GSAP 做场景入场、地图视角切换、数字滚动这些。选 GSAP 是因为它的时间轴能力比手写 CSS 动画强,多个动画要按顺序编排的时候,用 timeline 排会比一堆 setTimeout 清晰得多,而且它底层也是走的 requestAnimationFrame,性能上没问题。


六、项目四:运营及商户管理平台|2024.08 — 2026.01 ​

Q:从 0 到 1 搭建怎么选型的? ​

面向的是公司运营人员和场站合伙人两个角色,一个是后台、一个是小程序端,所以选了 Vue2/Vue3 + Element UI/Element Plus 做主后台,商户端助手用 uni-app。这样后端接口一套、公共业务逻辑能复用,多端发布也不用重复开发。

Q:你提到排查过重复渲染和冗余请求,具体是什么问题? ​

两个典型问题:

一是组件重复渲染:有个经营看板页面,父组件每次轮询更新数据,把整个大对象重新赋值,导致下面所有子组件全部重渲染,虽然子组件的数据没变。解决方式是拆分响应式数据粒度,把子组件不依赖的字段拆出去,子组件用 props 精确依赖需要的字段,另外对纯展示组件做了缓存。

二是冗余请求:多个组件在同一个页面各自请求了同一份基础数据(比如场站列表),同一份数据被请求了四五次。解决方式是把这个数据提到公共状态里做一次请求 + 缓存,其他组件从状态里取。

这类问题单看代码不容易发现,都是靠 Performance 面板和 Network 面板看出来的。


七、医疗系列项目(React 18)|2023.01 — 2024.05 ​

Q:登录态持久化和动态路由权限怎么做的? ​

医疗管理平台用的是 React 18 + Zustand + React Router 6。

登录态用 Zustand 存 token 和用户信息,配合持久化中间件存到 localStorage,刷新页面不会掉登录。动态路由的做法是:路由表分静态和动态两部分,静态的放登录页、404 这些公共路由,动态路由根据后端返回的权限在后端数据回来之后动态生成并挂载,同时用路由守卫拦截未授权的访问。

Q:看板里的请求竞态问题怎么解决的? ​

这个是医疗数据看板的典型问题:页面上有多个筛选条件,用户快速切换的时候,先发的请求可能后返回,把新数据覆盖掉了。

解决方式三个层面:

  • 筛选变化防抖,减少无效请求;
  • 请求取消,新请求发出时取消上一次未完成的请求;
  • 标记校验,给每个请求打上当前筛选条件的标记,回来的时候比对,不是最新条件的响应直接丢弃。

另外多个独立维度的数据用 Promise.all 并行请求,而不是串行等,首屏能快不少。

Q:聊天模块的万级消息怎么渲染的? ​

用 @tanstack/react-virtual 做虚拟滚动。核心思路是只渲染视口内的 DOM,列表多长对 DOM 节点数都没影响——只有可视区域加少量缓冲区的元素真实存在,滚动的时候动态计算位置并复用节点。

主要处理了三个问题:一是动态高度,消息长度不一样,需要用测量机制拿到真实高度并缓存;二是滚动锚定,新消息来的时候如果用户在看历史消息,不能强行拉到底部,要提示「有新消息」;三是底部定位,初始化时要精确定位到最后一条。

Q:为什么用 SSE 不用 WebSocket? ​

场景是问诊时 AI/医生回复的流式输出,是单向的——服务端推、客户端收就够了,用 WebSocket 属于杀鸡用牛刀。

SSE 的优势是:基于 HTTP 协议,走标准请求,不用额外的协议升级和心跳维护,服务端实现也简单,而且浏览器自带断线重连。WebSocket 更适合双向高频通信的场景,比如实时协作、游戏。这个场景 SSE 是更合适的选择。

补充一句:SSE 本身不支持自定义请求头,所以鉴权信息我们是放在 URL 参数或者 cookie 里带过去的,这也是选型时要考虑的点。

Q:Taro 多端是怎么做的? ​

家小医那个项目要覆盖微信、支付宝、百度、头条四个平台。做法和 uni-app 类似:

  • 用条件编译处理平台差异,process.env.TARO_ENV 判断当前平台,各端不同的 API 和组件在编译期就分开了,不会打进同一个包;
  • 状态用 Zustand 统一管理登录态、用户设置这类全局数据,保证跨页面一致;
  • 样式统一用 Tailwind + weapp-tailwindcss,把小程序不支持的 CSS 转成兼容写法,避免每个平台各写一套。

沉淀下来的是跨端组件和编码规范,后面新页面直接复用。


八、高频技术追问 ​

Q:Vue2 和 Vue3 的响应式区别? ​

Vue2 用的是 Object.defineProperty,问题是没法监听数组下标和对象新增属性,得用 $set 这种 hack,而且初始化时要递归遍历所有属性,数据量大时性能差。

Vue3 换成 Proxy,能监听整个对象包括新增删除和数组操作,而且配合 effect 是懒执行的,只有真正访问到的属性才会被追踪,初始化开销小很多。

另外 Vue3 通过编译时优化(静态提升、Patch Flag)减少了 diff 的开销。我在项目里从 Vue2 迁 Vue3 的时候,最直观的收益就是不再需要到处写 $set 了。

Q:你的前端性能优化体系是怎样的? ​

我会分四个层面讲:

  • 加载:资源拆包、路由懒加载、组件按需引入、图片压缩和 CDN、关键资源预加载;
  • 渲染:减少 DOM 数量(虚拟滚动)、减少重排重绘、合并 draw call(3D 场景)、缓存纯展示组件;
  • 请求:防抖节流、请求取消、并发请求、缓存复用(SWR 那类思路)、减少冗余请求;
  • 运行时:内存泄漏排查(事件监听、定时器、ECharts 实例的销毁),长时间运行场景的资源回收。

优化一定要先测量再动手,用 Performance 和 Network 面板定位瓶颈,不能凭感觉。

Q:Monorepo 为什么用 Turbo? ​

因为后台、商户端、组件库要共享代码。传统做法是组件库单独发包,改一个组件要发版、升级、重新构建,链路太长。Monorepo 让公共包以源码形式直接引用,改完立即生效。

Turbo 的作用是提供任务编排和缓存,比如能识别哪些包变了只构建受影响的包,构建结果做缓存,CI 时间能明显降下来。

Q:前端安全你会关注什么? ​

主要是 XSS 和 CSRF。XSS 上,用户输入的内容一律转义,富文本/Markdown 渲染用白名单过滤,不用 innerHTML 直接插;CSRF 上,状态变更的请求用 POST,token 放在请求头而不是 cookie 里,配合后端的校验。

另外我们的权限体系虽然前端做了菜单和按钮控制,但我一直强调那只是体验优化,真正的权限校验必须在后端。


九、容易被问倒的点 & 应对 ​

1. 「你没在大厂待过,技术深度够吗?」

直接接:大厂和小公司在技术问题上没有本质区别,我们的场景里同样有高并发、性能优化、工程化治理的诉求,比如 3D 大屏的性能瓶颈、订阅消息和海量列表渲染。可以现场挑一个深聊。

2. 「3D 这块你是一直在做吗?还是只做过一个项目?」

坦诚说:Three.js 是项目驱动学的,从 0 做了这个大屏,中间踩了很多性能的坑。不能说自己是图形学专家,但业务场景下从建模数据接入、场景搭建到性能优化的完整链路我是走通的。

3. 「跨端框架你更推荐 uni-app 还是 Taro?」

看团队技术栈:Vue 系团队选 uni-app,React 系选 Taro。功能上都能满足多端需求,差异主要在生态和团队学习成本。我们两个都用过,结论是「没有最好,只有和团队最匹配」。

4. 「重构期间怎么保证不出线上事故?」

三个保障:一是分页面迁移,每迁一个验收一个;二是新老代码并存期做好兼容,公共层先抽,保证两边都能调;三是上线走灰度,有回滚预案。

5. 「为什么看机会?」

准备一个正向理由:希望在技术挑战更大的环境里,做更复杂的前端系统。


十、反问面试官(准备 3 个) ​

  1. 团队前端现在有几个人?技术栈是统一的还是各个业务线各自选型?
  2. 这个岗位主要负责哪块业务?是偏中后台、C 端还是可视化?
  3. 团队对前端工程化和性能这块有明确的要求或规划吗?

十一、表达纪律(自查清单) ​

  • [ ] 说「我主导 / 我负责」,不说「参与」,也不把团队成果全揽
  • [ ] 讲优化先讲「怎么测量出来的瓶颈」,再讲方案,最后讲收益
  • [ ] 跨端重构强调「渐进式 + 不影响业务迭代」,不说「推倒重来」
  • [ ] 权限话题一定补一句「前端只是体验优化,安全在后端兜底」
  • [ ] 3D / 图形学相关不装专家,说清「项目驱动、业务场景走通了」
  • [ ] 不确定的细节坦承「这块记不太清了」,不硬编

十二、延伸面试题(按简历技能点拆解) ​

简历上每一项技能都可能被单独拎出来问。下面按模块把「可能问什么」列全,答案只写结合项目的口径,通用八股见 前端笔记 里的 JavaScript / Vue / 浏览器相关章节。 后端技能点的延伸题见 全栈开发工程师口述稿。

JS / TS ​

1. ES6+ 你常用哪些特性?

箭头函数与 this、解构、可选链 ?. / 空值合并 ??、模板字符串、模块化、Promise / async-await、扩展运算符、Map/Set、Proxy/Reflect。 项目里 Proxy 用得最多的地方是 Vue3 的响应式和做数据代理层。

2. 事件循环(宏任务/微任务)怎么理解?

同步代码先跑完;然后清空微任务队列(Promise.then、queueMicrotask、MutationObserver);再取一个宏任务(setTimeout、setInterval、IO、UI 渲染)执行,执行完又清空微任务,如此循环。 实际意义:想让 UI 先更新再做重计算,用 setTimeout 或 requestAnimationFrame;await 后面相当于 then,是微任务。

3. 手写题会考什么?

防抖节流、深拷贝(处理循环引用和特殊类型)、Promise.all / 并发控制、柯里化、数组扁平化、EventEmitter。 这类题的重点不是背答案,而是说清边界:比如深拷贝要处理 Date/RegExp/Map/Set 和循环引用,Promise.all 要处理 reject 和空数组。

4. TS 泛型怎么用?什么是类型收窄?

泛型是把类型当参数传,比如统一封装请求 request<T>(url): Promise<T>,调用方 request<OrderList>(...) 就能拿到类型提示。 类型收窄是 TS 通过条件判断(typeof、in、instanceof、可辨识联合)把宽类型缩小到具体类型。项目里接口返回的联合类型、组件的 props 都靠它。

5. interface 和 type 的区别?

interface 能被继承和合并声明,适合描述对象结构;type 更灵活,能定义联合、交叉、元组、条件类型,但不能重复声明。 我们的习惯:对象结构用 interface,联合类型和工具类型用 type。

Vue ​

6. Vue2 和 Vue3 响应式原理区别?

Vue2 用 Object.defineProperty,不能监听数组下标和新增属性,要 $set 兜底,且初始化时递归遍历所有属性,数据量大时慢。 Vue3 用 Proxy,能监听整个对象含新增删除和数组操作,配合 effect 是懒执行的,只有访问到的属性才被追踪,初始化开销小。

7. Vue3 的编译优化做了什么?

静态提升(把静态节点提到 render 外只创建一次)、Patch Flag(标记动态绑定类型,diff 时只比较会变的部分)、Block Tree(用动态节点集合跳过静态子树)、事件缓存。 结果就是 diff 的量级从「整棵树」降到「动态节点」。

8. ref 和 reactive 怎么选?

ref 什么类型都能装,模板里自动解包、脚本里要 .value;reactive 只能接对象,且解构会丢响应式。 我们的约定是统一用 ref,避免 reactive 解构踩坑。

9. 组合式 API 怎么组织复用逻辑?和 mixin 比优势在哪?

把相关逻辑写成一个 useXxx 函数返回 ref 和方法,按功能而不是按选项聚合。 相比 mixin:来源清晰(变量从哪来的能直接看到)、不会命名冲突、类型推导友好、可以传参。我们跨端登录、分页列表、表格配置都抽成了 composable。

10. Pinia 和 Vuex 的区别?

Pinia 去掉了 mutation,直接改 state;对 TS 支持更好(不需要额外类型声明);没有嵌套模块,每个 store 独立且能互相引用;体积也更小。新项目我们直接用 Pinia。

11. 路由权限怎么做?

路由分静态(登录、404)和动态两部分。登录后拿到后端权限数据,动态路由通过 router.addRoute 挂载,同时在全局前置守卫里做拦截:没登录跳登录页,没权限跳 403。菜单树也是同一份权限数据渲染出来的。

React ​

12. Hooks 的原理是什么?为什么不能在条件语句里用?

Hooks 在 Fiber 节点上以链表形式按调用顺序存储,靠调用顺序匹配状态。 如果在条件里调用,顺序变了,后面的 hook 就会读到别人的状态。所以规则是「只在顶层调用」。useState 返回的 setState 之所以稳定,也是因为它存在这个链表节点上。

13. useEffect 的闭包陷阱是什么?

effect 捕获的是当次渲染的作用域,如果依赖数组写漏了,就会一直读到旧的值,比如定时器里读到的永远是初始 state。 解法:依赖写全、用 useRef 存最新值、或者把逻辑放进 setState 的函数式更新里。清理函数也必须有——定时器、事件监听、ECharts 实例都要在 cleanup 里销毁,否则就是内存泄漏。

14. useMemo / useCallback / React.memo 什么时候用?

缓存计算结果、缓存函数引用、缓存组件渲染。但不要无脑加,缓存本身也有比较成本,加多了反而更慢。 我的判断标准是:这个值确实会传给子组件或作为其他 hook 的依赖,且计算/渲染成本可观,才包。

15. React 18 有哪些新特性?

并发特性:useTransition / useDeferredValue 把不紧急的更新降优先级;自动批处理(现在 setTimeout、Promise 里也会批处理了);Suspense 配合懒加载;useId;流式 SSR。 我们在 3D 大屏里用 useTransition 处理切换区域时的大量图表更新,避免阻塞地图交互。

16. Zustand 和 Redux 怎么选?

Zustand 没有 Provider 包裹、模板代码极少、按选择器订阅所以重渲染范围小,对中小型项目明显更轻。Redux 的优势是生态(DevTools、中间件)和严格的单向数据流约束,大团队协作规范上更强。 我们医疗项目选 Zustand,主要就是不想写 action/reducer 那套样板。

17. React Router 6 动态路由 + 路由守卫怎么做?

路由表用 useRoutes 或 createBrowserRouter 配置。守卫没有 Vue 那种全局钩子,通常封装一个 <AuthRoute> 组件包在需要鉴权的路由外层,内部判断登录态和权限,不满足就 <Navigate> 重定向。

跨端 / 小程序 ​

18. uni-app 的条件编译原理?

编译期处理:#ifdef MP-WEIXIN ... #endif 这类注释语法在构建时被解析,只有当前平台的代码会被编进产物,其他平台的代码在 AST 层面就被剔除了,不会进包。 所以我们把平台差异都收敛在基础层,业务代码不感知。

19. Taro 的编译原理?和 uni-app 的区别?

Taro 3 之后走的是运行时方案:用 React/Vue 的运行时 + 一套 DOM/BOM 的模拟层(Taro runtime),把组件的增删改映射成小程序的 setData,所以能用完整的 React 语法和生态。 区别:uni-app 更偏编译时 + Vue 生态,Taro 3 偏运行时 + React 生态。选型主要看团队栈。

20. 小程序分包怎么做?为什么要分包?

微信主包体积有 2M 限制,超过就发布不了。做法是把低频模块(比如商城、活动)拆到分包,主包只留核心链路。 配套优化:分包预下载(进入某页面时提前下载关联分包)、按需注入、组件和图片资源懒加载。我们把扫码充电放主包,其它拆走,首屏明显变快。

21. 小程序登录流程?

wx.login 拿临时 code → 传给后端 → 后端用 code + appid + secret 调 code2session 换 openid / session_key → 后端签发自己的登录态给前端。 关键点:session_key 不能下发到前端;前端只存自己的 token。支付宝是 my.getAuthCode,流程类似。

22. 微信支付完整流程?

前端调后端下单接口 → 后端调微信统一下单拿 prepay_id 并返回签名参数 → 前端 wx.requestPayment 拉起收银台 → 用户支付 → 微信异步通知后端 → 后端验签、改订单状态 → 前端轮询查订单真实状态刷新 UI。 重点:前端拿到支付成功不等于订单已支付,异步回调可能晚到,所以必须以后端订单状态为准。

23. 渐进式重构怎么落地?

三步:先抽公共层(请求、登录、支付、定位这些跨端能力)让新老代码都能调;再按页面逐个迁移、迁一个验收一个;最后统一切构建发布。整个过程业务需求是并行迭代的,所以每一步都要保证线上可用,上线走灰度 + 有回滚预案。

工程化 ​

24. Vite 为什么快?和 Webpack 的区别?

开发阶段:Vite 不做打包,直接利用浏览器原生 ESM 按需请求模块,启动只做依赖预构建(用 esbuild,Go 写的,比 JS 快一个量级),所以冷启动基本是秒级。Webpack 要先打包完整个依赖图才能起服务。 生产阶段:Vite 用 Rollup 打包,产物质量不差。Webpack 的优势是 loader/plugin 生态更全,处理老项目和非标准资源更成熟。

25. HMR 的原理?

Vite 的 HMR 基于 ESM:文件变更后,服务端通过 WebSocket 通知浏览器,浏览器只重新请求那个变更的模块,并沿着 import 链向上查找,找到接受更新的边界模块(import.meta.hot.accept)就地替换,不需要刷新页面。 所以 Vite 的 HMR 是模块级的,项目越大优势越明显。

26. Webpack 构建优化做过哪些?

时间维度:cache 持久化缓存、thread-loader / esbuild-loader 提速、resolve.alias 和 extensions 减少查找。 体积维度:splitChunks 拆包、tree-shaking + sideEffects、compression-webpack-plugin 开 gzip/brotli、图片压缩、路由懒加载、externals 把大依赖走 CDN。

27. Tree-shaking 的原理?什么情况会失效?

基于 ESM 的静态结构做死代码消除:import/export 在编译期就能确定,打包器能分析出哪些导出没被用到。 失效场景:用了 CommonJS(require 是动态的)、有副作用(顶层执行代码、修改全局)且没标 sideEffects: false、通过对象属性间接访问导出、Babel 把 ESM 转成了 CJS。

28. Monorepo 为什么用 Turbo?

后台、商户端、组件库要共享代码。传统「组件库单独发包」链路太长(改一个组件要发版、升级、重新构建)。 Monorepo 让公共包以源码直接引用,改完立即生效。Turbo 提供任务编排和缓存:能识别哪些包变了只构建受影响的包,构建结果按输入哈希缓存,CI 时间明显下降。

29. 你的前端性能优化体系?

分四层:

  • 加载:路由懒加载、按需引入、拆包、图片压缩 + CDN、关键资源预加载;
  • 渲染:减少 DOM 数量(虚拟滚动)、减少重排重绘、合并 draw call(3D)、纯展示组件缓存;
  • 请求:防抖节流、请求取消、并发请求、缓存复用、消除冗余请求;
  • 运行时:内存泄漏排查(监听器、定时器、ECharts 实例、WebGL 资源)。

原则是先测量再优化:用 Performance / Network 面板定位瓶颈,再动手,不做凭感觉的优化。

可视化 ​

30. ECharts 大数据量怎么优化?

数据层面:服务端聚合,别把几十万点直接丢给前端;必须前端渲染时开 sampling(降采样)、large: true(大数据量模式)、关掉动画和 symbol。 资源层面:setOption 用 notMerge 只更新变化的部分而不是整体重建;组件卸载和页面切换时一定 dispose,否则实例泄漏、内存越来越高。

31. Three.js 3D 大屏的性能优化做了什么?(简历重点)

分「渲染量」和「渲染频率」:

  • 减少渲染量:几何体合并(几千个场站如果每个一个 mesh,draw call 爆炸,合并成 BufferGeometry 后 draw call 从几千降到个位数)、视锥体剔除、按需渲染;
  • 控制频率:静态场景不跑无限 requestAnimationFrame,只在数据变化或交互时渲染,动画结束就停;
  • 资源管理:场景切换时 dispose 几何体、材质、纹理,否则显存只增不减。

大屏是 7×24 挂着跑的,所以还加了 WebGL 上下文丢失的恢复兜底,不能白屏。

32. D3.js 在项目里做什么?

我们不拿 D3 画图,主要用它做地理数据处理:把 GeoJSON 投影、三角剖分转成 Three.js 能用的顶点数据,还用它做过色阶映射和数值比例尺。画图还是 ECharts。

33. 大屏长时间运行怎么保证不卡不崩?

三点:一是资源回收(ECharts 实例、WebGL 几何体/纹理、事件监听都在生命周期里销毁);二是渲染按需触发,不要空转;三是加异常兜底和内存监控,WebGL 上下文丢失、接口异常都要能自动恢复。上线前会挂着跑一两天观察内存曲线。

交互 / 数据 ​

34. 虚拟滚动的原理?难点在哪?

只渲染视口内的 DOM:监听滚动 → 算出可视区起始索引 → 渲染可视区 + 缓冲区的元素 → 用 padding 或绝对定位撑出完整滚动高度。 难点是:动态高度(消息长度不一,要测量并缓存真实高度)、滚动锚定(用户在看历史消息时新消息来了不能强行拉底)、初始化定位(要精确落到最后一条)、滚动抖动(快速滚动时用 requestAnimationFrame 节流计算)。

35. SSE 和 WebSocket 怎么选?

SSE 是单向(服务端 → 客户端)、基于 HTTP、浏览器自带断线重连、服务端实现简单;WebSocket 是全双工、需要协议升级和心跳维护。 我们的流式输出是单向的,SSE 就够了。注意 SSE 不支持自定义请求头,所以鉴权信息要放 URL 参数或 cookie。

36. SWR 的缓存策略是什么?

名字就是策略:先返回缓存里的旧数据(stale),同时后台发请求拿新数据(revalidate),回来再更新,用户感觉是瞬间加载。 配套能力:focus 重新验证(切回标签页刷新)、revalidateOnInterval、去重(同一 key 并发只发一次请求)、错误重试。很适合「数据实时性要求不那么极致、但要求快」的场景。

37. 内存泄漏怎么排查?

先看 Performance 面板的内存曲线:反复操作同一功能,内存只涨不落就是泄漏。再用 Memory 面板抓两次堆快照对比,看什么对象在增长。 常见来源:没清除的定时器/事件监听、全局缓存无上限、ECharts 实例和 WebGL 资源没销毁、闭包里持有大对象、ResizeObserver 没 disconnect。

安全 / 网络 ​

38. XSS / CSRF 怎么防?

XSS:用户输入一律转义(Vue/React 默认做了),富文本和 Markdown 渲染用白名单过滤(我们接的 markdown-it 就是白名单思路),原则上不用 innerHTML,必要时用 CSP 兜底。 CSRF:状态变更请求用 POST,鉴权 token 放请求头而不是 cookie,配合后端的 Referer / Origin 校验和 SameSite。 另外补一句:前端的权限控制只是体验优化,真正的权限校验必须在后端。

39. 跨域怎么解决?

开发环境用 Vite / Webpack 的 devServer proxy 代理解掉;生产环境同源部署或者后端配 CORS(Access-Control-Allow-Origin + 预检请求处理)。带 cookie 的跨域要同时开 withCredentials 和 Allow-Credentials,且 Origin 不能是 *。 兜底方案还有 Nginx 反向代理,最稳。

40. HTTP 缓存怎么配?

强缓存:Cache-Control: max-age / immutable,浏览器直接用本地副本,不发请求。协商缓存:ETag / Last-Modified,发请求带 If-None-Match / If-Modified-Since,没变返回 304。 我们的实践是:HTML 不缓存(否则发版后拿到旧页面引用旧 JS),带 hash 的静态资源用长缓存 + immutable,接口按业务特性单独配。