HTTP你们都不陌生,可是HTTP的许多细节就并非不少人都知道了,本文将讨论一些容易被忽略但又比较重要的点。css
首先,怎么用原生JS写一个GET请求呢?以下代码,只需3行:html
let xhr = new XMLHttpRequest();
xhr.open("GET", "/list");
xhr.send();
复制代码
xhr.open第一个参数是请求方法,第二个参数是请求url,而后把它send出去就好了。webpack
若是须要加上请求参数,若是用jQuery的ajax,那么是这么写的:nginx
$.ajax({
url: "/list",
data: {
page: 5
}
});复制代码
若是是用原生的话就得拼在请求url上面,即open的第二个参数:git
而且参数须要转义,以下代码所示:github
function ajax (url, data) {
let args = [];
for (let key in data) {
// 参数须要转义
args.push(`${encodeURIComponent(key)} =
${encodeURIComponent(data[key])}`);
}
let search = args.join("&");
// 判断当前url是否已有参数
url += ~url.indexOf("?") ? `&${search}` : `?${search}`;
let xhr = new XMLHttpRequest();
xhr.open("GET", url);
xhr.send();
}复制代码
那为何用jq就不用呢?由于jq帮咱们作了,jq的ajax支持一个叫processData的参数,默认为true:web
$.ajax({
url: "/list",
data: {
page: 5
},
processData: true
});
复制代码
这个参数的做用是用来处理传进来的data的,以下jq的源码:ajax
若是传了data,而且processData为true,而且data不是一个string了,就会调param处理data。而后咱们来看下这个param函数是怎么实现的:数据库
能够看到,它也是跟我本身实现的ajax相似,把key和value转义用"="拼接,而后push到一个数组,最后再join地一下。不同的地方是它的判断逻辑比个人复杂,它会再调一个buildParams的函数去处理key/value,由于value多是一个数组,也多是一个Object。若是value是一个Object,那么直接encode一下就会变成"[object Object]"的转义了:json
因此buildParams在处理每一个key/value时,会先判断当前value是不是一个数组或者是Object,以下图所示:
若是它是一个数组的话,这个数组的每个元素都会变成单独的一个请求字段,它的key是父级的key拼上数组的索引获得,如{ids: [1, 2, 3]}就会被拼成:ids[0]=一、ids[1] = 二、ids[2] = 3,若是是一个Object的话key的后缀就是子Object的key,如{user: {id: 1333, name: "yin"}}会被拼成:user[id]=133三、user[name]=yin,不然就认为它是一个简单的类型,就直接调一下param函数定义的add,把它push到s那个数组。这里用到了递归调用,会不断地拼key值,直到value是一个普通变量了,就到了最后面的else逻辑。
也就是说,如下代码:
$.ajax({
url: "/list",
data: {
user: {
name: "yin",
age: 18
}
},
});复制代码
将会被拼成的url为:
/list?user[name]=yin&user[age]=18
注意上面的中括号尚未转义。而若是是一个数组的话:
$.ajax({
url: "/list",
data: {
ids: [1, 2, 3]
},
});复制代码
拼成的请求url为:
/list?ids[0]=1&ids[1]=2&ids[2]=3
若是后端用的Java的Spring MVC框架的话,是理解这种格式的,框架收到这样的参数后会生成一个Array,传递给业务代码,业务代码是不用关心怎么处理这种参数的。其它的框架应该也相似。
怎么用原生JS写一个POST请求呢?以下图所示:
POST请求的参数就不是放在url了,而是放在send里面,即请求体。你可能会问:难道就不能放url么?我就要放url。若是你够任性,那么能够,前提是后端所使用的http框架可以在url里面取数据,由于它必定会收到url,也必定会收到请求体,因此取决于它要怎么处理,按照http标准,若是请求方法是POST,那么应该是得去请求体拿的,就不会在url的search上取了,固然它能够改一下,改为两个均可以拿。
而后咱们会发现请求的mime类型是text/plain:
而且查看请求参数的时候,并非平时所看到可以按照字段一行行地展现:
这是为何呢?这是由于咱们没有设置它的Content-Type,以下代码:
let xhr = new XMLHttpRequest();
xhr.open("POST", "/add");
xhr.setRequestHeader("Content-type",
"application/x-www-form-urlencoded");
xhr.send("id=5&name=yin");复制代码
若是设置Content-Type为x-www-form-urlencoded的话,那么在检查的话,Chrome也会按字段分行展现了:
这个也是jq默认的Content-Type:
它是一种最经常使用的一种请求编码方式,支持GET/POST等方法,特色是:全部的数据变成键值对的形式key1=value1&key2=value2的形式,而且特殊字符须要转义成utf-8编号,如空格会变成%20:
因为中文在utf-8须要占用3个字节,因此它有3个%符号。
咱们刚刚是xhr.send一个字符串,若是send一个Object会怎么样呢?以下代码所示:
let xhr = new XMLHttpRequest();
xhr.open("POST", "/add");
xhr.send({id:5, name: "yin"});复制代码
检查控制台的时候是这样的:
也就是说,其实是调了Object的toString方法。因此能够看到,在send的数据须要转成字符串。
除了字符串以外,send还支持FormData/Blob等格式,如:
let form = $("form")[0];
xhr.send(new FormData(form));
复制代码
但最后都是被转成字符串发送。
咱们再看下其它的请求格式,如Github的REST API是使用json的格式发请求:
这个时候要求格式要变成json,就须要指定Content-Type为application/json,而后send的数据要stringify一下:
let xhr = new XMLHttpRequest();
xhr.open("POST", "/add");
xhr.setRequestHeader("Content-type", "application/json");
let data = {id:5, name: "yin"};
xhr.send(JSON.stringify(data));复制代码
若是是用jq的话,那么能够这样:
$.ajax({
processData: false,
data: JSON.stringify(data),
contentType: "application/json"
});
复制代码
这个时候processData为false,告诉jq不要处理数据了——即拼成key1=value1&key2=value2的形式,直接把传给它的数据send就行了。
咱们能够比较json和urlencoded这两种形式的优缺点,json的缺点是parse解析的工做量要明显高于split("&")的工做量,可是json的优势又在于表达复杂结构的时候比较简洁,如二维数组(m * n)在urlencoded须要拆成m * n个字段,而json就不用了。因此相对来讲,若是请求数据结构比较简单应该是使用经常使用的urlencoded比较有利,而比较复杂时使用json比较有利。一般来讲比较经常使用的仍是urlencoded.
还有第3种常见的编码是multipart/form-data,这种也能够用来发请求,以下代码所示:
let formData = new FormData();
formData.append("id", 5); // 数字5会被当即转换成字符串 "5"
formData.append("name", "#yin");
// formData.append("file", input.files[0]);
let xhr = new XMLHttpRequest();
xhr.open("POST", "/add");
xhr.send(formData);复制代码
它一般用于上传文件,上传文件必需要使用这种格式。上面代码发送的内容以下图所示:
每一个字段之间用一个随机字符串隔开,保证发送的内容不会出现这个字符串,这样发送的内容就不须要进行转义了,由于若是文件很大的话,转义须要花费至关的时间,体积也可能会成倍地增加。
而后再讨论一个问题,咱们知道在浏览器地址栏输入一个网址请求数据,这个时候是用的GET请求,而咱们在代码里面用ajax发的GET请求也是GET,浏览器访问网址的GET和ajax的GET有什么区别吗?
为了可以观察到浏览器自已发出去的GET,须要用一个抓包工具看一下这个GET是怎么样的,以下图所示:
浏览器本身发的GET有一个明显的特色,它会设置http请求头的Accept字段,而且把text/html排在第一位,即它最但愿收到的是html格式。而动态的ajax抓包显示是这样的:
能够看到,使用地址栏访问的和使用ajax的get请求本质上都是同样的,只是使用ajax咱们能够设置http请求头,而使用地址栏访问的是由浏览器添加默认的请求头。
上面是使用http抓的包,咱们能够看到请求的完整url,包括请求的参数,而若是是抓的https的包的话,GET放在url上的参数就看不到了:
也就是说https的请求报文是加密的,包括请求的uri等,须要解密后才能看到。那是否是说使用https的GET也是安全的,而不是https的POST不会比GET安全?
咱们先来看一下http的请求报文,以下图所示:
若是使用抓包工具的话,能够看到请求报文确实是按照上图排列的,如图所示的GET:
而POST是这样的:
因此本质上GET/POST是同样的,只是GET把数据拼到url,而POST是放到请求体。另一点,url是有长度限制的,包括浏览器和接收的服务如nginx,而请求体是没有限制的(浏览器没有限制,可是通常nginx接收会有限制),还有POST的数据支持多种编码格式。
虽然如此,POST仍是比GET安全的,体如今如下几点:
接着讨论请求响应状态码。不少人都知道200、40四、500这几个,对于其它的可能就不甚了解了。这里我把一些经常使用的状态码列一下。
1. 301 永久转移
当你想换域名的时候,就可使用301,如以前的域名叫www.renfed.com,后来换了一个新域名www.rrfed.com,但愿用户访问老域名的时候可以自动跳转到新的域名,那么就可使用nginx返回301:
server {
listen 80;
server_name www.rrfed.com;
root /home/fed/wordpress;
return 301 https://www.rrfed.com$request_uri;
}复制代码
浏览器收到301以后,就会自动跳转了。搜索引擎在爬的时候若是发现是301,在若干天以后它会把以前收录的网页的域名给换了。
还有一个场景,若是但愿访问http的时候自动跳转到https也是能够用301,由于若是直接在浏览器地址栏输入域名而后按回车,前面没有带https,那么是默认的http协议,这个时候咱们但愿用户可以访问安全的https的,不要访问http的,因此要作一个重定向,也可使用301,如:
server {
listen 80;
server_name www.rrfed.com;
if ($scheme != "https") {
return 301 https://$host$request_uri;
}
}
复制代码
2. 302 Found 资源暂时转移
不少短连接跳转长连接就是使用的302,以下图所示:
3. 304 Not Modified 没有修改
在本地使用webpack-dev-server开发的时候,若是没有改js/css,那么刷新页面的时候请求本地的js/css文件将返回304,以下图所示:
webpack-dev-server的服务怎么知道没有修改呢,由于浏览器在请求的时候带上了etag,如:
W/"10e632-Oz38I6asQyS459XpsaJYkjMUoZI"
服务会计算当前文件的etag,它是一个文件的哈希值,而后比较一下传过来的etag,若是相等,则认为没有修改返回304。若是有修改的话就会返回200和文件的内容,并从新给浏览器一个新的etag。下次请求的时候浏览器就会带上这个新的etag。若是把控制台的disable cached打开的话,那么浏览器即便有etag也不会带上。另一个判断有没有修改的字段是Last Modified Time,这是根据文件的修改时间。
4. 400 Bad Request 请求无效
当必要参数缺失、参数格式不对时,后端一般会返回400,以下图所示:
并带上了提示信息:
{"message":"opportunityId type mismatch required type 'long' "}
经过400能够知道请求参数有误,结合提示信息,说明须要传一个数字,而不是字符串。
5. 403 Forbidden 拒绝服务
服务可以理解你的请求,包括传参正确,可是拒绝提供服务。例如,服务容许直接访问静态文件:
可是不容许访问某个目录:
不然,别人对你服务器上的文件就一览无遗了。
403和401的区别在于,401是没有认证,没有登录验证之类的错误。
6. 500 内部服务器错误
如业务代码出现了异常没有捕获,被tomcat捕获了,就会返回500错误:
如:数据库字段长度限制为30个字符,若是没有判断直接插入一条31个字符的记录,就会致使数据库抛异常,若是异常没有捕获处理,就直接返回500。
当服务完全挂了,连返回都没有的时候,那么就是502了。
7. 502 Bad Gateway 网关错误
以下图所示:
这种状况是由于nginx收到请求,可是请求没有打过去,多是由于业务服务挂了,或者是打过去的端口号写错了:
server {
location / {
# webpack的服务
proxy_pass https://127.0.0.1:7071;
}
}
复制代码
nginx返回了502.
8. 504 Gateway Timeout 网关超时
一般是由于服务处理请求过久,致使超时,如PHP服务默认的请求响应最长处理时间为30s,若是超过30s,将会挂掉,返回504,以下图所示:
这种状况多是由于服务还要请求第三方的服务,第三方服务处理时间较久没有返回,如在向FCM发送Push的时候,若是一个请求里面须要发送的订阅的浏览器(subscriptions)太多了,就常常会处理好久,致使504.
9. 101 协议转换
websocket是从http升级而来,在创建链接前须要先经过http进行协议升级:
还有一个600,600是一种不太经常使用的状态码,表示服务器没有返回响应头部,只返回实体内容。
这些状态码实际上就是一个数字,能够任意返回,可是最好是按照http的规定返回合适的状态码。若是返回四、五、6开头的http状态码,浏览器将会打印错误,认为当前请求失败。
咱们尚未说怎么判断请求成功了,以下代码所示:
xhr.open("POST", UPLOAD_URL);
xhr.onreadystatechange = function() {
// readyState为4表示请求完成
if (this.readyState === 4){
if (this.status === 200) {
let response = JSON.parse(this.responseText);
if (!response.status || response.status.code !== 0) {
// 失败
callback.failed && callback.failed();
} else {
// 成功
callback.success(response.data.url);
}
} else if (this.status >= 400 || this.status === 0) {
// 失败
callback.failed && callback.failed();
// 正常不该该返回20几的状态码,这种状况也认为是失败
} else {
callback.failed && callback.failed();
}
}
};
xhr.send(formData);复制代码
这里有个问题,若是返回的状态码是3开头的重定向,须要本身再去发一个请求吗?
实践证实,不须要,浏览器会自动重定向,以下图所示:
最后,本文提到了3种经常使用的请求编码,分别是application/www-x-form-urlencoded、application/json、multipart/form-data,第一种是最经常使用的一种,适用于GET/POST等,第二种常见于请求响应的数据格式,第三种一般用于上传文件。而后还比较了POST和GET,虽然二者的请求数据都在http报文里面,只是位置不同,可是考虑到用户、搜索引擎等使用场景,POST仍是会比GET更安全。最后说了几个经常使用的http状态码,并用一些实际的例子加深印象和理解。