Skip to content

附录 A2 · 15 道大厂场景设计题完整解答

每道题统一按 背景 / 核心需求 / 设计方案(含 ASCII 架构图)/ 关键技术点 / 难点与权衡 / 加分项 / 兜底容错 七段展开。 答题时按 README 中的"五段式"组织语言:需求 → 高层设计 → 核心组件 → 难点权衡 → 扩展兜底


Q1:大文件分片上传(断点续传 + 秒传 + 并发控制)

背景

教育平台、网盘、视频站、企业云盘场景,用户经常上传 100MB ~ 10GB 的大文件。直接 <input type=file> + XHR 上传,碰到弱网就要从头来过,体验崩塌。

核心需求

  1. 分片:把大文件切成固定大小(5MB),逐片上传
  2. 断点续传:上传中断 / 关闭浏览器后,下次继续从断点开始
  3. 秒传:服务端已存在相同文件 → 跳过上传,直接秒传
  4. 并发控制:最多同时上传 4 个分片,防止占满带宽
  5. 进度显示 + 失败重试 + 暂停/恢复

设计方案

┌──────────────────── 前端 ────────────────────┐    ┌──────── 后端 ────────┐
│                                              │    │                      │
│  ① 选择文件                                  │    │                      │
│      │                                       │    │                      │
│      ▼                                       │    │                      │
│  ② 计算 Hash(Web Worker + spark-md5)       │    │                      │
│      │                                       │    │                      │
│      ▼                                       │    │                      │
│  ③ POST /verify  { hash, name, size }       │───▶│ 查询:文件是否已存在  │
│      │                                       │    │ 已上传哪些分片?      │
│      │  ◀────────── { existed, uploaded[] }  │◀───│                      │
│      ▼                                       │    │                      │
│  ④ 已存在? ── 是 ──▶ 秒传成功 ✅            │    │                      │
│      │ 否                                    │    │                      │
│      ▼                                       │    │                      │
│  ⑤ 切片 (Blob.slice) → 过滤已上传的          │    │                      │
│      │                                       │    │                      │
│      ▼                                       │    │                      │
│  ⑥ asyncPool(4) 并发上传分片                 │───▶│ POST /upload?index=  │
│      │                                       │    │ 存储分片              │
│      ▼                                       │    │                      │
│  ⑦ POST /merge { hash }                      │───▶│ 合并所有分片         │
│      │                                       │    │ 返回最终 URL         │
│      ▼                                       │    │                      │
│  ⑧ 上传完成 ✅                                │    │                      │
└──────────────────────────────────────────────┘    └──────────────────────┘

关键技术点

模块技术
切片File.prototype.slice(start, end)
Hash 计算spark-md5 增量算法 + Web Worker 避免主线程卡顿;可改用抽样 hash(首 + 中 + 尾各 2MB)加速
并发控制自己实现 asyncPool(limit, tasks) 或用 p-limit
断点续传上传前请求 /verify 拿到已上传的 index 列表,前端跳过
秒传hash 比对,服务端已有就直接 link
进度XHR onprogress 累加,全局 = sum(已传分片) / total
失败重试单分片失败重试 3 次,指数退避 (1s/2s/4s)
暂停AbortController.abort() 取消进行中的请求

核心代码(见 examples/01-chunk-upload.js):

js
async function chunkUpload(file) {
  const CHUNK_SIZE = 5 * 1024 * 1024;
  const hash = await calcHashInWorker(file);                 // ① 计算 hash
  const { existed, uploaded } = await verify(hash, file.name); // ② 校验
  if (existed) return { url: existed };                       // ③ 秒传

  const chunks = [];
  for (let i = 0; i < file.size; i += CHUNK_SIZE) {
    chunks.push({ index: chunks.length, blob: file.slice(i, i + CHUNK_SIZE) });
  }
  const todo = chunks.filter((c) => !uploaded.includes(c.index));

  await asyncPool(4, todo.map((c) => () => uploadChunk(hash, c)));
  return await merge(hash, file.name);
}

难点与权衡

难点方案 A方案 B推荐
Hash 算 1GB 太慢(10s+)全文 hash(准确)抽样 hash(快)大文件抽样,误差可接受;服务端再精确比对
并发数选多少2(保守)8(激进)4 比较平衡;可根据 navigator.connection.effectiveType 动态调整
分片大小1MB10MB5MB;太小请求多,太大单片重传贵
Hash 用主线程 vs Worker主线程(简单)Worker(不卡 UI)Worker;大文件必须
秒传 hash 唯一性MD5SHA-256MD5 已够,碰撞概率 < 10^-15,对业务不影响

加分项

  • 抽样哈希:首 2MB + 中间 2MB + 末 2MB,1 秒内算完 1GB
  • Service Worker 拦截上传请求,离线时缓存到 IndexedDB,上线后自动恢复
  • WebTransport / HTTP/3:替代 XHR,弱网下吞吐高 30%
  • 可视化进度:分片网格(绿=完成、蓝=进行中、红=失败、灰=待上传)
  • 预签名 URL:直接传到 OSS / S3,跳过应用服务器(COS 的 Multi-Part Upload)
  • 分片二进制差分:rsync 思路,只传改动部分(高级场景)

兜底容错

场景兜底
网络断开online / offline 事件监听,断开时自动暂停
单分片失败重试 3 次,指数退避;3 次后标红,提示用户手动重试
关闭浏览器上传记录存 localStorage,下次进来恢复
Hash 计算超时5 分钟还没算完,降级为不秒传,直接进入分片阶段
服务端合并失败保留 24 小时分片,次日重试合并
浏览器内存爆分片不一次性 readAsArrayBuffer,按需读取

Q2:无限滚动列表(虚拟列表 + IntersectionObserver)

背景

电商商品列表、信息流、聊天历史、日志页面:动辄 1 万条数据,全部渲染会卡到死。

核心需求

  1. 滚到底部自动加载下一页
  2. 数据量过大时只渲染可视区域(虚拟列表)
  3. 上拉刷新 / 下拉加载
  4. 滚动流畅(60fps)
  5. 跳转回来位置不丢

设计方案

                可视区域 (viewport)

  ┌─────────────────────────────────────────────┐
  │                                             │
  │    ┌─────────────────────────────────┐     │
  │    │   缓冲区上 (3 项)              │     │
  │    ├─────────────────────────────────┤     │
  │    │   可见 item 1                    │     │
  │    │   可见 item 2                    │ ◀── 视口
  │    │   可见 item 3                    │     │
  │    │   可见 item 4                    │     │
  │    │   可见 item 5                    │     │
  │    ├─────────────────────────────────┤     │
  │    │   缓冲区下 (3 项)              │     │
  │    └─────────────────────────────────┘     │
  │              ↓ 哨兵 sentinel                │ ◀── IntersectionObserver
  │              (进入视口触发 loadMore)       │
  └─────────────────────────────────────────────┘

  总滚动高度由 N * itemHeight 撑开(占位 div)

关键技术点

模块技术
滚动监听IntersectionObserver 监听底部哨兵元素,性能优于 scroll 事件
虚拟列表维护 startIndex / endIndex,只渲染 [start, end]
占位用一个 height = N * itemHeight 的空 div 撑出滚动条
偏移渲染区域用 transform: translateY(start * itemHeight) 定位
不定高渲染后用 ResizeObserver 测量实际高度,缓存修正
上拉刷新touchstart / touchmove 计算 dy,超过阈值触发
滚动恢复路由切换前保存 scrollTop,回来 el.scrollTop = saved

核心代码(见 examples/02-virtual-list.js):

js
class VirtualList {
  constructor(el, { itemHeight, buffer = 3, fetchMore }) {
    this.itemHeight = itemHeight; this.buffer = buffer; this.fetch = fetchMore;
    this.data = []; this.start = 0;
    el.addEventListener("scroll", () => this.onScroll());
    new IntersectionObserver(([e]) => e.isIntersecting && this.loadMore()).observe(this.sentinel);
  }
  onScroll() {
    const { scrollTop, clientHeight } = this.el;
    this.start = Math.max(0, Math.floor(scrollTop / this.itemHeight) - this.buffer);
    this.end = Math.min(this.data.length, this.start + Math.ceil(clientHeight / this.itemHeight) + this.buffer * 2);
    this.render();
  }
  // ...
}

难点与权衡

难点方案 A方案 B推荐
检测底部scroll 监听IntersectionObserverIO,零成本,无 layout thrashing
高度处理定高(计算简单)不定高(更通用)优先定高,做不到再用 ResizeObserver 测量
key 复用index业务 id业务 id,避免数据更新错位
列表变更全量替换diff 增量增量,避免抖动
上拉刷新自己写 touch用 BetterScroll中小项目自己写够;复杂场景用库

加分项

  • 渲染层用 react-window / vue-virtual-scroll-list 现成方案
  • 配合 骨架屏 缓解首次加载白屏
  • 滚动到底部 200px 时预加载下一页,体验无缝
  • requestIdleCallback 在空闲帧预渲染下一屏
  • 极端长列表用 二维虚拟化(行 + 列都虚拟)

兜底容错

场景兜底
接口失败toast + "重试"按钮 + 上一次缓存数据保留
没有更多数据"没有更多了"提示,停止 IO 监听
弱网loading 超时 5s 显示"网速较慢",但不报错
切换路由回来缓存 scrollTop 与已加载页码
数据空empty 状态图 + CTA

Q3:前端权限系统(菜单 / 按钮 / 接口三级)

背景

中后台系统标配:不同角色看到不同菜单、不同按钮、调用不同接口。

核心需求

  1. 菜单级:根据角色动态渲染左侧导航
  2. 按钮级:同一页面内不同按钮按权限隐藏/禁用
  3. 接口级:兜底,前端漏拦截时后端拒绝
  4. 角色变更后热更新,不用刷新页面
  5. 支持RBAC(角色) + ABAC(属性)混合模型

设计方案

┌─────────────────── 登录 ───────────────────┐
│  POST /login → 返回 { token, userInfo }    │
└────────────────────┬───────────────────────┘


┌─────────────────── 拉取权限 ───────────────────────┐
│  GET /perm/list → { menus, buttons, apis }        │
│  存入 Pinia/Redux:authStore                       │
└────────────────────┬──────────────────────────────┘

        ┌────────────┼────────────┬─────────────┐
        ▼            ▼            ▼             ▼
   ┌────────┐  ┌────────┐  ┌────────────┐  ┌────────┐
   │ 路由层 │  │ 菜单层 │  │ 按钮指令   │  │ Axios  │
   │ Guard  │  │ Render │  │ v-perm     │  │ 拦截器 │
   └────────┘  └────────┘  └────────────┘  └────────┘
       │           │            │              │
       │           │            │              ▼
       │           │            │      接口前端校验失败
       │           │            │      → 直接 reject
       │           │            ▼
       │           │       <button v-perm="user:delete">
       │           │       无权限则 v-if=false(隐藏)或 disabled
       │           ▼
       │      根据 menus 渲染侧边栏

   beforeEach: 当前路由 not in perms → /403

关键技术点

层级实现方式
菜单后端返菜单树,前端递归渲染
路由router.beforeEach 校验 to.meta.code in userPerms
按钮Vue: v-perm="code" 自定义指令;React: <Auth code="x"> 高阶组件
接口Axios 请求拦截器加 code 头;响应 401/403 统一跳登录 / 提示
缓存userPerms 存 Pinia,刷新时从 token 还原
角色变更后端 SSE / WebSocket 推送 → 前端清缓存重新拉权限

核心代码(见 examples/03-permission.js):

js
// Pinia
export const useAuth = defineStore('auth', {
  state: () => ({ menus: [], buttons: new Set(), apis: new Set() }),
  actions: {
    async fetch() {
      const { menus, buttons, apis } = await api.perm();
      this.menus = menus;
      this.buttons = new Set(buttons);
      this.apis = new Set(apis);
    },
    can(code) { return this.buttons.has(code); },
  }
});

// Vue 指令
app.directive('perm', {
  mounted(el, { value }) {
    if (!useAuth().can(value)) el.parentNode?.removeChild(el);
  }
});

// 路由守卫
router.beforeEach((to) => {
  const code = to.meta.code;
  if (code && !useAuth().can(code)) return { path: '/403' };
});

// Axios 拦截
http.interceptors.response.use(null, (err) => {
  if (err.response?.status === 403) message.error('无权限');
  if (err.response?.status === 401) location.href = '/login';
  return Promise.reject(err);
});

难点与权衡

难点方案 A方案 B推荐
路由表全部预置,按权限隐藏按权限动态 addRoute动态,更安全,菜单和路由一致
按钮控制v-ifv-show / disabledv-if,不渲染更安全
权限模型RBACABAC中后台 RBAC 够用;ToB 复杂场景 RBAC + ABAC
数据权限后端做前端做必须后端(数据可见列表过滤)

加分项

  • 权限编码规范化:模块:对象:动作order:detail:edit
  • 支持多租户:用户的 perms 加上 tenantId 命名空间
  • 超级管理员走通配 *:*:*,避免维护爆炸
  • 提供 <Auth fallback={<Tooltip>无权限</Tooltip>}> 替代单纯隐藏,提升体验
  • 配合操作日志:用户每次操作上报,便于审计

兜底容错

  • 前端拦截只是体验,不能信任 → 后端必须有同等校验
  • 接口 403 → 全局 toast + 上报埋点(防止恶意篡改 token)
  • 权限拉取失败 → 回退 fallback 角色(最低权限)
  • 用户无任何菜单 → 引导到"申请权限"页

Q4:单点登录(SSO)

背景

公司有 OA、CRM、ERP、邮箱、文档……用户希望"登录一次,全部可用"。

核心需求

  1. 多个子系统只登录一次
  2. 任一子系统登出,其他系统同步登出
  3. 支持跨域子系统
  4. 支持第三方接入(OAuth 2.0)
  5. Token 安全,能对抗 CSRF / XSS

设计方案(CAS 模式 + JWT)

                                ┌──────── SSO 中心 ────────┐
                                │  https://sso.x.com       │
                                │  - 登录页                 │
                                │  - 颁发 ticket            │
                                │  - 全局 session           │
                                └─────────┬─────────────────┘

        ┌─────────────────────┬───────────┼───────────┬────────────────┐
        ▼                     ▼           ▼           ▼                ▼
   ┌────────┐           ┌────────┐  ┌────────┐  ┌────────┐      ┌────────┐
   │  OA    │           │  CRM   │  │  ERP   │  │ 邮箱   │      │ 文档   │
   │ a.x.com│           │ b.x.com│  │ c.x.com│  │d.x.com │      │ e.x.com│
   └────────┘           └────────┘  └────────┘  └────────┘      └────────┘

登录流程:
1. 用户访问 a.x.com,未登录 → 302 跳到 sso.x.com/login?service=a.x.com
2. SSO 拿用户登录 → 设置 SSO 域 cookie(标记已登录)→ 302 回 a.x.com?ticket=xxx
3. a.x.com 拿 ticket 调 sso.x.com/validate → 拿到 userInfo → 设置 a.x.com 的 cookie
4. 用户访问 b.x.com → 302 sso.x.com → 已有 cookie → 直接颁发 ticket → 回跳

登出流程:
1. 用户在 a.x.com 点击退出 → 通知 sso 中心
2. SSO 通知所有已登录子系统(保存的回调 URL)
3. 各子系统清除本地登录态

关键技术点

技术说明
CAS (Central Authentication Service)经典 SSO 协议,ticket 中转
OAuth 2.0第三方授权(GitHub / 微信登录)
OIDCOAuth 2.0 + Identity Layer,主流方案
JWT自包含 token,避免每次查 session
Cookie SameSite=None; Secure跨域子系统共享 cookie 必须
CORS + withCredentials跨域接口带 cookie
iframe 同步登出在主域加 iframe 监听消息
BroadcastChannel同源多 Tab 同步登出

核心代码

js
// 子系统检测登录状态
function ensureLogin() {
  const token = getCookie('token');
  if (token) return Promise.resolve();
  return new Promise((resolve) => {
    const ticket = new URL(location.href).searchParams.get('ticket');
    if (ticket) {
      validateTicket(ticket).then(setLocalToken).then(resolve);
    } else {
      location.href = `https://sso.x.com/login?service=${encodeURIComponent(location.href)}`;
    }
  });
}

// 全局登出
function logout() {
  fetch('https://sso.x.com/logout', { credentials: 'include' });
  // 通知本系统其他 Tab
  new BroadcastChannel('auth').postMessage({ type: 'logout' });
}

难点与权衡

难点方案 A方案 B推荐
Token 存储localStoragehttpOnly CookieCookie,防 XSS(localStorage 可被脚本读取)
Session vs JWTSession(服务端存)JWT(自包含)中小Session,大流量JWT
跨域CORSpostMessageCORS 现代浏览器都支持
登出同步轮询iframe / WebSocketiframe + postMessage 最通用

加分项

  • 静默续期:access_token 5 分钟,refresh_token 7 天
  • CSRF 防护:双重 Cookie / SameSite=Lax
  • 设备指纹:异常设备登录二次验证
  • 审计日志:登录 IP、设备、时间全记录
  • OAuth 第三方接入:直接接微信、企业微信、钉钉

兜底容错

  • SSO 中心宕机 → 本地缓存 token 继续可用,过期后失败回登录
  • 跨域 cookie 被禁用 → 降级到 URL 传 token(仅一次)
  • 时钟漂移导致 JWT 失效 → 客户端容差 5 分钟

Q5:SPA 首屏优化

背景

Vue/React SPA 默认首屏慢:HTML 是空 div,加载 JS → 解析 → 渲染才有内容,FCP/LCP 经常 3s+。

核心需求

  1. FCP < 1s,LCP < 2.5s(Google 评级 Good)
  2. 不影响 SEO
  3. 不影响交互响应(FID < 100ms)
  4. 兼容低端机

设计方案

┌─────────────────────────────────────────────────────────────┐
│                       首屏优化 6 大维度                       │
├─────────────────────────────────────────────────────────────┤
│                                                              │
│  ① 网络层                                                     │
│     - HTTP/2 多路复用 + Server Push                           │
│     - CDN + 边缘缓存                                          │
│     - 预连接 <link rel=preconnect>                            │
│     - DNS Prefetch                                           │
│                                                              │
│  ② 渲染层                                                     │
│     - SSR (Next/Nuxt) → 首屏 HTML 直出                        │
│     - SSG → 极致首屏,CDN 静态                                │
│     - 骨架屏 → 感知性能                                       │
│     - <link rel="preload"> 关键资源                           │
│                                                              │
│  ③ 资源层                                                     │
│     - 路由懒加载 import()                                     │
│     - Tree-shaking 移除死代码                                 │
│     - 图片懒加载 IntersectionObserver                         │
│     - 图片格式 WebP / AVIF                                    │
│     - 字体子集化 (font-spider)                                │
│                                                              │
│  ④ 缓存层                                                     │
│     - Service Worker 缓存                                     │
│     - HTTP 缓存(Cache-Control / ETag)                       │
│     - localStorage 缓存接口数据                               │
│                                                              │
│  ⑤ JS 优化                                                    │
│     - Code Splitting                                         │
│     - 动态 polyfill                                          │
│     - 预渲染(Vite SSG / React Snap)                         │
│     - Hydration 优化(React 18 Selective)                   │
│                                                              │
│  ⑥ 体验层                                                     │
│     - 骨架屏 / Loading Placeholder                            │
│     - PWA 离线访问                                            │
│     - Web Vitals 实时监控                                    │
│                                                              │
└─────────────────────────────────────────────────────────────┘

关键技术点

优化项收益实现
SSRLCP -50%Next.js / Nuxt / Vite SSR
路由懒加载首屏 JS -60%() => import('./Page')
骨架屏FCP 感知 -30%预渲染插件
CDNTTFB -70%静态资源全量上 CDN
HTTP/2多路复用Nginx 配置
图片懒加载首屏请求 -50%loading="lazy" 或 IO
WebP体积 -30%<picture>
Brotli 压缩体积 -20%Nginx brotli on;
Preload关键资源提前<link rel="preload" as="font">
Tree Shaking体积 -10%用 ESM 写代码,构建工具自动

难点与权衡

难点方案 A方案 B推荐
SSR vs CSRCSR 简单SSR 首屏快内容站SSR,工具站CSR + SW
拆包粒度大块(少请求)细块(缓存友好)路由级 + 公共 chunk
预渲染 vs SSR预渲染(构建慢)SSR(运行慢)静态页预渲染,动态页SSR
骨架屏自己写工具生成工具(page-skeleton-webpack-plugin)

加分项

  • 关键 CSS 内联:首屏必需 CSS 直接 inline 到 HTML
  • HTTP/3 + QUIC:弱网首屏快 30%
  • Bundle Analyzer 持续优化:上 CI 卡 size budget
  • 基于 RUM 数据迭代:用户真实数据驱动优化方向
  • WASM 加速(图像处理、加密等场景)

兜底容错

  • SSR 渲染失败 → 降级返回普通 HTML,CSR 接管
  • CDN 挂了 → DNS CNAME 切回源站
  • 图片加载失败 → 默认占位图
  • 弱网超时 → 显示"加载较慢,请检查网络" + 重试按钮

Q6:前端埋点系统(曝光 / 点击 / 性能 / 异常)

背景

业务方需要"用户怎么用产品"的数据:哪个按钮点击多、哪个页面跳出率高、哪些性能瓶颈。

核心需求

  1. 曝光埋点:元素进入视口才算
  2. 点击埋点:点击事件 + 元素信息
  3. 性能埋点:FP / FCP / LCP / FID / CLS
  4. 异常埋点:JS 报错 / 接口失败
  5. 路由埋点:PV / UV / 停留时长
  6. 批量上报 + 离线缓存 + 采样

设计方案

┌─────────────── 业务代码 ───────────────┐
│                                         │
│  <button data-track="btn:order:submit"> │
│  <div v-track:expose="banner:home">     │
│  router.afterEach → tracker.pv()        │
│  window.onerror → tracker.error()       │
│                                         │
└────────────────┬───────────────────────┘


┌─────────────── SDK ──────────────────────────────┐
│                                                   │
│   Tracker                                         │
│    ├── click()    ──┐                            │
│    ├── expose()   ──┤                            │
│    ├── pv()       ──┼──▶  Queue (内存数组)       │
│    ├── error()    ──┤        │                   │
│    ├── perf()     ──┘        │                   │
│                              │                   │
│   触发上报:                  │                   │
│    - 队列满 10 条             │                   │
│    - 5 秒定时                ▼                   │
│    - visibilitychange  → sendBeacon              │
│    - beforeunload      → sendBeacon              │
│                              │                   │
│  离线兜底:失败 → IndexedDB ─┘                   │
└────────────────┬─────────────────────────────────┘


┌─────────────── 后端 ──────────────┐
│   POST /track   (gzip)            │
│   → Kafka → 数据仓库 → BI 看板    │
└───────────────────────────────────┘

关键技术点

模块实现
曝光IntersectionObserver,元素 50% 可见持续 1s 才算曝光
点击事件代理 document.addEventListener('click', e => ...),自动收集 data-track
PV/UVVue Router afterEach / React Router useLocation
性能PerformanceObserver + web-vitals
JS 异常window.onerror + unhandledrejection
资源异常addEventListener('error', e => ..., true) 用捕获阶段
接口异常Axios 拦截器
批量上报队列 + 定时 + 阈值
离开上报navigator.sendBeaconfetch keepalive,绝对不能用 XHR(浏览器关闭会取消)
离线缓存上报失败存 IndexedDB,下次进入页面重传
采样性能数据 10% 采样,异常数据 100%

核心代码(见 examples/06-tracker.js):

js
class Tracker {
  constructor() {
    this.queue = [];
    setInterval(() => this.flush(), 5000);
    document.addEventListener('visibilitychange', () => {
      if (document.visibilityState === 'hidden') this.flush(true);
    });
    this.bindClick();
    this.bindError();
    this.bindPerf();
  }
  bindClick() {
    document.addEventListener('click', (e) => {
      const target = e.target.closest('[data-track]');
      if (target) this.push({ type: 'click', code: target.dataset.track });
    });
  }
  bindError() {
    window.addEventListener('error', (e) => this.push({ type: 'jsError', msg: e.message, stack: e.error?.stack }));
    window.addEventListener('unhandledrejection', (e) => this.push({ type: 'promise', msg: String(e.reason) }));
  }
  bindPerf() {
    new PerformanceObserver((list) => {
      list.getEntries().forEach((e) => this.push({ type: 'perf', name: e.name, value: e.startTime }));
    }).observe({ entryTypes: ['paint', 'largest-contentful-paint'] });
  }
  push(data) {
    this.queue.push({ ...data, ts: Date.now(), uid: getUid() });
    if (this.queue.length >= 10) this.flush();
  }
  flush(isUnload = false) {
    if (!this.queue.length) return;
    const data = this.queue.splice(0);
    if (isUnload) navigator.sendBeacon('/track', JSON.stringify(data));
    else fetch('/track', { method: 'POST', body: JSON.stringify(data), keepalive: true });
  }
}

难点与权衡

难点方案 A方案 B推荐
上报方式XHR / fetchsendBeacon平时fetch,离开Beacon
数据格式JSONProtobufJSON 简单;高频用 Protobuf 体积省 30%
采样100%10%异常 100%,行为按 UID 分组采样
命名规范自由命名强约束 模块:对象:动作强约束,便于聚合
接入方式业务侵入AOP/装饰器声明式(指令 / data 属性)

加分项

  • 可视化埋点:管理后台框选元素自动生成埋点代码
  • 无埋点:全量采集 click / view,业务后选取
  • A/B 标签:埋点带实验组信息
  • 链路追踪:traceId 串起前后端,定位慢请求
  • 隐私合规:GDPR / 个保法,敏感字段脱敏

兜底容错

  • 上报接口挂 → IndexedDB 缓存,重试 3 次后丢
  • 数据量超出 → 队列截断(保留最近 100 条)
  • SDK 自身崩溃 → try-catch 包裹,绝不影响业务
  • 大数据 hot key → 客户端聚合后上报

Q7:前端错误监控系统

背景

线上偶发 bug,用户反馈不清不楚,开发查不到日志。需要主动捕获、定位、告警。

核心需求

  1. 捕获 JS 报错 / Promise 异常 / 资源加载失败 / 接口异常
  2. 上报关键上下文(用户 / 页面 / 浏览器 / 操作录像)
  3. Source Map 还原压缩后的栈
  4. 去重 + 聚合:相同错误不重复告警
  5. 告警阈值:5 分钟内 100 次自动通知

设计方案

┌────────────────── 客户端 SDK ──────────────────┐
│                                                 │
│  ┌────────────────────┐                         │
│  │  错误捕获层         │                         │
│  │  - window.onerror   │                         │
│  │  - unhandledrejection│                        │
│  │  - addEventListener('error', cap=true) │     │
│  │  - axios.interceptors.response          │    │
│  │  - try/catch wrap (Vue.errorHandler)    │    │
│  └─────────┬───────────┘                        │
│            │                                    │
│            ▼                                    │
│  ┌────────────────────┐                         │
│  │  数据增强层         │                         │
│  │  + url / userAgent  │                         │
│  │  + uid / sessionId  │                         │
│  │  + breadcrumbs (用户最近 50 个操作) │         │
│  │  + UA / 屏幕 / 网络 │                         │
│  └─────────┬───────────┘                        │
│            │                                    │
│            ▼                                    │
│  ┌────────────────────┐                         │
│  │  上报层             │                         │
│  │  - 去重 (相同 hash 1 分钟内只发 1 次)│        │
│  │  - 节流上报         │                         │
│  │  - sendBeacon       │                         │
│  └─────────┬───────────┘                        │
└────────────┼────────────────────────────────────┘


┌────────────── 服务端 ──────────────────┐
│  接收 → 解析 → Source Map 反查 → 写库   │
│  → 聚合分组 → 告警(钉钉/企微/邮件)     │
└────────────────────────────────────────┘

关键技术点

类型监听方式注意
JS 同步异常window.onerror跨域脚本要 crossorigin + Access-Control-Allow-Origin
Promise 异常unhandledrejection全局兜底未 catch 的 Promise
资源加载失败addEventListener('error', cb, true) 捕获阶段onerror 拿不到
Vue 异常Vue.config.errorHandler拦截组件渲染错误
React 异常ErrorBoundary 组件hooks 用 useErrorBoundary
Axios 异常拦截器4xx/5xx 都上报
白屏检测MutationObserver 1s 后无 DOM 节点兜底
性能异常PerformanceObserver LCP > 4s

核心代码(见 examples/07-error-monitor.js):

js
function setup() {
  window.addEventListener('error', (e) => {
    if (e.target?.tagName) {
      report({ type: 'resource', src: e.target.src || e.target.href, tag: e.target.tagName });
    } else {
      report({ type: 'jsError', msg: e.message, stack: e.error?.stack, line: e.lineno });
    }
  }, true);

  window.addEventListener('unhandledrejection', (e) => {
    report({ type: 'promise', msg: String(e.reason), stack: e.reason?.stack });
  });

  axios.interceptors.response.use(null, (err) => {
    report({ type: 'api', url: err.config?.url, status: err.response?.status, msg: err.message });
    return Promise.reject(err);
  });

  Vue.config.errorHandler = (err, vm, info) => report({ type: 'vue', msg: err.message, stack: err.stack, info });
}

难点与权衡

难点方案 A方案 B推荐
Source Map上传到 CDN上传到监控服务上传到服务,安全(不公开)
去重全文 hash(msg + stack 前 3 行) hash后者,避免抖动
用户操作录像rrweb自己写rrweb(成熟)
告警通道邮件企微/钉钉机器人机器人,及时

加分项

  • Source Map 服务自动反查(生产不带 sourceMappingURL)
  • rrweb 录屏:复现用户操作轨迹(可控制采样率)
  • 自动归因:根据 PR 时间 / 提交人自动 @ 责任人
  • 趋势告警:基线偏离 3σ 才告警,避免噪音
  • 流量保护:错误风暴时自动降级上报

兜底容错

  • 监控 SDK 本身要 try-catch 全包,绝不能崩溃业务
  • 上报失败重试 3 次后丢弃
  • 跨域错误 Script error. → 提示用户加 crossorigin
  • 死循环错误抑制:1 分钟内同 hash 只上报 5 次

Q8:多 Tab 通信方案对比

背景

同一用户开了 3 个 Tab,A Tab 退出登录,希望 B、C 同步登出;A Tab 收到新消息,B、C 也要刷新红点。

核心需求

  1. 同源跨 Tab 通信
  2. 支持广播(一对多)
  3. 支持定向(一对一)
  4. 兼容性好,覆盖到 IE / 老 Safari

5 种方案全对比

方案原理优点缺点兼容性
BroadcastChannel浏览器原生广播 API最简洁IE 不支持Edge/FF/Chrome ✅
localStorage storage 事件存储变化触发其他 Tab 事件兼容好触发 Tab 自身收不到全部 ✅
SharedWorker共享 Worker 中转长连接、可计算iOS Safari 不支持部分
IndexedDB + 轮询数据库变更检测适合数据量大实时性差全部
Service WorkerSW 中转消息离线也支持复杂现代浏览器

设计方案:组合最佳实践

┌────────── 主方案 ──────────┐    ┌──── 降级方案 ────┐
│  BroadcastChannel          │ ── 不支持 ──▶│ localStorage │
│  channel.postMessage(msg)  │              │ storage 事件 │
└─────┬──────────────────────┘              └──────┬───────┘
      │                                             │
      └──────────────────┬──────────────────────────┘

              统一封装为 BusEmitter

       ┌─────────────────┼─────────────────┐
       ▼                 ▼                 ▼
   Tab A             Tab B             Tab C
   on('logout')      on('logout')      on('logout')

核心代码

js
class TabBus {
  constructor(name) {
    this.name = name;
    this.listeners = {};
    if (typeof BroadcastChannel !== 'undefined') {
      this.bc = new BroadcastChannel(name);
      this.bc.onmessage = (e) => this.dispatch(e.data);
    } else {
      window.addEventListener('storage', (e) => {
        if (e.key === `__tabbus__${name}` && e.newValue) {
          this.dispatch(JSON.parse(e.newValue));
        }
      });
    }
  }
  emit(event, payload) {
    const data = { event, payload, ts: Date.now() };
    if (this.bc) this.bc.postMessage(data);
    else localStorage.setItem(`__tabbus__${this.name}`, JSON.stringify(data));
  }
  on(event, fn) { (this.listeners[event] ||= []).push(fn); }
  dispatch({ event, payload }) {
    this.listeners[event]?.forEach((fn) => fn(payload));
  }
}

// 使用
const bus = new TabBus('auth');
bus.on('logout', () => location.href = '/login');
// 在 A Tab:
bus.emit('logout');

难点与权衡

场景推荐方案
简单广播BroadcastChannel + localStorage 兜底
跨域 TabpostMessage + iframe 中转(同 SSO)
长连接SharedWorker(一个 WebSocket 多 Tab 共享)
离线消息Service Worker
数据同步IndexedDB + change listener(如 Dexie)

加分项

  • 用 SharedWorker 共用一个 WebSocket,节省连接数
  • 主 Tab 选举(leader election):一个 Tab 负责轮询接口,其他 Tab 共享数据
  • BroadcastChannel 上加 traceId,便于调试
  • 提供 request/response 模式(一对一通信)

兜底容错

  • BroadcastChannel 失败自动降级 localStorage
  • 消息体大于 5MB(localStorage 上限)时分片
  • 监听 storage 事件要做 try-catch 解析
  • Tab 关闭时清理订阅

Q9:复杂表单设计(嵌套 / 动态字段 / 校验联动)

背景

ToB 后台经常有"配置型表单":100+ 字段、嵌套 List、字段间联动(A 选 1 → B 显示,A 选 2 → B 隐藏)。

核心需求

  1. 动态字段:增减子表单(数组型)
  2. 嵌套:表单里套表单
  3. 联动:字段值变化触发其他字段变化
  4. 校验:同步 / 异步 / 跨字段校验
  5. 回填 / 重置 / 草稿保存
  6. 性能:100+ 字段不卡

设计方案

┌──────────── 表单架构 ────────────┐
│                                   │
│  Schema (JSON 配置)               │
│      │                            │
│      ▼                            │
│  ┌─────────────────┐              │
│  │  Form Engine    │              │
│  │  - 字段树管理    │              │
│  │  - 值的双向同步  │              │
│  │  - 校验调度      │              │
│  │  - 联动调度      │              │
│  └────────┬────────┘              │
│           │                       │
│           ▼                       │
│  ┌─────────────────┐              │
│  │   Field 组件     │              │
│  │   <Input>        │              │
│  │   <Select>       │              │
│  │   <DatePicker>   │              │
│  │   <FormList>     │              │
│  └─────────────────┘              │
└───────────────────────────────────┘

Schema 示例

js
const schema = [
  { name: 'type', type: 'radio', options: ['企业', '个人'], rules: [{ required: true }] },
  { name: 'companyName', type: 'input', when: (val) => val.type === '企业', rules: [{ required: true }] },
  { name: 'idCard', type: 'input', when: (val) => val.type === '个人', rules: [{ pattern: /^\d{18}$/ }] },
  {
    name: 'contacts', type: 'list', itemFields: [
      { name: 'name', type: 'input' },
      { name: 'phone', type: 'input', rules: [{ pattern: /^1\d{10}$/ }] },
    ]
  },
];

关键技术点

难点实现
字段管理Form 实例维护 values / errors / touched Map
联动when(values) 函数判断显示,change 时重新计算依赖项
嵌套字段路径用点 address.city 或数组 contacts.0.phone
校验用 yup / zod 描述 schema;async-validator 老牌选择
跨字段校验校验函数接收 (value, allValues)
性能用细粒度订阅(只重渲染变更字段,不全量 render)
草稿每 5s 存 localStorage,进入页面回填

难点与权衡

难点方案 A方案 B推荐
表单库手写Formily / antd FormFormily(专为复杂表单)
状态管理整体一个对象字段级 atomatom(jotai 思路),性能好
联动配置 when自动响应(MobX)配置式,可序列化
校验时机onChangeonBluronBlur(不打断输入)

加分项

  • JSON Schema 驱动:完全可视化配置表单
  • TypeScript 类型推导:字段类型从 schema 推
  • 可视化拖拽:拖出表单设计器
  • AI 自动校验规则:根据字段名推测正则
  • 防回退丢失:路由切换前 confirm

兜底容错

  • 接口提交失败 → 保留输入,按钮恢复
  • 字段配置异常 → 渲染 fallback "字段配置错误"
  • 联动循环依赖 → 检测并打断
  • 草稿恢复时校验已变化的 schema

Q10:富文本编辑器选型与实现思路

背景

需要做评论 / 文档 / 邮件编辑器,比 textarea 高级,比 Word 简单。

选型对比

编辑器类型优点缺点推荐场景
Quill自研模型简洁、文档全扩展难评论 / 简单文档
TinyMCE / CKEditor老牌功能全体积大企业邮件 / CMS
ProseMirror框架极致灵活、协同好学习曲线陡需深度定制
SlateReact 生态API 优雅大版本不稳React 项目
Lexical (Meta)现代性能极好、A11y较新新项目
TiptapProseMirror 包装易用 + 强大商业插件收费协同文档

实现核心架构

┌────────── 富文本三层模型 ──────────┐
│                                     │
│  ① 数据层(Document Model)         │
│     树状结构,节点有类型/属性/子节点│
│     {type:'doc',content:[...]}      │
│                                     │
│  ② 视图层(Renderer)               │
│     根据数据生成 DOM                │
│     contenteditable 容器            │
│                                     │
│  ③ 交互层(Plugins)                │
│     - 输入处理(IME)               │
│     - 选区管理(Selection / Range) │
│     - 命令系统(execCommand)       │
│     - 快捷键(Ctrl+B 等)           │
│     - 上传图片 / 粘贴               │
└─────────────────────────────────────┘

关键技术点

难点实现
数据模型自己设计 JSON 树(如 ProseMirror Schema),别用 HTML 字符串(脏数据多)
DOM 与模型同步MutationObserver 监听 DOM 变化反向更新模型;模型变化重渲染 DOM
选区window.getSelection() + Range API
粘贴paste 事件,过滤 HTML,保留必要样式
拖拽dragstart / dragover / drop
图片上传拦截粘贴 / 拖拽 / 工具栏,转 Blob 上传,回填 URL
协同OT (Operational Transform) 或 CRDT (Y.js)
历史栈命令模式 + 双栈(undo / redo)

难点与权衡

难点选择
contenteditable vs Canvascontenteditable 兼容性好;Canvas(如腾讯文档)性能极好但门槛高
自研 vs 用框架99% 场景别自研,水深无比
协同简单选 OT,复杂选 CRDT (Y.js / Automerge)
数据存储JSON > HTML > Markdown,JSON 最灵活

加分项

  • 协同:基于 Y.js + WebSocket
  • AI 续写:调用 GPT 流式插入
  • @提及 / 表情 / 链接预览
  • 大文档虚拟滚动(lexical 内置)

兜底容错

  • 输入丢失:每 5s 自动保存草稿到 localStorage
  • IME 中文输入:compositionstart / end 期间不触发任何处理
  • 粘贴富文本污染:白名单过滤,移除 style script
  • 移动端虚拟键盘弹起后 input 被遮 → scrollIntoView

Q11:实时聊天界面(WebSocket + 消息状态)

背景

IM、客服、协作工具的核心。

核心需求

  1. 实时收发消息(WebSocket)
  2. 消息状态:发送中 / 已发送 / 已送达 / 已读 / 失败
  3. 离线消息 + 历史消息分页
  4. 断线重连
  5. 多端同步

设计方案

┌─────────── 客户端 ────────────┐
│                                │
│  ChatStore (Pinia/Zustand)     │
│   - sessions[]                 │
│   - messages: {sessionId: []}  │
│   - draft / unread / typing    │
│                                │
│  WSManager                     │
│   - connect / reconnect        │
│   - heartbeat (ping/pong)      │
│   - send / receive             │
│   - subscribe(event, fn)       │
│                                │
│  MessageQueue (发送中)         │
│   - 本地立即上屏               │
│   - status: pending → sent     │
│   - 超时重发                   │
│                                │
└────────┬───────────────────────┘
         │ WSS

┌─────────── 后端 ──────────────┐
│  长连接网关 → 消息中心 → 存储 │
└───────────────────────────────┘

消息状态机

[本地输入] → [pending] ──发送成功─▶ [sent] ─对端 ack──▶ [delivered] ─对端阅读─▶ [read]

            超时/失败

            [failed] ──手动重试──▶ [pending]

关键技术点

模块实现
WebSocketnew WebSocket(url) + 重连 + 心跳
重连指数退避 1s → 2s → 4s → ... 最大 30s
心跳每 30s 发 ping,60s 没收到 pong 重连
乐观更新发送瞬间本地上屏(pending 状态)
去重客户端 clientMsgId,服务端用此判断幂等
顺序服务端 server seq + 客户端排序
历史消息翻页用 cursor (lastMsgId)
离线消息上线后 pull 最近 N 条
多端同步服务端通知所有在线设备
未读进入会话清零 + 通知服务端

核心代码

js
class WSManager {
  constructor(url) {
    this.url = url; this.handlers = {};
    this.connect();
  }
  connect() {
    this.ws = new WebSocket(this.url);
    this.ws.onopen = () => { this.flushQueue(); this.startHeartbeat(); };
    this.ws.onmessage = (e) => this.dispatch(JSON.parse(e.data));
    this.ws.onclose = () => this.reconnect();
  }
  reconnect() {
    this.retry = (this.retry || 1000) * 2;
    setTimeout(() => this.connect(), Math.min(this.retry, 30000));
  }
  startHeartbeat() {
    this.hb = setInterval(() => {
      if (this.ws.readyState === 1) this.ws.send('{"type":"ping"}');
    }, 30000);
  }
  send(msg) {
    if (this.ws.readyState === 1) this.ws.send(JSON.stringify(msg));
    else (this.queue ||= []).push(msg);
  }
  // ...
}

难点与权衡

难点方案 A方案 B推荐
长连接WebSocketSSEWS(双向)
消息列表全部存内存虚拟列表历史长的会话虚拟列表
顺序服务端 seq客户端 ts服务端 seq(ts 不可信)
离线上线全 pull增量 pull增量 + 最大限制

加分项

  • 消息端到端加密(Signal 协议)
  • @提及推送 / 全员消息广播
  • 消息撤回(2 分钟内)
  • 输入中状态 typing indicator
  • 消息搜索(全文索引)

兜底容错

  • WS 断开 → UI 显示"重连中"
  • 发送失败 → 红色感叹号 + 重试按钮
  • 服务端踢出 → 提示重新登录
  • 消息超大 → 自动转图片附件

Q12:撤销重做画板

背景

设计工具、流程图工具、白板的核心能力。

核心需求

  1. 画图(直线 / 矩形 / 圆 / 自由笔触)
  2. 撤销 / 重做(Ctrl+Z / Ctrl+Shift+Z)
  3. 多对象选中 / 移动 / 缩放
  4. 性能流畅(60fps)
  5. 序列化保存

设计方案:命令模式

┌─────────────── 编辑器 ────────────────┐
│                                        │
│  ┌──────────┐                          │
│  │  Canvas  │ ◀── render(state)        │
│  └──────────┘                          │
│       ▲                                │
│       │                                │
│  ┌──────────┐                          │
│  │  State   │ {shapes:[...]}           │
│  └──────────┘                          │
│       ▲                                │
│       │                                │
│  ┌─────────────────────┐               │
│  │  HistoryManager      │               │
│  │  undoStack: Command[]│               │
│  │  redoStack: Command[]│               │
│  └─────────────────────┘               │
│       ▲                                │
│       │                                │
│  ┌─────────────────────┐               │
│  │  Command 接口         │               │
│  │  - execute()         │               │
│  │  - undo()            │               │
│  └─────────────────────┘               │
│  Implementations:                       │
│   AddShapeCommand                       │
│   MoveShapeCommand                      │
│   DeleteShapeCommand                    │
│   StyleChangeCommand                    │
└────────────────────────────────────────┘

关键技术点

模块实现
画布Canvas 2D 或 SVG,复杂场景用 PixiJS / Konva
命令模式每个操作封装为 Command,有 execute / undo
栈管理undoStack 限长 100;新操作清空 redoStack
拾取hitTest(坐标判断在哪个 shape 内)
渲染requestAnimationFrame 重绘脏区
性能离屏 Canvas / OffscreenCanvas / 分层(背景 + 内容 + 选中)
序列化shapes 数组转 JSON,反向 deserialize

核心代码

js
class History {
  constructor() { this.undoStack = []; this.redoStack = []; }
  do(cmd) { cmd.execute(); this.undoStack.push(cmd); this.redoStack = []; }
  undo() { const c = this.undoStack.pop(); if (c) { c.undo(); this.redoStack.push(c); } }
  redo() { const c = this.redoStack.pop(); if (c) { c.execute(); this.undoStack.push(c); } }
}

class AddShapeCommand {
  constructor(state, shape) { this.state = state; this.shape = shape; }
  execute() { this.state.shapes.push(this.shape); }
  undo() { this.state.shapes.pop(); }
}

难点与权衡

难点方案 A方案 B推荐
撤销实现状态快照(immer)命令模式命令模式(粒度细,省内存)
渲染全量重绘脏区更新简单全量;复杂脏区
高频操作(拖拽)每帧新 Command拖拽完合并为 1 个合并,否则栈炸
大画布一张大 Canvas切片渲染切片 + 视口剔除

加分项

  • 多人协作:每个 Command 通过 WS 广播,本地 + 远程合并
  • 自动保存:每 30s 序列化存 IndexedDB
  • 操作录像 → 回放
  • WebWorker 离屏渲染(OffscreenCanvas)

兜底容错

  • 栈过大 → 截断最旧命令
  • 操作失败 → catch 后从快照恢复
  • 浏览器崩溃 → 草稿恢复

Q13:微前端架构(qiankun vs Module Federation)

背景

巨型管理后台多团队协作,单仓库部署慢、技术栈难统一。

核心需求

  1. 子应用独立开发 / 部署
  2. 主应用聚合,路由分发
  3. 子应用之间通信
  4. 技术栈无关(React / Vue 共存)
  5. 样式 / JS 隔离

主流方案对比

方案原理优点缺点适合
iframe浏览器原生隔离最稳体验差、通信难完全独立子应用
qiankun (single-spa)JS 沙箱 + Style 隔离主流,文档全需要主应用配置多中后台
Module FederationWebpack 5 远程加载共享依赖、打包优强依赖 Webpack现代项目
Web Components浏览器原生标准、轻状态管理麻烦简单组件
micro-app (京东)Web Components + 沙箱接入简单生态小京东系
Garfish (字节)qiankun 思路性能优生态小字节系

qiankun 架构

┌──────────── 主应用 (Base) ────────────┐
│  - 路由表:/app1 → app1, /app2 → app2 │
│  - 注册子应用                          │
│  - 全局状态 / 通信中心                 │
│                                        │
│   registerMicroApps([                  │
│     { name:'app1', entry:'http://...', │
│       container:'#sub', activeRule:'/app1' },│
│   ]);                                  │
│   start();                             │
└────────┬─────────────┬─────────────────┘
         │             │
   匹配路由             │
         ▼             ▼
┌────────────┐  ┌────────────┐
│  子应用 1   │  │  子应用 2   │
│  (React)   │  │   (Vue)    │
└────────────┘  └────────────┘
   - bootstrap()
   - mount()
   - unmount()

Module Federation 架构

┌──── shell (host) ────┐    ┌──── app1 (remote) ────┐
│  webpack:            │    │  webpack:              │
│   remotes: {         │    │   exposes: {           │
│     app1: 'app1@..'  │    │     './App':'./src/App'│
│   }                  │    │   }                    │
│                      │    │                        │
│  代码:              │    │                        │
│   const App = await  │    │                        │
│     import('app1/App')   │                         │
└──────────────────────┘    └────────────────────────┘

关键技术点

难点qiankunMF
JS 隔离Proxy / Snapshot 沙箱webpack scope
CSS 隔离Shadow DOM / 加 ID 前缀scoped CSS
通信initGlobalStateshared module
资源加载import-html-entry 解析子应用 HTMLwebpack remote
路由主应用劫持 popstate客户端路由分发
共享依赖externalsshared 配置

难点与权衡

维度qiankunMF
接入成本中(子应用需改 main.js)高(webpack 配置)
运行时性能每次切换重启子应用模块级共享
适合场景后台聚合、多技术栈现代单技术栈、共享组件
生态蚂蚁开源webpack 5 原生

加分项

  • 子应用预加载:空闲时预下载未激活子应用
  • 路由级灰度:5% 用户访问新版子应用
  • 依赖去重:React、Vue 走 CDN externals
  • 统一登录态:全局 store 共享 token

兜底容错

  • 子应用加载失败 → 显示 fallback 页 + 重试
  • 子应用 JS 报错不崩主应用(沙箱 try-catch)
  • 主应用 down → 子应用可独立访问 URL(独立部署)
  • 版本不一致 → 强制刷新

Q14:前端国际化(i18n)方案

背景

产品出海 / 多语言需求。

核心需求

  1. 文案多语言
  2. 时间 / 数字 / 货币按区域格式化
  3. 复数 / 性别变体
  4. 切换语言不刷新
  5. 支持懒加载(不一次性加载所有语言包)

设计方案

┌──────────── 配置 ────────────┐
│  locales/                      │
│    zh-CN.json                  │
│    en-US.json                  │
│    ja-JP.json                  │
│  { "hello": "你好 {name}",     │
│    "count": "{n, plural,       │
│      one {1 item}              │
│      other {# items}}"}        │
└────────┬───────────────────────┘


┌──────────── i18n 引擎 ──────────┐
│  - locale 当前语言               │
│  - messages 字典                 │
│  - t(key, vars) 翻译函数         │
│  - format ICU 占位符             │
│  - 切换语言:动态 import         │
│  - fallback 找不到回 enUS        │
└────────┬────────────────────────┘


┌──────────── UI ─────────────────┐
│  Vue: $t('hello', { name:'X' }) │
│  React: useTranslation()         │
│  自动响应 locale 变化            │
└─────────────────────────────────┘

关键技术点

模块实现
库选型Vue: vue-i18n;React: react-i18next / formatjs
ICU 格式复数 / 选择 / 嵌套占位
懒加载import(./locales/${lang}.json)
持久化locale 存 localStorage / cookie
检测navigator.language 自动判断初始语言
数字 / 时间Intl.NumberFormat / Intl.DateTimeFormat
方向阿语等 RTL:<html dir="rtl">
图片 / 链接资源也要按 locale 切换

难点与权衡

难点方案 A方案 B推荐
翻译来源写死 JSONCMS 动态拉静态文案JSON,运营文案CMS
切语言整页刷新不刷新动态切不刷新,更顺滑
翻译协作工程师手填Crowdin/Phrase协作平台,避免漏翻
Key 管理嵌套扁平点分扁平,便于工具扫描

加分项

  • 自动扫描 missing key 上报
  • A/B 测试不同译文转化率
  • 富文本翻译:用占位符保留组件 <a>{0}</a>
  • 服务端预渲染对应语言(SEO)
  • AI 翻译辅助初稿

兜底容错

  • key 找不到 → 显示 key(不要空白)+ 上报
  • 语言包加载失败 → 回退到默认语言
  • 时区差:服务端时间统一 UTC,客户端格式化
  • 字符宽度差:UI 设计预留 30% 弹性

Q15:前端 A/B 测试系统

背景

新功能不知道效果,需要小流量验证。

核心需求

  1. 流量分桶(按 userId / deviceId)
  2. 变体配置(控制组 / 实验组)
  3. 实时下发(不发版改实验)
  4. 埋点关联(行为带实验组标签)
  5. 效果分析(转化率 / 留存)

设计方案

┌──────────── 实验平台后台 ────────────┐
│  - 创建实验:实验 ID / 变体 / 流量    │
│  - 用户分桶规则                        │
│  - 实时启停                            │
└────────────┬───────────────────────────┘


┌──────────── 客户端 SDK ──────────────┐
│  init: 拉取所有实验配置(缓存)        │
│  getVariant(expId): 返回 'A' / 'B'    │
│       │                                │
│       ▼                                │
│  hash(uid + expId) % 100              │
│       │                                │
│  根据流量分配落到 A or B               │
│       │                                │
│  曝光埋点:上报 user 进入哪个实验组    │
└────────────┬───────────────────────────┘


┌──────────── 业务代码 ────────────────┐
│  if (ab.getVariant('newCheckout') === 'B') {│
│    renderNewVersion();                 │
│  } else {                              │
│    renderOldVersion();                 │
│  }                                     │
└────────────────────────────────────────┘

关键技术点

模块实现
分桶算法hash(uid + expId) % 100(保证同一用户始终同组)
配置下发启动时拉一次,本地缓存;变更时 SSE 实时推送
曝光埋点进入实验立即上报(不等转化)
Sticky同一用户多次访问命中同一组
冲突管理互斥实验(A 实验和 B 实验不能同时命中)
首屏一致性SSR 时也要分桶,避免客户端切换闪烁

核心代码

js
class ABTest {
  constructor() { this.experiments = {}; this.exposed = new Set(); }
  async init(uid) {
    this.uid = uid;
    this.experiments = await fetch('/ab/config').then((r) => r.json());
  }
  getVariant(expId) {
    const exp = this.experiments[expId];
    if (!exp || !exp.enabled) return 'A';
    const bucket = this.hash(this.uid + expId) % 100;
    let acc = 0;
    for (const v of exp.variants) {
      acc += v.weight;
      if (bucket < acc) {
        this.expose(expId, v.name);
        return v.name;
      }
    }
    return 'A';
  }
  expose(expId, variant) {
    const key = expId + ':' + variant;
    if (this.exposed.has(key)) return;
    this.exposed.add(key);
    tracker.push({ type: 'abExpose', expId, variant });
  }
  hash(s) {
    let h = 0;
    for (const c of s) h = ((h << 5) - h + c.charCodeAt(0)) | 0;
    return Math.abs(h);
  }
}

难点与权衡

难点方案 A方案 B推荐
分桶服务端分客户端分客户端(不依赖网络)
配置启动拉实时推启动拉 + SSE 增量推
多实验冲突自由叠加互斥分层分层,避免相互污染
SSR不支持Cookie 分桶Cookie,前后端一致

加分项

  • 多变量实验(MVT,A/B/C/D 同时)
  • 分层实验:流量正交分层
  • 自动停损:转化率显著下降自动暂停
  • 显著性计算:UI 显示 p-value
  • 灰度回滚:一键关闭实验

兜底容错

  • SDK 初始化失败 → 全部走默认变体 A
  • 实验配置异常 → fallback 到 A
  • 上报失败 → 队列重试
  • 用户无 uid(未登录) → 用 deviceId

总结:场景题答题"金句库"

把这些话术练熟,能在面试中本能脱口而出。

场景金句
开场"我先确认几个前提:用户量 / 性能要求 / 兼容性范围"
架构"整体我会拆成 4 层:UI 层 / 逻辑层 / 数据层 / 服务层"
选型"这里有 3 个方案,我会选 X 因为……,但 Y 在 N 场景下更好"
性能"用 IO 而不是 scroll,避免 layout thrashing"
监控"上线前会做埋点,用数据验证优化效果"
兜底"如果接口失败,我会从 LocalStorage 读上次缓存兜底"
灰度"新功能 1% → 10% → 50% → 100% 灰度,配合 A/B 验证"
收尾"如果还有时间,我可以聊一下 X 的扩展性 / 协同方案 / 离线场景"

写到最后:场景题没有标准答案,体现的是思考路径。能讲清"为什么这么设计、有什么取舍、还有哪些坑",就比 80% 的候选人强。