Firebase JS SDK UploadTask 接口深度解析:可监控、可暂停、可恢复、可取消的文件上传任务

原创2026-10-15 15:44:101,753 阅读
文章标签:开发工具后端

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 中查看完整的实现与测试用例。

登录后查看全文
firebase-js-sdk