广告位 · 页头(后台-模块管理-新建模块后替换此区域)

如何使用jQuery的$.ajaxTransport实现blob: URL上传与下载进度?

在浏览器端使用 URL.createObjectURL 生成 blob: 地址后,如果直接把这个地址交给 jQuery.ajax 下载,多数项目会在进度回调里踩坑:要么 progress 事件不触发,要么 e.total 一直是 0。出现这个现象的根源并不是 jQuery 本身坏了,而是默认的 http/https/文件传输器没有针对 blob: 协议做专门的响应处理和进度换算。jQuery 提供的 $.ajaxTransport 恰好可以补上这一层。下面先围绕下载与上传两条链路,展开一套可以直接放入业务项目的封装思路。

如何使用jQuery的$.ajaxTransport实现blob: URL上传与下载进度?

一、为什么需要 $.ajaxTransport 而不是简单封装 XHR

jQuery.ajax 默认的传输器是 xhr,它主要负责创建 XMLHttpRequest、设置请求头、读取响应头,并根据 dataType 调用转换器。对于常见的 http 或 https 地址,这套流程足够。但 blob: URL 是浏览器本地对象引用,它没有 Content-Type 响应头,也不会像普通远程请求那样提供完整的 lengthComputable 进度信息。若直接把 blob: URL 交给 jQuery.ajax,默认传输器仍会创建 XHR 打开这个地址,但后续的响应解析和进度通知并不完全符合预期。

$.ajaxTransport 是 jQuery 暴露给开发者的低级扩展接口,允许添加新的 transport 或替换现有 transport。它的核心是一个工厂函数,接收内部 options、原始选项和 jqXHR,返回一个包含 send 与 abort 方法的对象。send 方法里通常自定义 XMLHttpRequest 行为,最终通过 completeCallback 把状态码、状态文本、响应体和响应头交给 jQuery 的 ajax 状态管理。相比 xhr 工厂,Transport 更靠近响应处理层,能够控制 dataType、进度通知和响应内容转换。

这里需要特别区分两个概念:jQuery 的 xhr 工厂只是创建一个 XMLHttpRequest 实例,你可以在这里绑定 upload.onprogress,但它不负责响应数据类型转换;而 $.ajaxTransport 则接管了请求发送与完成的整个阶段,更适合处理 blob: URL 这种需要特殊响应类型的场景。

二、下载进度:用 responseType='blob' 包装 blob: URL

下载侧的核心目标是从 blob: URL 中取出 Blob 对象,并在读取过程中持续回调已读字节和总字节。原生 XMLHttpRequest 对 blob: URL 发起 GET 请求时,如果设置了 xhr.responseType='blob',响应结果就是 Blob 实例。我们可以通过 xhr.onprogress 拿到 ProgressEvent,但不同浏览器下 e.lengthComputable 对 blob: URL 可能为 false,e.total 也可能是 0。这时外部传入的原始文件大小就非常重要,可以用 originalOptions.blobSize 兜底计算百分比。

在 transport 中,我们直接绑定 xhr.onprogress,并调用 jqXHR.notify(loaded, total)。jQuery 的 progress 事件会沿着 deferred 通知给 $.ajax 的 progress 回调。这样调用方可以获得流畅的进度输出。为了让 jQuery 正确认为请求成功,需要在 onload 中处理 status 为 0 的情况。blob: URL 的 xhr.status 通常为 0,如果直接把 0 回传,jQuery 会按失败处理,所以这里统一映射为 200。响应体以 { blob: response } 的结构返回,dataType 设置为 blob 时,done 回调拿到的就是 Blob。

下面是一个可复用的下载 transport 注册代码。注意 send 方法中要手动调用 xhr.send,并通过 completeCallback 回传响应。abort 方法负责中断请求,避免页面切换后仍有进度回调。请求完成后建议在业务侧调用 URL.revokeObjectURL 释放对象地址。

  
$.ajaxTransport('blob', function(options, originalOptions, jqXHR) {  
  return {  
    send: function(headers, completeCallback) {  
      var xhr = new XMLHttpRequest();  
      xhr.open(originalOptions.type || 'GET', originalOptions.url, true);  
      xhr.responseType = 'blob';  
      var total = originalOptions.blobSize || 0;  
      var loaded = 0;  
      xhr.onprogress = function(e) {  
        if (e.lengthComputable) {  
          total = e.total;  
          loaded = e.loaded;  
        } else if (total) {  
          loaded = Math.min(e.loaded || 0, total);  
        }  
        jqXHR.notify(loaded, total);  
      };  
      xhr.onload = function() {  
        var status = xhr.status === 0 ? 200 : xhr.status;  
        var response = xhr.response;  
        completeCallback(status, xhr.statusText, { blob: response }, xhr.getAllResponseHeaders());  
      };  
      xhr.onerror = function() {  
        completeCallback(xhr.status, xhr.statusText);  
      };  
      xhr.onabort = function() {  
        completeCallback(0, 'abort');  
      };  
      jqXHR.xhr = xhr;  
      xhr.send(null);  
    },  
    abort: function() {  
      if (jqXHR.xhr) {  
        jqXHR.xhr.abort();  
      }  
    }  
  };  
});  

使用时,将 dataType 写成 blob,并传入 file.size 作为 blobSize。progress 回调中计算百分比即可更新进度条。如果文件非常大,还可以在 onprogress 中做节流,避免频繁触发 DOM 更新。这个模式同样适用于 File 对象通过 createObjectURL 生成的缩略图、本地预览或 Web Worker 中回传的 Blob 数据。

  
var fileInput = document.getElementById('file');  
fileInput.addEventListener('change', function() {  
  var file = this.files[0];  
  var blobUrl = URL.createObjectURL(file);  
  $.ajax({  
    url: blobUrl,  
    dataType: 'blob',  
    blobSize: file.size,  
    progress: function(loaded, total) {  
      var percent = total ? Math.floor((loaded / total) * 100) : 0;  
      console.log(percent + '%');  
    }  
  }).done(function(blob) {  
    console.log('下载完成', blob.size);  
    URL.revokeObjectURL(blobUrl);  
  });  
});  

三、上传进度:用 upload.onprogress 回传统计

上传与下载不同。blob: URL 本身是只读引用,不能作为上传目标,实际场景中真正被上传的是 Blob 或 File 对象。很多开发者会把文件放入 FormData 后交给 jQuery.ajax,但默认情况下 jQuery.ajax 没有直接暴露上传进度事件;jQuery 的 xhr 选项虽然能拿到 XMLHttpRequest 实例,但每次调用都要手写绑定。通过 $.ajaxTransport 可以把上传进度的绑定逻辑抽离出来,调用方只需要设置一个 uploadBlob 标志。

在上传 transport 中,关键在于 xhr.upload.onprogress,而不是 xhr.onprogress。它监听的是请求体上行过程,加载总量来自 ProgressEvent.total,通常是由浏览器根据请求体大小计算出的。我们可以额外计算瞬时速度,把第三个参数透传给 progress 回调,方便展示更友好的上传状态。请求完成时根据数据格式返回 responseText 或 JSON,以 completeCallback 回传。

上传 transport 的注册如下。这里给 transport 名称加了 + 前缀,目的是在 jQuery 解析 dataType 之前先判断 originalOptions.uploadBlob,避免因为自定义 dataType 不匹配而跳过。send 内部根据标志将 XHR 的 onprogress 或 xhr.upload.onprogress 绑定到不同的进度源。

  
$.ajaxTransport('+upload', function(options, originalOptions, jqXHR) {  
  if (!originalOptions.uploadBlob) {  
    return;  
  }  
  return {  
    send: function(headers, completeCallback) {  
      var xhr = new XMLHttpRequest();  
      xhr.open(originalOptions.type || 'POST', originalOptions.url, true);  
      var lastUploadTime = Date.now();  
      var lastLoaded = 0;  
      xhr.upload.onprogress = function(e) {  
        if (e.lengthComputable) {  
          var now = Date.now();  
          var speed = now > lastUploadTime ? (e.loaded - lastLoaded) / ((now - lastUploadTime) / 1000) : 0;  
          lastUploadTime = now;  
          lastLoaded = e.loaded;  
          jqXHR.notify(e.loaded, e.total, speed);  
        }  
      };  
      xhr.onload = function() {  
        var response = xhr.responseText;  
        completeCallback(xhr.status, xhr.statusText, { text: response }, xhr.getAllResponseHeaders());  
      };  
      xhr.onerror = function() {  
        completeCallback(xhr.status, xhr.statusText);  
      };  
      xhr.onabort = function() {  
        completeCallback(0, 'abort');  
      };  
      jqXHR.xhr = xhr;  
      xhr.send(options.data || null);  
    },  
    abort: function() {  
      if (jqXHR.xhr) {  
        jqXHR.xhr.abort();  
      }  
    }  
  };  
});  

调用时记得设置 processData 为 false 和 contentType 为 false,让浏览器自动生成 multipart 边界。progress 回调可以拿到 loaded、total、speed 三个参数。若需要支持取消,可以保留 jqXHR 引用,调用 jqXHR.abort(),transport 里的 abort 会转发到 xhr.abort()。

  
var formData = new FormData();  
formData.append('file', file);  
formData.append('name', 'demo.png');  
$.ajax({  
  url: '/upload',  
  type: 'POST',  
  data: formData,  
  processData: false,  
  contentType: false,  
  uploadBlob: true,  
  progress: function(loaded, total, speed) {  
    var percent = total ? Math.floor((loaded / total) * 100) : 0;  
    console.log(percent + '%', speed + 'B/s');  
  }  
}).done(function(res) {  
  console.log('上传响应', res);  
});  

四、边界处理与调试建议

实际落地时最容易踩到的是 blob URL 的 status 为 0。很多封装会把 status 0 当成断网失败,但本地 blob 请求成功时 status 通常就是 0。因此 transport 必须在 onload 中显式判断并转为 200。其次是 progress 的 total 不可靠问题。对下载而言,优先使用 ProgressEvent.lengthComputable 提供的 total,如果不可用再回退到原始 Blob.size;对上传而言,现代浏览器基本会给出 lengthComputable 为 true 的数据,但仍要空值保护。

另一个容易被忽略的细节是响应类型。下载必须设置 xhr.responseType = 'blob',否则响应体会被当作文本读取,Blob 内容会被破坏或出现编码错误。上传不要设置 responseType,否则服务器返回的文本可能无法按预期读取。jQuery 版本差异也要注意:1.12 与 3.x 的 transport 回调签名一致,但内部对 dataType 的处理稍有区别,建议使用 3.x 以上版本测试。

调试时可以在 onprogress 里打印 e.lengthComputable、e.loaded 和 e.total,快速区分是事件未触发还是分母为零。进度回调如果高频触发,可以结合 requestAnimationFrame 做界面更新,或者每 100 毫秒采样一次。最后,上传或下载完成后要记得释放 blob URL,避免浏览器长期持有对象导致内存升高。

广告位 · 文章底部(后台-模块管理替换此区域)