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

温馨提示:市面上的面经鱼龙混杂,甄别真伪、把握时效,是我们对抗内卷最有效的武器。
面经原文内容
📍面试公司:TME QQ音乐
🕐面试时间:4月22日下午3点,时长1小时
💻面试岗位:前端暑期二面
📝面试体验:道心破碎,项目深度拷问,几乎无八股无手撕
❓面试问题:
- 自我介绍
- 虚拟列表怎么实现的
- 一道性能指标采集代码找错误(用户未回忆出具体代码)
- 文件上传是怎么实现的
- 大文件分片上传时,计算 5MB 分片 MD5 大概要多久
- 如果文件很大,计算完整文件 MD5 很耗时,有什么性能优化方案
- Web Worker 在大文件 MD5 计算里能怎么用
- 服务端保存所有分片索引和分片文件,会不会导致碎片文件越来越多
- 分片合并完成后,服务端临时分片目录应该怎么清理
- 如果清理了分片,下次上传同一个文件还能不能做分片级别的秒传
- 秒传应该基于完整文件 hash 还是分片 hash
- 服务端怎么设计分片管理,才能避免既存完整文件又存所有分片造成空间浪费
- 如果两个文件部分分片相同、整体文件不同,怎么判断和复用分片
- 歌曲列表页点击歌曲后,如何打开一个独立播放页
- 如果播放页已经存在,列表页怎么通知已有播放页切换歌曲
- 怎么判断播放页是否已经存在或是否被关闭
- 如何用 LocalStorage 实现跨页面通信
- 如何用 LocalStorage 实现页面间心跳检测
- LocalStorage 轮询方案有什么性能问题
- 除了 LocalStorage,跨页面通信还有哪些更好的方案
- postMessage 和 Service Worker 怎么用于跨页面通信
- 歌曲列表中大量图片加载时,如何先展示占位图
- 图片加载成功后怎么切换为真实图片
- 图片加载失败后怎么展示失败图
- 如何通过图片的 load 和 error 事件判断加载状态
- 你接触过 React Native 或 Flutter 这类跨端技术吗
- Vite 相比 Webpack,为什么开发阶段启动更快
- Webpack 能不能也配置成使用 ES Module
- Vite 的热更新 HMR 是怎么实现的
- WebSocket 和 SSE 有什么区别
来源:牛客网 前端死了咩
💡 木木有话说(刷前先看)
TME QQ音乐这场二面,是一场"实战场景深度拷问"。30个问题中,前13题围绕大文件上传/分片/MD5/秒传/服务端设计 层层递进,后10题围绕跨页面通信/图片加载 场景展开,最后是工程化基础。面试官显然不满足于"会不会用",而是要考察你在大文件上传、跨页面通信等真实场景下的系统设计能力 。用户反馈"道心破碎",可见难度之高。这份面经适合有一定项目经验、准备冲击中大厂的同学反复研读。
📝 TME QQ音乐前端二面·深度解析
🎯 面试整体画像
| 维度 | 特征 |
|---|---|
| 面试风格 | 实战场景深挖型 + 系统设计型 + 前后端协作型 |
| 难度评级 | ⭐⭐⭐⭐⭐(五星,大文件上传设计链路极深) |
| 考察重心 | 大文件分片上传、MD5计算优化、秒传设计、跨页面通信、图片加载、Vite原理 |
| 特殊之处 | 无八股无手撕,围绕真实业务场景层层追问设计细节 |
🔍 逐题深度解析
二、虚拟列表怎么实现的
回答思路:参考之前面经。核心是只渲染可视区域,动态计算起始索引。
javascript
// 核心实现
function VirtualList({ items, itemHeight, containerHeight }) {
const [startIndex, setStartIndex] = useState(0)
const visibleCount = Math.ceil(containerHeight / itemHeight)
const handleScroll = (e) => {
const scrollTop = e.target.scrollTop
const newStartIndex = Math.floor(scrollTop / itemHeight)
setStartIndex(newStartIndex)
}
const visibleItems = items.slice(startIndex, startIndex + visibleCount)
const paddingTop = startIndex * itemHeight
return (
<div onScroll={handleScroll} style={{ height: containerHeight, overflow: 'auto' }}>
<div style={{ paddingTop, height: items.length * itemHeight }}>
{visibleItems.map(item => <div key={item.id}>{item.content}</div>)}
</div>
</div>
)
}
四、文件上传是怎么实现的
回答思路:分片上传 + 断点续传 + 秒传。
核心流程:
- 文件分片(
Blob.prototype.slice) - 计算文件/分片MD5(用于秒传和校验)
- 并发上传分片(控制并发数)
- 服务端记录已上传分片,支持断点续传
- 全部分片上传完成后,服务端合并
五、大文件分片上传时,计算 5MB 分片 MD5 大概要多久
回答思路 :取决于设备性能和算法,大约20-100ms。
- 现代PC:5MB数据约20-40ms
- 移动端/低端设备:50-100ms
- 使用
Web Crypto API比纯JS实现快2-3倍
六、如果文件很大,计算完整文件 MD5 很耗时,有什么性能优化方案
优化方案:
- 抽样计算:只计算开头、中间、结尾部分数据,而非全量
- 增量计算:读取文件流,边读边更新hash,不一次性加载到内存
- Web Worker:在Worker线程计算,不阻塞主线程
- 分片复用:使用分片MD5,完整MD5由分片MD5组合得到
- 采样秒传:先快速计算采样hash,命中后再计算完整hash
javascript
// 增量计算示例
async function computeMD5(file) {
const chunkSize = 1024 * 1024 // 1MB
const hasher = new CryptoJS.algo.MD5()
for (let i = 0; i < file.size; i += chunkSize) {
const chunk = file.slice(i, i + chunkSize)
const buffer = await chunk.arrayBuffer()
hasher.update(CryptoJS.lib.WordArray.create(buffer))
// 更新进度
}
return hasher.finalize().toString()
}
七、Web Worker 在大文件 MD5 计算里能怎么用
回答思路:将耗时的MD5计算移到Worker线程,避免阻塞UI。
javascript
// main.js
const worker = new Worker('md5-worker.js')
worker.postMessage({ file })
worker.onmessage = (e) => {
console.log('MD5:', e.data)
}
// md5-worker.js
self.onmessage = async (e) => {
const file = e.data.file
const md5 = await computeMD5(file)
self.postMessage(md5)
}
优势:UI不卡顿,用户可继续操作;可同时计算多个文件。
八~十三:大文件分片服务端设计链路
8. 服务端保存分片会导致碎片文件越来越多吗?
- 会。每个未合并的分片都会占用存储空间,尤其是上传中断、未完成合并的分片成为碎片。
9. 分片合并完成后,服务端临时分片目录应该怎么清理?
- 合并后立即删除临时分片
- 定时任务:扫描超时未合并的分片(如上传中断超过24小时),自动删除
- 上传取消时主动触发删除
10. 清理了分片,下次上传同一个文件还能做分片级别的秒传吗?
- 不能。秒传依赖于分片hash,分片被删除后无法定位
- 解决方案 :上传完成后保存分片hash索引,合并后保留分片hash记录但不保留分片文件
11. 秒传应该基于完整文件 hash 还是分片 hash?
- 两者结合 :完整文件hash用于整文件秒传 ,分片hash用于断点续传和分片级秒传
12. 服务端怎么设计分片管理,避免既存完整文件又存分片造成空间浪费?
- 分片重用:完整文件存储后,将分片hash指向完整文件的位置
- 引用计数:同一分片被多个文件共享时,计数管理
- 去重存储:分片内容唯一存储,文件由分片引用组成
13. 两个文件部分分片相同、整体文件不同,怎么判断和复用分片?
- 分片级去重:每个分片独立存储,用hash标识
- 文件分片表:文件A:[hash1, hash2, hash3],文件B:[hash1, hash4, hash5]
- 复用逻辑:上传分片前检查hash是否已存在,存在则跳过
十四、歌曲列表页点击歌曲后,如何打开一个独立播放页
方案:
window.open('player.html')打开新标签页- 如果是SPA,可以用路由跳转 + 新标签页
十五、如果播放页已经存在,列表页怎么通知已有播放页切换歌曲
跨页面通信方案:
- LocalStorage + storage事件(最常用)
- BroadcastChannel(现代浏览器推荐)
- postMessage(需维护目标窗口引用)
- Service Worker(复杂,用于离线场景)
javascript
// 列表页
localStorage.setItem('play-song', JSON.stringify({ id: 123, name: '稻香' }))
// 播放页
window.addEventListener('storage', (e) => {
if (e.key === 'play-song') {
const song = JSON.parse(e.newValue)
playSong(song)
}
})
十六、怎么判断播放页是否已经存在或是否被关闭
方案:
- 心跳检测:播放页每N秒写入LocalStorage时间戳,列表页轮询检查,超时则认为已关闭
- BroadcastChannel :监听
close事件 - SharedWorker:维护活动页面计数
十七、如何用 LocalStorage 实现跨页面通信
javascript
// 发送消息
localStorage.setItem('message', JSON.stringify({ type: 'play', data: song }))
// 接收消息
window.addEventListener('storage', (e) => {
if (e.key === 'message') {
const { type, data } = JSON.parse(e.newValue)
handleMessage(type, data)
}
})
十八、如何用 LocalStorage 实现页面间心跳检测
javascript
// 播放页:每5秒更新时间戳
setInterval(() => {
localStorage.setItem('player_heartbeat', Date.now())
}, 5000)
// 列表页:轮询检查
setInterval(() => {
const lastHeartbeat = localStorage.getItem('player_heartbeat')
if (lastHeartbeat && Date.now() - lastHeartbeat > 10000) {
console.log('播放页已关闭')
localStorage.removeItem('player_heartbeat')
}
}, 5000)
十九、LocalStorage 轮询方案有什么性能问题
问题:
- 主线程阻塞 :
storage事件监听本身无性能问题,但轮询检查会占用主线程 - 频繁读写:高频率写入localStorage会有同步I/O开销
- 不适合高实时性场景:延迟约几十毫秒
二十~二十一、更好的跨页面通信方案
| 方案 | 优点 | 缺点 |
|---|---|---|
| BroadcastChannel | API简单,低延迟 | 同源限制,不支持跨域 |
| postMessage | 双向通信,跨域 | 需维护窗口引用 |
| SharedWorker | 可维护状态,支持多页面 | 实现复杂 |
| Service Worker | 离线支持,可做中转 | 生命周期管理复杂 |
BroadcastChannel示例:
javascript
// 列表页
const channel = new BroadcastChannel('player')
channel.postMessage({ type: 'play', song })
// 播放页
const channel = new BroadcastChannel('player')
channel.onmessage = (e) => {
playSong(e.data.song)
}
二十二~二十五:图片加载占位图方案
javascript
function LazyImage({ src, alt }) {
const [status, setStatus] = useState('loading')
useEffect(() => {
const img = new Image()
img.src = src
img.onload = () => setStatus('success')
img.onerror = () => setStatus('error')
}, [src])
if (status === 'loading') return <Skeleton />
if (status === 'error') return <ErrorIcon />
return <img src={src} alt={alt} />
}
二十七~二十九:Vite vs Webpack
27. Vite为什么启动更快?
- 利用浏览器ESM,开发环境不打包,直接按需编译
- 预构建依赖(esbuild),比Webpack快10-100倍
28. Webpack能不能配置成使用ES Module?
- 能 ,通过
experiments.outputModule: true,但生态兼容性一般
29. Vite HMR怎么实现的?
- 基于ESM的HMR,只更新变更的模块
- Webpack HMR需要重新打包相关模块
三十、WebSocket和SSE区别
| 维度 | SSE | WebSocket |
|---|---|---|
| 方向 | 单向(服务端→客户端) | 双向 |
| 协议 | HTTP | WS/WSS |
| 自动重连 | 内置 | 需手动实现 |
| 二进制数据 | 需编码 | 原生支持 |
📚 知识点速查表
| 知识点 | 核心要点 |
|---|---|
| 虚拟列表 | 可视区域渲染、动态起始索引 |
| 大文件上传 | 分片(slice)、MD5、断点续传、秒传 |
| MD5优化 | Web Worker、增量计算、采样 |
| 分片服务端设计 | 临时分片清理、分片hash复用、引用计数 |
| 跨页面通信 | LocalStorage+storage事件、BroadcastChannel、postMessage |
| LocalStorage心跳 | 定期写时间戳,轮询检测超时 |
| 图片加载 | load/error事件、占位图/失败图 |
| Vite启动快 | 利用ESM不打包、esbuild预构建 |
| SSE vs WS | 单向/双向、协议、自动重连 |
📌 最后一句:
TME QQ音乐这场二面,是一场"实战场景课"。从大文件上传的分片MD5计算、秒传设计、服务端分片管理,到跨页面通信、图片加载、构建工具原理,面试官用30个问题构建了一个完整的前端知识体系树 。用户感慨"道心破碎",但这样的面试即使挂了,也是收获巨大的------它划出了大厂对前端工程师的能力期望:不仅要会写代码,更要懂系统设计、懂前后端协作、懂性能优化。