使用纯 JavaScript 进行 Ajax 请求

上一篇文章 中,讲到如何使用原生 JavaScript 进行 DOM 操作。 除了 DOM 操作,jQuery 还封装了一个优秀功能,Ajax 请求操作。现在的大多项目中,几乎所有的数据全部基于 Ajax 请求实现,实行前后端分离了嘛,不再像之前 HTML 与动态语言(PHP,Java…)混在一起。那么到后来,Ajax 也有很多高级一点的进化,比如使用 fetch APIPromise对象。这暂不在本文范畴内,本文讲原始一点的 XMLHttpRequest 请求,包括跨域之类的,因为兼容性强嘛。

GET 请求

最简单最常见的请求,比如我们需要用向服务器请求用户名,唯一的参数是 ID,ID 不重复,那 URL 即是 myservice/username?id=some-unique-id 如果请求错误或者失败需要有报错或错误提示。

使用 jQuery 有方法 $.ajax() 还有简写的 $.get()。如果使用第一种:

$.ajax('myservice/username', {
    data: {
        id: 'some-unique-id'
    }
})
.then(
    function success(name) {
        alert('用户名:' + name);
    },
    function fail(data, status) {
        alert('请求失败,返回状态:' + status);
    }
);

如果使用原生的 XMLHttpRequest 进行请求:

var xhr = new XMLHttpRequest();
xhr.open('GET', 'myservice/username?id=some-unique-id');
xhr.onload = function() {
    if (xhr.status === 200) {
        alert('用户名:' + xhr.responseText);
    }
    else {
        alert('请求失败,返回状态:' + xhr.status);
    }
};
xhr.send();

上述的原生方法,只能在 IE 7+ 上运行,如果是 IE 6 则需要把 new XMLHttpRequest() 替换成 new ActiveXObject("MSXML2.XMLHTTP.3.0")。使用原生方法,虽然看起来不够复杂,但是在直观上美观上难免差强人意,毕竟没经过包装嘛!

POST 请求

POST 也是很常见的请求方式,至于和 GET 有和差别,在此不表,也不是三两句可以说得完的。把上述 GET 的场景升级一下,还是原来的接口,还是一个参数 ID,但是这次我们不是要取得用户名,而是修改用户名,这意味着需要多一个参数 name ,把参数放在请求报文内,并且 URL 参数需要先进行 URL 编码,服务器会返回是否更新。为什么要进行编码,因为在某些浏览器,可能会报错。

使用 jQuery 和 GET 方式差不多,但 method 要改成 POST,如下:

var newName = 'John Smith';

$.ajax('myservice/username?' + $.param({id: 'some-unique-id'}), {
    method: 'POST',
    data: {
        name: newName
    }
})
.then(
    function success(name) {
        if (name !== newName) {
            alert('发生错误.  当前用户名是:' + name);
        }
    },
    function fail(data, status) {
        alert('请求失败,返回状态:' + status);
    }
);

如果使用原生的 XMLHttpRequest 进行请求:

var newName = 'John Smith',
    xhr = new XMLHttpRequest();

xhr.open('POST', 'myservice/username?id=some-unique-id');
xhr.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded');
xhr.onload = function() {
    if (xhr.status === 200 && xhr.responseText !== newName) {
        alert('发生错误.  当前用户名是:' + xhr.responseText);
    }
    else if (xhr.status !== 200) {
        alert('请求失败,返回状态:' + xhr.status);
    }
};
xhr.send(encodeURI('name=' + newName));

再看一下,还是感觉 jQuery 的方法会更优雅一些。但考虑到需要引入整个 jQuery 文件,那么如果是几个简单的请求,显然是不需要引入 jQuery 的,或者自己封装一下 XMLHttpRequest ,也会看起来很优雅!

URL 编码

jQuery 内置了一个方法,$.param() 可以把一个对象编码成符合 URL 规范的参数,比如:

$.param({
    key1: 'some value',
    'key 2': 'another value'
});

转换之后

key1=some+value&key+2=another+value

如果是参数里含有中文

$.param({
    key1: '金正恩',
    'key 2': 'another value'
});

转换之后

key1=%E9%87%91%E6%AD%A3%E6%81%A9&key+2=another+value

原生的有提供 encodeURIencodeURIComponent 两个方法,二者差别,自行去研究。但是实现起来并没有如同 jQuery 一样来得优雅。 不过没关系,可以自己写一个类似的函数,如下:

function param(object) {
    var encodedString = '';
    for (var prop in object) {
        if (object.hasOwnProperty(prop)) {
            if (encodedString.length > 0) {
                encodedString += '&';
            }
            encodedString += encodeURIComponent(prop + '=' + object[prop]);
        }
    }
    return encodedString;
}

发送和接收 JSON

JSON 数据格式,已经是前后端通讯 API 的标配了,通常也是请求的参数和返回的值都是 JSON 格式,比如我们要更新服务器数据库某个表的值,更新成功后,会把最新的值返回回来,在此我们使用 PUT 请求,jQuery 方式:

$.ajax('myservice/user/1234', {
    method: 'PUT',
    contentType: 'application/json',
    processData: false,
    data: JSON.stringify({
        name: 'John Smith',
        age: 34
    })
})
.then(
    function success(userInfo) {
        // userInfo 是一个 JavaScript 对象
    }
);

如果使用原生方式:

var xhr = new XMLHttpRequest();
xhr.open('PUT', 'myservice/user/1234');
xhr.setRequestHeader('Content-Type', 'application/json');
xhr.onload = function() {
    if (xhr.status === 200) {
        var userInfo = JSON.parse(xhr.responseText);
    }
};
xhr.send(JSON.stringify({
    name: 'John Smith',
    age: 34
}));

上面的原生方式只能在 IE 8+ 上起作用,因为更低级的浏览器是不支持原生 JSON 对象的。修复方案是是引入 json.js。如果是使用 POST 请求,修改 method 即可。

上传文件

在包括 IE 9 在内及以下的浏览器,上传文件需要具备两个条件,<form> 标签和标签内的字段 <input type="file"> ,这两个是必须的。但是高级浏览器是可以通过 File API 上传文件,也就是说用 Ajax 的方式上传。

首先需要一个 HTML 标签

<input type="file" id="test-input">

使用 jQuery 上传

var file = $('#test-input')[0].files[0],
    formData = new FormData();

formData.append('file', file);

$.ajax('myserver/uploads', {
    method: 'POST',
    contentType: false,
    processData: false,
    data: formData
});

注意 contentType: falseprocessData: false 这两个参数,这是什么意思?这是必须必须设置的,主要是为了防止 jQuery 不会在请求头部插入自己的 Content-Type,因为浏览器必须指明内容类型,以至于服务器才可以正确解析。当然也可以换一种写法,不要把数据 append 到 formData 对象里,直接作为一个数据载体:

var file = $('#test-input')[0].files[0];

$.ajax('myserver/uploads', {
    method: 'POST',
    contentType: file.type,
    processData: false,
    data: file
});

这样看起来会更好一点点,但是仍然必须指明 processData 为 false,也是用来防止 jQuery 对数据进行 URL 编码。

使用原生的 XMLHttpRequest 进行上传:

var formData = new FormData(),
    file = document.getElementById('test-input').files[0],
    xhr = new XMLHttpRequest();

formData.append('file', file);
xhr.open('POST', 'myserver/uploads');
xhr.send(formData);

如果不使用 append() 则可以写成如下:

var file = document.getElementById('test-input').files[0],
    xhr = new XMLHttpRequest();

xhr.open('POST', 'myserver/uploads');
xhr.setRequestHeader('Content-Type', file.type);
xhr.send(file);

在这点上,原生的比 jQuery 看起来会更加好看一点。

CORS 跨域

CORS 意思是 Cross Origin Resource Sharing,字面理解是,跨资源共享,即发送跨域Ajax请求。这也是相当复杂的话题,网上各种文章,各种分析也很多了。如果不清楚 CORS 和同源策略,请参考这个 HTTP访问控制(CORS)

在高级浏览器,发送一个跨域 Ajax 请求难度并不大。但是在 IE8,IE9 会略显麻烦一点。主要重点是,设置自身的请求头 Origin 属性,在被请求域需要设置响应头 Access-Control-Allow-Origin 属性,并且注意 https 不用发起 http 请求。还有一点要注意的,在默认的跨域请求中,是不会携带凭证信息(如:cookie)的,如果有此需求,需要在 XMLHttpRequest 对象设置属性 withCredentials = true

下面是使用 jQuery 发起一个跨域请求:

$.ajax('http://someotherdomain.com', {
    method: 'POST',
    contentType: 'text/plain',
    data: 'sometext',
    beforeSend: function(xmlHttpRequest) {
        xmlHttpRequest.withCredentials = true;
    }
});

如果用原生的则如下:

var xhr = new XMLHttpRequest();
xhr.open('POST', 'http://someotherdomain.com');
xhr.withCredentials = true;
xhr.setRequestHeader('Content-Type', 'text/plain');
xhr.send('sometext');

在这一点上,jQuery 也没啥优势。如果需要在 IE 8/9 上进行跨域请求,则 jQuery 不但没有任何优势,反而会让人有点头疼。那为什么 jQuery 在 IE 8/9 上进行跨域请求的时候会如此无力,首选需要先学习/复习几个知识点:

1,首先在 IE 8/9 上进行跨域请求只能使用 IE 专有的 XDomainRequest 对象,这是一个非标准特性,显然得到的社区支持也是很有限的。

2,恰恰比较致命的是,jQuery 的 $.ajax() 是在 XMLHttpRequest 基础上封装的,也就是所有请求都基于 XMLHttpRequest 实现。那么如何实现在 IE 8/9 实现跨域请求?首先,可以使用插件,毕竟它有强大的插件支持。还一招,最直接的,你可以不使用 jQuery,直接使用原生的 JavaScript,重点代码如下:

if (new XMLHttpRequest().withCredentials === undefined) {
    var xdr = new XDomainRequest();
    xdr.open('POST', 'http://someotherdomain.com');
    xdr.send('sometext');
}

要注意一点,在使用 XDomainRequest 是没办法设置任何请求报头的。

JSONP 请求

不大建议使用 JSONP ,涉及到安全问题,如果你的项目属于 HTML 5,强烈建议使用 CORS。如果你不了解,先来理解一下什么是 JSONP,不要被它的名字所欺骗,它跟 JSON 没有任何关系,甚至跟 XMLHttpRequest 都没有关系。因为它返回的是一串像调用函数一样的文本,然后 把它当作 JavaScript 脚本来执行,以达到跨域的目的。简单思路如下:

之所以说跟 XMLHttpRequest 没关系,是因为 JSONP 是一个脚本,而脚本是不受同源策略限制的,也可以理解成是一个注入脚本攻击。用 jQuery 实现 JSONP 请求:

$.ajax('http://jsonp-aware-endpoint.com/user', {
    jsonp: 'callback',
    dataType: 'jsonp',
    data: {
        id: 123
    }
}).then(function(response) {
});

注意两个参数 jsonp: 'callback'和 dataType: 'jsonp'。如果使用原生方式,如下:

function jsonp(url, callback) {
		var callbackName = 'jsonp_callback_' + Math.round(100000 * Math.random());
		window[callbackName] = function(data) {
			delete window[callbackName];
			document.body.removeChild(script);
			callback(data);
		};
		var script = document.createElement('script');
		script.src = url + (url.indexOf('?') >= 0 ? '&' : '?') + 'callback=' + callbackName;
		document.body.appendChild(script);
}

//调用方式
jsonp('http://jsonp-aware-endpoint.com/user?id=1',function(data){
	alert(data)
	})

扩展阅读

Fetch API,相比于 XMLHttpRequest 更强大更灵活。

xdomain,一个迷你的 CORS 库,支持 IE 8 9 10。

Lightweight-JSONP,如名称所示,一个迷你的 JSONP 请求库。