一、Content-Type
Content-Type 用于规定客户端通过http或https协议向服务器发起请求时,传递的请求体中数据的编码格式。因为get请求是直接将请求数据以键值对通过&号连接(key1=value1&key2=value2
)的方式附加到url地址后面,不在请求体中,所以get请求中不需要设置Content-Type。通过浏览器抓取get请求数据可以发现其请求头中并没有Content-Type这一属性。
当我们通过<form>表单标签提交数据的时候,如果将其method设置为get,那么是属于get请求,所有表单数据会被附加到url地址后面,如:
<form method="get"><!--属于get方式提交-->
<input name="user"/>
<button type="submit">提交</button>
</form>
在浏览器中输入localhost:8080,显示出页面后,在表单中填入数据lihb
后,浏览器地址会变为http://localhost:8080/?user=lihb
,说明其使用的是get的提交方式。
POST请求中常见的几种Content-Type形式:
① application/x-www-form-urlencoded
这是最常见的POST请求提交方式,我们通过<form>表单标签提交数据的时候,如果将其method设置为post,那么就是属于post请求,表单提交的数据会被放到请求体中,并且表单提交的数据也是采用键值对通过&号连接(key1=value1&key2=value2
)的方式,也就是说,该Content-Type下,get和post请求提交的数据格式是一样的,只不过post请求是将数据放到了请求体中。
<form method="post"><!--属于post方式提交-->
<input name="user"/>
<input name="age"/>
<button type="submit">提交</button>
</form>
在浏览器中输入localhost:8080,显示出页面后,在表单中填入数据lihb
和18
后,浏览器地址仍然是http://localhost:8080,但是在请求头中,我们可以看到Content-Type:application/x-www-form-urlencoded
,请求体中可以看到user=lihb&age=18
说明其使用的是post的提交方式。
② multipart/form-data
这种通常用于文件上传,我们通过<form>表单标签提交数据的时候,需要将其method设置为post,同时还要将其enctype设置为multipart/form-data,那么这个时候就是属于post数据提交,只不过其数据会编码为一条一条的消息。需要注意的是文件上传必须设置为post请求,如果method为get,即使设置了enctype为multipart/form-data,那么仍然为get请求。
<form method="post" enctype="multipart/form-data"><!--属于post方式提交,并且数据会被编码为一条一条的消息-->
<input name="user"/>
<button type="submit">提交</button>
</form>
在浏览器中输入localhost:8080,显示出页面后,在表单中填入数据lihb
后,浏览器地址仍然是http://localhost:8080,但是在请求头中,我们可以看到Content-Type:multipart/form-data; boundary=----WebKitFormBoundarysa9SPcvOKqKEDdBb
------WebKitFormBoundarysa9SPcvOKqKEDdBb
Content-Disposition: form-data; name="user"
lihb
------WebKitFormBoundarysa9SPcvOKqKEDdBb--
浏览器会自动生成一个boundary分界线,用于将每条消息数据分割开。
③ application/json
这种通常会采用json的格式进行数据传递,主要用于一些比较复杂的数据的传递,如果仍然采用application/x-www-form-urlencoded的方式,解析起来就会变得非常复杂,所以可以利用该类型直接传递json数据到服务器,需要注意的是,浏览器中通过设置<form>的enctype为application/json是不会起作用的,需要通过ajax或axios等第三方库进行设置,即form表单只支持application/x-www-form-urlencoded 和 multipart/form-data 这两种。 如:
// 以下数据如果通过键值对和&号进行连接的方式就会变得很复杂
{
name: [
{
first: "Li",
last: "hb"
}
]
}
二、服务器端数据解析
由于发送post请求的时候,传递的数据有多种编码格式,所以服务端也会对应不同的方式进行解析。我们以node + express服务器为例
① 对于get请求
对于get请求传递的数据,我们可以直接通过req的query属性即可获取到,如:
app.get("/", (req, res) => {
console.log(`req.query: ${JSON.stringify(req.query)}`);
});
② 对于post请求,编码方式为application/x-www-form-urlencoded
对于键值对形式,我们可以通过body-parser进行解析,如:
const bodyParser = require("body-parser");
// 处理x-www-form-urlencoded编码后的数据
app.use(bodyParser.urlencoded({extended: false}));
// 处理json编码后的数据
app.use(bodyParser.json());
③ 对于post请求,编码方式为multipart/form-data
body-parser是无法解析消息数据的,这个时候可以通过formidable进行解析。如:
var formidable = require('formidable');
app.post("/", (req, res) => {
var form = new formidable.IncomingForm();
form.parse(req, function(err, fields, files) {
console.log(fields);
});
}
三、axios基本用法
axios 是一个基于 promise 的 HTTP 库,可以用在浏览器和 node.js 中。
① axios(config)
安装axios模块后,引入的axios是创建好的是一个Axios实例,可以直接用于发请求,axios其实是一个函数,可以直接传递一个请求配置对象,常见配置参数如下:
baseURL:用于设置请求的域名和端口号,axios中不支持host和port的设置,是直接通过baseURL一起设置的。
method:用于设置请求的方式。
url:用于设置服务器的相对地址或者绝对地址,如果是相对地址,那么是直接将相对地址附加到baseURL后面,如果是绝对地址,那么用该绝对地址覆盖掉baseURL的地址,即url比baseURL优先级更高。
headers:用于设置请求头数据,如Content-Type
axios({
headers: {
'Content-Type':'application/x-www-form-urlencoded'
},
});
transformRequest: 用于在请求发送到服务器之前对请求的数据做出一些改动,其是一个函数,会接收传递的data数据和headers请求头作为参数,并且函数必须返回(处理后的)data,如:
transformRequest(data, headers) {
console.log(data);
headers["Content-Type"] = "application/x-www-form-urlencoded";
return qs.stringify(data);
}
params:用于设置get请求时的数据,其必须是一个JS对象或者是URLSearchParams对象,其中的数据会被解析为键值对附加到url地址后面。
data:用于设置post请求时的数据,需要注意的是,默认情况下,如果传递的是一个普通的JS对象,那么data数据会被JSON.stringify(data)处理,并且Content-Type会被设置为application/json;charset=utf-8
,即以json的格式进行提交,后端解析的时候必须支持json的解析才能正常拿到数据;如果传递是字符串,并且是符合application/x-www-form-urlencoded
格式的字符串,那么才会以application/x-www-form-urlencoded
的方式提交到服务器;如果data数据是一个URLSearchParams对象,那么服务器可以正常获取到该数据,即data支持URLSearchParams对象,其默认源码如下:
if (utils.isURLSearchParams(data)) { // 如果传递的data是一个URLSearchParams对象
setContentTypeIfUnset(headers, 'application/x-www-form-urlencoded;charset=utf-8');
return data.toString(); // 这里就会变成&号连接的键值对字符串
}
if (utils.isObject(data)) { // 如果传递的data是一个JS对象
setContentTypeIfUnset(headers, 'application/json;charset=utf-8');
return JSON.stringify(data);
}
当然,这个是默认的情况,我们可以通过配置transformRequest重写改方法进行headers的设置和data数据的修改,如:
import QS from "qs";
axios({
data: {
user: "lihb"
},
transformRequest(data, headers) {
// data为JS对象的时候,强制修改Content-Type和data
headers["Content-Type"] = "application/x-www-form-urlencoded";
return QS.stringify(data);
}
})
transformResponse:用于我们在数据传送到then/catch方法之前对数据进行改动,其是一个函数,接收响应数据作为参数,可以在其中对响应数据进行修改。
timeout:用于设置请求超时时间,单位为毫秒,到了超时时间,请求将会被终止。
onUploadProgress:用于监听上传进度事件,值为一个函数。
onDownloadProgress:用于监听下载进度事件,值为一个函数
② axios.get(url, {params: getRequestObj})
axios.get是对axios()的封装,其第一个参数为请求的url地址,第二个参数为一个对象,对象中有一个params属性,属性值为get请求发送的数据,是一个JS对象或者URLSearchParams对象,如:
axios.get("http://localhost:3000/", {
params: { // 必须要有params属性
user: "lihb" // 传递JS对象
}
});
const params = new URLSearchParams();
params.append("user", "lihb");
axios.get("http://localhost:3000/", {
params // 传递URLSearchParams对象
})
③ axios.post(url, postRequestObj, config)
axios.post是对象axios()的封装,其第一个参数为请求的url地址,第二个参数为一个对象,即post请求发送的对象,第三个是config配置对象,同axios的config对象,如:
import QS from "qs";
// 由于直接传递JS对象默认会被转换为application/json,所以需要通过QS解析为符合urlencoded的参数字符串
axios.post("http://localhost:3000/", QS.stringify({user: "lihb"}));
// 二者等价,重写transformRequest方法,去除默认修改
axios.post("http://localhost:3000/", {user: "lihb"}, {
transformRequest(data, headers) {
return QS.stringify(data);
}
});
四、axios封装
为什么要进行axios的封装?封装是通过更少的调用代码覆盖更多的调用场景。在浏览器端他通过xhr方式创建ajax请求。在node环境下,通过http库创建网络请求。在实际开发中,一个项目有很多组件,每个组件中又有很多请求,如果每个请求在发送前都进行设置,比如baseURL、请求头、超时时间、跨域、token、响应处理等等,就会造成大量代码的重复。所以我们需要对这些通用的操作进行封装。
首先在src目录下新建一个http目录,并在其中新建一个index.js作为axios的封装。
① 创建一个独立的axios实例,我们安装好axios之后,默认拿到的全局的axios实例,为了避免对全局axios的污染,以及某个请求修改后影响到其他请求,所以我们需要创建一个单独的axios实例对象,如:
import axios from "axios";
const instance = axios.create({ // 创建一个独立的axios实例
});
export default instance;
② baseURL,接下来我们需要给这个实例配置一个baseURL,以便让我们可以根据不同的环境切换不同的baseURL,如:
import axios from "axios";
const instance = axios.create({ // 创建一个独立的axios实例
baseURL: process.env.NODE_ENV === "production" ? "http://www.lihb.com" : "http://localhost:3000"
});
export default instance;
vue项目初始化后,我们通过npm run serve
启动的项目默认模式为development,当我们通过npx vue-cli-service serve --mode production
启动项目后模式将切换未production,此时我们请求的baseURL也会跟着进行相应的变化。
③ 统一设置请求头,我们可以将一些通用的请求头事先设置好,如post请求一般都是采用application/x-www-form-urlencoded
编码,如:
import axios from "axios";
const instance = axios.create({ // 创建一个独立的axios实例
baseURL: process.env.NODE_ENV === "production" ? "http://www.lihb.com" : "http://localhost:3000",
headers: { // 定义统一的请求头部
post: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
}
}
});
export default instance;
④超时、响应码处理,我们可以给每个请求都设置一个超时时间,默认情况下,axios只会将状态码为2系列或者304的请求设为resolve状态,其余为reject状态,如果我们在代码里面使用了async-await,而众所周知,async-await捕获catch的方式是极为麻烦的,所以在此处,我们需要将所有响应都设为resolve状态,统一在then中处理。可以通过配置validateStatus,其是一个函数,让其返回true,即可实现所有http状态码都被resolve,即在then中接收,如:
import axios from "axios";
const instance = axios.create({ // 创建一个独立的axios实例
baseURL: process.env.NODE_ENV === "production" ? "http://www.lihb.com" : "http://localhost:3000",
headers: { // 定义统一的请求头部
post: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
}
},
timeout: 20 * 1000, // 配置请求超时时间
validateStatus: () => {
return true; // 使用async-await,处理reject情况较为繁琐,所以全部返回resolve,在业务代码中处理异常
}
});
export default instance;
⑤ 跨域,在跨域请求的时候,如果想要将客户端cookie也一起传到服务器端,那么我们就需要将withCredentials
设置为true,这里需要注意的是,withCredentials设置为true后,服务端就不能将Access-Control-Allow-Origin
设置为*,必须设置为指定的域名地址,如: "http://localhost:3000",同时还需要将Access-Control-Allow-Credentials
设置为true,如:
import axios from "axios";
const instance = axios.create({ // 创建一个独立的axios实例
baseURL: process.env.NODE_ENV === "production" ? "http://www.lihb.com" : "http://localhost:3000",
headers: { // 定义统一的请求头部
post: {
"Content-Type": "application/x-www-form-urlencoded;charset=UTF-8"
}
},
timeout: 20 * 1000, // 配置请求超时时间
validateStatus: () => {
return true; // 使用async-await,处理reject情况较为繁琐,所以全部返回resolve,在业务代码中处理异常
},
withCredentials: true, // 跨域请求的时候允许带上cookie发送到服务器
});
export default instance;
这里简单介绍一下cookie在vue中的使用,vue中使用cookie,可以通过vue-cookies
这个模块,引入之后通过Vue.use()进行安装,安装之后会产生一个全局的$cookies
对象,就可以进行cookie的操作了,如:
import VueCookies from 'vue-cookies';
Vue.use(VueCookies);
$cookies.set("user", "lihb");
console.log($cookies.get("user"));
服务端可以通过cookie-parser模块进行解析,然后交给express中间件处理,就会在req对象中添加一个cookies属性,可以拿到传递给服务器的cookie,如:
var cookieParser = require("cookie-parser");
app.use(cookieParser());
app.post("/login", (req, res) => {
console.log(req.cookies);
}
为什么将withCredentials设置为true就能够实现cookie的跨域访问呢?
因为withCredentials本身是XMLHttpRequest对象的属性,通过将XMLHttpRequest对象的withCredentials属性设置为true就可以实现cookie的跨域访问了,如:
- 对于XMLHttpRequest的Ajax请求
var xhr = new XMLHttpRequest();
xhr.open('GET', url);
xhr.withCredentials = true; // 携带跨域cookie
xhr.send();
- 对于JQuery的Ajax请求
$.ajax({
type: "GET",
url: url,
xhrFields: {
withCredentials: true // 携带跨域cookie
},
processData: false,
success: function(data) { console.log(data); }
});
如何禁止客户端通过JS访问cookie?
要想禁止客户端通过JS访问cookie,那么我们需要给cookie添加上httpOnly属性,如:
app.post("/login", (req, res) => {
res.cookie("user", "lihb", {httpOnly: true}); // 静止客户端通过JS操作cookie
});
此时客户端就无法通过JS获取到user的值了。
⑥ 请求拦截器,我们发送请求的时候通常需要到服务器验证token有没有过期,所以每个请求都会带上token到服务器验证,我们可以通过在请求拦截器中设置,避免每个请求都设置一遍,如:
// 请求拦截器
instance.interceptors.request.use(config => {
const token = localStorage.getItem("token");
token && (config.headers.Authorization = token);
return config;
}, error => {
error.data = {}
error.data.msg = '服务器异常,请联系管理员!'
return Promise.resolve(error)
});
⑦ 响应拦截器,我们发送的每个请求都会有对应的响应,对于一些通用的错误码,我们可以进行统一处理,同时将错误resolve回去,以便await的时候能够获取到错误信息。如:
// 响应拦截器
instance.interceptors.response.use((response) => {
errorHandle(response.status, response.data);
return response
}, (error) => {
// 错误抛到业务代码
error.data = {};
if (!window.navigator.onLine) { // 如果网络已断开
alert("网络已断开");
error.data.msg = '网络异常,请检查网络是否正常连接';
} else if (error.code === "ECONNABORTED") {
alert("请求超时");
error.data.msg = '请求超时';
} else {
alert("服务器异常");
error.data.msg = '服务器异常,请联系管理员';
}
return Promise.resolve(error); // 将错误resolve出去
});
const toTargetView = (targetUrl) => {
history.pushState({}, '', targetUrl);
}
const errorHandle = (status, message) => {
switch(status) {
case 401:
toTargetView("/login");
break;
case 403:
// 处理token过期等禁止访问问题
localStorage.removeItem("token");
、 setTimeout(() => {
toTargetView("/login");
}, 1000);
break;
case 404:
// 处理404问题
toTargetView("/404");
break;
default:
console.log(message);
}
}
五、api管理
为了更好的管理api接口以及方便模块化开发,我们需要对我们的api接口进行模块化,首先在src目录下新建一个api目录,然后新建一个index.js,作为所有api接口的输出口,然后在index.js中分别引入各个模块对应的api,在index.js中导出即可。同样根据不同的模块,在src/api目录下创建即可,不同模块下的api都引入上面封装好的axios进行发送请求即可。
六、options请求
在跨域请求中,options请求是浏览器自发起的preflight request(预检请求),以检测实际请求是否可以被浏览器接受。
当跨域请求是简单请求时不会进行preflight request, 只有复杂请求才会进行preflight request。
符合以下任一情况的就是复杂请求:
a.使用方法put或者delete;
b.发送json格式的数据(content-type: application/json)
c.请求中带有自定义头部,比如Authorization
当跨域遇到这些复杂请求的时候,浏览器会自动发送options请求,所以我们必须对这些options请求进行处理,否则浏览器拿不到对应ok状态码,就会拒绝发送请求了,即跨域请求失败。
所以服务器端完整的跨域处理为:
app.all('*', (req, res, next) => {
res.header("Access-Control-Allow-Origin", "http://localhost:8080");
res.header('Access-Control-Allow-Headers', "*");
res.header("Access-Control-Allow-Credentials", true);
res.header("Access-Control-Allow-Methods", "*");
next();
});
app.options("*", (req, res, next) => { // 处理浏览器options请求
console.log("预检"+req.path);
res.statusCode = 200;
res.send("ok");
});
跨域的复杂请求之所以需要preflight request是因为复杂请求可能对服务器数据产生副作用。例如delete或者put,都会对服务器数据进行修改,所以在请求之前都要先询问服务器,当前网页所在域名是否在服务器的许可名单中,服务器允许后,浏览器才会发出正式的请求,否则不发送正式请求。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。