先问约束,再给方案
场景题考的不是"你会不会写",而是你会不会问。同一个需求,10MB 和 10GB 的答案完全不同。这份每个场景都先给判断依据,再给实现和它的边界。
01先问四个问题数据量、网络、能否改服务端、失败代价。答完方案基本就定了。
02说清瓶颈分片解决的是失败成本、虚拟列表解决的是 DOM 数量,别把手段当目的。
03主动报边界能说出"这个方案到多大规模就不行了",比假装完美可信得多。
1
这类题怎么答:先问清约束★
没有约束就没有最优解
场景题考的不是"你会不会写",而是"你会不会问"。上来就给方案,对方会觉得你没做过真需求。正确姿势是先问四个问题:数据量多大、网络条件如何、能不能改服务端、失败重试的成本高不高。答完这四个,方案基本就定了。
以"大文件上传"为例,四个问题怎么问Q1 文件多大?单文件上限、并发上传数
10MB 以内 → 不用分片,直接传
100MB 以上 → 必须分片 + 断点续传
Q2 网络环境?内网还是公网、移动端多不多
内网稳定 → 可以少做重试
移动端弱网 → 必须做断点续传和分片重传
Q3 服务端能不能改?有没有对象存储
能改 → 用直传对象存储 + 预签名 URL,前端不经过业务服务器
不能改 → 只能在现有接口上做分片
Q4 失败的代价?用户会不会重传整个文件
会 → 秒传、断点续传是刚需
不会 → 简单校验就够了
问完这四个,"标准答案"反而没那么重要了。
- 答题结构建议:先复述需求确认理解 → 问约束 → 给出主干方案 → 主动说出瓶颈和取舍 → 补一句"如果要进一步优化可以……"。这个结构本身就是加分项。
- 主动说边界:比如"这个方案假设文件在 2GB 以内,再大就要考虑浏览器的 Blob 内存限制"。主动暴露边界比假装完美更可信。
- 常见的错误回答:直接背一段分片上传代码。面试官接着问"如果上传到 90% 失败了怎么办"、"两个用户同时传同一个文件会怎样",就答不上来了。
面试怎么答场景题我更关注先确认约束。我会先问四件事:数据量多大、网络条件怎么样、服务端和存储能不能改、失败重试的代价高不高。因为同样是文件上传,10MB 以内直接传就够了,100MB 以上才需要分片和断点续传;服务端能改的话可以直接预签名传对象存储,前端不经过业务服务器。问清这些再给方案,才不会给出一个用不上的架构。
2
大文件上传:分片、并发与顺序★
核心是"把一次大失败拆成多次小失败"
分片解决的不是速度,是失败成本:传 1GB 传到 999MB 断了要重来,切成 5MB 一片就只重传那一片。所以设计的重点是分片大小、并发数、以及失败片的追踪,而不是"怎么切开"。
◐ 可交互:改分片大小,看"一片失败"要重传多少
文件 100MB · 第 5 片失败一次
分片数 / 请求数20
失败重传5 MB
若不分片,失败要重传100 MB
分片解决的不是速度,是失败成本。切得越细,单次失败要重传的越少。
分片大小的权衡太小(1MB)
请求数暴涨:1GB = 1000 个请求
每个请求都有头部开销、连接复用受限
服务端合并压力大
太大(50MB)
一片失败重传 50MB,弱网下几乎必然反复失败
浏览器内存占用高(slice 是引用,但 FormData 序列化会读进内存)
常见值:2-10MB
移动端偏小(2-5MB),桌面端可以 5-10MB
更聪明的做法:根据网络速度动态调整(首片测速)
- 为什么不能
Promise.all直接发:1000 个分片会同时发起 1000 个请求,浏览器会对同域限流(6 个),其余排队,但内存里会堆满 FormData,移动端可能直接崩。必须用并发池控制。 - 合并必须由服务端做:分片上传到对象存储(如 OSS)时,用服务端的"分片上传"接口,前端拿 uploadId 和 partNumber,最后调 complete。不要自己拼二进制。
- 顺序问题:分片本身可以乱序上传,服务端按 partNumber 拼。但"合并"必须等所有片都成功,所以要维护一个成功计数或者失败列表。
- 取消上传:用
AbortController,每个分片请求带同一个 signal,取消时全部中断,同时通知服务端清理临时分片,否则存储会残留。
面试怎么答大文件上传的核心是把一次大失败拆成多次小失败,所以分片解决的是失败成本不是速度。设计要点有三个:分片大小通常 2-10MB,移动端偏小;并发数用并发池控制在 3-6,不能直接 Promise.all 否则几百个请求同时发会把内存撑爆;每个分片独立重试用指数退避。合并一定要用服务端的能力,前端不拼二进制,取消上传要同时清理服务端的临时分片。
3
断点续传与秒传★
一个靠 hash,一个靠查询
两个能力都建立在文件内容 hash 上:秒传是"这个 hash 服务端已经有了,直接返回成功";断点续传是"这个 hash 已经传了 3 片,你从第 4 片开始"。所以关键在于怎么快速算出 hash。
两段流程【秒传】
1. 前端算 hash
2. 请求服务端 /api/exists?hash=xxx
3. 命中 → 直接把文件记录关联到当前用户,返回成功
4. 未命中 → 走正常上传
注意:秒传要做权限校验,
不能因为"内容相同"就把别人的文件给你。
【断点续传】
1. 前端算 hash
2. 请求 /api/check?hash=xxx
3. 服务端返回 { uploadId, uploaded: [0,1,2] }
4. 前端跳过已有分片,从第 3 片开始
5. 全部完成后调 merge
关键:分片进度要能持久化
浏览器关闭再打开还能续传 → 把 hash 和进度写进 localStorage
或者完全依赖服务端的 uploaded 列表(更可靠)
- 抽样 hash 有碰撞风险:碰撞会导致"秒传成功但拿到的是别人的文件",这是严重的数据问题。所以抽样 hash 只能当"快速预判",服务端还得用完整 hash 或者 size + 抽样 hash + 文件名做联合校验。
- hash 计算必须放 Worker:
crypto.subtle.digest是异步的但仍可能占主线程,大文件会卡住页面。用 Web Worker 或 WASM 版本(如 hash-wasm)快很多。 - 为什么不用文件修改时间:
lastModified不可靠,复制文件会变、不同系统精度不同。只能当辅助信息。 - 断点续传的进度存在哪:存服务端最可靠(换浏览器也能续),存 localStorage 只能同一浏览器。两者可以都做,服务端为准。
面试怎么答秒传和断点续传都依赖文件内容 hash。秒传是算完 hash 先问服务端"这个文件有没有",有就直接关联返回成功;断点续传是问"这个 hash 传了多少片",返回已上传的分片列表,前端跳过这些从下一个开始。关键难点是 hash 计算,全量 SHA-256 对大文件太慢还会卡主线程,所以实际做法是抽样取头中尾加上文件大小算,放到 Worker 里;但抽样有碰撞风险,服务端要做二次校验,否则会出现秒传成功却拿到别人文件的问题。
4
扫码登录:两种实现★
PC 端怎么知道手机上确认了
扫码登录的本质是PC 端拿不到手机上的登录态,需要一个中间人传递。流程是:PC 生成一个临时票据并轮询,手机扫码后用自己的登录态"授权"这个票据,PC 轮询到授权结果再用票据换 token。关键设计是票据要短时效、一次性、且不能直接放 token。
完整流程1. PC 请求 /api/qrcode
→ { ticket: "abc123", expire: 120 }
→ 同时渲染二维码,内容是一个短链 ?ticket=abc123
2. PC 开始轮询 /api/qrcode/status?ticket=abc123
状态:waiting(未扫)→ scanned(已扫待确认)→ confirmed(已确认)
3. 手机扫码,打开短链(此时手机已有登录态)
→ 服务端记录 ticket 对应的用户,状态置为 scanned
4. 手机上点"确认登录"
→ 服务端把 ticket 置为 confirmed,同时生成一个一次性 code
5. PC 轮询到 confirmed,拿到 code
→ 请求 /api/login/exchange { code } → 返回真正的 token
6. 二维码作废,ticket 标记已使用
- 为什么不让 PC 直接拿 token:如果轮询接口直接返回 token,任何拿到 ticket 的人都能登录。所以要多一步"用一次性 code 换 token",code 只能换一次且有效期极短(30 秒)。
- ticket 要能防爆破:不能是自增数字,要用足够随机的字符串(uuid 或 32 位随机),并且限制轮询频率。
- 轮询 vs 长轮询 vs SSE:轮询 1-2 秒一次最简单,扫码是个低频操作,浪费可忽略。要求极快响应可以用 SSE,但没必要。
- 二维码有效期:通常 1-2 分钟。过期后要能自动刷新(前端重新请求 qrcode),且旧 ticket 必须失效。
- 安全要点:手机上必须显示"在 xxx 设备登录"让用户确认;确认前要展示设备信息(浏览器 UA、IP 归属地),防止用户扫了钓鱼二维码还不自知。
面试怎么答扫码登录的核心问题是 PC 端拿不到手机上的登录态,需要一个中间票据传递。流程是 PC 请求生成 ticket 并渲染二维码,然后轮询状态;手机扫码后用自己已有的登录态把 ticket 标记为已扫,用户确认后置为已确认;PC 轮询到确认后用一次性 code 换真正的 token。关键是 ticket 要随机、短时效、只能用一次,并且不能直接返回 token,否则任何拿到 ticket 的人都能登录。手机上还要显示设备信息让用户确认,防止扫到钓鱼码。
5
前端水印
防的是顺手截图,不是专业破解
前端水印做不到真正防泄露,它防的是"顺手截图发出去"和"截图上没有归属信息"。做法有两类:DOM 水印(一层 div 铺文字,可被删)和 Canvas 水印(画成图片再平铺,删了能监听重建)。真正的防护要靠服务端加盲水印和审计日志。
- 为什么必须
pointer-events: none:不加的话水印层会挡住所有点击,页面直接不能用了。 - DOM 水印为什么容易被删:用户打开 DevTools 删掉那个 div 就没了。MutationObserver 能重建,但用户也可以禁用 JS 或者改 CSS。所以这只是"提高门槛"。
- 更狠的做法:把水印做成背景图的一部分、或者用
::after伪元素配合content,比独立 div 难删一点。真正的方案是服务端在图片上打标或者存盲水印。 - 水印内容带什么:用户名 + 用户 id + 时间戳。目的是泄露后能追溯,不是阻止截图。这一点要在需求沟通时说清楚,否则会做出一个"看起来很安全但没用"的功能。
- 打印和截图:CSS 的
@media print里也要保留水印,否则打印出来就没了。截图没法防,只能用盲水印兜底。
面试怎么答前端水印防不住专业破解,它防的是顺手截图和"截图没有归属信息"。实现上是生成一张带文字的 Canvas 图片作为背景平铺,铺在一个 pointer-events: none 的全屏层上,再用 MutationObserver 监听被删除后重建。要提高门槛可以做成伪元素或者背景图的一部分。水印内容放用户名、id 和时间戳,目的是泄露后可追溯。真正的防护要靠服务端在图片上打标或者盲水印,加上访问审计日志。
6
无限滚动:什么时候加载下一页★
判断点选错就体验崩了
无限滚动的核心是什么时候触发加载。三种做法:监听 scroll 算距离(最灵活)、
IntersectionObserver 观察哨兵元素(最简单)、以及"预加载提前量"(用根容器的 rootMargin 实现)。推荐后两种。- 为什么
IntersectionObserver比 scroll 好:scroll 事件触发频率高,要节流;算scrollHeight - scrollTop - clientHeight会强制同步布局,容易引起掉帧。IO 由浏览器在合成阶段判断,不占主线程。 rootMargin是预加载的关键:设 200px 意味着哨兵进入"距离底部 200px"就开始加载,用户滚到底时数据已经到了。不设的话会看到明显的加载指示器。- 必须防重复触发:IO 在滚动中可能多次触发,要有一个
loading锁。不加的话一次滚动能发出好几个请求。 - 没有更多了要 disconnect:否则哨兵一直在视口里反复触发,每次都要检查
hasMore。虽然逻辑上正确,但白白消耗。 - 返回顶部后位置丢失:如果列表用虚拟滚动或者重新渲染,滚动位置会跳。要记录并恢复,或者用 keep-alive 缓存页面(见路由那份)。
面试怎么答无限滚动的关键是触发时机。推荐用 IntersectionObserver 观察一个哨兵元素,而不是监听 scroll 算距离,因为 scroll 事件频率高还要算 scrollHeight 会强制同步布局引起掉帧,而 IO 在合成阶段判断不占主线程。要设置 rootMargin 提前 200px 左右触发,用户滚到底时数据已经到了,看不到加载态。必须加 loading 锁防重复触发,没有更多数据时 disconnect 掉观察器。
7
虚拟列表:只渲染看得见的那些★
一万行表格为什么能秒开
虚拟列表的核心是只渲染视口内的几十个节点,用空白把滚动高度撑起来。所以关键参数只有三个:每项高度、视口高度、当前滚动位置。难度全在"高度不固定"和"快速滚动时白屏"这两件事上。
三种虚拟化方案【固定高度】最简单的
已知每项 H,直接 start = scrollTop / H
适合:表格、聊天记录、固定行高的日志
【不定高度 + 预估】
先按预估值排,渲染后测量真实高度,修正偏移表
需要一个数组记录每项的实际偏移
适合:列表项内容长短不一(评论、商品卡片)
【不定高度 + 滚动锚定】
渲染后如果测量值变了,调整 scrollTop 保持视觉不跳
难点:用户正在滚动时不能动,否则会抖
这是所有虚拟列表库最复杂的部分
- 为什么要 BUFFER:滚动事件是离散的,浏览器在两帧之间可能跳过几十像素。不预渲染的话快速滚动时会看到白块。上下各留 3-10 个是常见值。
- 为什么用 transform 而不是 top:
top会触发布局,transform只走合成。高频滚动时差别明显。 - 一万行真的能秒开吗:也取决于数据结构。如果每行都绑定事件,用事件委托;如果有复杂子组件,要避免每次滚动都重新创建。所以虚拟列表常配合"渲染结果缓存"。
- 和无限滚动的区别:无限滚动解决"数据太多不一次加载",虚拟列表解决"DOM 太多渲染慢"。两者经常一起用:分页拉数据,虚拟列表渲染。
面试怎么答虚拟列表只渲染视口内的节点,用总高度把滚动条撑出来,然后用 transform 把实际内容平移到正确位置。三个核心参数是每项高度、视口高度、滚动位置。上下要预留几个 buffer 防止快速滚动时白屏,位移用 transform 而不是 top 因为不触发布局。固定行高直接用除法算,不定高度要先预估再测量修正偏移表,最难的部分是测量值变化时不能让它视觉上跳动。
8
图片裁剪与压缩
上传前先瘦身
前端压缩的价值是省上传时间、省服务端存储和带宽。做法是画到 Canvas 再
toBlob 导出。三个关键点:用 canvas 重绘而不是直接改 quality、注意 EXIF 方向、以及大图要在 Worker 里处理。createImageBitmap会自动应用 EXIF 方向:用new Image加载手机照片时,很多情况下图片是旋转的,要手动读 EXIF 的 Orientation 再转。前者省了这个麻烦。- 不要反复压缩:JPEG 是有损的,压一次画质降一次。用户如果编辑后再保存,要基于原图重新压,不能基于上一次的结果。
toBlob是异步的、toDataURL是同步的:大图用toDataURL会阻塞主线程几百毫秒,还会生成一个巨大的 base64 字符串(比二进制大 33%)。- WebP 更好:同等质量下体积比 JPEG 小 25-35%,现代浏览器都支持。可以用
canvas.toBlob指定image/webp,不支持时自动回落 JPEG。 - 裁剪框要处理 devicePixelRatio:显示尺寸和实际像素不是一回事,导出时要按原图比例换算,否则会糊。
面试怎么答前端压缩是把图片画到 Canvas 再导出,价值是省上传时间和存储带宽。三个要点:用 createImageBitmap 而不是 new Image,因为它会自动处理 EXIF 方向,否则手机照片会是躺着的;导出用异步的 toBlob 而不是同步的 toDataURL,后者对大图会阻塞主线程且 base64 比二进制大三分之一;尺寸要按 maxSide 等比缩放,格式优先 WebP 再回落 JPEG。大图处理要放 Worker 避免卡界面。
9
Excel 导入导出
前端生成还是后端生成
判断标准是数据量和是否已经有服务端。导出:小数据(几千行)前端用 SheetJS 生成,大数据(十万行)必须后端流式生成,前端生成会直接把浏览器内存撑爆。导入:小文件前端解析校验后提交,大文件直接上传由后端解析。
四种组合【导出-前端生成】SheetJS / exceljs
优点:不占服务端资源,能导出"当前页面筛选后的数据"
缺点:十万行以上浏览器会卡死甚至崩溃
适合:几千行以内、需要在客户端做格式处理
【导出-后端生成】服务端流式写 xlsx
优点:能处理百万行,内存可控
缺点:占服务端 CPU,大文件要异步任务 + 下载通知
适合:大数据量、定时报表
【导出-纯前端流式】手写 CSV
CSV 其实不需要 SheetJS,字符串拼接就行
缺点:没有格式、多 sheet、公式;中文要用 BOM 防乱码
适合:只要数据不要样式的场景
【导入】
小文件(<5MB):前端用 SheetJS 解析,先做格式和必填校验,把错误行标出来让用户改
大文件:直接上传,后端异步解析,用轮询或 WebSocket 通知进度
- Excel 中文乱码:导出 CSV 时前面加 BOM(
\uFEFF),否则 Excel 会按 GBK 解析导致乱码。这是最常见的导出 bug。 - 数字被吃成科学计数法:身份证号、订单号这类长数字,Excel 会自动转成
1.23E+17。导出时要设成文本格式('@')或者拼一个前导单引号。 - 导入必须做双重校验:前端校验给即时反馈,但后端必须重新校验——前端校验可以被绕过,而且导入的数据会入库。
- 导入的错误提示要精确到单元格:只说"格式错误"用户不知道改哪。正确做法是返回"第 5 行 B 列:手机号格式不正确",并且把错误标在原文件上让用户下载。
面试怎么答Excel 导出选前端还是后端,看数据量。几千行以内可以前端用 SheetJS 生成,优点是能导出当前筛选后的数据、不占服务端资源;十万行以上必须后端流式生成,前端生成会撑爆内存。导入方面小文件前端解析做格式校验给即时反馈,但后端必须重新校验,因为前端校验能被绕过。导出要注意两个细节:CSV 要加 BOM 否则中文乱码,长数字要设成文本格式否则会被转成科学计数法。
10
拖拽排序
HTML5 拖拽 API 为什么不好用
原生 HTML5 拖拽(
draggable + dragstart 那一套)在移动端完全不支持,而且拖拽影像是浏览器自己画的,样式没法控制。所以实际项目几乎都用 Pointer Events 自己算,或者用 dnd-kit / SortableJS 这类库。setPointerCapture解决什么问题:不设的话鼠标快速移动会跑出元素范围,事件就丢了,拖拽会中断。设置之后所有指针事件都被这个元素捕获。- 拖拽过程中只改 transform:不要真的移动 DOM(
insertBefore),否则每帧都要重排。等放下时再提交顺序变更。 - 触屏要
touch-action: none:不加的话手指滑动会触发页面滚动,拖拽和滚动打架。 - 为什么需要"自动滚动":拖到列表边缘要自动滚屏,否则拖不到远处。这是这类组件最容易漏的功能。
- 可访问性:拖拽对键盘用户不可用,所以必须提供替代操作(上下移动按钮或者快捷键)。这是很多拖拽组件的通病。
面试怎么答原生 HTML5 拖拽 API 在移动端不支持,而且拖拽影像样式不可控,所以实际都用 Pointer Events 自己实现或者用 dnd-kit 这类库。实现要点是 pointerdown 时调 setPointerCapture 让后续事件都被这个元素捕获,否则鼠标快速移出元素就会中断;拖动过程中只改 transform 不真的移动 DOM,放下时再提交顺序;触屏要加 touch-action: none 否则和页面滚动打架。还要补自动滚屏和键盘替代操作。
11
速答清单
考前十分钟只看这一页
01场景题该怎么开头?
先问约束:数据量多大、网络条件、能不能改服务端、失败重试代价高不高。问完再给方案,不要上来就背代码。
02大文件上传为什么分片?
分片解决的不是速度,是失败成本。1GB 传到 999MB 断了要重来,切成 5MB 一片只重传那一片。
03分片大小怎么定?
常见 2-10MB,移动端偏小桌面端偏大。太小请求数暴涨,太大弱网下反复重传。
04为什么不能 Promise.all 发所有分片?
几百个请求同时发会把 FormData 堆在内存里,移动端可能直接崩,且浏览器同域会限流。要用并发池控制在 3-6。
05秒传的原理和风险?
算文件 hash 问服务端"有没有这个文件",有就直接关联。风险是抽样 hash 有碰撞,会出现秒传成功却拿到别人文件,所以服务端要做二次校验。
06断点续传的进度存哪?
存服务端最可靠,换浏览器也能续。存 localStorage 只能同浏览器。两者都做时以服务端为准。
07文件 hash 怎么算才不卡?
全量 SHA-256 对大文件太慢还会卡主线程。实际做法是抽头中尾三段加文件大小算,放 Worker 里执行。
08扫码登录为什么不能直接返回 token?
任何拿到 ticket 的人都能登录。所以要多一步用一次性 code 换 token,code 只能换一次且有效期极短。
09扫码登录的状态有哪几个?
waiting 未扫码、scanned 已扫待确认、confirmed 已确认。手机上还要显示设备信息让用户确认,防钓鱼。
10前端水印能防住破解吗?
不能。它防的是顺手截图和"截图没有归属信息"。要做到删了重建,但用户禁用 JS 就没了。真正的防护靠服务端盲水印加审计日志。
11无限滚动为什么用 IntersectionObserver?
scroll 事件频率高还要算 scrollHeight,会强制同步布局掉帧。IO 在合成阶段判断,不占主线程。
12无限滚动要设什么防白屏?
rootMargin 提前 200px 左右触发,用户滚到底时数据已经到了。还要加 loading 锁防重复请求。
13虚拟列表的核心参数?
每项高度、视口高度、当前滚动位置。用总高度撑滚动条,用 transform 把内容平移到正确位置。
14虚拟列表为什么要 BUFFER?
滚动事件是离散的,两帧之间可能跳过几十像素。上下各预留几个避免快速滚动时白屏。
15图片压缩为什么用 createImageBitmap?
它会自动应用 EXIF 方向。用 new Image 加载手机照片常常是躺着的,要手动读 Orientation 再旋转。
16导出 CSV 中文乱码怎么解?
文件开头加 BOM(\uFEFF),否则 Excel 按 GBK 解析。长数字要设成文本格式,否则会被转成科学计数法。
17拖拽为什么不用原生 HTML5 API?
移动端完全不支持,且拖拽影像样式不可控。用 Pointer Events 自己实现,注意 setPointerCapture 防止移出元素后中断。