axios的封装和api的管理

1、Content-Type

Content-Type 用于规定客户端经过http或https协议向服务器发起请求时,传递的请求体中数据的编码格式。由于get请求是直接将请求数据以键值对经过&号链接(key1=value1&key2=value2)的方式附加到url地址后面,不在请求体中,因此get请求中不须要设置Content-Type。经过浏览器抓取get请求数据能够发现其请求头中并无Content-Type这一属性。vue

当咱们经过<form>表单标签提交数据的时候,若是将其method设置为get,那么是属于get请求,全部表单数据会被附加到url地址后面,如:node

<form method="get"><!--属于get方式提交-->
     <input name="user"/>
     <button type="submit">提交</button>
</form>

在浏览器中输入localhost:8080,显示出页面后,在表单中填入数据lihb后,浏览器地址会变为http://localhost:8080/?user=lihb,说明其使用的是get的提交方式。ios

POST请求中常见的几种Content-Type形式: ajax

① application/x-www-form-urlencoded
这是最多见的POST请求提交方式,咱们经过<form>表单标签提交数据的时候,若是将其method设置为post,那么就是属于post请求,表单提交的数据会被放到请求体中,而且表单提交的数据也是采用键值对经过&号链接(key1=value1&key2=value2)的方式,也就是说,该Content-Type下,get和post请求提交的数据格式是同样的,只不过post请求是将数据放到了请求体中。vue-cli

<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的提交方式。express

② multipart/form-data
这种一般用于文件上传,咱们经过<form>表单标签提交数据的时候,须要将其method设置为post,同时还要将其enctype设置为multipart/form-data,那么这个时候就是属于post数据提交,只不过其数据会编码为一条一条的消息。须要注意的是文件上传必须设置为post请求,若是method为get,即便设置了enctype为multipart/form-data,那么仍然为get请求。npm

<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=----WebKitFormBoundarysa9SPcvOKqKEDdBbjson

------WebKitFormBoundarysa9SPcvOKqKEDdBb
Content-Disposition: form-data; name="user"

lihb
------WebKitFormBoundarysa9SPcvOKqKEDdBb--

浏览器会自动生成一个boundary分界线,用于将每条消息数据分割开axios

③ 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"
      }
    ]
}

2、服务器端数据解析

因为发送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);
    });
}

3、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);
  }
});

4、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);
}

⑥ 请求拦截器,咱们发送请求的时候一般须要到服务器验证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);
    }
}

5、api管理

为了更好的管理api接口以及方便模块化开发,咱们须要对咱们的api接口进行模块化,首先在src目录下新建一个api目录,而后新建一个index.js,做为全部api接口的输出口,而后在index.js中分别引入各个模块对应的api,在index.js中导出便可。一样根据不一样的模块,在src/api目录下建立便可,不一样模块下的api都引入上面封装好的axios进行发送请求便可。

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

6、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");
});