12

一、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,显示出页面后,在表单中填入数据lihb18后,浏览器地址仍然是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进行发送请求即可。

屏幕快照 2020-03-14 下午5.13.30.png

六、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,都会对服务器数据进行修改,所以在请求之前都要先询问服务器,当前网页所在域名是否在服务器的许可名单中,服务器允许后,浏览器才会发出正式的请求,否则不发送正式请求。


JS_Even_JS
2.6k 声望3.7k 粉丝

前端工程师