
3个高频面试题拆解:从零手写可以下载视频的浏览器
看了一堆教程还是不会写项目?别慌,这往往是把“看代码”当成了“做开发”。今天咱们不聊虚的,直接上手一个可以下载视频的浏览器实战项目。这不仅是练手,更是为了吃透那些高频面试题背后的底层逻辑。很多兄弟在 Stack Overflow 上问“为什么我的下载请求被拦截了”,其实核心就卡在流处理这一环。
项目目标:不只是个下载器
咱们要做的不是一个简单的“右键另存为”,而是一个具备完整下载管理能力的浏览器内核组件。目标很明确:
拦截视频流:能识别 video 标签或 XHR/Fetch 请求中的 MP4/M3U8 流。
断点续传支持:利用 HTTP Range 请求头,实现大文件分片下载。
多线程合并:模拟真实浏览器内核,将分片数据在内存中重组。
这直接对应了后端高并发场景下的流式传输与内存管理,也是前端大文件处理的核心考点。
目录结构:工程化思维
别一上来就写代码,先搭骨架。一个标准的 Node.js + WebSocket 架构项目结构如下:
video-browser/
├── server/
│ ├── index.js # 入口文件,启动 HTTP 服务
│ ├── downloader.js # 核心下载逻辑,处理 Range 请求
│ └── merger.js # 分片合并逻辑,Buffer 操作
├── client/
│ ├── index.html # 前端页面,模拟浏览器环境
│ └── app.js # 前端逻辑,发起下载请求
└── package.json
这种结构清晰分离了“网络层”(downloader)和“数据层”(merger),方便后续单元测试。很多初学者喜欢把所有逻辑堆在 index.js 里,导致后期维护噩梦。记住:模块单一职责原则,这是面试必问。
核心代码实现:逐行拆解
1. 服务端:拦截与分片请求
在 server/downloader.js 中,我们要模拟一个视频服务器,支持 Range 请求。这是实现断点续传的关键。
const http = require('http');
const fs = require('fs');
const path = require('path');
// 模拟视频文件,实际项目中可以是远程 URL
const VIDEO_PATH = './assets/sample.mp4';
function handleDownload(req, res) {
const filePath = VIDEO_PATH;
const stat = fs.statSync(filePath);
const fileSize = stat.size;
// 关键:解析 Range 头,实现分片
let start = 0;
let end = fileSize - 1;
if (req.headers.range) {
const parts = req.headers.range.replace(/bytes=/, ).split(-);
start = parseInt(parts[0], 10);
if (parts[1]) {
end = parseInt(parts[1], 10);
}
end = Math.min(end, fileSize - 1);
}
// 计算分片大小
const chunkSize = (end - start) + 1;
// 设置响应头,告诉客户端我们支持 Range
res.writeHead(206, {
'Content-Range': `bytes ${start}-${end}/${fileSize}`,
'Accept-Ranges': 'bytes',
'Content-Length': chunkSize,
'Content-Type': 'video/mp4'
});
// 创建读取流,避免一次性加载大文件到内存
const stream = fs.createReadStream(filePath, { start, end });
stream.pipe(res);
}
module.exports = { handleDownload };
逐行解析:
req.headers.range:这是 HTTP 协议的标准头,浏览器下载大文件时会自动发送。很多新手忽略这一点,导致只能一次性下载全量数据,内存直接爆炸。
res.writeHead(206, ...):206 状态码表示 Partial Content,即部分内容。如果返回 200,客户端可能不会触发分片逻辑。
stream.pipe(res):使用流(Stream)处理大文件是 Node.js 的精髓。绝对不要用 fs.readFileSync 读取几百兆的视频文件,那会让你的服务器瞬间卡死。
2. 服务端:分片合并器
在 server/merger.js 中,我们负责接收分片并合并。这里涉及到底层 Buffer 操作。
const fs = require('fs');
const path = require('path');
class Merger {
constructor(fileName, totalSize) {
this.fileName = fileName;
this.totalSize = totalSize;
this.chunks = new Map(); // 用 Map 存储分片,Key 为分片索引
this.receivedSize = 0;
}
// 接收一个分片
addChunk(index, buffer) {
this.chunks.set(index, buffer);
this.receivedSize += buffer.length;
// 检查是否所有分片都接收完毕
if (this.chunks.size === this.totalChunks) {
this.merge();
}
}
merge() {
const writeStream = fs.createWriteStream(path.join(__dirname, '../downloads', this.fileName));
// 按顺序写入分片
for (let i = 0; i this.totalChunks; i++) {
if (!this.chunks.has(i)) {
throw new Error(`Missing chunk ${i}`);
}
writeStream.write(this.chunks.get(i));
}
writeStream.end();
console.log(`File ${this.fileName} merged successfully.`);
}
}
module.exports = { Merger };
避坑指南:
顺序问题:HTTP 请求是无序的,分片 2 可能比分片 1 先到。所以必须用 Map 或数组暂存,等待所有分片到齐后再按顺序写入磁盘。
内存泄漏:如果分片数量巨大,Map 会占用大量内存。在生产环境中,应该先写入临时文件,再移动,而不是全部缓存在内存中。
3. 前端:模拟浏览器行为
在 client/app.js 中,我们模拟用户点击下载。
async function downloadVideo(url) {
const response = await fetch(url);
const contentLength = parseInt(response.headers.get('Content-Length'));
// 假设我们分成 4 个块下载
const chunkSize = Math.ceil(contentLength / 4);
const promises = [];
for (let i = 0; i 4; i++) {
const start = i * chunkSize;
const end = (i + 1) * chunkSize - 1;
// 发送带 Range 头的请求
const res = await fetch(url, {
headers: {
'Range': `bytes=${start}-${end}`
}
});
const reader = res.body.getReader();
const chunks = [];
while (true) {
const { done, value } = await reader.read();
if (done) break;
chunks.push(value);
}
// 这里简化处理,实际应发送给后端 Merger
console.log(`Chunk ${i} received: ${chunks.length} buffers`);
}
}
运行与测试:验证你的理解
启动服务:node server/index.js
打开前端:访问 localhost:3000
打开开发者工具:按 F12,切换到 Network 面板。
点击下载:观察请求。
关键观察点:
你应该看到 4 个独立的请求,每个请求的 Range 头不同。
响应状态码应为 206。
如果状态码是 200,说明你的 Range 解析逻辑有误,或者服务器不支持 Range。
在 Stack Overflow 上,关于“206 状态码无效”的问题比比皆是。通常是因为 Nginx 反向代理层没有正确转发 Range 头,或者后端代码没有正确设置 Accept-Ranges: bytes。
优化扩展:从 Demo 到生产
并发控制:不要一次性发起所有分片请求,使用 p-limit 库限制并发数,避免打爆服务器。
错误重试:网络抖动很常见,如果某个分片下载失败,应自动重试。
加密传输:实际视频中,视频流往往是 AES 加密的,需要在前端解密后再合并。
进度条实时更新:通过 WebSocket 推送每个分片的完成状态,前端实时更新进度条。
这些优化点,正是区分“会写 Demo”和“能做项目”的分水岭。面试官问“你的项目有什么难点”,如果你能答出“通过 Range 请求实现断点续传,并处理了分片乱序和并发控制问题”,基本就稳了。
小结:从代码到思维
通过这个可以下载视频的浏览器项目,你不仅学会了如何解析 HTTP Range 头,还掌握了流式处理、分片合并等核心技能。这些技能在高频面试题中出现的频率极高,比如“如何实现大文件上传”、“如何解决视频流卡顿”等。
记住,编程不是背八股文,而是理解数据在网络中如何流动。每一个字节从服务器到浏览器,中间经历了多少次协议转换、内存拷贝,只有你自己动手写过,才能体会其中的奥妙。
别光看着,把代码跑起来,改改参数,看看报错信息。还有什么不懂的?评论区留言挨个回。