跳到主要内容

RYZZ-面试高频问题与回答思路

RYZZ 是一个社交视频直播 + Web3 平台,基于 Next.js 13 + Turborepo Monorepo 架构。以下问答覆盖架构设计、核心技术实现、性能优化、问题挑战等面试高频方向。

面试准备建议

本文按六大方向组织问答:架构设计、核心技术实现、性能优化、问题排查、技术选型、综合能力。面试时重点准备 Q1(架构)、Q2(WebSocket)、Q6(DAuth)、Q20(重新设计),这四题几乎必问。


一、架构设计类

Q1:介绍一下你们项目的整体架构?为什么选择 Monorepo?

:RYZZ 采用 Turborepo + PNPM Workspace 的 Monorepo 架构,仓库中包含 5 个前端应用(site 主站、admin 管理后台、public/binding/landPages 三个 H5 应用)和 8 个共享包。

选择 Monorepo 的原因:

  1. 代码复用:5 个应用共享认证逻辑(packages/login)、工具函数(packages/function)、类型定义(packages/types)等,避免多仓库间的版本同步问题
  2. 原子化变更:一个 PR 可以同时修改共享包和消费它的应用,保证变更的原子性
  3. 统一工程标准:ESLint、TypeScript、Prettier 配置全局统一,通过 packages/eslint-config-custompackages/tsconfig 共享

选 Turborepo 而非 Nx 的原因:配置更轻量(一个 turbo.json 搞定),任务编排基于声明式依赖关系(dependsOn: ["^build"]),自带 Remote Cache 加速 CI 构建。PNPM 的幽灵依赖防护 + 硬链接节省磁盘空间,比 Yarn/npm Workspace 更适合大型 Monorepo。

追问:共享包是怎么组织的?

我把共享包按四层职责分层:

  • 基础设施层(constant、function、hooks、types):无业务语义,纯工具
  • 业务能力层(login、contract、report-sdk、dauth-web):封装认证、合约交互、埋点等领域能力
  • UI 表现层(ryzz-uikit):品牌 UI 组件
  • 工程配置层(tsconfig、eslint-config-custom):统一工程规范

层级之间上层依赖下层,不存在循环依赖。login 包通过 ELoginOrg 枚举支持多应用切换,一套认证代码服务 site/admin/mobile 三端。

面试加分项

回答 Monorepo 问题时,强调「四层职责分层」和「依赖方向单向流动」,这体现了架构设计能力,而不仅仅是工具使用。


Q2:WebSocket 通讯系统是怎么设计的?为什么不用 Socket.IO?

:我设计了一套三层 WebSocket 通讯架构

WebSocket 三层通讯架构
应用层:useSubscription / useMsgPushSubscription(React Hook)
→ 业务组件只关心「监听哪个事件 + 处理数据」

协议层:protobuf.ts
→ reqProtocolMap / rspProtocolMap 双向映射表
→ 自动 encode/decode,业务代码无感知
→ commentTypeMap 实现二级路由

传输层:core.ts (Ws 类)
→ 连接管理 / 心跳 / 重连 / 消息队列缓冲
→ EventEmitter 事件分发
→ MessageBatch 批量消息

为什么不用 Socket.IO?

  1. 协议开销:Socket.IO 基于 JSON + 自有握手协议,我们用 Protobuf 二进制编码,相比 JSON 节省约 50% 带宽,对直播间这种高频消息场景(弹幕、礼物、竞猜更新每秒几十条)影响很大
  2. 协议控制:我们需要定制 Protobuf 消息格式、实现两级事件路由,Socket.IO 的抽象层反而是阻碍
  3. 服务端对齐:后端也是自研 WebSocket 服务,直接用原生 WebSocket + Protobuf 更高效

追问:两级事件路由是什么意思?

这是整个通讯系统最精巧的设计。服务端通过一个统一的 comment 通道下发所有互动消息(弹幕、礼物、系统通知、竞猜结果...),客户端收到后根据消息体中的 type 字段做二次分发

两级事件路由示意
服务端 ─→ comment 通道 ─→ 客户端收到 ─→ 按 type 分发
├── type=1 → 弹幕组件
├── type=2 → 礼物特效组件
├── type=3 → 系统通知组件
└── type=4 → 竞猜更新组件

这样做的好处是「单通道多业务」——服务端只维护一个 comment 通道,不需要为每种消息类型建立独立的通道;客户端通过 commentTypeMap 映射表做二次路由,新增消息类型只需在映射表中加一行,不影响传输层。

追问:消息队列缓冲是做什么的?

WebSocket 连接建立需要时间(握手 + 认证),在此期间业务代码可能已经开始发送消息。我在 Ws 类中实现了一个消息缓冲队列:连接未就绪时,消息暂存到队列中;连接建立后,批量发送队列中的消息。这避免了「组件挂载比 WS 连接快」导致的消息丢失问题。


Q3:15 层 Provider 嵌套是怎么回事?不会有性能问题吗?

_app.tsx 中有 15 层 Provider,看起来很多,但每一层都有明确的职责:

_app.tsx Provider 嵌套顺序
RecoilRoot → i18n → NextUI → WagmiConfig → Auth → Portal
→ Page → WebSocket → CustomWrapper → Currency → Activity
→ Betting → Wallet → Modal → Layout

设计原则:每个 Wrapper 只负责一个跨关注点(认证、国际化、WebSocket、货币数据...),严格按依赖顺序排列

  • WebSocket 必须在 Auth 之后(WS 连接需要 token)
  • Currency 必须在 WebSocket 之后(余额通过 WS 推送更新)
  • Betting 必须在 Currency 之后(投注需要知道余额)

关于性能:React Context/Provider 的性能开销不在于嵌套层数,而在于 value 变化时的重渲染范围。我们的 Provider 有两类:

  1. 静态 Provider(RecoilRoot、NextUI、i18n):value 几乎不变,无重渲染开销
  2. 动态 Provider(Currency、Betting):内部使用 Recoil Atom 细粒度订阅,只有订阅了特定 Atom 的组件才会重渲染,不会触发全树更新

追问:如果让你重新设计,会怎么改?

我会考虑用 compose 函数将多个 Provider 组合,降低 JSX 嵌套深度,提升代码可读性。另外,一些依赖关系不强的 Provider(如 Modal、Activity)可以合并为一个 InteractiveProvider,减少层数。但从功能正确性角度看,当前设计没有问题。


Q4:跨应用的第三方登录是怎么实现的?

:我设计了一个 binding 应用作为 OAuth 回调中枢,解决了「多应用 × 多登录提供商」的 N×M 问题。

为什么需要独立应用? 每个 OAuth 提供商(Google、Twitter、Apple)都要求注册回调 URL。如果 site、admin、mobile 各自注册,就需要管理 3×3=9 个回调配置。而 binding 作为统一中枢,只需 3 个回调 URL,新增登录方式只改 binding 一处。

核心流程

  1. site/admin 通过 window.open() 打开 binding 的 OAuth 页面
  2. binding 重定向到 Google/Twitter/Apple
  3. 用户授权后回调到 binding
  4. binding 完成 Token 交换,通过 双通道 把结果传回原始页面

双通道设计

  • 主通道window.postMessage — 现代浏览器直接使用
  • 降级通道Cookie + window.focus — Safari 因隐私限制可能阻止跨域 postMessage,降级为写 Cookie 后 focus 回原窗口

安全措施:全流程使用 PKCE(Proof Key for Code Exchange),防止授权码在传输中被截获。


Q5:Docker 部署是怎么设计的?怎么处理 Monorepo 的构建?

:核心是 turbo prune + Docker 四阶段构建

Monorepo 的 Docker 难点:直接 COPY . . 会把整个仓库(30+ 个包)都放进镜像,体积和构建时间都不可接受。

解决方案turbo prune site --docker 会精准提取 site 及其依赖的共享包,输出三部分:

  • out/json/ — 仅相关包的 package.json(用于依赖安装缓存层)
  • out/full/ — 仅相关包的源码
  • out/pnpm-lock.yaml — 精简后的锁文件

四阶段构建

Dockerfile 四阶段构建
base     → Node.js 基础镜像 + pnpm 安装
builder → COPY json → pnpm install(代码不变时依赖安装命中缓存)
installer → COPY full → turbo build(仅构建 site 及其依赖)
runner → 仅复制 standalone 产物 + PM2 运行时

最终镜像只包含 Next.js standalone 产物和 PM2,体积很小。另外容器以非 root 用户运行,符合安全最佳实践。


二、核心技术实现类

Q6:DAuth MPC 钱包是怎么实现的?什么是 2-of-3 分片?

:DAuth 是我们实现 Web3 无感化 的核心——让用户通过邮箱/手机号就能拥有链上钱包,不需要记助记词。

2-of-3 分片指的是 MPC(多方计算)签名方案中的门限设置:

  • 私钥被拆分为 3 个分片(key shares)
  • 任意 2 个分片 就能完成签名,无需还原完整私钥
  • 3 个分片分别由:用户设备、DAuth 服务器、备份服务器 持有

技术原理(GG20 协议)

  1. 密钥生成:三方通过 DKG(分布式密钥生成)各自生成自己的分片,全程没有任何一方持有完整私钥
  2. 签名:用户发起交易时,用户设备分片 + DAuth 服务器分片联合计算签名(2-of-3 门限),交易上链
  3. 密钥恢复:用户设备丢失时,DAuth 服务器分片 + 备份分片可以帮助用户在新设备上恢复

用户体验:邮箱注册 → 自动生成钱包地址 → 发送代币只需点击按钮 → 后台自动完成 MPC 签名 + 上链。整个过程用户不接触私钥、不记助记词。

追问:什么是 EIP-4337 账户抽象?和 MPC 有什么关系?

EIP-4337 是以太坊的账户抽象标准,它允许用智能合约作为钱包(而非传统的 EOA 外部账户)。核心好处:

  • Gas 代付:用户不需要持有 ETH 来支付 Gas 费,由 Relayer(Paymaster)代付
  • 批量操作:一笔交易可以打包多个操作
  • 自定义验签:可以用 MPC 签名替代传统的 ECDSA 签名

MPC 和 EIP-4337 是互补关系:MPC 解决「私钥管理」问题(用户不用记助记词),EIP-4337 解决「Gas 费和交互体验」问题(用户不用买 ETH)。两者结合实现了完全的 Web3 无感化。

核心回答要点

DAuth MPC 是本项目最有技术深度的模块。面试时可以从用户体验出发("邮箱注册即拥有链上钱包"),再深入到技术原理(2-of-3 分片、GG20 协议),最后延伸到 EIP-4337 的互补关系。


Q7:Recoil 状态管理是怎么设计的?为什么选 Recoil 不选 Redux?

:我们使用 Recoil 的原子化状态管理,项目中有 17+ 个 Atom,按业务域严格分离。

设计原则

  • 按业务域划分 AtomcurrencyAtom(货币余额)、channelAtom(当前频道)、bettingAtom(竞猜状态)、giftAtom(礼物数据)等
  • 每个 Atom 独立订阅:组件只订阅需要的 Atom,避免不必要的重渲染。比如弹幕组件只订阅 channelAtom,不会因为余额变化而重渲染
  • 全部有泛型约束和默认值:TypeScript 严格类型,避免运行时类型错误

为什么选 Recoil? 直播场景有个特殊需求:频道切换时部分状态重置、部分状态保留。用户从 A 直播间切到 B 直播间时:

  • 需要重置:弹幕列表、在线人数、竞猜状态
  • 需要保留:用户余额、认证信息、语言设置

Recoil 的原子化天然适合这个场景——频道切换时只重置频道相关的 Atom,保留全局 Atom。如果用 Redux,需要手动设计 reducer 的重置逻辑,复杂度更高。

追问:Recoil 已经停止维护了,有什么替代方案?

是的,Recoil 自 2023 年起基本停止维护。如果重新选型,我会选 Jotai,原因:

  1. API 与 Recoil 高度相似(atom + useAtom),迁移成本低
  2. 体积更小(~3KB vs Recoil ~50KB)
  3. 支持 React Suspense 深度集成,更适合未来的 App Router 架构
  4. 内置 atomWithStorage 支持状态持久化,Recoil 需要手动实现
  5. 社区活跃,持续维护

迁移策略是渐进式的:新功能用 Jotai,旧功能保持 Recoil,最终统一。


Q8:Protobuf 在前端是怎么使用的?和 JSON 相比有什么优势?

:我们在 WebSocket 通讯中使用 Protobuf 二进制编码 替代 JSON,项目中有 90+ 个 proto 定义文件

前端使用方式

  1. 定义 .proto 文件(描述消息结构)
  2. 通过编译工具生成 TypeScript 类型和编解码函数
  3. 在协议层维护 reqProtocolMap / rspProtocolMap 双向映射表,key 是协议路径,value 是对应的 proto 类
  4. 发送消息时自动 encode,接收消息时自动 decode,业务代码只操作 TypeScript 对象

相比 JSON 的优势

维度JSONProtobuf
编码方式文本(含字段名)二进制(仅值 + 字段编号)
体积较大节省约 50%
解析速度较慢(字符串解析)较快(二进制直接映射)
类型安全运行时才能发现类型错误编译期类型检查
Schema 演进无版本管理支持字段新增/废弃的向后兼容

在直播场景下,高频弹幕和礼物消息(每秒几十条)的 50% 带宽节省是非常显著的。


Q9:你们的埋点体系是怎么设计的?

:我们实现了三平台同步上报的埋点体系,封装在 packages/report-sdk 共享包中:

  • Google Analytics:用于流量分析和用户行为追踪
  • Facebook Pixel:用于广告投放效果追踪和再营销
  • Twitter Conversion:用于 Twitter 广告转化追踪

report-sdk 封装了统一的上报接口,业务代码调用一次,SDK 内部同时向三个平台发送。另外还有 PageTime 工具记录用户在每个页面的停留时长,用于分析用户粘性。


三、性能优化类

Q10:直播低延迟方案是怎么做的?

:我们使用阿里云 Aliplayer SDK,实现了两个关键的低延迟优化:

  1. 帧追赶(Frame Chasing):直播流天然有延迟(通常 3-10 秒),当检测到播放器缓冲区积压过多帧时,自动跳过部分帧追赶到最新画面,保持直播的实时性
  2. 倍速追赶:当延迟在可接受范围内时(比如 2-3 秒),用 1.2x 倍速播放逐渐缩小延迟,而非直接跳帧,用户观感更平滑

追问:Aliplayer 在 SSR 环境下有什么问题?

Aliplayer 是纯浏览器库,依赖 windowdocument 等浏览器 API。如果在 Next.js 中直接 import,SSR 阶段会报错。正确做法是使用 next/dynamic 动态导入并设置 ssr: false

components/LivePlayer.tsx
const Player = dynamic(() => import('./Player'), { ssr: false });

不过我们当时项目中是直接 static import 的,这是一个待改进的点。


Q11:Bundle 体积优化做了哪些?还有哪些优化空间?

:已做的优化:

  • Turbo prune 确保每个应用只包含自身依赖,不打包无关代码
  • Next.js standalone 输出模式,只包含运行时必需的代码
  • VConsole 条件加载isDebug && isMobile 时才动态导入调试工具
  • BigNumber 精确处理链上 Token 余额,避免引入更重的数学库

还有优化空间的地方:

  • **moment.js(~287KB)**只在 2 个文件中使用,其余 10+ 个文件用 date-fns,可以把 moment 调用迁移过来并移除依赖
  • react-hot-toast 和 react-toastify 两套 Toast 库共存,应统一
  • 两套 HTTP 请求工具(request.ts 和 requestLib.ts)应该合并
  • next/image 图片优化被关闭(images: { unoptimized: true }),开启后可以自动 WebP/AVIF 转换 + 懒加载
  • 安装 @next/bundle-analyzer 做系统性分析

Q12:SSR 在你们项目中是怎么使用的?

:坦率说,我们的 SSR 利用率不高。大多数页面的 getStaticProps / getServerSideProps 只用于加载国际化翻译文件,没有做业务数据预取。实际上 Next.js 被当作纯 CSR 应用使用。

这是一个明确的改进方向

  • 首页 应该用 getStaticProps + ISR(增量静态生成,每 60 秒刷新),预取推荐频道列表
  • 直播间 /channel/[channelId] 应该用 getServerSideProps 预取频道基本信息(名称、封面、在线人数),改善首屏 LCP 并注入 SEO meta 标签
  • 用户资料页 使用 getServerSideProps 预取公开信息

如果让我重新设计,会把 SSR 数据预取和翻译加载合并到同一个函数中,减少客户端请求次数。


四、问题排查与挑战类

Q13:WebSocket 断线重连是怎么处理的?遇到过什么问题?

:当前实现是固定 5 秒延迟重连,这个设计有几个问题:

  1. 惊群效应:服务器宕机恢复后,所有客户端同时在 5 秒后发起重连,可能再次压垮服务器
  2. 无最大重试限制:用户在无网络环境下,客户端无限重试,浪费电池
  3. 无网络状态感知:不检测 navigator.onLine,离线时也在尝试重连
面试注意

这道题的核心不是展示"我做了完美的重连",而是展示发现问题 → 分析原因 → 提出改进方案的能力。坦诚指出不足比掩盖问题更加分。

改进方案是指数退避 + 抖动(Jitter):

指数退避 + 抖动重连策略
delay = min(60s, 1s × 2^attempt + random(0, 1000)ms)
最大重试 20 次,超过后提示用户手动刷新
监听 online 事件,上线后立即重连
监听 visibilitychange,后台时降低频率

抖动(random 部分)是关键——它让不同客户端的重连时间随机错开,避免惊群。


Q14:项目没有测试,你是怎么保障代码质量的?

:这确实是项目的一个短板。我们当时的质量保障手段主要依赖:

  1. TypeScript strict 模式:从类型层面避免大量运行时错误
  2. ESLint + Prettier:代码风格统一,catch 常见问题
  3. Code Review:PR 必须 Review 后才能合并
  4. 灰度发布:先发测试环境验证,再发生产
  5. Sentry 错误追踪:虽然配置不完整,但基本的异常捕获有在运行

如果让我补测试,优先级是

  1. DAuth MPC 钱包模块——涉及资金安全,一旦签名逻辑出错可能导致资金损失
  2. WebSocket 通讯模块——核心功能,需要测试连接管理、重连、Protobuf 编解码、消息队列
  3. useAuth Hook——三种登录流程(MetaMask + DAuth + OAuth),分支多容易出错

框架选型会用 Vitest(与 Vite 生态一致,速度快)+ React Testing Library。


Q15:你们前端直接对接 14+ 个微服务,有什么问题?怎么解决?

:这是架构上的一个痛点。前端通过 API Gateway 直接调用 14 个微服务(live、coin、user、guild、common、trade、report...),导致:

  1. 一个页面多次请求:比如直播间页面需要调用 live 服务(频道信息)+ user 服务(主播信息)+ coin 服务(余额)+ guild 服务(公会信息),4 次请求才能渲染完成
  2. 前端承担数据编排:不同微服务返回的数据格式不同,前端需要大量的数据转换和组装逻辑
  3. 微服务变更直接影响前端:任何一个微服务的接口变更都可能导致前端报错

理想的解决方案是 BFF(Backend For Frontend)

BFF 架构示意
site → site-bff → API Gateway → 微服务

BFF 层负责多服务数据聚合(一次请求代替 4 次)、数据裁剪(只返回前端需要的字段)、隔离微服务变更。可以用 Next.js API Routes 实现,零额外部署成本。


Q16:遇到过安全相关的问题吗?怎么处理的?

:在做代码审查时发现了几个安全问题:

  1. 凭证泄露.sentryclirc 文件包含 Sentry Auth Token 被提交到 Git;WalletConnect Project ID 硬编码且生产/测试环境用同一个值(三元表达式无效:isProduction ? ID : ID
  2. target="_blank" 安全风险:41 处 <a target="_blank"> 缺少 rel="noopener noreferrer",外部页面可通过 window.opener 操作原始页面
  3. Promise 静默吞错:20+ 处 .catch(() => {}),用户操作失败完全无感知

处理措施

  • 将凭证迁移到环境变量,.sentryclirc 加入 .gitignore
  • 全局搜索 target="_blank" 统一添加 rel="noopener noreferrer"
  • 安装 eslint-plugin-react 启用 jsx-no-target-blank 规则防止复发
  • 逐一检查 .catch(() => {}),添加 Toast 提示或 Sentry 上报
  • 建议团队引入 gitleaks 作为 pre-commit hook,防止未来凭证提交

五、技术选型与权衡类

Q17:为什么选 Next.js 而不是 Vite + React?

:主要考虑三点:

  1. SEO 需求:直播平台需要搜索引擎收录频道页面和主播页面,Next.js 的 SSR/SSG 能力是关键(虽然我们最终没有充分利用)
  2. 部署一体化:Next.js 自带 Node.js 服务器,SSR + API Routes + 静态资源一体化部署,不需要额外的 Nginx 配置
  3. 社区生态:next-i18next、next-pwa、@sentry/nextjs 等社区包成熟,减少自建成本

选 Pages Router 而非 App Router 是因为项目启动时(2023 年初)App Router 还处于早期阶段,稳定性不够。如果现在重新开始,会直接用 App Router + Server Components。


Q18:Wagmi v1 和 v2 有什么区别?为什么还没迁移?

:Wagmi v2 的主要变化:

  • 纯 Viem:v1 底层用 ethers.js,v2 完全切换到 Viem,体积更小、TypeScript 类型更好
  • TanStack Query:v2 基于 React Query 做数据获取和缓存,提供更好的加载状态管理
  • 多链支持改进:v2 的链配置更灵活,支持动态添加链
  • Web3Modal → AppKit:配套的钱包连接 UI 升级,体验更好

没迁移的原因

  1. API 有破坏性变更,迁移工作量不小
  2. 我们集成了自研的 DAuth MPC 钱包,需要确保 v2 兼容
  3. 项目已上线运行,优先保障稳定性

迁移计划:v2 + Viem v2 + AppKit 是确定的方向,可以结合 Next.js 升级一起做。


Q19:Web3 相关的前端库(ethers.js, viem, wagmi)是什么关系?

:这三个库是不同层次的抽象:

Web3 前端库层次关系
应用层:Wagmi(React Hooks)
→ useAccount, useConnect, useContractRead, useSendTransaction
→ 自动管理连接状态、缓存、重试

中间层:Viem / ethers.js(以太坊交互库)
→ 提供与区块链交互的底层 API
→ 编码/解码交易、签名、调用合约

底层:JSON-RPC Provider
→ 与以太坊节点通信

ethers.js vs Viem:ethers.js 是老牌库,API 丰富但体积大;Viem 是后起之秀,TypeScript 优先、Tree-shaking 更好、体积更小。我们项目中两个都安装了(ethers 用于 DAuth 签名,Viem 通过 Wagmi 间接使用),理想情况应该统一到 Viem。


六、综合能力类

Q20:如果让你从零重新设计这个项目,你会怎么做?

:保持不变的部分:

  • Monorepo 架构(Turborepo + PNPM)
  • WebSocket + Protobuf 三层通讯架构
  • DAuth MPC + EIP-4337 的 Web3 无感化方案
  • Docker turbo prune 构建策略

会改进的部分:

  1. Next.js App Router + Server Components:减少客户端 JS 体积,充分利用 SSR
  2. Jotai 替代 Recoil:更轻量、持续维护、内置持久化
  3. React Query / SWR 统一数据获取层:HTTP 请求的缓存/去重/重试 + 与 WebSocket 推送打通
  4. BFF 聚合层:用 Next.js API Routes 做数据聚合,减少前端直连微服务
  5. 完善测试:核心模块(DAuth、WebSocket、Auth)必须有测试覆盖
  6. Wagmi v2 + Viem v2:统一 Web3 工具链
  7. 特性开关系统:支持灰度发布和 A/B 测试

Q21:你在这个项目中最有成就感的技术实现是什么?

WebSocket Protobuf 三层通讯架构

这不是简单的"用 WebSocket 发消息",而是设计了一套完整的通讯框架——传输层管连接和心跳,协议层管编解码和路由,应用层通过声明式 Hook 订阅。新增消息类型只需添加 proto 文件 + 注册映射 + 组件监听,不需要改任何底层代码。

特别是「两级事件路由」的设计——用一个 comment 通道承载所有互动消息,客户端按 type 二次分发。这个设计既减少了服务端的通道管理复杂度,又保持了客户端的模块化。整个通讯系统上线后非常稳定,支撑了直播间内弹幕、礼物、竞猜、系统通知等多种实时交互。


Q22:你在这个项目中踩过最大的坑是什么?

Safari 跨域 postMessage 的兼容性问题

在实现第三方登录时,我们用 window.open() 打开 OAuth 页面,授权后通过 postMessage 把 token 传回主窗口。在 Chrome、Firefox 上一切正常,但 Safari 因为其严格的隐私策略(ITP),会阻止跨域的 postMessage。

排查这个问题花了不少时间,最终设计了双通道回调

  • 主通道:postMessage(现代浏览器)
  • 降级通道:Cookie + window.focus(Safari)

降级通道的逻辑是:OAuth 回调页将 token 写入 Cookie,然后 focus 回原窗口。原窗口通过 focus 事件触发 Cookie 读取,获取 token。虽然不如 postMessage 优雅,但完美解决了 Safari 兼容性。


Q23:怎么处理直播间的大量实时数据更新而不卡顿?

:直播间场景的核心挑战是高频数据更新——弹幕、礼物特效、在线人数、竞猜赔率等每秒可能有几十次更新。我们的策略:

  1. Recoil 原子化订阅:每种数据是独立的 Atom,弹幕更新不会触发竞猜组件重渲染
  2. MessageBatch 批量处理:WebSocket 支持批量消息协议,多条消息一次性推送和解析
  3. useMemoizedFn(ahooks):避免回调函数重建导致的不必要渲染
  4. Protobuf 二进制编码:解析速度快于 JSON,减少 CPU 占用

如果还要进一步优化,可以考虑:

  • 弹幕组件使用虚拟列表(只渲染可见区域)
  • 礼物特效使用 Canvas 渲染(脱离 React 渲染树)
  • 竞猜赔率使用节流(throttle)控制更新频率

Q24:如果面试官问你项目的不足,你会怎么回答?

:我会坦诚地说几个主要不足:

  1. 零测试覆盖:项目所有应用没有任何测试,这是最大的技术债。特别是 DAuth MPC 钱包这样涉及资金安全的模块,应该有完善的单元测试
  2. SSR 能力浪费:选了 Next.js 但当 CSR 用,首屏性能和 SEO 都没有发挥框架优势
  3. 安全意识不足:凭证硬编码在代码中、target="_blank" 安全问题、Promise 静默吞错等,说明团队的安全编码规范需要加强
  4. 技术栈老化:Recoil 停止维护、Wagmi v1 已过时、NextUI 用的 beta 版

但我会强调:这些问题我都已经识别到了,并且有明确的改进方案和优先级排序。发现问题比没有问题更重要,说明我对代码质量有持续的关注和思考。

回答策略

面试官问项目不足,其实是在考察你的技术判断力和自我反思能力。回答时遵循「承认不足 → 分析原因 → 给出方案 → 点明收获」的结构,比一味展示亮点更有说服力。


Q25:你们项目的国际化是怎么做的?

:使用 next-i18next,支持英文和中文两种语言。

实现方式

  • 翻译文件存放在 public/locales/{en,zh}/ 目录下
  • 每个页面通过 getStaticProps / getServerSideProps 加载对应的翻译 namespace
  • 组件中使用 const { t } = useTranslation() 获取翻译函数
  • 语言切换通过 i18n.changeLanguage() + Next.js Router 重定向

存在的问题

  • 部分文案(如 _document.tsx 的 title)硬编码英文未翻译
  • 没有翻译完整性检查脚本,可能存在遗漏的翻译 key

Q26:你们的错误监控是怎么做的?

:计划使用 Sentry,但配置不完整:

  • sentry.client.config.ts 已初始化 Sentry SDK
  • next.config.js 中的 withSentryConfig 被注释掉了,导致 source map 没有上传到 Sentry
  • 生产环境的错误虽然能捕获,但堆栈无法还原到源码
  • 整个应用缺少 ErrorBoundary,一个组件的渲染错误会导致整页白屏

改进方案

  1. 恢复 withSentryConfig,确保 source map 上传
  2. _app.tsx 最外层添加全局 ErrorBoundary
  3. tracesSampleRate 从 1(100%)调低到 0.1(10%)
  4. 为直播间等核心页面添加局部 ErrorBoundary

Q27:Monorepo 中怎么处理包之间的依赖关系?构建顺序怎么确保?

:通过 Turborepo 的任务依赖声明来保证构建顺序。在 turbo.json 中:

turbo.json
{
"pipeline": {
"build": {
"dependsOn": ["^build"]
},
"lint": {
"dependsOn": ["build"]
}
}
}

"^build"^ 表示「先构建我所依赖的包」。当执行 turbo run build --filter=site 时,Turborepo 会自动分析 site 的 package.json 依赖,先构建 packages/loginpackages/function 等共享包,再构建 site 本身。

包之间使用 workspace:* 版本引用,确保始终使用 Monorepo 内的最新版本。Turborepo 还提供了 Remote Cache,CI 中如果某个包的代码没有变化,直接复用上一次的构建产物,大幅加速 CI。


Q28:你对前端架构设计有什么理解?好的架构应该具备什么特征?

:基于 RYZZ 项目的实践,我认为好的前端架构应该具备:

  1. 关注点分离:就像我们的 WebSocket 三层架构——传输、协议、应用三层各司其职,修改一层不影响其他层。或者 15 层 Provider 的跨关注点分离——每层只管一件事

  2. 开闭原则:对扩展开放,对修改关闭。WebSocket 新增消息类型只需添加 proto + 注册映射,不需要修改底层通讯代码

  3. 合理的抽象层次:共享包的四层分层(基础设施 → 业务能力 → UI → 工程配置),每层有明确的抽象边界,不越层依赖

  4. 可测试性:虽然我们项目测试覆盖为零(这是教训),但架构上的分层和解耦为未来补测试创造了条件——比如协议层的 encode/decode 可以独立测试

  5. 渐进式迁移能力:技术栈更新不需要全量重写。比如 Recoil → Jotai 可以渐进迁移,新功能用 Jotai,旧功能暂留 Recoil

反面教训

  • SSR 能力浪费说明「选了框架但不用其核心能力」是架构决策的失误
  • 前端直连 14 个微服务说明缺少 BFF 聚合层是一个架构缺失
  • 零测试说明「架构再好,没有质量保障兜底也不可靠」