怎么用 这是格式与深度的参考,不是让你照抄的模板。数字全部是示例,直接抄进简历一问就穿——必须换成你自己的真实数据,而且要能说清是怎么测出来的。下面每个模块都有「数字是怎么测的」和「面试追问」两段,写不出那两段的模块就不要往简历上写

面试题库(电商 / 短视频 / 社区 / 本地生活 / 旅游 / 企业服务 多业务场景)

项目背景设定 多个前端项目(官网、运营后台、H5 活动页)共用的前端基建,Vue 3 + Vite + 自建监控上报 + CI/CD。团队十来个前端,一周发几次版。
为什么选这个方向 这类项目有一个别的项目没有的优势:它不依赖公司给你多大的业务体量。「日活千万」这种背景实习生说出来会被怀疑,但「我们团队发版老出问题所以我做了灰度和回滚」是任何规模都成立的、可信度很高的叙事。而且它能反向支撑你其他项目里的所有性能数字——面试官问「你怎么知道 LCP 是 1.4s」,答案就在这里。

模块一:前端监控与错误追踪

  1. 前端监控与错误追踪(多源采集 + sourcemap 还原 + 上报治理)★★★
    简历这样写 前端监控与错误追踪体系(Vue errorHandler + window.onerror + Web Vitals + sendBeacon + sourcemap 还原):统一采集JS 异常、Promise 未捕获、资源加载失败、接口错误、性能指标五类数据,上报做合并、采样、客户端限流与去重;构建产物的 sourcemap 上传至内部平台不随包发布,线上堆栈可还原到源码行;告警按「新增错误」而非「错误总量」触发。线上 JS 错误可定位到具体版本与源码位置,上报请求量由客户端限流控制在每会话数十次以内,一次死循环导致的错误风暴由限流拦住、未击穿上报服务。
    展开完整拆解
    为什么要这么设计

    做这件事的起点是一个很具体的窘境:用户反馈「页面白了」,我们完全无法复现。他用什么机型、什么浏览器、哪个版本的代码、报了什么错,一概不知。只能让客服要截图,然后猜。

    接了一个最简单的 window.onerror 上报之后,问题变成了另一种:拿到的堆栈是压缩后的乱码——a.js:1:52831 t is not a function,完全无法定位。而且很快发现更多缺口:

    一是 window.onerror 抓不全。Promise 里的异常不走它(要监听 unhandledrejection)、Vue 组件内的错误被 Vue 自己的错误边界吞了(要挂 app.config.errorHandler)、图片和脚本加载失败压根不是 JS 错误(要在捕获阶段监听 error 事件)。四个来源,缺一个就有一类问题永远看不到。

    二是上报把自己搞挂了。某次上线有个循环里的报错,单个用户一分钟上报了几千次,上报接口被打满,其他项目的监控也一起瞎了。监控本身必须是安全的,不能成为故障源。

    三是告警按错误总量触发,全是噪音。总量每天几千条(大部分是浏览器插件、爬虫、老旧机型的固有报错),阈值设高了漏报,设低了天天响,最后没人看。

    所以四个设计点:五类数据源全覆盖sourcemap 还原但不泄露源码上报侧做限流去重告警按「新增」而非「总量」

    整体链路
    采集(五类,缺一类就有盲区) │ ├─ JS 运行时异常 window.onerror / addEventListener('error') ├─ Promise 未捕获 window.onunhandledrejection ├─ 框架内部错误 app.config.errorHandler(Vue) ├─ 资源加载失败 error 事件的捕获阶段(冒泡阶段拿不到) └─ 性能与接口 Web Vitals(LCP / CLS / INP) 接口失败:在请求库拦截器里统一采集(状态码、耗时、路径) 每条上报必带的上下文(没有这些就无法定位) ├─ 应用标识 + 构建版本号(决定用哪份 sourcemap 还原) ├─ 页面路径 + 路由名 ├─ 用户标识(脱敏,只为串联同一用户的多条记录) ├─ 设备与环境:机型 / 系统 / 浏览器 / 屏幕 / 网络类型 └─ 用户操作轨迹:最近几步点击与路由跳转(面包屑) 上报治理(监控不能成为故障源) │ ├─ 去重:同一错误指纹(消息 + 堆栈首帧 + 路径)本会话只报一次 ├─ 限流:每会话上报总数设上限,超过丢弃并记一条「已限流」 ├─ 合并:攒批发送,不是一条一发 ├─ 采样:性能数据按比例采样;错误数据全量(量本身不大) └─ 发送用 sendBeacon,页面卸载时也能发出去且不阻塞 sourcemap 还原 │ ├─ 构建产出 sourcemap → 上传到内部监控平台 → 从发布产物中删除 │ 关键:sourcemap 绝不能部署到公网,否则源码等于公开 │ ├─ 上报的堆栈带版本号 → 平台按版本找对应 sourcemap │ └─ 还原出源码文件名、行列号、原始函数名 告警 ├─ 按「新增错误」触发:这个错误指纹在之前的版本里没出现过 ├─ 按「突增」触发:某个已知错误的量突然涨了一个数量级 ├─ 按「影响面」分级:影响用户数 > 阈值 才叫人 └─ 不按错误总量告警(总量里全是固有噪音,会淹没真问题)
    分步拆解
    1. 四个错误来源必须都挂上。window.onerror 抓同步异常;unhandledrejection 抓未处理的 Promise 拒绝(现在的异步代码里这类占比很高);框架的 errorHandler 抓组件渲染和生命周期里的错误(这些会被框架捕获,不会冒泡到 window);资源加载失败必须在捕获阶段监听,因为资源的 error 事件不冒泡,用 addEventListener('error', fn, true) 的第三个参数为 true 才拿得到。这四个的区别是高频考点。
    2. 版本号是还原堆栈的钥匙。上报必须带构建版本号,平台据此找对应的 sourcemap。没有版本号,sourcemap 就对不上——用新版本的 map 去还原旧版本的堆栈,得到的行号是错的,比没有还糟(会把人引向错误的代码)。
    3. sourcemap 上传后必须从发布产物里删除。如果 sourcemap 跟着部署到 CDN,任何人都能还原出完整源码。正确流程是 CI 里构建 → 上传 map 到内部平台 → 删除 map 文件 → 部署剩下的产物。这一步是安全红线,面试时主动提会加分。
    4. 操作轨迹(面包屑)是定位的关键上下文。光有堆栈往往不够——同一行代码在不同操作路径下报错原因不同。记录最近几步的点击、路由跳转、接口调用,还原出「用户做了什么导致了这个错」。要注意脱敏,不能把输入框内容记进去。
    5. 去重用「错误指纹」。指纹 = 错误消息 + 堆栈首帧 + 页面路径 的哈希。同一指纹在一个会话内只上报一次,这一条就能消掉绝大部分重复量
    6. 客户端限流是必须的,不是优化。死循环里的报错能一秒产生几千条。限流必须在客户端做——等到服务端限流时请求已经发出去了,带宽和服务端资源已经消耗了。每会话设一个上报总数上限,超过就丢弃并单独记一条「已触发限流」(这条信息本身很有价值,说明有错误风暴)。
    7. sendBeacon 而不是 fetch页面卸载时 fetch 可能被浏览器取消,而 sendBeacon 是浏览器保证在后台发送的,且不阻塞页面卸载。「用户看到错误就关页面」是很常见的行为,这时候的上报最容易丢。
    8. 性能数据采样,错误数据全量。性能指标每次访问都产生,量极大而且只需要看分布,采样足够。错误数据量本身不大(去重限流之后),而且每一条都可能是唯一线索,不该采样。两类数据的策略不同,这个区分要说出来。
    9. 告警按「新增」而不是「总量」。线上错误总量里绝大部分是固有噪音——浏览器插件注入的脚本报错、爬虫触发的异常、老旧机型不支持某个 API。真正需要立刻处理的是「这个版本新出现的错误」。做法是维护已知错误指纹的基线,新指纹出现就告警。这一条是让告警从「没人看」变成「有人看」的关键。
    关键决策与取舍

    为什么自建而不直接用现成的监控服务。现成服务(Sentry 之类)功能完善,如果条件允许应该优先用。我们自建的原因是数据合规——业务覆盖多个国家,用户相关的数据出境有限制,把包含用户标识和行为轨迹的数据发到第三方平台需要走合规评审。自建的代价很实在:要自己做数据存储、聚合、查询界面、sourcemap 管理,工作量不小,而且功能远不如成熟产品。面试时要诚实说这个取舍——如果没有合规约束,我会直接用现成的,自建不是为了炫技。

    用户标识的采集要脱敏且有边界。需要一个标识来串联同一用户的多条上报(否则无法判断影响了多少人、无法还原完整轨迹),但不能上报手机号、邮箱这类可直接识别个人的信息。做法是用后端下发的匿名 ID。面包屑里的输入内容一律不采集,只记「在某个输入框输入了」这个事件。

    踩过的坑:sourcemap 误发布到 CDN。构建配置里开了 sourcemap,CI 直接把整个 dist 目录传上去了,源码在公网上挂了两周才被发现。修法是在 CI 里加一步显式删除并加校验(部署前检查产物里不含 .map 文件,有就中断部署)。这个坑的教训是:安全约束必须做成流水线里的自动检查,靠人记住一定会漏。

    踩过的坑二:上报接口自己报错,形成死循环。上报失败时代码里有一个 catch 记录了错误,而这个记录动作又触发了上报,上报又失败,无限循环把浏览器卡死。修法是上报模块内部的所有异常一律静默吞掉,绝不再触发上报。监控代码必须是「绝对不会抛错也绝对不会重入」的,这是它的特殊要求。

    没做的部分:没做录屏回放。它对复现问题极有帮助,但存储成本高、隐私风险大(会录到用户输入),需要严格的脱敏和用户授权,当时判断投入产出比不划算。

    数字是怎么测的

    「错误可定位到源码位置」怎么验证:做一次故意的验证——在预发环境埋一个必然抛错的代码,走完整的构建发布流程,然后在监控平台上检查:能否看到这条错误、堆栈是否还原到了正确的源码文件和行号、上下文(版本、机型、面包屑)是否完整。这个端到端验证比任何数字都有说服力,而且能暴露 sourcemap 版本对不上之类的问题。

    上报量:统计单会话的平均上报请求数与最大值。去重限流后控制在每会话数十次以内。要说清这个数字包含性能数据(采样后)和错误数据,只报错误数会显得很低但不完整。

    「错误风暴被限流拦住」怎么验证:构造一个死循环报错的场景(在测试页面里故意写),观察实际发出的上报请求数是否被限流上限卡住、页面是否仍可用。这类保护机制必须演练过,说「我做了限流」但没验证过,一问「上限是多少、超了之后怎么处理」就答不上来。

    不要报「错误率下降 X%」。建监控这件事本身不会降低错误率——它让你看见错误,短期内统计到的错误数反而会上升(因为以前看不见)。把「看见问题」说成「解决了问题」是很容易被问穿的。可以报的是「从发现问题到定位到具体代码的时间」这类过程指标。

    面试追问
    Q:前端错误监控要监听哪几个事件?为什么不能只用 window.onerror? A:至少四个来源,各自覆盖不同类型。window.onerror 抓同步的 JS 运行时异常。unhandledrejection 抓未被 catch 的 Promise 拒绝——现在的异步代码里这类占比很高,而它压根不会触发 onerror框架的 errorHandler(Vue 是 app.config.errorHandler)抓组件渲染、生命周期、watcher 里的错误,这些被框架内部捕获了,不会冒泡到 window资源加载失败(图片、脚本、样式 404)要用 addEventListener('error', fn, true),注意第三个参数必须是 true,因为资源的 error 事件不冒泡,只能在捕获阶段拿到。另外接口错误通常在请求库的拦截器里单独采集,因为它是「业务失败」不是「JS 异常」,需要的字段也不同。
    Q:线上代码是压缩的,堆栈是乱码,怎么定位到源码? A:靠 sourcemap。构建时生成 sourcemap,上传到内部监控平台,然后从发布产物中删除(绝不能部署到公网,否则源码等于公开——我们踩过这个坑,源码在 CDN 上挂了两周)。上报的每条错误都带构建版本号,平台按版本号找到对应的 map,把压缩后的行列号还原成源码文件名、行号和原始函数名。版本号是关键:用新版本的 map 还原旧版本的堆栈会得到错误的行号,比没有还原更糟,因为会把人引向无关的代码。另外 CI 里要加校验:部署前检查产物目录不含 .map 文件,有就中断发布——安全约束必须做成自动检查,靠人记一定会漏。
    Q:如果一个页面死循环报错,一秒上报几千次,会怎样? A:我们真遇到过,上报接口被打满,导致其他项目的监控也一起失效。防护必须在客户端做,服务端限流时请求已经发出去了,用户带宽和服务端资源都已经消耗。三层:去重——按错误指纹(消息 + 堆栈首帧 + 路径)在同一会话只报一次,这一条就能消掉大部分重复;限流——每会话设上报总数上限,超过就丢弃,但要单独记一条「已触发限流」(这个信号本身很有价值,说明存在错误风暴);合并——攒批发送而不是一条一发。还有一个特殊要求:上报模块内部的所有异常必须静默吞掉,绝不能再触发上报,否则「上报失败 → 记录错误 → 触发上报」会形成死循环把浏览器卡死。这也是我们踩过的坑。
    Q:错误告警怎么配才不会变成没人看的噪音? A:不要按错误总量告警,这是关键。线上错误总量里绝大部分是固有噪音——浏览器插件注入的脚本报错、爬虫触发的异常、老旧机型不支持某个 API、用户网络中断导致的资源加载失败。这些每天都有几千条,阈值设高了漏真问题,设低了天天响,最后所有人都把告警静音。正确做法是按「新增」和「突增」告警:维护已知错误指纹的基线,某个指纹在这个版本第一次出现就告警(很可能是新代码引入的 bug);已知错误的量突然涨一个数量级也告警。再叠加影响面分级——影响用户数超过阈值才叫人,只影响一两个用户的先进工单不打扰值班。告警的可持续性取决于准确率,一个总是误报的告警等于没有告警。

模块二:发布、灰度与回滚

  1. 发布、灰度与回滚(产物版本化 + 按比例灰度 + 秒级回滚)★★★
    简历这样写 前端发布与灰度回滚机制(CI/CD + 产物版本化 + HTML 与静态资源分离缓存 + 灰度分流 + 版本更新提示):静态资源按内容哈希版本化并长期缓存、入口 HTML 不缓存,实现发布即生效且可秒级回滚(切回上一版本的 HTML);支持按用户比例灰度并在灰度期观察错误与性能指标;单页应用发版后提示已打开的页面刷新,避免旧版本请求已删除的资源块而白屏。回滚耗时由重新构建部署的十几分钟降到 1 分钟以内,发版后因资源块缺失导致的白屏问题改造后未再出现。
    展开完整拆解
    为什么要这么设计

    做这块的直接动因是一次事故:上线后发现严重 bug,回滚花了二十多分钟——因为回滚的方式是「把代码 revert 掉,重新走一遍 CI 构建部署」。这二十分钟里用户一直在用坏的版本。

    顺着往下查,发现前端发布链路有一串问题。

    一是缓存策略混乱。入口 HTML 被 CDN 缓存了几分钟,导致发布后一部分用户还是旧页面;而 JS 文件名没带哈希,浏览器缓存了旧文件,新旧代码混在一起跑出各种诡异错误。

    二是单页应用发版后老页面白屏。用户页面开着没刷新,此时发了新版本,旧的资源块(chunk)在部署时被删掉了。用户点击一个还没加载过的路由,去请求那个已经不存在的 chunk,直接白屏。这类反馈很难复现——因为开发者刷新一下就好了。

    三是没有灰度。每次发版都是全量,有问题就是所有用户一起中招。

    所以三个设计:产物按内容哈希版本化 + HTML 不缓存(这一条同时解决了缓存混乱和秒级回滚)、旧版本资源保留一段时间 + 版本更新提示(解决白屏)、按比例灰度 + 灰度期指标观察

    核心认知是:前端的「回滚」本质上只是「让用户加载另一个版本的 HTML」。只要各版本的静态资源都还在 CDN 上,回滚就是改一个指针的事,不需要重新构建。想清楚这一点,回滚时间从分钟级降到秒级是必然的。

    整体链路
    构建产物的组织(这是秒级回滚的前提) │ ├─ 静态资源:文件名带内容哈希 app.a1b2c3.js │ 缓存策略:长期强缓存(内容变了文件名就变,不存在过期问题) │ 部署方式:只增不删,新版本的文件加进去,旧版本的留着 │ └─ 入口 HTML:文件名固定,内容里引用带哈希的资源 缓存策略:不缓存(或极短缓存 + 协商缓存) 它是唯一的「版本指针」 发布 │ ├─ CI:安装依赖 → 校验(lint / 类型 / 单测)→ 构建 ├─ 门禁:产物体积检查、产物中不得含 sourcemap(见模块一) ├─ 上传静态资源到 CDN(保留历史版本,不覆盖不删除) ├─ 上传 sourcemap 到监控平台,然后从产物中删除 ├─ 更新入口 HTML(这一刻新版本生效) └─ 记录本次发布:版本号、产物清单、上一版本 HTML 备份 灰度 │ ├─ 网关按规则决定返回哪个版本的 HTML │ 规则:用户 ID 哈希取模 / 内部账号白名单 / 地区 │ ├─ 灰度比例逐步放大:内部 → 1% → 10% → 50% → 全量 │ ├─ 每一档观察:新增错误、错误量、核心页面性能、关键接口成功率 │ 指标用「灰度组 vs 对照组」对比,不看绝对值 │ └─ 任一档异常 → 灰度比例调回 0(等价于回滚) 回滚(秒级) │ ├─ 把入口 HTML 换回上一版本的备份 │ 旧版本引用的资源还在 CDN 上(因为只增不删)→ 立刻可用 │ ├─ 清 CDN 上 HTML 的缓存(HTML 本来就不缓存,所以很快) │ └─ 无需重新构建、无需等 CI,1 分钟以内完成 单页应用的版本更新提示 │ ├─ 构建时把版本号写进一个独立的小文件 version.json │ ├─ 页面定期(或路由切换时)拉一次 version.json 比对 │ ├─ 版本不一致 → 提示「有新版本,点击刷新」 │ 不强制刷新:用户可能正在填表单,强刷会丢数据 │ └─ 兜底:动态导入 chunk 失败时捕获 → 提示刷新页面 这是防白屏的最后一道,旧 chunk 被清理时会走到这里
    分步拆解
    1. 静态资源必须带内容哈希,且部署只增不删。内容哈希保证「内容变了 URL 就变」,所以可以设长期强缓存,浏览器永不需要重新校验。只增不删是回滚的前提——旧版本 HTML 引用的资源必须还在,否则切回去也是白屏。代价是 CDN 上文件越来越多,靠定期清理超过保留期的版本控制(保留期要长于可能需要回滚的时间窗口,比如两周)。
    2. 入口 HTML 绝对不能长期缓存。它是唯一的版本指针,缓存了就意味着发布不生效、回滚也不生效。设为不缓存或者极短缓存加协商缓存。这一条配错的话,后面所有机制都失效。
    3. 回滚就是把 HTML 换回上一版。每次发布备份当前 HTML,回滚时切回去即可。不要用「revert 代码重新构建」的方式回滚——那要等 CI 跑完,而且重新构建的产物哈希可能和之前不同,引入新的不确定性。
    4. 灰度靠网关分流 HTML。网关按用户标识哈希取模决定返回哪个版本的 HTML。关键是同一个用户必须稳定命中同一版本(用用户 ID 哈希而不是随机数),否则用户刷新一次就在新旧版本间跳,会遇到各种混乱状态。
    5. 灰度期要看「灰度组 vs 对照组」的对比,不看绝对值。错误量的绝对值受时段、活动、流量影响,单看新版本报了 50 个错说明不了什么。要对比同一时段灰度组和对照组的指标差异,这才能归因到版本变更。
    6. 灰度比例要逐步放大且每档观察足够时间。内部账号 → 1% → 10% → 50% → 全量。每档停留时间要够长到能覆盖主要使用场景——如果某个 bug 只在下单流程出现,而观察期内没人下单,那这档就是白观察。
    7. 版本更新提示不要强制刷新。用户可能正在填一个长表单,强刷会丢数据引起投诉。做成可见但不打扰的提示,让用户自己选时机。只有安全类的紧急更新才考虑强制。
    8. 动态导入失败必须捕获并兜底。这是防白屏的最后一道。旧版本页面请求已被清理的 chunk 时,import() 会 reject,捕获后提示用户刷新页面(刷新就会拿到新的 HTML 和新的资源)。没有这个兜底,用户看到的就是纯白屏加控制台一行报错。
    9. 门禁要放在构建之后、部署之前。体积检查、sourcemap 检查、lint 和测试,任一失败就中断,不产生部署。门禁的价值在于「必须过」,如果可以轻易跳过就等于没有。
    关键决策与取舍

    CDN 上保留所有历史版本的成本。只增不删意味着文件持续累积。实际算下来前端产物本身不大(每个版本几百 KB 到几 MB),保留两周的量级完全可接受,存储成本远低于「回滚要等二十分钟」的代价。清理策略是定时删除超过保留期的版本,但要先确认这些版本已经没有活跃用户在用(可以从监控上报的版本号分布看)。

    灰度粒度选用户而不是请求。按请求灰度(每次请求随机决定版本)实现更简单,但用户会在两个版本间来回跳,本地缓存的状态、已加载的资源、localStorage 里的数据格式都可能不兼容,会造成难以排查的问题。按用户哈希灰度保证一致性,代价是灰度比例的调整粒度较粗。

    前端灰度和后端灰度必须协调,这是个容易被忽略的坑。新版本前端往往依赖新版本后端接口。如果前端先灰度而后端没上,灰度用户会调到不存在的接口;反过来后端先上而前端未更新,旧前端可能不兼容新的响应格式。做法是接口先做向后兼容的上线,前端再灰度——后端新接口上线但不影响旧接口,前端灰度切过去,全量后再考虑下线旧接口。这个顺序说清楚会明显加分。

    踩过的坑:回滚了 HTML 但用户还是旧的(坏的)版本。因为 CDN 上 HTML 的缓存配置写的是几分钟强缓存,回滚后要等缓存过期。那几分钟里回滚等于没生效,当时非常慌乱。修法是把 HTML 改为不缓存,并且回滚流程里加一步主动刷新 CDN 缓存。教训是:回滚链路必须像发布链路一样被演练过,我们当时是第一次真正回滚才发现这个配置问题。

    踩过的坑二:灰度时用随机数分流,用户版本乱跳。用户刷新一次进新版、再刷新回旧版,而新版本在 localStorage 里写了新格式的数据,旧版本读到后解析失败报错。修法是改用用户 ID 哈希取模,保证同一用户稳定命中同一版本顺带的教训是:涉及本地存储格式变更的发布,必须考虑新旧版本共存期的兼容。

    没做的部分:没做自动化的灰度决策。目前灰度比例的推进和异常时的回滚都是人工判断,理想状态是「指标异常自动回滚」。做这个需要对指标的置信度有足够把握,否则误触发的回滚本身就是故障,当时不敢做。

    数字是怎么测的

    回滚耗时:实际演练——在预发环境走一遍完整回滚,从「决定回滚」到「验证用户拿到旧版本」计时。1 分钟以内。要说清这个时间包含什么:切换 HTML、刷新 CDN 缓存、验证。改造前的十几分钟是「revert 代码 + 等 CI 构建 + 部署」的实际记录(可以从 CI 的历史耗时里取)。

    「白屏未再出现」怎么验证:依据是监控里「动态导入失败」这类错误的数量(模块一建的监控派上用场了)。改造前每次发版后这类错误都会出现一批,改造后被版本提示和兜底捕获消化掉。这是两个模块联动的例子,面试时可以主动串起来讲。

    灰度的有效性:可以报「灰度期间拦下的问题次数」——即在灰度阶段发现异常并回退、没有影响全量用户的次数。这是个绝对数,口径清晰,而且直接证明灰度有价值。比说「我们做了灰度」有力得多。

    不要报「发布成功率 100%」或「零故障发布」。绝对化断言一问就穿。正确的表述是「灰度阶段拦下 N 次问题」「回滚耗时从十几分钟降到 1 分钟以内」——承认问题会发生,但影响面和恢复时间被控制住了,这才是可信的工程叙事。

    面试追问
    Q:前端怎么做到秒级回滚? A:关键认知是前端的「版本」其实只存在于入口 HTML 里——HTML 决定加载哪些带哈希的 JS 和 CSS。所以只要满足两个条件,回滚就是改一个指针:一是静态资源带内容哈希且部署只增不删,旧版本的资源永远还在 CDN 上;二是入口 HTML 不缓存,改了立刻生效。回滚就是把 HTML 换回上一版的备份,1 分钟以内完成,不需要重新构建也不需要等 CI我们踩过一个坑:HTML 当时配了几分钟强缓存,回滚后要等缓存过期,那几分钟等于没回滚。所以 HTML 的缓存策略是这套机制的命门,而且回滚流程必须演练过——我们是第一次真回滚才发现配置有问题。
    Q:单页应用发新版本,用户页面还开着,会有什么问题? A:最典型的是点击某个路由直接白屏。原因是用户页面加载的是旧版本 HTML,里面记录的是旧的 chunk 文件名;他点击一个还没访问过的路由,浏览器去请求那个 chunk,而新版本部署时如果把旧 chunk 删了,请求 404,import() reject,页面白屏。三层解决一是部署只增不删,旧 chunk 保留一段时间,这一条就消掉了大部分问题;二是版本更新提示——构建时把版本号写进一个小文件,页面定期拉取比对,不一致就提示用户刷新(不要强制刷新,用户可能正在填表单);三是捕获动态导入失败,作为最后兜底,捕获到就提示刷新。另一类问题是本地存储格式不兼容:新版本改了 localStorage 的数据结构,旧版本页面读不了或者反过来,所以涉及存储格式变更的发布要考虑共存期兼容。
    Q:灰度按什么维度分流?为什么不能随机? A:按用户标识哈希取模,不能用随机数。随机的问题是同一个用户刷新一次就可能切到另一个版本,会造成一串难排查的问题:本地存储的数据格式在新旧版本间不兼容、已加载的资源和新 HTML 对不上、用户看到的界面一会儿新一会儿旧。用用户 ID 哈希保证同一用户稳定命中同一版本灰度维度还可以叠加:内部账号白名单(第一档,先让自己人用)、地区(先在小市场放)、设备类型。观察指标时要用「灰度组 vs 对照组」对比而不是看绝对值——错误量的绝对值受时段和流量影响,只有同时段的两组对比才能归因到版本变更。
    Q:前端灰度了,但新代码要调后端的新接口,怎么协调? A:后端先上且必须向后兼容,前端再灰度。具体顺序是:后端上线新接口(或给旧接口加新字段),保证旧版本前端不受影响;确认后端稳定后,前端开始灰度,灰度用户走新接口;前端全量并观察一段时间后,才考虑下线旧接口。反过来的顺序都会出事:前端先灰度会调到不存在的接口;后端上线不兼容的改动(比如删字段、改字段类型)会直接让旧前端报错。如果新旧接口无法兼容,就要在接口上做版本区分(路径或请求头带版本),让两个版本的前端各调各的。还有个细节:灰度期的问题排查需要知道「这个用户是哪个前端版本」,所以监控上报必须带版本号——这又回到了监控模块,两块基建是互相支撑的。

模块三:组件库与规范门禁

  1. 组件库与规范门禁(版本化发布 + 按需引入 + CI 质量与体积门禁)★★★
    简历这样写 公共组件库与工程规范落地(Vue 3 + 组件库打包 + 语义化版本 + Changelog + CI 门禁):把多项目重复的业务组件抽成可按需引入的内部组件库,走语义化版本与变更日志、破坏性改动必须升主版本;配套 CI 门禁(lint、类型检查、单测、产物体积、sourcemap 泄露检查)在合并前拦截。重复的业务组件代码减少 约 4000 行(净减少,已扣除组件库自身代码),接入项目的首包体积因按需引入未因引入组件库而增长,体积劣化在合并前被门禁拦下过十余次
    展开完整拆解
    为什么要这么设计

    起因很朴素:同一个「商品选择器」组件,在三个项目里各写了一遍,行为还不一致——一个支持多选一个不支持,一个有搜索一个没有。运营在不同后台看到的同一个功能长得不一样,来提需求说要统一。

    更麻烦的是修 bug 要修三遍,而且总有一处漏掉。这类组件不是通用 UI 组件(那些用现成组件库就行),而是带业务语义的组件——商品选择器、门店选择器、地区级联、金额输入(要处理多币种和精度)、审核状态标签。它们只在自己公司有意义,外部组件库不会有。

    第二个问题是规范靠人靠不住。约定了代码风格、约定了不许引入大依赖、约定了 sourcemap 不能发布,但这些都写在文档里,没有强制手段。结果就是每个人都遵守九成,加起来就有一成不遵守,几个月后代码风格五花八门、首包体积悄悄涨了一倍。

    所以两件事:把重复的业务组件抽成有版本管理的内部库把规范从文档变成 CI 门禁。第二件事的认知是关键:任何依赖自觉的规范都会退化,只有自动化检查能长期守住。

    整体链路
    组件库的边界(先划清什么该进、什么不该进) │ ├─ 该进:多项目复用且带业务语义的组件 │ 商品选择器 / 门店选择器 / 地区级联 │ 金额输入(多币种 + 最小单位整数 + 精度约束) │ 审核状态标签 / 权限按钮 / 空状态与错误态 │ ├─ 该进:跨项目共用的工具与约定 │ 请求封装(拦截器 / 错误码映射 / 重试策略) │ 格式化(金额 / 日期 / 数字,统一走 Intl) │ └─ 不该进:只有一个项目用的、或强绑定某项目业务流程的 判断标准:第三个项目要用它时才抽(两个可能是巧合) 组件库的发布 │ ├─ 独立仓库,独立版本,语义化版本号 │ 修 bug → 补丁版本 │ 加功能且向后兼容 → 次版本 │ 破坏性改动(改 props / 改默认行为)→ 主版本 │ ├─ 打包成支持按需引入的形式 │ 每个组件独立产出,配合构建工具的 tree-shaking │ 样式也要能按需引入,不能一个大 CSS 全量塞进去 │ ├─ 必须有 Changelog,破坏性改动要写迁移说明 │ └─ 内部私有源发布,各项目按版本号依赖(不用 latest) CI 门禁(在合并之前,不是之后) │ ├─ 1 代码风格与静态检查(lint) ├─ 2 类型检查(TypeScript 全量检查,不只查改动文件) ├─ 3 单元测试(组件库要求覆盖核心交互,业务项目不强制) ├─ 4 构建成功 ├─ 5 产物体积检查:超过阈值则失败 │ 阈值 = 当前值 + 少量余量 │ 要变大必须显式提阈值并说明原因(走评审) ├─ 6 安全检查:产物中不得含 .map 文件 ├─ 7 依赖检查:新增依赖需要说明(防止随手引入大库) │ └─ 任一项失败 → 阻止合并(不是警告,是硬拦) 组件库升级的推进 ├─ 主版本升级不强制各项目立刻跟(会打断业务排期) ├─ 但要设一个「最低支持版本」,过旧的版本停止维护 └─ 破坏性改动尽量提供兼容层与废弃警告,给一个过渡期
    分步拆解
    1. 先划清边界,不要一上来就抽。判断标准是「第三个项目要用它时才抽」——两个项目用可能是巧合,三个就说明它真的通用。过早抽象的组件往往参数设计不合理,因为只见过一两种用法。
    2. 不要把 UI 组件库重新做一遍。按钮、表格、弹窗这些用成熟的开源组件库就好。内部组件库只做「带业务语义」的部分,它们才是外部库不可能提供的。分不清这个边界会做出一个又大又难维护的四不像。
    3. 组件设计要留逃生舱。用插槽暴露关键区域,让业务方能覆盖某一部分渲染而不必抛弃整个组件。没有逃生舱的组件,遇到一个特殊需求就会被整体绕过,然后又出现一个复制版本。
    4. 必须支持按需引入,包括样式。如果引入组件库就意味着全量打进包里,各项目为了体积会拒绝使用。组件要能被 tree-shaking,样式要能按组件拆分。这一条是组件库能否被接受的前提。
    5. 语义化版本要严格遵守,尤其是破坏性改动升主版本。改 props 名、改默认行为、删除某个功能都是破坏性的。偷偷在次版本里做破坏性改动,会让所有项目的升级都变成雷区,最后大家都锁死版本再也不升。
    6. 各项目依赖具体版本号,不用 latest 或范围通配。用通配的话某天组件库发了个有问题的版本,所有项目下一次构建就一起挂。锁版本,升级是显式动作。
    7. Changelog 和迁移说明是义务不是可选。破坏性改动必须写清「原来怎么写、现在怎么写」。没有迁移说明的主版本升级,业务方只能自己试,成本极高。
    8. 门禁必须放在合并前且是硬拦。放在合并后是「事后通知」,代码已经进主干了。设成警告等于没有——大家会习惯性忽略。硬拦才有约束力。
    9. 体积门禁的阈值要允许被显式提升。设成永不可变会挡住正常的功能开发(有时候确实需要引入一个库)。做法是允许提阈值但必须走 PR 评审并说明原因——目的不是禁止变大,而是让变大成为一个有意识的决定而不是悄悄发生。
    10. 类型检查要全量而不是只查改动文件。改动一个文件的类型可能破坏另一个文件的使用,只查改动会漏掉。全量检查慢一些,但这是唯一可靠的方式。
    关键决策与取舍

    组件库会引入版本地狱,这是真实代价。三个项目依赖三个不同版本的组件库,修一个 bug 要判断影响哪些版本、要不要往老版本回合。缓解手段是设「最低支持版本」并主动推进升级——只维护最近的一到两个主版本,更老的不再修。但推进升级会占用业务排期,需要和团队达成共识,这不是技术问题而是协作问题。

    门禁会引起团队摩擦,引入方式很重要。突然加一堆硬拦的检查,大家的感受是「被拖慢了」。我们的做法是先以警告模式运行一两周,让大家看到实际会拦下什么、有多少误报,调整规则之后再转成硬拦。而且要先沟通目的——门禁是为了防止大家的劳动被别人悄悄破坏(比如你辛苦优化的体积被下一个人一个依赖加回去),不是为了考核。说明「我怎么推动这件事落地」,比只说「我加了门禁」更能体现工程能力。

    踩过的坑:组件库的一个小改动,把三个项目一起搞挂了。在一个「修 bug」的补丁版本里顺手改了某个 prop 的默认值(觉得原来的默认值不合理),发布后三个项目升级都出现了行为变化。教训是「什么算破坏性改动」的判断要保守——改默认值就是破坏性的,即使你觉得新的更合理。正确做法是加一个新的 prop 让业务方显式选择,老行为保持不变,并在文档里标记原行为将在下个主版本废弃。

    踩过的坑二:抽得太早,组件参数设计不合理。看到两个项目有相似的表单就抽成了组件,结果第三个项目的需求略有不同,为了兼容加了一堆 props 和条件分支,组件内部变成了一坨 if,最后不得不拆回去重做。这就是为什么标准是「第三个项目要用时才抽」——只有见过三种真实用法,才知道哪些是共性哪些是差异。

    没做的部分:没做组件库的可视化文档站(带在线示例和 props 说明的那种)。目前文档是 Markdown 加代码示例,业务方要看效果得自己跑起来。这明显影响接入体验,但搭文档站的工作量不小,一直排在后面。

    数字是怎么测的

    「减少约 4000 行」:git diff --stat 统计抽组件库那几次改动的删除行数减去新增行数,而且必须把组件库自身的代码算进新增里。只报「删了 6000 行」不提「组件库写了 2000 行」是不诚实的,面试官一算就知道你在挑数字。报净减少。

    「首包体积未因引入组件库而增长」:用构建分析工具对比接入前后的产物构成,确认只有实际用到的组件进了包。这个验证很重要——按需引入配错的话会把整个库打进去,体积反而暴涨。要说明是 gzip 前还是 gzip 后的体积。

    「门禁拦下十余次」:从 CI 的失败记录里统计因体积检查失败的次数。这是个绝对数、口径清晰、可复现,而且直接证明门禁有价值——比说「我加了体积门禁」有说服力得多。可以顺带说出几个典型案例(某次引入了一个日期库导致体积涨了几十 KB,被拦下后改用原生 Intl)。

    不要报「开发效率提升 30%」。这个数字算不出来——没有基线、没有可比的任务、受人员和需求复杂度影响。硬报一个效率提升的百分比是最容易被追问穿的一类数字。可以报的是具体的、可核查的事实:几个项目接入了、重复代码净减少多少行、门禁拦下多少次。

    面试追问
    Q:什么样的组件该进组件库?你怎么判断? A:两条标准。一是「带业务语义且多项目复用」——按钮、表格这类通用 UI 用成熟开源库就行,内部库只做外部库不可能提供的部分,比如商品选择器、门店选择器、金额输入(要处理多币种和最小单位整数)、审核状态标签。二是时机:第三个项目要用它时才抽。两个项目用可能是巧合,抽早了参数设计一定不合理——我们踩过这个坑,看到两个相似表单就抽了,第三个项目需求略有不同,为了兼容加了一堆 props 和条件分支,组件内部变成一坨 if,最后拆回去重做。只有见过三种真实用法,才知道哪些是共性哪些是差异。另外组件必须留插槽当逃生舱,否则遇到一个特殊需求就会被整体绕过,然后又出现一个复制版本,等于没抽。
    Q:组件库改了一个东西,怎么保证不把使用方搞挂? A:核心是严格的语义化版本,而且「什么算破坏性改动」的判断要保守。改 props 名、改默认值、改默认行为、删功能,全部算破坏性,必须升主版本。我们踩过坑:在一个补丁版本里顺手改了某个 prop 的默认值(觉得原来的不合理),三个项目升级后行为全变了。正确做法是加新 prop 让业务方显式选择,老行为保持不变,文档标记原行为将在下个主版本废弃,给一个过渡期。配套还要有:各项目锁具体版本号(不用 latest,否则组件库发个坏版本所有项目一起挂)、Changelog 必须写迁移说明核心交互有单测。另外要设最低支持版本,只维护最近一到两个主版本,否则会陷入给五个版本回合补丁的困境。
    Q:CI 门禁具体拦哪些?体积阈值怎么定? A:拦七项:lint、类型检查(全量而非只查改动文件)、单测、构建成功、产物体积、产物中不得含 sourcemap、新增依赖需说明体积阈值定成「当前值加少量余量」,超了构建失败。关键设计是允许显式提阈值但必须走评审——目的不是禁止变大(有时候确实需要引入一个库),而是让变大成为一个有意识的决定,而不是每个人加一点点、三个月后体积翻倍。门禁必须在合并前而且是硬拦,放在合并后是事后通知(代码已进主干),设成警告等于没有(大家会习惯性忽略)。推行方式也有讲究:我们先以警告模式跑了一两周,看实际会拦什么、有多少误报,调整规则后才转硬拦,并且先跟团队讲清目的是「防止你优化的成果被下一个人悄悄破坏」而不是考核。
    Q:这些基建工作,你怎么衡量它的价值? A:不用「效率提升百分之几」这种算不出来的数字,用可核查的事实。组件库:几个项目接入了、重复代码净减少多少行(净减少,要扣掉组件库自身的代码)。门禁:拦下过多少次体积劣化,能举出具体案例。发布机制:回滚耗时从多少降到多少、灰度阶段拦下过多少次问题没影响全量用户。监控:从发现问题到定位到具体源码位置需要多久。这些都是绝对数、口径清晰、可复现。更重要的是基建的价值往往体现在「别的项目的数字」上——我在其他项目里报的 LCP、内存、错误定位,全都依赖这套监控和发布体系才测得出来、才敢改。面试时把这个联动讲出来,比孤立地夸基建有用得多。

没有匹配的内容,换个关键词试试。

项目拆解 · 前端工程效能(Vue)· 共 3 个模块 · 数字均为示例,需替换成自己项目的真实数据