jQuery的ajax方法封装得非常完善,但它默认只对常见的GET、POST等做了友好支持,遇到WebDAV的PROPFIND、MKCOL、COPY这类非常规方法时,经常会碰到请求发不出去、状态码判断不对等问题。jQuery其实早就预留了扩展口子,也就是$.ajaxTransport,配合$.ajaxPrefilter可以完全接管底层的传输行为。本文就从原理到实战,把这套机制讲透。

ajaxTransport的工作原理与注册方式
jQuery内部把一次ajax请求分成两个阶段:第一阶段是预过滤(prefilter),负责修改请求选项;第二阶段才是真正派发传输(transport)。传输器本质上是一个工厂函数,根据请求的dataType决定由谁来处理。jQuery内置了三个传输器,分别对应* text、* xml和* script json jsonp,它们底层都是用XMLHttpRequest实现的。
注册传输器的语法是$.ajaxTransport(dataType, handler),其中dataType可以写成一个以空格分隔的类型列表,第一个类型作为匹配key,用加号或者星号前缀可以表示额外接受的类型。handler函数接收两个参数:options是这次请求的最终配置对象,originalOptions是用户传入的原始配置。handler必须返回一个对象,这个对象要实现send和abort两个方法,否则jQuery会直接抛出错误。
send方法接收callback参数,回调函数按照callback(status, statusText, responses, headers)的契约来通知jQuery请求结果。abort方法负责在超时或者调用方主动取消时终止请求。理解了这个契约,就可以用任何底层手段实现传输,比如Fetch API、WebSocket,甚至是iframe,WebDAV场景下用XMLHttpRequest是最稳妥的选择,因为它对二进制响应和自定义头部的支持最完整。
实现一个支持WebDAV方法的传输器
WebDAV在HTTP方法之外,还大量依赖自定义头部,比如Depth、Destination、Overwrite、If等。写传输器之前,先用prefilter把这些信息规范化,避免散落在每次调用的参数里。下面是一个相对完整的实现示例:
// 预过滤:把WebDAV请求标记出来,统一处理dataType
$.ajaxPrefilter(function(options, originalOptions, jqXHR) {
var davMethods = ['PROPFIND', 'MKCOL', 'COPY', 'MOVE', 'LOCK', 'UNLOCK', 'PROPPATCH', 'REPORT'];
if (davMethods.indexOf(options.type.toUpperCase()) !== -1) {
// 标记为自定义传输类型,强制走下面的transport
options.dataTypes.unshift('webdav');
// PROPFIND默认需要Depth头,深度为1表示列出当前集合的直接子项
if (options.type.toUpperCase() === 'PROPFIND' && !options.headers) {
options.headers = { 'Depth': '1' };
}
}
});
// 注册webdav类型对应的传输器
$.ajaxTransport('webdav', function(options) {
var xhr;
return {
send: function(headers, completeCallback) {
xhr = new XMLHttpRequest();
xhr.open(options.type, options.url, true);
// 合并jQuery整理好的headers与用户自定义headers
var allHeaders = $.extend({}, headers, options.headers);
$.each(allHeaders, function(key, value) {
// Content-Type在xhr的setRequestHeader里统一处理
if (value !== null) {
xhr.setRequestHeader(key, value);
}
});
xhr.onload = function() {
var responses = { text: xhr.responseText };
var parsed = {};
try {
// PROPFIND返回的是XML,顺便解析好交给jQuery
parsed.xml = $.parseXML(xhr.responseText);
} catch (e) { /* 非XML响应忽略 */ }
completeCallback(xhr.status, xhr.statusText, $.extend(responses, parsed), xhr.getAllResponseHeaders());
};
xhr.onerror = function() {
completeCallback(0, 'error', null, null);
};
xhr.send(options.data);
},
abort: function() {
if (xhr) {
xhr.abort();
}
}
};
});
// 调用示例:列出远程目录内容
$.ajax({
url: 'https://dav.ipipp.com/files/',
type: 'PROPFIND',
dataType: 'xml',
headers: { 'Depth': '1' }
}).done(function(xml) {
$(xml).find('D\\:response, response').each(function() {
console.log($(this).find('D\\:href, href').text());
});
});
这段代码里有几个细节值得注意。第一,prefilter里把dataType头部插入webdav,这样jQuery在派发传输时就会匹配到我们注册的传输器;第二,send回调里的responses对象是按dataType作key的,返回xml时必须以xml为key,否则后续的dataType转换链会拿不到数据;第三,WebDAV的返回体是Multi-Status格式的XML,解析时要注意命名空间前缀在不同服务器实现中可能是D、d或 dav 等写法,所以选择器要做兼容处理。
另外,PUT上传和GET下载其实不需要自定义传输器,XMLHttpRequest原生就支持,直接用$.ajax({type: 'PUT', processData: false, contentType: 'application/octet-stream'})即可,关键是要把data处理成Blob或ArrayBuffer,避免jQuery误做表单序列化。自定义传输器真正发挥价值的地方在于PROPFIND、LOCK这类需要精细控制头部和响应解析的方法。
跨域限制与服务器端CORS配置要点
WebDAV场景几乎必然涉及跨域,而浏览器对非简单请求会先发一个OPTIONS预检。PROPFIND这种方法不在简单方法的白名单里,Depth、Destination这些也不在简单头部的白名单里,所以预检是躲不掉的。服务器必须明确放行这些方法,否则请求会直接死在预检阶段,连真正的业务请求都发不出去。
以Nginx为例,需要在对应的location块里加上类似这样的配置:
location /files/ {
# 预检请求单独处理
if ($request_method = OPTIONS) {
add_header 'Access-Control-Allow-Origin' 'https://app.ipipp.com' always;
add_header 'Access-Control-Allow-Methods' 'GET, PUT, DELETE, PROPFIND, MKCOL, COPY, MOVE, LOCK, UNLOCK, PROPPATCH' always;
add_header 'Access-Control-Allow-Headers' 'Depth, Destination, Overwrite, If, Content-Type, Authorization, Lock-Token' always;
add_header 'Access-Control-Max-Age' '86400' always;
return 204;
}
# 非预检请求也要带上来源放行头
add_header 'Access-Control-Allow-Origin' 'https://app.ipipp.com' always;
add_header 'Access-Control-Expose-Headers' 'ETag, Lock-Token, DAV' always;
proxy_pass http://dav_backend;
}
这里特别容易忽略的是Access-Control-Expose-Headers。LOCK请求返回的Lock-Token头、PROPFIND涉及的ETag,浏览器默认不会暴露给JavaScript,前端代码读这些头永远是null,很多人以为是自己代码写错了,实际上是服务器没配置暴露头部。另外预检请求的响应头必须使用always参数,否则4xx状态码下的预检失败会看不到任何放行头,排查起来非常绕。
还有一个隐蔽的坑:COPY和MOVE请求里的Destination头部值必须是绝对URL。如果前端只传了相对路径,部分WebDAV服务器会返回400,而另一些会静默按相对路径处理,行为不一致。建议在prefilter里统一把相对路径拼成绝对地址再发出去,这样不同后端的兼容性问题就能在客户端层面抹平。
调试技巧与生产环境的可靠性建议
调试WebDAV请求,浏览器的开发者工具只能看到一半的真相。预检请求、实际请求、以及服务器对Multi-Status响应的原始XML,最好用curl先在命令行里验证一遍,确认服务端行为正确后再回到前端排查。curl的-X PROPFIND加-H "Depth: 1"可以直接模拟传输器发出的请求,两边对比能快速定位是服务端配置问题还是前端代码问题。
生产环境还需要考虑几个可靠性问题。其一是超时与重试,WebDAV的LOCK操作不是幂等的,简单重试可能造成死锁堆积,建议对LOCK做请求级别的去重,并设置合理的Timeout头部让锁自动过期;其二是大文件上传,PUT大文件时最好用xhr.upload.onprogress补充进度回调,可以在send方法里把xhr对象暴露出去,或者通过options上的自定义字段挂载进度处理器;其三是错误码语义,WebDAV大量使用207 Multi-Status表示部分成功,jQuery的done/fail只看2xx状态码,207会被当成成功,所以业务层必须再检查响应体里每个条目的status子元素,不能只依赖Promise的状态。
总结一下,$.ajaxTransport提供的是一个干净的扩展点:prefilter负责请求的规范化改写,transport负责底层的发送与回调契约,两者配合可以在不修改jQuery源码的前提下,把任意非标准协议揉进熟悉的$.ajax调用方式里。WebDAV只是其中一个典型应用,同样的思路也适用于流式协议、分块上传等场景。核心是把send与abort的契约实现完整,把跨域的服务端配置做到位,剩下的就是按业务需求打磨细节了。
jQuery.ajaxTransportWebDAV自定义传输修改时间:2026-09-06 09:57:32