Firebase JS SDK UploadTask 接口深度解析:可监控、可暂停、可恢复、可取消的文件上传任务
Firebase JS SDK UploadTask 接口深度解析:可监控、可暂停、可恢复、可取消的文件上传任务
UploadTask 是 Firebase JS SDK 中 @firebase/storage 模块用于表示可恢复上传过程的核心接口:它由 uploadBytesResumable() 创建,允许开发者实时监控上传进度、随时暂停/恢复/取消上传,并像使用 Promise 一样等待上传结果。本文以官方参考文档 storage.uploadtask.md 为主体,结合仓库源码(public-types.ts、task.ts、taskenums.ts 及单元/集成测试)逐项拆解其接口签名、事件回调模型、状态机实现与底层分块上传原理,帮助你写出健壮、可中断续传的上传代码。
UploadTask 从哪来:uploadBytesResumable() 的返回值
在 storage.md 的公开函数清单中,uploadBytesResumable() 被明确标注为:"Uploads data to this object's location. The upload can be paused and resumed, and exposes progress updates."——它是唯一一个直接返回 UploadTask 的上传入口(uploadBytes 与 uploadString 返回普通 Promise,属于一次性上传,不可暂停恢复)。
export declare function uploadBytesResumable(
ref: StorageReference,
data: Blob | Uint8Array | ArrayBuffer,
metadata?: UploadMetadata
): UploadTask;
在源码层面,模块化 API 入口 api.ts 会将传入引用解包为内部 Reference,随后在 reference.ts 中真正实例化任务:
export function uploadBytesResumable(
ref: Reference,
data: Blob | Uint8Array | ArrayBuffer,
metadata?: Metadata
): UploadTask {
ref._throwIfRoot('uploadBytesResumable');
return new UploadTask(ref, new FbsBlob(data), metadata);
}
可见 UploadTask 既是面向开发者的公开 TypeScript 接口(定义于 public-types.ts),也是内部实现类(packages/storage/src/task.ts 中以 export class UploadTask 实现,标注为 @internal)的名字。
接口全景:一个 snapshot 属性 + 五个方法
官方参考文档 storage.uploadtask.md 定义的公开接口结构如下:
| 成员 | 类型/签名 | 说明 |
|---|---|---|
snapshot |
UploadTaskSnapshot |
当前任务状态的快照,任何时刻可读取 |
cancel() |
(): boolean |
取消运行中的任务;对已完成或失败的任务无效 |
catch(onRejected) |
(error: StorageError) => unknown |
等价于 then(null, onRejected) |
on(event, nextOrObserver, error, complete) |
Unsubscribe | Subscribe<UploadTaskSnapshot> |
监听任务事件,支持多种回调注册方式 |
pause() |
(): boolean |
暂停当前运行中的任务;对已暂停或失败的任务无效 |
resume() |
(): boolean |
恢复已暂停的任务;对运行中或失败的任务无效 |
then(onFulfilled, onRejected) |
Promise<unknown> |
行为类似 Promise,上传完成时以快照数据 resolve |
接口源码(public-types.ts)与文档完全一致,其中 on() 的完整签名为:
on(
event: TaskEvent,
nextOrObserver?:
| StorageObserver<UploadTaskSnapshot>
| null
| ((snapshot: UploadTaskSnapshot) => unknown),
error?: ((a: StorageError) => unknown) | null,
complete?: Unsubscribe | null
): Unsubscribe | Subscribe<UploadTaskSnapshot>;
监听上传事件:on() 的三种回调注册方式
on() 监听的是 TaskEvent 类型的事件。文档与源码(taskenums.ts)都确认:目前公开事件只有 state_changed 一种,在模块化 API 中以 'state_changed' 字符串字面量出现,在兼容 API 中则写作 firebase.storage.TaskEvent.STATE_CHANGED。
事件流的三个回调语义(源码注释原话):
next:在进度更新、以及任务被暂停/恢复时触发,参数是UploadTaskSnapshot;error:上传被取消或因其他原因失败时触发,参数是StorageError;complete:上传成功完成时触发。
方式一:分别传参或传入 observer 对象
原文档示例(storage.uploadtask.md 的 Example 1)展示了四种等价写法:
var next = function(snapshot) {};
var error = function(error) {};
var complete = function() {};
// 写法 1:三个回调作为独立参数
uploadTask.on(
firebase.storage.TaskEvent.STATE_CHANGED,
next,
error,
complete);
// 写法 2:以对象形式传入(等价于写法 1)
uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED, {
'next': next,
'error': error,
'complete': complete
});
// 写法 3:先只传事件,用返回的函数注册回调(等价于写法 1)
var subscribe = uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED);
subscribe(next, error, complete);
// 写法 4:返回的函数同样接受 observer 对象(等价于写法 1)
var subscribe = uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED);
subscribe({
'next': next,
'error': error,
'complete': complete
});
也就是说:当只传入 event 一个参数时,on() 返回的是一个"订阅函数"(Subscribe<UploadTaskSnapshot>),你可以稍后用它在任意时刻追加回调;当传入不止事件参数时,返回的则是一个"退订函数"(Unsubscribe),调用它即可移除回调。
方式二:任一回调都可省略
原文档 Example 2 强调任意回调都是可选的,只要至少指定一个:
// 只关心完成事件,合法
uploadTask.on(
firebase.storage.TaskEvent.STATE_CHANGED,
null,
null,
function() {
console.log('upload complete!');
});
// 只关心进度/状态变化,合法
uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED, function(snapshot) {
var percent = snapshot.bytesTransferred / snapshot.totalBytes * 100;
console.log(percent + "% done");
});
// 只传对象中的 complete,同样合法
uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED, {
'complete': function() {
console.log('upload complete!');
}
});
这里进度计算 snapshot.bytesTransferred / snapshot.totalBytes * 100 正是 UploadTaskSnapshot 的核心用途,详见下文。
方式三:用返回的 Unsubscribe 移除回调
原文档 Example 3 演示"收到一次更新后立即停止监听":
var unsubscribe = uploadTask.on(
firebase.storage.TaskEvent.STATE_CHANGED,
function(snapshot) {
var percent = snapshot.bytesTransferred / snapshot.totalBytes * 100;
console.log(percent + "% done");
// 收到一次更新后停止监听
unsubscribe();
});
// 与上面等价的两段式写法
var handle = uploadTask.on(firebase.storage.TaskEvent.STATE_CHANGED);
unsubscribe = handle(function(snapshot) {
var percent = snapshot.bytesTransferred / snapshot.totalBytes * 100;
console.log(percent + "% done");
unsubscribe();
});
实现层面,UploadTask.on()(task.ts)将回调封装进内部 Observer,加入 _observers 数组后立即执行一次 _notifyObserver(让新注册的观察者立刻收到当前状态),并返回移除函数:
on(...): Unsubscribe | Subscribe<UploadTaskSnapshot> {
const observer = new Observer(nextOrObserver || undefined, error || undefined, completed || undefined);
this._addObserver(observer);
return () => { this._removeObserver(observer); };
}
注意:TaskEvent 在类型上是一个字符串类型(type TaskEvent = 'state_changed'),源码注释也坦言 type 参数当前并未被实际使用,仅保留以兼容调用习惯。
UploadTaskSnapshot:任务状态快照的六个字段
每次 next 回调、以及读取 task.snapshot 属性,拿到的都是 UploadTaskSnapshot(定义于 public-types.ts):
| 字段 | 类型 | 说明 |
|---|---|---|
bytesTransferred |
number |
截至目前已成功上传的字节数 |
totalBytes |
number |
待上传的总字节数 |
state |
TaskState |
任务当前状态 |
metadata |
FullMetadata |
上传完成前为发送给服务器的元数据;上传完成后为服务器返回的元数据 |
task |
UploadTask |
该快照所属的上传任务 |
ref |
StorageReference |
产生该上传任务的存储引用 |
state 字段的取值由 TaskState 定义:'running' | 'paused' | 'success' | 'canceled' | 'error'。内部实现(task.ts)中快照是即时组装出来的:
get snapshot(): UploadTaskSnapshot {
const externalState = taskStateFromInternalTaskState(this._state);
return {
bytesTransferred: this._transferred,
totalBytes: this._blob.size(),
state: externalState,
metadata: this._metadata!,
task: this,
ref: this._ref
};
}
值得注意的细节:bytesTransferred 在个别情况下可能"变小"(例如服务器只完成了部分分片后进度重置到本次请求起点),源码 _updateProgress 因此在数值发生变化时才通知观察者,避免发送无意义的重复快照。
控制任务:pause() / resume() / cancel() 的返回值语义
三个控制方法的签名一致,均为 (): boolean,语义(官方文档)分别为:
cancel():取消运行中的任务。对已完成或失败的任务无效果。 返回true表示取消生效。pause():暂停当前运行中的任务。对已暂停或失败的任务无效果。 返回true表示操作生效,false表示被忽略。resume():恢复已暂停的任务。对运行中或失败的任务无效果。 返回true表示操作生效,false表示被忽略。
实现(task.ts)清晰地体现了"仅特定状态允许转换"的约束:
resume(): boolean {
const valid = this._state === InternalTaskState.PAUSED || this._state === InternalTaskState.PAUSING;
if (valid) { this._transition(InternalTaskState.RUNNING); }
return valid;
}
pause(): boolean {
const valid = this._state === InternalTaskState.RUNNING;
if (valid) { this._transition(InternalTaskState.PAUSING); }
return valid;
}
cancel(): boolean {
const valid = this._state === InternalTaskState.RUNNING || this._state === InternalTaskState.PAUSING;
if (valid) { this._transition(InternalTaskState.CANCELING); }
return valid;
}
底层采用"中间态 + 完成态"双状态设计:内部状态机(taskenums.ts)包含 RUNNING / PAUSING / PAUSED / SUCCESS / CANCELING / CANCELED / ERROR 七个内部状态,其中 PAUSING、CANCELING 是过渡态——此时若网络请求正在飞行中,_transition 会先调用 this._request.cancel() 终止请求,再在请求回调里通过 completeTransitions_() 落到 PAUSED / CANCELED。对外暴露时,taskStateFromInternalTaskState 会把 PAUSING、CANCELING 归一化为 RUNNING,因此你永远只会看到文档中的五种公开状态。
Promise 化:then() / catch() 与 await
UploadTask 实现了 Promise 语义,文档明确说明"对象行为类似 Promise,上传完成时以其快照数据 resolve":
then(
onFulfilled?: ((snapshot: UploadTaskSnapshot) => unknown) | null,
onRejected?: ((error: StorageError) => unknown) | null
): Promise<unknown>;
catch(onRejected: (error: StorageError) => unknown): Promise<unknown>;
因此可以直接 await 一个上传任务:
const task = uploadBytesResumable(storageRef, file);
try {
const snapshot = await task; // 上传完成后 resolve 快照
console.log('上传成功', snapshot.metadata.fullPath);
} catch (e) {
console.error('上传失败', e); // 失败/取消时 reject StorageError
}
实现上(task.ts),then() 直接委托给内部 _promise,catch() 则是 then(null, onRejected) 的等价实现;内部 Promise 在成功、取消、出错三种终态通过 _finishPromise() 分别 resolve(携带 this.snapshot)或 reject(携带 StorageError)。同时构造函数末尾给内部 Promise 挂了一个空拒绝处理器,避免未捕获的 rejection 冒泡到顶层——这也是"可以先监听、后 await"不会产生 Unhandled Rejection 的原因。
源码级原理:task.ts 中的状态机与分块上传
UploadTask 类的核心字段(task.ts)包括:_transferred(已传字节)、_observers(观察者数组)、_state(内部状态)、_request(当前网络请求)、_chunkMultiplier(分块倍率)、sleepTime / maxSleepTime(重试退避参数)等。
构造函数中有一个关键决策——"是否需要走可恢复上传":
private _shouldDoResumable(blob: FbsBlob): boolean {
return blob.size() > 256 * 1024;
}
- 大于 256 KiB 的对象:走可恢复上传协议。先
_createResumable()创建上传会话拿到_uploadUrl,随后_continueUpload()以 RESUMABLE_UPLOAD_CHUNK_SIZE(256 KiB) 为基准、按_chunkMultiplier递增(上限 32 MiB)分片续传;期间若出错,会通过_fetchStatus()查询服务器端已接收进度(status.current)再继续,这正是"暂停/恢复/断点续传"的协议基础。 - 小于等于 256 KiB 的对象:走
_oneShotUpload()一次性 multipart 上传。
重试策略方面,_errorHandler(task.ts)对可重试状态码采用指数退避:sleepTime 从 0 起按 Math.max(sleepTime * 2, DEFAULT_MIN_SLEEP_TIME_MILLIS) 翻倍增长(最小睡眠 1 秒,见 constants.ts),一旦超过 maxSleepTime(默认 DEFAULT_MAX_UPLOAD_RETRY_TIME = 10 * 60 * 1000,即 10 分钟,constants.ts)则抛出 retryLimitExceeded() 错误。注意续传请求本身(continueResumableUpload)不重试,因为每次重试前都会先查询状态。
观察者通知链路为:_updateProgress / _transition → _notifyObservers → 对每个 observer 按外部状态分发到 next / error / complete,且所有回调都通过 fbsAsync 异步调度,保证回调不阻塞网络层。
测试佐证:状态转换与暂停恢复的行为验证
仓库测试从单元与集成两个层面印证了上述行为:
- task.test.ts 中的
Works for a small upload w/ an observer验证了on()回调在完成时触发;Works for a small upload canceled w/ a promise验证cancel()后 Promise 走 rejection 分支;Works properly with multiple observers验证多个观察者、退订函数h1()/h2()的移除行为;runNormalUploadTest则完整断言了事件序列['resume', 'pause', 'timeout', 'resume', 'complete'],即:任务运行 → 暂停 → 超时 → 恢复 → 完成,说明暂停/恢复确实会触发状态切换通知。 - integration.test.ts 中的
can pause uploads without an error展示了真实场景用法:在next回调里当bytesTransferred > 0时调用task.pause(),等待 4 秒后task.resume(),最终await task并校验下载字节与原数据一致。
综合示例:带进度条、可取消的模块化上传
综合文档 API 与仓库实现,一个完整的上传控制示例(模块化 API,uploadBytesResumable 返回 UploadTask):
import { getStorage, ref, uploadBytesResumable } from 'firebase/storage';
const storage = getStorage();
const task = uploadBytesResumable(ref(storage, 'images/photo.jpg'), file, {
contentType: 'image/jpeg'
});
// 监听进度(next)、错误(error)与完成(complete)
task.on(
'state_changed',
snapshot => {
const percent = (snapshot.bytesTransferred / snapshot.totalBytes) * 100;
console.log(`进度 ${percent}%`);
},
error => console.error('上传失败:', error),
() => console.log('上传完成')
);
// 通过返回的 Unsubscribe 在某一时刻移除监听
// const unsubscribe = task.on('state_changed', snapshot => { ... });
// unsubscribe();
// 控制与等待:暂停、恢复、取消
buttonPause.onclick = () => { const ok = task.pause(); console.log(ok ? '已暂停' : '忽略'); };
buttonResume.onclick = () => { const ok = task.resume(); console.log(ok ? '已恢复' : '忽略'); };
buttonCancel.onclick = () => { const ok = task.cancel(); console.log(ok ? '已取消' : '忽略'); };
// 也可以直接 await:成功拿到最终快照,失败/取消则抛出 StorageError
try {
const snapshot = await task;
console.log('上传成功:', snapshot.metadata.fullPath);
} catch (e) {
console.error('上传被取消或失败:', e);
}
小结
UploadTask 是 Firebase JS SDK 上传能力中"可中断、可续传"的关键抽象:on() 提供了灵活的进度/错误/完成监听(三种注册方式、任一回调可省略、返回退订函数),pause()/resume()/cancel() 基于内部七态状态机精确控制任务流转并返回布尔值告知是否生效,then()/catch() 使其可直接融入 Promise/async 链路,而 snapshot 则随时给出 bytesTransferred/totalBytes/state 等状态快照。如果你想进一步深入,可继续阅读同一参考文档系列的 storage.uploadtasksnapshot.md、storage.storageobserver.md 与 storage.md,或在源码目录 packages/storage/src/task.ts、packages/storage/test/unit/task.test.ts 中查看完整的实现与测试用例。