前言
大家好,我是木斯佳。
相信很多人都感受到了,在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原理 |
| 特殊之处 | 无八股无手撕,围绕真实业务场景层层追问设计细节 |
🔍逐题深度解析
二、虚拟列表怎么实现的
回答思路:参考之前面经。核心是只渲染可视区域,动态计算起始索引。
// 核心实现functionVirtualList({items,itemHeight,containerHeight}){const[startIndex,setStartIndex]=useState(0)constvisibleCount=Math.ceil(containerHeight/itemHeight)consthandleScroll=(e)=>{constscrollTop=e.target.scrollTopconstnewStartIndex=Math.floor(scrollTop/itemHeight)setStartIndex(newStartIndex)}constvisibleItems=items.slice(startIndex,startIndex+visibleCount)constpaddingTop=startIndex*itemHeightreturn(<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
// 增量计算示例asyncfunctioncomputeMD5(file){constchunkSize=1024*1024// 1MBconsthasher=newCryptoJS.algo.MD5()for(leti=0;i<file.size;i+=chunkSize){constchunk=file.slice(i,i+chunkSize)constbuffer=awaitchunk.arrayBuffer()hasher.update(CryptoJS.lib.WordArray.create(buffer))// 更新进度}returnhasher.finalize().toString()}七、Web Worker 在大文件 MD5 计算里能怎么用
回答思路:将耗时的MD5计算移到Worker线程,避免阻塞UI。
// main.jsconstworker=newWorker('md5-worker.js')worker.postMessage({file})worker.onmessage=(e)=>{console.log('MD5:',e.data)}// md5-worker.jsself.onmessage=async(e)=>{constfile=e.data.fileconstmd5=awaitcomputeMD5(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(复杂,用于离线场景)
// 列表页localStorage.setItem('play-song',JSON.stringify({id:123,name:'稻香'}))// 播放页window.addEventListener('storage',(e)=>{if(e.key==='play-song'){constsong=JSON.parse(e.newValue)playSong(song)}})十六、怎么判断播放页是否已经存在或是否被关闭
方案:
- 心跳检测:播放页每N秒写入LocalStorage时间戳,列表页轮询检查,超时则认为已关闭
- BroadcastChannel:监听
close事件 - SharedWorker:维护活动页面计数
十七、如何用 LocalStorage 实现跨页面通信
// 发送消息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 实现页面间心跳检测
// 播放页:每5秒更新时间戳setInterval(()=>{localStorage.setItem('player_heartbeat',Date.now())},5000)// 列表页:轮询检查setInterval(()=>{constlastHeartbeat=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示例:
// 列表页constchannel=newBroadcastChannel('player')channel.postMessage({type:'play',song})// 播放页constchannel=newBroadcastChannel('player')channel.onmessage=(e)=>{playSong(e.data.song)}二十二~二十五:图片加载占位图方案
functionLazyImage({src,alt}){const[status,setStatus]=useState('loading')useEffect(()=>{constimg=newImage()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个问题构建了一个完整的前端知识体系树。用户感慨“道心破碎”,但这样的面试即使挂了,也是收获巨大的——它划出了大厂对前端工程师的能力期望:不仅要会写代码,更要懂系统设计、懂前后端协作、懂性能优化。