前言
大家好,我是木斯佳。
相信很多人都感受到了,在AI浪潮的席卷之下,前端领域的门槛在变高,纯粹的"增删改查"岗位正在肉眼可见地减少。曾经热闹非凡的面经分享,如今也沉寂了许多。但我们都知道,市场的潮水退去,留下的才是真正在踏实准备、努力沉淀的人。学习的需求,从未消失,只是变得更加务实和深入。
这个专栏的初衷很简单:拒绝过时的、流水线式的PDF引流贴,专注于收集和整理当下最新、最真实的前端面试资料。我会在每一份面经和八股文的基础上,尝试从面试官的角度去拆解问题背后的逻辑,而不仅仅是提供一份静态的背诵答案。无论你是校招还是社招,目标是中大厂还是新兴团队,只要是真实发生、有价值的面试经历,我都会在这个专栏里为你沉淀下来。专栏快速地址

温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。
面经原文内容
📍面试公司:字节跳动-存储部门
🕐面试时间:近期
💻面试岗位:前端一面
❓面试问题:
- SDD + TDD 开发新组件的完整流程是什么?
- Story 文档容易遗漏状态或 Props,怎么保证覆盖度?
- 组件文档 Skills、MCP、llm.txt 三者的关系是什么?
- MCP 怎么解决组件库版本号问题?
- 逻辑层与 UI 层分离的具体实现
- 工厂函数运行时组装有没有性能损耗
- 有没有遇到核心 Hook 入参在 PC / 移动端类型不同(如 MouseEvent vs TouchEvent)?
- 图片预览的缩放、拖拽手势是自己实现还是用的库?
- 预览 50M 大图片第一次加载慢怎么解决?
- 财务系统高度保密场景下,ImageViewer 需要扩展哪些安全功能?
- 页面模板系统 SSO 完整鉴权流程?
- 多个接口同时触发 401,怎么避免重复刷新 token?
- iframe 静默刷新 token 解决什么问题?
- 手写 addEventListener
- 常用的 React Hook 有哪些?
- useEffect 与 useLayoutEffect 的区别?
- 父子组件通信有多少种方式?
- 子组件暴露内部状态给外部调用的 Hook 是什么?
- Context 失效的常见场景?
来源:牛客网 苏九222
💡 木木有话说(刷前先看)
5、6月份明显看出过了招聘的黄金时段,目前面经质量高的比较少,大部分都是老题或者是水面,本着重精不重多,没有合适的我就更新的慢一些。字节的面试题还是比较靠谱的,都是比较值得反复刷的。
📝 字节存储前端一面·深度解析
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 面试风格 | 工程化深挖型 + 组件库设计型 + 安全与性能兼顾型 |
| 难度评级 | ⭐⭐⭐⭐(四星,SDD/TDD、MCP、工厂函数性能等较深) |
| 考察重心 | 组件开发流程、文档工程化、逻辑UI分离、大图优化、鉴权、React原理 |
| 特殊之处 | 大量问题围绕"组件库开发"和"AI工程化"展开,非传统业务场景 |
🔍 逐题深度解析
一、SDD + TDD 开发新组件的完整流程
回答思路:SDD(Specification-Driven Development)是规格驱动开发,TDD(Test-Driven Development)是测试驱动开发,两者结合可提升组件质量和可维护性。
完整流程:
- 需求分析与规格编写(SDD):编写组件规格文档,包含Props接口、使用场景、边界条件、交互行为
- 编写测试用例(TDD红阶段):根据规格编写失败的单元测试
- 实现组件(TDD绿阶段):编写最小实现让测试通过
- 重构优化(TDD重构阶段):优化代码结构,保持测试通过
- 文档生成:基于规格和代码注释生成Storybook文档
- 集成验证:在业务项目中试用,收集反馈
typescript
// 规格示例(SDD)
interface ButtonProps {
/** 按钮类型 */
type?: 'primary' | 'default' | 'danger'
/** 是否禁用 */
disabled?: boolean
/** 点击回调 */
onClick?: () => void
}
// 测试用例(TDD)
describe('Button', () => {
it('should render primary button', () => {
render(<Button type="primary">Click</Button>)
expect(screen.getByRole('button')).toHaveClass('primary')
})
})
二、Story文档遗漏状态或Props,怎么保证覆盖度
回答思路:使用自动化工具强制覆盖,而非依赖人工检查。
方案:
- 类型驱动文档生成 :使用
react-docgen-typescript从TS类型自动生成Props表格 - CSF(Component Story Format)3.0 :使用
@storybook/test配合Play函数覆盖交互状态 - 覆盖率工具 :
@storybook/addon-coverage检测哪些Props/状态未被Story覆盖 - CI检查:PR流水线中检查Story覆盖率,低于阈值则阻断合并
typescript
// CSF 3.0 覆盖多种状态
export const Primary: Story = {
args: { type: 'primary', children: 'Button' }
}
export const Disabled: Story = {
args: { disabled: true, children: 'Disabled' }
}
export const Loading: Story = {
args: { loading: true, children: 'Loading' },
play: async ({ canvasElement }) => {
// 测试加载状态
}
}
三、组件文档 Skills、MCP、llm.txt 三者的关系
回答思路:这三者都是AI辅助开发中的工程化产物。
| 概念 | 定位 | 作用 |
|---|---|---|
| Skills | 预定义能力单元 | 封装特定任务的Prompt+工具,如"生成组件文档Skill" |
| MCP | 模型上下文协议 | 标准化AI与工具/数据源的交互,Skill可基于MCP实现 |
| llm.txt | 项目上下文文件 | 为LLM提供项目结构、技术栈、代码规范等上下文信息 |
关系:
llm.txt提供项目上下文(是什么)MCP提供工具交互协议(怎么调)Skills封装具体任务能力(做什么)
text
llm.txt内容示例:
- 技术栈:React 18 + TypeScript + Vite
- 组件目录结构:src/components/
- 代码规范:函数组件优先,Props使用interface
MCP Server:提供文件读写、搜索等工具
Skill:读取llm.txt + 调用MCP工具 → 生成组件文档
四、MCP怎么解决组件库版本号问题
回答思路 :MCP可以作为版本感知的工具层,在AI生成代码时自动匹配正确的组件版本API。
方案:
- MCP Server维护版本索引:存储各版本组件的API差异
- AI请求时携带项目版本信息:通过MCP协议告知当前组件库版本
- MCP返回该版本的正确用法:避免AI生成过时或超前API
typescript
// MCP Server端
const versionAPI = {
'1.0.0': { Button: { props: ['type', 'onClick'] } },
'2.0.0': { Button: { props: ['variant', 'onPress'] } }
}
// AI调用示例
askMCP('如何创建Button', { libVersion: '1.0.0' })
// 返回:使用type和onClick,而不是variant和onPress
五、逻辑层与UI层分离的具体实现
回答思路 :核心是自定义Hook封装逻辑,UI组件只负责渲染。
typescript
// 逻辑层(自定义Hook)
function useCounter(initialValue = 0) {
const [count, setCount] = useState(initialValue)
const increment = useCallback(() => setCount(c => c + 1), [])
const decrement = useCallback(() => setCount(c => c - 1), [])
return { count, increment, decrement }
}
// UI层(纯展示组件)
function CounterUI({ count, onIncrement, onDecrement }) {
return (
<div>
<span>{count}</span>
<button onClick={onIncrement}>+</button>
<button onClick={onDecrement}>-</button>
</div>
)
}
// 组装层
function Counter() {
const { count, increment, decrement } = useCounter()
return <CounterUI count={count} onIncrement={increment} onDecrement={decrement} />
}
优势:逻辑可复用、UI可替换、易于测试。
六、工厂函数运行时组装有没有性能损耗
回答思路:有损耗,但通常可忽略。
损耗来源:
- 函数调用开销(每次调用工厂函数)
- 闭包创建(每次返回新函数)
- 内存分配(新对象/函数)
优化建议:
- 使用依赖注入而非工厂函数
- 使用单例模式复用实例
- 在组件外创建工厂,避免每次渲染都调用
typescript
// ❌ 每次渲染都创建新函数
function Component() {
const handler = createHandler() // 损耗较大
}
// ✅ 只在组件外创建一次
const handler = createHandler()
function Component() {
// 使用handler
}
七、Hook入参在PC/移动端类型不同的问题
回答思路 :使用适配器模式 或类型守卫统一处理。
问题 :PC端使用MouseEvent,移动端使用TouchEvent
解决方案:
typescript
type UnifiedEvent = MouseEvent | TouchEvent
function useDrag(onDrag: (event: UnifiedEvent) => void) {
const handleStart = (e: UnifiedEvent) => {
// 类型守卫
if ('touches' in e) {
const touch = e.touches[0]
onDrag({ clientX: touch.clientX, clientY: touch.clientY } as MouseEvent)
} else {
onDrag(e as MouseEvent)
}
}
// ...
}
九、预览50M大图片第一次加载慢怎么解决
回答思路:分层加载 + 渐进式渲染。
方案:
- 缩略图优先:先加载低质量缩略图(如256x256),快速显示
- 分块加载 :使用
createImageBitmap分块解码图片 - Web Worker解码:将图片解码移到Worker线程
- 渐进式JPEG:使用渐进式编码,先显示模糊轮廓,逐步清晰
- CDN动态裁剪:请求时指定尺寸参数,服务端实时裁剪
javascript
// 分块解码示例
async function loadLargeImage(url) {
const response = await fetch(url)
const blob = await response.blob()
const imageBitmap = await createImageBitmap(blob, {
resizeWidth: 800, // 先渲染小尺寸
resizeQuality: 'medium'
})
ctx.drawImage(imageBitmap, 0, 0)
// 后台加载全尺寸
setTimeout(async () => {
const fullBitmap = await createImageBitmap(blob)
ctx.drawImage(fullBitmap, 0, 0)
}, 100)
}
十、财务系统ImageViewer需要扩展哪些安全功能
回答思路:财务系统对数据安全有极高要求。
扩展功能:
- 水印:每次截图/预览时叠加用户ID+时间戳水印
- 操作审计:记录谁、何时、查看了哪个图片
- 禁止下载/保存:禁用右键、拖拽保存、快捷键
- 动态脱敏:根据权限对图片特定区域打码
- 过期失效:预览链接设置短时效(如5分钟)
- 数字水印:嵌入不可见水印,用于溯源
javascript
// 水印实现
function addWatermark(imageUrl, userId) {
const canvas = document.createElement('canvas')
const ctx = canvas.getContext('2d')
// 绘制图片后叠加半透明文字
ctx.fillStyle = 'rgba(0,0,0,0.3)'
ctx.fillText(userId, canvas.width - 100, canvas.height - 20)
}
十二、多个接口同时触发401,怎么避免重复刷新token
回答思路 :使用请求队列 + 刷新锁。
javascript
let isRefreshing = false
let pendingRequests = []
axios.interceptors.response.use(
response => response,
async error => {
if (error.response?.status === 401 && !error.config._retry) {
if (isRefreshing) {
// 等待刷新完成,重试请求
return new Promise(resolve => {
pendingRequests.push(() => resolve(axios(error.config)))
})
}
error.config._retry = true
isRefreshing = true
try {
await refreshToken()
// 重试所有等待的请求
pendingRequests.forEach(cb => cb())
pendingRequests = []
return axios(error.config)
} catch {
redirectToLogin()
} finally {
isRefreshing = false
}
}
return Promise.reject(error)
}
)
十三、iframe静默刷新token解决什么问题
回答思路 :解决多标签页token同步问题。
问题:一个标签页刷新了token,其他标签页还在用旧token。
解决方案 :创建一个隐藏iframe,所有标签页通过iframe统一刷新token,iframe刷新后通过postMessage广播新token到所有标签页。
十四、手写addEventListener
javascript
function addEventListener(element, type, handler, options = false) {
if (!element._eventListeners) {
element._eventListeners = {}
}
if (!element._eventListeners[type]) {
element._eventListeners[type] = []
element[`on${type}`] = (event) => {
element._eventListeners[type].forEach(fn => fn(event))
}
}
element._eventListeners[type].push(handler)
}
十八、子组件暴露内部状态给外部调用的Hook
回答思路 :使用useImperativeHandle配合forwardRef。
typescript
const Child = forwardRef((props, ref) => {
const [count, setCount] = useState(0)
useImperativeHandle(ref, () => ({
getCount: () => count,
resetCount: () => setCount(0)
}))
return <div>{count}</div>
})
// 父组件调用
const childRef = useRef()
childRef.current?.getCount()
十九、Context失效的常见场景
常见场景:
- 组件使用
React.memo:memo会阻止重渲染,如果Context值变化但组件Props未变,组件不会更新 - 中间组件使用了
React.memo:阻止了Context传递 - Context值每次都是新对象:即使值相同,新对象也会触发重渲染
- 组件未在Provider内:使用Context但外层无Provider
📚 知识点速查表
| 知识点 | 核心要点 |
|---|---|
| SDD+TDD | 规格先行→写测试→实现→重构 |
| Story覆盖度 | 类型驱动、CSF 3.0、Play函数、CI检查 |
| Skills/MCP/llm.txt | 能力单元/交互协议/项目上下文 |
| MCP版本管理 | 版本索引+版本感知API建议 |
| 逻辑UI分离 | 自定义Hook逻辑 + 纯渲染组件 |
| 工厂函数性能 | 有损耗可接受,避免渲染内调用 |
| 跨端事件 | 适配器模式统一MouseEvent/TouchEvent |
| 大图优化 | 缩略图优先、分块解码、Worker |
| 财务安全 | 水印、审计、禁止下载、脱敏、时效 |
| token刷新 | 请求队列+刷新锁,iframe多标签同步 |
📌 最后一句:
字节存储这从SDD/TDD开发流程、Story覆盖度、MCP版本管理,到逻辑UI分离、大图优化、财务安全扩展,再到token刷新防并发、iframe多标签同步,面试官层层递进,考察的不是你会不会写组件,而是能否从工程化角度设计、开发、维护一个企业级组件库。能答好这套题,说明你已经具备了独立负责组件库建设的能力。