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

一、为什么需要 $.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,避免浏览器长期持有对象导致内存升高。

