HUYA-中台视频上传SDK
一、项目概述
1.1 项目定位
video-middleend-upload(v1.0.0)是一个面向虎牙内部各业务线的通用中台视频上传 SDK,提供文件选择、分片上传、断点续传、进度追踪、失败重试等核心能力,支持默认 UI 和无 UI 两种集成模式,可灵活嵌入到各个视频相关的后台管理系统中。
1.2 核心特性
| 特性 | 说明 |
|---|---|
| 分片上传 | 大文件切割为 5MB 分片,逐片上传 |
| 失败重试 | 每片支持 2 次重试,隔离错误不影响其他分片 |
| MD5 校验 | 文件整体 + 每片 MD5,保证数据完整性 |
| 多文件并发 | 支持同时上传多个文件(可配置并发数) |
| 暂停/恢复 | 支持单文件暂停和恢复上传 |
| 默认 UI | 内置文件列表、进度条、操作按钮 |
| 无 UI 模式 | 纯逻辑 SDK,业务方自定义界面 |
| 事件驱动 | 完整的事件系统,覆盖上传全生命周期 |
| 数据上报 | 上传成功率、分片耗时等关键指标 |
SDK 最大的设计价值在于分片 + 事件驱动 + UI 解耦三位一体:分片上传解决大文件传输可靠性,EventBus 事件系统让上传逻辑与界面完全分离,使同一个 SDK 能零修改嵌入 React / Vue / jQuery 等任意技术栈。
二、技术栈
┌──────────────────────────────────────────────────┐
│ video-middleend-upload 技术栈 │
├──────────────┬───────────────────────────────────┤
│ 语言 │ ES6+ JavaScript (Babel 7 转译) │
│ 构建工具 │ Webpack 4 + Babel 7 │
│ HTTP 客户端 │ Axios 0.19 │
│ MD5 计算 │ spark-md5 3.0 │
│ 参数序列化 │ qs 6.7 │
│ 样式方案 │ SCSS (node-sass 4.12) │
│ 模板引擎 │ EJS (ejs-loader) │
│ 开发服务 │ Express + webpack-dev-server │
│ 代码压缩 │ UglifyJS Webpack Plugin │
│ 输出格式 │ 单文件 dist/main.js │
└──────────────┴───────────────────────────────────┘
三、项目结构
video-middleend-upload/
├── src/
│ ├── index.js # 入口文件
│ ├── uploader.js # 主类 HuyaUploader(核心协调器)
│ ├── env.js # 环境配置(baseURL)
│ ├── core/ # 核心模块
│ │ ├── upload.js # 上传引擎 HYUpload(分片逻辑)
│ │ ├── queue.js # 文件队列 HYQueue(并发管理)
│ │ ├── file.js # 文件对象 HYFile(状态封装)
│ │ ├── emitter.js # 事件发射器基类
│ │ ├── bus.js # 全局事件总线(单例)
│ │ ├── status.js # 上传状态枚举
│ │ ├── utils.js # 工具函数(MD5、格式化)
│ │ ├── report.js # 数据上报
│ │ ├── store.js # LocalStorage 封装
│ │ ├── cycle.js # 生命周期管理(预留)
│ │ └── dnd.js # 拖拽上传(预留)
│ ├── server/
│ │ ├── api.js # HTTP 请求层(Axios 封装)
│ │ └── mid-back.js # 后端配置接口(预留)
│ ├── template/ # 默认 UI 模板
│ │ ├── index.html # 上传区域模板
│ │ ├── list.html # 文件列表项模板 (EJS)
│ │ └── index.scss # 默认样式
│ └── assets/
│ └── iconfont/ # 图标字体
├── server/
│ ├── server.js # 本地开发 Express 服务
│ └── package.json
├── webpack.base.config.js # Webpack 基础配置
├── webpack.config.js # 开发环境配置
├── webpack.prod.config.js # 生产环境配置
├── .babelrc # Babel 配置
├── index.html # 默认 Demo
├── custom.html # 自定义 UI Demo
└── package.json
四、核心架构设计
4.1 整体架构
4.2 类关系图
五、上传流程详解
5.1 完整上传流程
5.2 分片上传原理
5.3 进度计算
// 单片进度 → 整体进度
e.percent = ((fseq * chunkSize + loaded) / totalSize) * 100;
e.percent = Math.min(e.percent, 100);
file.percent = e.percent;
// 示例: 文件 23MB, chunkSize 5MB, 正在上传第 3 片, 已传 2MB
// percent = ((2 * 5MB + 2MB) / 23MB) * 100 ≈ 52.2%
六、文件状态机
6.1 状态枚举
6.2 状态定义
const UPLOAD_STATUS = {
INITED: 'inited', // 已创建
QUEUED: 'queued', // 排队中
READY: 'ready', // 准备中 (MD5 计算)
PROGRESS: 'progress', // 上传中
ERROR: 'error', // 错误 (可重试)
COMPLETE: 'complete', // 完成
CANCELLED: 'cancelled', // 已取消
INTERRUPT: 'interrupt', // 已暂停 (可恢复)
INVALID: 'invalid' // 无效 (校验失败)
};
七、并发控制与队列管理
7.1 并发上传模型
7.2 并发控制代码
async uploadFiles() {
const files = queue.getQueueFile(); // 获取所有 QUEUED 文件
const threads = Math.min(files.length, this.options.threads);
for (let j = 0; j < threads; j++) {
this.next(); // 启动 N 个并行上传
}
}
async next() {
const files = queue.getQueueFile();
if (files && files.length) {
await upload.startUpload(files[0]); // 取第一个排队文件
this.next(); // 完成后递归取下一个
}
}
八、MD5 校验机制
8.1 双模式 MD5 计算
8.2 技术实现
// 快速 MD5: 只取文件头/中/尾三段
simpleMd5File(file) {
const chunks = Math.ceil(file.size / (2 * 1024 * 1024)); // 2MB 一段
const spark = new SparkMD5.ArrayBuffer();
// 读取 Head
readChunk(0);
// 读取 Middle
readChunk(Math.floor(chunks / 2));
// 读取 Tail
readChunk(chunks - 1);
return spark.end(); // 返回 MD5 字符串
}
九、API 层设计
9.1 接口总览
9.2 接口详解
POST /upload/init — 初始化上传
// 请求
{
fname: '演示视频.mp4', // 文件名
fsuffix: 'mp4', // 扩展名
fsize: 24117248, // 文件大小 (bytes)
fmd5: 'a1b2c3d4...', // 文件 MD5
extendInfo: '{"key":"val"}' // 业务扩展信息 (JSON)
}
// 响应
{
success: 1,
data: {
vid: 'v_20230101_abc', // 服务端分配的视频 ID
clips: [ // 分片列表
{ fseq: 0, status: 0 }, // status: 0=未上传, 1=已上传
{ fseq: 1, status: 0 },
{ fseq: 2, status: 1 }, // 此片已存在 (秒传)
...
],
chunkSize: 5242880, // 分片大小 (bytes)
vstatus: 0 // 0=待上传, 其他=已完成
}
}
如果 vstatus 表示已完成,说明服务端已有相同 MD5 的文件,直接跳过上传——零字节传输,毫秒级完成。
POST /upload/clip — 分片上传
// 请求 (multipart/form-data)
FormData {
vid: 'v_20230101_abc', // 视频 ID
fseq: 0, // 分片序号
cmd5: 'e5f6g7h8...', // 分片 MD5
chunk: Blob // 分片数据
}
// 响应
{
success: 1,
data: {
status: 1, // 分片状态
vstatus: 0 // 整体状态 (最后一片返回完成)
}
}
9.3 请求安全
// 每个请求必须携带的 Headers
{
APPID: '1048576', // 应用 ID
TOKEN: '2f6a4e2071474c77...', // 认证令牌
VERSION: '20190617', // API 版本
TIMESTAMP: Date.now(), // 时间戳 (ms)
NONCE: Math.random() * 10000 // 随机数 (防重放)
}
9.4 请求取消 (CancelToken)
// 每个上传请求绑定 CancelToken
const config = {
cancelToken: new axios.CancelToken(executor => {
requestCancelQueue[file.uid] = executor; // 保存取消函数
})
};
// 用户取消时调用
function cancelFile(file) {
file.cancel = true;
requestCancelQueue[file.uid](); // 中断 HTTP 请求
}
十、事件系统
10.1 事件流
10.2 完整事件列表
| 事件名 | 触发时机 | 回调参数 | 说明 |
|---|---|---|---|
append | 文件加入队列 | (file, queue) | 新文件已添加 |
remove | 文件从队列移除 | (file, queue) | 文件已移除 |
accept | 文件类型校验失败 | (error, file) | 不支持的文件类型 |
limit | 文件大小超限 | (error, file) | 超过 limitSize |
start | 开始上传 | (file, response) | init 接口返回后 |
progress | 上传进度更新 | (event, file) | event.percent: 0-100 |
complete | 上传完成 | (file) | 所有分片完成 |
error | 上传错误 | (error, file) | 重试耗尽后 |
cancel | 上传取消 | (file) | 用户主动取消 |
statuschange | 文件状态变化 | (status, prevStatus) | 任何状态切换 |
10.3 使用示例
const uploader = new HuyaUploader({ ... });
uploader
.on('append', (file) => {
console.log(`文件已添加: ${file.name} (${file.formatSize})`);
})
.on('progress', (e, file) => {
console.log(`${file.name}: ${e.percent.toFixed(1)}%`);
})
.on('complete', (file) => {
console.log(`上传完成: ${file.name}, VID: ${file.vid}`);
// 查询成品 URL
uploader.searchFileUrl(file.vid, (res) => {
console.log('视频地址:', res.data.url);
});
})
.on('error', (err, file) => {
console.error(`上传失败: ${file.name}`, err);
})
.on('limit', (err, file) => {
alert(`文件 ${file.name} 超过大小限制`);
})
.on('accept', (err, file) => {
alert(`不支持的文件格式: ${file.ext}`);
});
十一、错误处理与重试机制
11.1 多层错误处理
11.2 HTTP 响应拦截
// Axios 响应拦截器
service.interceptors.response.use(
response => {
if (response.data.success !== 1) {
// 业务错误
bus.emit('result_error', response.data);
return Promise.reject(response.data);
}
return response;
},
error => {
// HTTP 错误码映射
const statusMap = {
400: '请求错误',
401: '未授权',
403: '拒绝访问',
404: '请求地址出错',
408: '请求超时',
500: '服务器内部错误',
501: '服务未实现',
502: '网关错误',
503: '服务不可用',
504: '网关超时',
505: 'HTTP版本不受支持'
};
bus.emit('http_error', statusMap[error.status] || '连接出错');
}
);
十二、UI 系统
12.1 默认 UI 结构
┌──────────────────────────────────────────┐
│ ┌────────────────┐ │
│ │ 选择文件上传 │ ← 按钮 (触发隐藏 input)│
│ └────────────────┘ │
│ 支持 MP4, AVI, MKV 等格式,最大 7GB │
│ │
│ ┌──────────────────────────────────────┐│
│ │ 视频文件.mp4 45.2 MB ││
│ │ ████████████████░░░░░ 72.3% ⏸ 🗑 ││
│ ├──────────────────────────────────────┤│
│ │ 演示视频.avi 128 MB ││
│ │ ██████████████████████ 100% ✓ ││
│ └──────────────────────────────────────┘│
└──────────────────────────────────────────┘
12.2 模板引擎
<!-- list.html (EJS 模板) -->
<li class="hy-file_<%= uid %>">
<div class="hy-upload-info">
<span class="hy-file-name"><%= name %></span>
<span class="hy-file-size"><%= formatSize %></span>
</div>
<div class="hy-upload-progress">
<span class="hy-upload-percent"><%= percent %>%</span>
<div class="hy-upload-progress-wrap">
<div class="hy-upload-progress-bar" style="width: <%= percent %>%"></div>
</div>
<span class="hy-upload-opera">
<a class="hy-upload-pause" data-type="pause" data-uid="<%= uid %>">
<i class="iconfont icon-pause">❚❚</i>
<i class="iconfont icon-upload">⬆</i>
</a>
<a class="hy-upload-remove" data-type="remove" data-uid="<%= uid %>">
<i class="iconfont">🗑</i>
</a>
</span>
</div>
</li>
12.3 UI 渲染方法
render() // 初始化 UI 结构
renderList() // 更新文件列表 (append/remove 触发)
renderProgress() // 更新进度条宽度和百分比文本
renderBtn() // 切换暂停/恢复按钮图标
bindInput() // 绑定隐藏 file input
bind() // 绑定列表操作事件代理
十三、数据上报
// 上报指标
reportUpload({
name: 'web.page.upload.clip', // 或 'web.page.upload.file'
pageview: appId,
dim: {
vid, // 视频 ID
app_id, // 应用 ID
token, // 认证令牌
name: fileName, // 文件名
uri: baseUrl, // 上传服务地址
success: 1 | 0, // 是否成功
clip: fseq, // 分片序号
err_code, // 错误码
err_msg, // 错误信息
// 以下为文件级上报额外字段
vformat, // 视频格式
vsize, // 文件大小
time, // 上传耗时 (ms)
chunk_size, // 分片大小
chunk_sum, // 总分片数
clips, // 剩余未传分片数 (失败时)
continuingly, // 续传标记
vstatus // 视频处理状态
}
});
十四、完整配置项
new HuyaUploader({
// === 容器与交互 ===
wrapper: document.querySelector('#uploader'), // UI 容器 (默认 UI 模式)
target: null, // 触发上传的元素 (无 UI 模式)
useUI: true, // 是否使用默认 UI
// === 文件限制 ===
accept: '3GP,AVI,FLV,MP4,MPG,ASF,WMV,MKV,MOV,TS,WebM', // 允许的格式
multiple: true, // 多选
limitSize: 7 * 1024 * 1024 * 1024, // 最大 7GB
// === 上传行为 ===
auto: false, // 自动上传
chunked: true, // 分片上传
chunkSize: 5 * 1024 * 1024, // 分片大小 5MB
chunkRetry: 2, // 每片重试次数
threads: 3, // 并发文件数
// === 认证 ===
headers: {
APPID: '1048576', // 应用 ID (必须)
TOKEN: '2f6a4e2071474c77b090f38447bc5c9c', // 认证令牌 (必须)
VERSION: '20190617' // API 版本 (必须)
},
// === 自定义参数 ===
params: {}, // 每次请求附加参数
initParams: {
extendInfo: JSON.stringify({ // init 接口扩展信息 (必须)
businessType: 'ugc',
category: 'gaming'
})
},
// === 服务地址 ===
baseUrl: '//v-platform-upload.huya.com', // 上传服务地址
// === 钩子 ===
beforeUpload: (file) => { // 上传前钩子 (支持 Promise)
return new Promise((resolve) => {
// 自定义预处理逻辑
resolve();
});
}
});
十五、使用示例
15.1 默认 UI 模式
<div id="uploader"></div>
<script src="dist/main.js"></script>
<script>
const uploader = new HuyaUploader({
wrapper: document.querySelector('#uploader'),
useUI: true,
auto: true,
threads: 3,
accept: 'MP4,AVI,MKV,MOV',
limitSize: 5 * 1024 * 1024 * 1024,
headers: {
APPID: '1048576',
TOKEN: 'your-token-here',
VERSION: '20190617'
},
initParams: {
extendInfo: JSON.stringify({ source: 'admin' })
}
})
.on('complete', (file) => {
alert(`上传完成: ${file.name}`);
})
.on('error', (err) => {
console.error('上传失败:', err);
});
</script>
15.2 无 UI 自定义模式
const uploader = new HuyaUploader({
target: document.querySelector('#custom-upload-btn'),
useUI: false,
auto: false,
headers: { APPID: '...', TOKEN: '...', VERSION: '...' }
});
// 自定义 UI
uploader.on('append', (file) => {
const li = document.createElement('li');
li.id = file.uid;
li.innerHTML = `${file.name} - 0%`;
document.querySelector('#file-list').appendChild(li);
});
uploader.on('progress', (e, file) => {
document.querySelector(`#${file.uid}`).innerHTML =
`${file.name} - ${e.percent.toFixed(1)}%`;
});
// 手动触发上传
document.querySelector('#start-btn').onclick = () => uploader.uploadFiles();
// 暂停/恢复
document.querySelector('#pause-btn').onclick = () => {
const file = uploader.getFileByUid(currentUid);
uploader.toggleFile(file);
};
十六、技术亮点
1. 分片上传架构——将"不可控的大任务"拆解为"可控的小任务"
这是整个 SDK 的核心设计思想。将一个可能 7GB 的大文件上传任务拆解为 1400+ 个 5MB 的小分片:
- 突破 HTTP 限制:Nginx/CDN 对单次请求大小通常有限制(默认 1MB-100MB),分片后每次只上传 5MB,绕过一切限制
- 失败粒度最小化:一个 7GB 文件直传失败需要从头重来(浪费 100% 已传数据),分片后单片失败只需重传 5MB(浪费 0.07%)
- 进度精确可控:
percent = ((fseq * chunkSize + loaded) / totalSize) * 100,精确到每片内部的字节级进度 - 服务端可感知:
/upload/init返回的clips[]数组标记每片状态(0=未传,1=已传),实现了服务端对上传进度的完整追踪
面试表述:分片上传的本质是把"传输可靠性"的责任从"一次 HTTP 请求"分散到"N 次独立请求"上。任何一次请求失败只影响 1/N 的进度,这是一个经典的"分治思想"在文件传输领域的应用。
2. 快速 MD5 算法——三段采样实现大文件秒级哈希
7GB 文件完整 MD5 需要 ~45 秒,快速 MD5(三段采样)只需 ~0.3 秒,配合分片级 MD5 校验作为第二道保障,在生产环境中完全可靠。
标准 MD5 需要读取文件的每一个字节,7GB 文件在浏览器中计算完整 MD5 可能需要 30-60 秒。我们设计了一套三段采样的快速 MD5 算法:
- 采样策略:以 2MB 为单位将文件分段,只读取 Head(第 0 段)、Middle(总段数/2)、Tail(最后一段)三段数据
- SparkMD5 增量计算:依次将三段数据追加到 SparkMD5.ArrayBuffer,最后
end()得到哈希值 - 性能对比:7GB 文件完整 MD5 需要 ~45 秒,快速 MD5 只需 ~0.3 秒(仅读取 6MB 数据),速度提升 150 倍
- 碰撞概率极低:两个不同文件的头、中、尾三段都完全相同的概率在实际业务中趋近于零
- 双重校验保底:即使快速 MD5 碰撞,每个分片还有独立的
chunkMd5校验,服务端会在合并时逐片验证完整性
面试表述:这是一个典型的"工程上的 trade-off"——牺牲极小概率的准确性,换取 150 倍的性能提升。关键在于通过分片级 MD5 校验提供了第二道保障,让这个 trade-off 在实际生产中完全可接受。
3. 分片级错误隔离与重试——"不因一片之失而废全功"
在移动端 4G 环境下上传 2GB 视频,偶尔一两片失败是常态。如果每次失败都从头重传整个文件,用户体验会非常糟糕。分片级重试将失败影响范围控制在单个 5MB 分片内。
错误处理不是简单的"失败就重试整个文件",而是精确到每个分片的隔离重试:
- 错误隔离:每片有独立的
error计数器,分片 A 失败不会影响分片 B/C/D 的上传 - 定向重试:
for循环包裹单片上传逻辑,error <= chunkRetry时重试当前片,不影响其他片 - 重试日志:
console.warn('分片上传出错,尝试第X次重新上传'),方便开发者定位问题 - 最终兜底:重试耗尽后 emit
error事件,上报{ clips: 剩余未传分片数 },业务方可以决定是放弃还是稍后手动恢复 - 断点续传基础:由于服务端记录了每片状态,即使页面刷新后重新上传,
/upload/init会返回已完成分片列表,只需上传未完成的部分
面试表述:类比于 TCP 的选择性重传(SACK)——不是重传整个窗口,而是只重传丢失的段。这在弱网环境下意义重大,用户用 4G 传一个 2GB 视频,偶尔一两片失败是常见的,如果每次都从头来,用户体验会非常糟糕。
4. 秒传机制——基于 MD5 的服务端去重
客户端计算 MD5 → /upload/init 发送 fmd5 → 服务端查找已有文件
→ 命中: vstatus=已完成, 零字节传输, 直接返回 vid
→ 未命中: 返回 clips[] 列表, 开始正常分片上传
对于已经有人上传过的同一个视频文件(比如热门游戏录像),秒传可以将上传时间从数分钟缩短到毫秒级,同时为服务端节省存储空间(去重)。
5. 线程池模型的并发控制——平衡速度与服务器压力
采用类似于线程池的固定并发模型:
// 启动 N 个"工作线程"(N = Math.min(files.length, threads))
for (let j = 0; j < threads; j++) {
this.next(); // 每个 next() 从队列取一个文件开始上传
}
// 每个"线程"完成后自动取下一个任务
async next() {
const file = queue.getQueueFile()[0]; // 取第一个 QUEUED 文件
if (file) {
await upload.startUpload(file);
this.next(); // 递归:完成后自动接续
}
}
- 并发数可配:
threads: 3,即同时上传 3 个文件 - 自动填充:某个文件完成后,空出的"线程"立即取队列中下一个文件,保持满负载运行
- 背压控制:如果
threads=1,退化为串行上传;如果threads=5,服务端可以拒绝过多并发并返回错误码,SDK 通过重试机制自适应
面试表述:这个模型的优势是代码极简(~10 行),但效果等价于一个完整的任务调度器。它的核心思想是"完成即调度"——不需要 setInterval 轮询,不需要维护复杂的线程状态,递归调用 next() 天然保证了并发度恒定。
6. 双模式集成架构——SDK 层与 UI 层的完全解耦
核心引擎(HYUpload / HYQueue / HYFile)不直接操作任何 DOM,只通过 EventBus 广播事件。默认 UI 只是事件总线的一个订阅者,和业务方自定义 UI 处于同等地位。
通过 EventBus 事件总线实现了上传逻辑和界面展示的完全分离:
- 默认 UI 模式:传入
wrapper+useUI: true,SDK 自动渲染文件列表、进度条、暂停/删除按钮 - 无 UI 模式:传入
target(文件选择触发器),业务方只监听事件自行渲染 - 关键设计:HYUpload / HYQueue / HYFile 只通过 EventBus 广播事件(start, progress, complete, error),不直接操作任何 DOM
- UI 是"监听者":默认 UI 模板只是 EventBus 的一个订阅者,和业务方自定义的 UI 处于同等地位
面试表述:这个设计让 SDK 可以零修改地嵌入到 React / Vue / Angular / jQuery 任何技术栈的管理后台中——因为它不假定任何 DOM 结构,只提供纯逻辑能力和事件接口。实际在虎牙内部,运营后台(Vue)和创作者中心(React)都在使用同一个 SDK。
7. CancelToken 实现真正的请求中断
不是"假取消"(标记状态后忽略响应),而是通过 Axios CancelToken 实现了 HTTP 请求的真正中断:
// 上传时为每个请求绑定 CancelToken
const config = {
cancelToken: new axios.CancelToken(executor => {
requestCancelQueue[file.uid] = executor; // 保存取消执行函数
})
};
// 用户暂停时,直接中断 TCP 连接
requestCancelQueue[file.uid](); // XMLHttpRequest.abort()
这意味着暂停上传后,正在传输的分片会立即停止网络传输,不会浪费用户带宽。对比"标记后丢弃"方案,在移动端(流量宝贵)场景下体验差异巨大。
8. 全链路数据上报——每一片都有迹可循
建立了从分片级到文件级的全链路监控:
- 分片级上报:每个分片上传成功/失败都上报
web.page.upload.clip,包含 vid、片序号、耗时、错误码 - 文件级上报:文件整体完成时上报
web.page.upload.file,包含总耗时、总分片数、剩余未传分片数、是否续传 - 错误链路:HTTP 响应拦截器捕获 400-505 所有状态码,映射为中文错误信息并通过 EventBus 广播
- 监控价值:可以精确回答"某用户某视频上传了多久"、"哪些分片失败了"、"失败原因是什么"等运维问题
十七、改进建议
| 方面 | 现状 | 建议 |
|---|---|---|
| 断点续传 | 分片状态依赖服务端查询 | 客户端 IndexedDB 持久化分片进度,刷新页面后自动恢复 |
| 拖拽上传 | dnd.js 未实现 | 实现 HTML5 Drag & Drop 支持 |
| TypeScript | 纯 JavaScript | 迁移到 TypeScript,提供类型声明 |
| Worker 计算 | 主线程计算 MD5 | 使用 Web Worker 计算 MD5,避免大文件阻塞 UI |
| 构建工具 | Webpack 4 | 升级到 Webpack 5 或 Vite |
| 并发控制 | 文件级并发 | 增加分片级并发(同一文件多片并行上传) |
| 速度预估 | 无 | 基于历史传输速率预估剩余时间 |
| 格式检测 | 仅扩展名校验 | 增加 Magic Number 检测,防止伪造扩展名 |
| 预处理 | 无 | 客户端视频转码(WebAssembly FFmpeg) |
十八、面试常见问题
Q1: 分片上传相比直传有什么优势?
回答思路:
- 突破大小限制:很多 Web 服务器/CDN 对单次请求大小有限制(如 Nginx 默认 1MB),分片可上传任意大小文件
- 失败恢复:直传失败需要从头重来,分片上传只需重传失败的片段
- 进度可控:可以精确到每片的进度,提供更细粒度的进度反馈
- 并发优化:理论上可以多片并行上传,利用多连接提升速度
- 秒传支持:先计算 MD5 发给服务端,命中即跳过,省时省流量
Q2: MD5 计算为什么要用"快速模式"?有什么权衡?
回答思路:
- 完整 MD5 需要读取整个文件,1GB 文件可能需要数十秒
- 快速模式只读取头、中、尾三段(各 2MB),秒级完成
- 权衡:快速 MD5 有极小概率碰撞(两个不同文件头中尾相同),但在实际业务中可接受
- 配合服务端的分片级 MD5 校验,可以保证数据完整性
- 如果业务对完整性要求极高,可以切换到完整模式
Q3: 如何实现暂停和恢复上传?
回答思路:
- 暂停:通过 Axios CancelToken 中断当前分片的 HTTP 请求,将文件状态设为 INTERRUPT
- 恢复:将状态改回 QUEUED,调用
/upload/clipsQuery查询已完成的分片,只上传未完成的 - 关键点:服务端需要保存已上传分片的状态,客户端恢复时根据 clips 数组中
status=0的分片继续
Q4: 并发上传是如何控制的?
回答思路:
- 通过
threads参数控制同时上传的文件数量(默认 3) uploadFiles()启动 N 个next()调用- 每个
next()从队列取第一个 QUEUED 文件开始上传 - 上传完成后递归调用
next(),自动接续队列中的下一个文件 - 类似线程池模型,保持固定数量的活跃上传
Q5: 事件系统是如何设计的?
回答思路:
- 基于发布-订阅模式实现 Emitter 基类(on/off/emit/once)
- 全局单例 EventBus 作为内部通信管道
- HYUpload、HYQueue、HYFile 都通过 EventBus 发送事件
- HuyaUploader 监听 EventBus 并转发给用户回调
- 默认 UI 也监听 EventBus 更新界面
- 优势:上传逻辑和 UI 展示完全解耦,支持无 UI 模式
Q6: 如何保证上传的安全性?
回答思路:
- 认证: 每个请求携带 APPID + TOKEN + VERSION + TIMESTAMP + NONCE
- 完整性: 文件 MD5 + 每片 MD5,服务端校验防止数据篡改
- 防重放: TIMESTAMP + NONCE 组合防止请求重放攻击
- 文件校验: 客户端校验文件类型(扩展名白名单)和大小(limitSize)
- 超时保护: 60 秒请求超时,避免挂起连接
- 改进空间: 可增加 Magic Number 检测、上传前病毒扫描等