1. 程式人生 > 其它 >基於Vue對axios進行上層封裝

基於Vue對axios進行上層封裝

技術標籤:前後端互動vueajax

基於Vue對axios進行上層封裝

這的裡的很多東西都是基於上一節 axios 講解的,如果你不是對axios瞭解,請先看該文章

一、要真正拿axios進行實戰之前要了解一些細節上的東西

(1) 先搞清楚請求傳送的配置和攔截問題

  • axios的配置是可以合併的也有優先順序,全域性的axios.create建立的axios例項會自動吸取全域性axios的配置

  • 修改任意axios例項的配置,都可以通過axios例項的defaults屬性獲取配置物件,再修改對應的配置項

  • axios的攔截器並不是可合併的,在每個單獨的例項上的interceptor對應的過程(request,response)上呼叫use表示在單個例項新增對應攔截器;同樣全域性的axios.create建立的axios例項並不會

    吸收全域性axios的攔截器

  • axios的攔截器的傳入的攔截器其實內部和呼叫then方法一樣,一個onFulfilled和onRejected函式,所以要注意函式返回的結果,或者是否需要丟擲錯誤以改變呼叫順序

(2) 令人頭疼的Content-Type

axios預設是會自己對傳送請求方法不同,和傳入資料(請求體)的不同變更其 Content-Type,但是有的時候,這種預設的Content-Type並不是我們所期待的,於是需要手動配置。
先說一下不同方式axios的預設Content-Type

  • GET 沒有,只有有請求體的請求方式才存在 Content-Type
  • POSTPUTPATCH
    application/x-www-form-urlencoded 如果傳入的data請求體資料型別有檔案或者FormData,那麼自動轉換為 application/form-data
  • DELETE text/plain 比較奇怪的是 Params 和Data都能傳遞引數

如果你希望使用Restful規範,delete只是text/plain 方式的肯定在後臺接受引數會產生問題,這裡兩種解決反式

  • DELETE 的資料存放在 Params中,後端從URL獲取,自然不會受到text/plain的影響
  • DELETE 的資料存放在 Data 中,Content-Type設定為application/x-www-form-urlencoded,後端可以正常的從請求體獲取資料

其它的請求方法表現的都計較正常(正常情況下,如果你有特殊需求另外分析)

(3) 官網文件請求配置一些有點爭議的地方

  • baseURL 說是識別相對地址,如果是相對路徑就自動的加上,否則不加。實際測試中 baseURL 對 請求路徑是http/https/域名開頭的直接忽略,識別為絕對地址;對請求其它請求路徑通過類似於path.join函式一樣,於是相對路徑帶 “/” 還是不帶可以隨意

  • transformRequest 官方說只是最終影響POST、PUT、PATCH,但是實際測試有點奇怪:無論什麼方法都會呼叫transformRequest的函式佇列,但是隻有擁有請求體的請求才會真正的處理資料併發出,得出結論:只要該請求允許攜帶請求體,transformRequest就有效

(4) 值得注意的幾個細節

  • 請求配置中,responseType 預設是JSON,這也是為什麼then返回的資料可以是物件

  • 請求配置中,paramsSerializer 負責序列化 Params的函式,它是有預設值,這也是為什麼傳入的Params物件最終以字串拼接在URL上

  • withCredentials 這幾乎是最重要的一點,很關鍵:withCredentials` 表示跨域請求時是否需要使用憑證,這個憑證就代表這Cookie和Token是否能被後端正確接受和寫回給前端。預設的值為false表示跨域不攜帶。注意的是:只有在跨域情況下這個欄位才有效果,非跨域下一定攜帶憑證 (如果有的話,並且不是手動去除)

Ajax跨域和Nginx反向代理 那篇有說明 withCredentials 具體怎麼使用

二、開始上層封裝

(1) 剝離攔截器

新建一個 axios-interceptors.js 檔案
將你需要的攔截器寫出這種形式

//你可能對option引數不太理解,等看完下一小段,你就能明白
const reqCommon = {
    /**
     * 傳送請求之前做些什麼
     * @param config axios config
     * @returns {*}
     */
    onFulfilled(config, option) {
        return config
    },
    /**
     * 請求出錯時做點什麼
     * @param error 錯誤物件
     * @returns {Promise<never>}
     */
    onRejected(error, option) {
        return Promise.reject(error)
    }
}

//響應攔截
const resCommon  ={
    onFulfilled(response, option) {
        return response
    },
    onRejected(error, option) {
        const { response, request } = error
        if (response) { //響應是否存在
            switch (response.status) {
                case 400:
                    error.message = '錯誤請求'
                    break
                case 401:
                    error.message = '未授權,請重新登入'
                    break
                case 403:
                    error.message = '拒絕訪問'
                    break
                case 404:
                    error.message = '請求錯誤,未找到該資源'
                    break
                case 405:
                    error.message = '請求方法未允許'
                    break
                case 408:
                    error.message = '請求超時'
                    break
                case 500:
                    error.message = '伺服器端出錯'
                    break
                case 501:
                    error.message = '網路未實現'
                    break
                case 502:
                    error.message = '網路錯誤'
                    break
                case 503:
                    error.message = '服務不可用'
                    break
                case 504:
                    error.message = '網路超時'
                    break
                case 505:
                    error.message = 'http版本不支援該請求'
                    break
                default:
                    error.message = `未知錯誤${error.response.status}`
            }
        }else if(request){// 響應不存在,很可能是請求都未發出
            if(!window.navigator.onLine) { //發現是斷網了
                error.message = "網路未連線"
            }
        }else {
            error.message = "配置請求發生了錯誤"
        }
        return Promise.reject(error)
    }
}

export default {
    request: [reqCommon], // 請求攔截
    response: [resCommon] // 響應攔截
}

可以看到request佇列中的元素是一個物件,物件有兩個方法,都將在被axios例項攔截時呼叫use方法傳入的兩個引數,表示成功和失敗;response佇列也是如此

(2) 生成 loadInterceptors => 載入攔截器函式

//httpRequest => axios例項;interceptors => 分離出來的攔截器;option => 閉包引數
export function loadInterceptors(httpRequest, interceptors, option) {
    const {request, response} = interceptors
    // 載入請求攔截器
    request.forEach(item => {
        let {onFulfilled, onRejected} = item
        if (!onFulfilled || typeof onFulfilled !== 'function') {
            onFulfilled = config => config
        }
        if (!onRejected || typeof onRejected !== 'function') {
            onRejected = error => Promise.reject(error)
        }
        httpRequest.interceptors.request.use(
            //生成閉包!!!這樣在攔截器呼叫的時候能夠有option引數
            config => onFulfilled(config, option),
            config => onRejected(config, option)
        )
    })
    // 載入響應攔截器
    response.forEach(item => {
        let {onFulfilled, onRejected} = item
        if (!onFulfilled || typeof onFulfilled !== 'function') {
            onFulfilled = response => response
        }
        if (!onRejected || typeof onRejected !== 'function') {
            onRejected = error => Promise.reject(error)
        }
        httpRequest.interceptors.request.use(
            config => onFulfilled(config, option),
            config => onRejected(config, option)
        )
    })
}

(2) 生成axios例項,並配置對應的攔截器

//config生成新的axios例項的配置;interceptors要繫結的攔截器;
//requestMethods在例項上繫結的自定義方法陣列
function createAxios({
    config, interceptors, requestMethods
}) {
    //建立axios例項
    const httpRequest = axios.create(config)
    //載入攔截器,並繫結到對於的axios例項
    loadInterceptors(httpRequest, interceptors) 
     //遍歷方法陣列
    for(let method of requestMethods) {
        if(method === 'upload') {
            httpRequest['$Upload'] = requestMethodCreator('post')
            continue
        }
        if(method === 'delete') {
            httpRequest['$' + method.toUpperCase()] = requestMethodCreator(method, {
                headers: {'Content-Type': 'application/x-www-form-urlencoded'}
            })
            continue
        }
        httpRequest['$' + method.toUpperCase()] = requestMethodCreator(method)
    }
    return httpRequest
}

(3) 一次性的自定義攔截器 => beforeRequest、onRequest、atferRequest

//匯出請求方法建立器
function requestMethodCreator(method, outConfig) {
    return function baseRequestMethod(API, body, option = {}) {
        let { beforeRequest, onRequest, atferRequest, ...innerConfig } = option
        //寫一個自己的攔截器
        const defaultConfig = {
            method,
            url: API,
        }
        //選擇請求體
        defaultConfig[method === 'get'? 'params' : 'data'] = body
        //合併配置
        Object.assign(defaultConfig, innerConfig, outConfig)
        //執行自定義攔截
        beforeRequest && beforeRequest()
        let result = this(defaultConfig)
        //真正傳送請求
        if(onRequest) {
            result = result.then(onRequest, errorReason => Promise.reject(errorReason))
            if(atferRequest) {
                result = result.finally(atferRequest)
            }
        }
        return result
    }
}

(4) 使用匯出utils的http-request.js檔案

import axios from "axios";
import qs from "qs";
import {createAxios} from './create-axios'
import interceptors from './axios-interceptors'

axios.defaults.timeout = 3000
axios.defaults.responseType = 'json'
axios.defaults.transformRequest = [function (data){
    console.log(data)
    data = {message: '被修改了'}
    if(data instanceof FormData) return data
    data = qs.stringify(data)
    return data;
}]

//服務請求路徑字首
const {
    VUE_APP_SERVICE_PROXY_URL: SERVICE_PROXY_URL,
    VUE_APP_SERVICE_API_PREFIX: SERVICE_API_PREFIX,
    VUE_APP_UPLOAD_PROXY_URL: UPLOAD_PROXY_URL,
    VUE_APP_UPLOAD_API_PREFIX: UPLOAD_API_PREFIX,
    VUE_APP_VERTIFY_PROXY_URL: VERTIFY_PROXY_URL,
    VUE_APP_VERTIFY_API_PREFIX: VERTIFY_API_PREFIX
} = process.env

// SERVICE_PROXY_URL 請求路徑
export const $BASE_URL = '/' + SERVICE_PROXY_URL + '/' + SERVICE_API_PREFIX

export const $VERTIFY_URL = '/' + VERTIFY_PROXY_URL + '/' + VERTIFY_API_PREFIX

export const $UPLOAD_URL = '/' + UPLOAD_PROXY_URL + '/'

export const httpRequest = createAxios({
    config: {baseURL: $BASE_URL},
    interceptors,
    requestMethods: ['get', 'post', 'put', 'delete', 'upload']
})

export const httpVertify = createAxios({
    config: {baseURL: $VERTIFY_URL},
    interceptors,
    requestMethods: ['get']
})

export const httpUpload = createAxios({
    config: {baseURL: $UPLOAD_URL},
    interceptors,
    requestMethods: ['upload']
})

export default {
    httpRequest, httpVertify, httpUpload
}

(5) vue.config.js和.env檔案配置

.env

## 這裡是配置環境變數,因為可能有多個代理,所以進行約束 =>  括號內表示你的代理地址 名稱
## VUE_APP_[]_URL 代理目標地址
## VUE_APP_[]_PROXY_URL  代理使用 識別符號
## VUE_APP_[]__API_PREFIX  代理真實API介面的字首 識別符號,預設為空

## 舉例
VUE_APP_SERVICE_URL=http://localhost:3000
## 代理伺服器地址路徑
VUE_APP_SERVICE_PROXY_URL=BLOG_API
## 請求字首
VUE_APP_SERVICE_API_PREFIX=api/v2.0/

## 簡書身份認證
## 真實地址
VUE_APP_VERTIFY_URL=https://www.jianshu.com
## 代理伺服器地址值路徑
VUE_APP_VERTIFY_PROXY_URL=VERTIFY
VUE_APP_VERTIFY_API_PREFIX=

## 上傳檔案到簡書的地址
## 真實地址
VUE_APP_UPLOAD_URL=https://upload.qiniup.com
## 代理伺服器地址值路徑
VUE_APP_UPLOAD_PROXY_URL=UPLOAD
VUE_APP_UPLOAD_API_PREFIX=

## 轉發請求,安全獲取簡書的圖片
## 真實路徑
VUE_APP_JIANSHU_IMAGE_URL=https://upload-images.jianshu.io
## 代理欄位
VUE_APP_JIANSHU_IMAGE_PROXY_URL=GET_JIAN_SHU_IMAGE
VUE_APP_JIANSHU_IMAGE_API_PREFIX=

vue.config.js

const path = require('path')

function resolve(dir) {
  return path.join(__dirname, dir)
}

//自動處理多個代理路徑
const proxyArr = ['SERVICE','UPLOAD','JIANSHU_IMAGE', 'VERTIFY']
const proxy = {} //代理物件
proxyArr.forEach((proxyname)=>{
  const targetUrl= process.env['VUE_APP_' + proxyname + '_URL']
  const targetProxyUrl= process.env['VUE_APP_' + proxyname + '_PROXY_URL']
  proxy['/' + targetProxyUrl] = {
    target: targetUrl,
    secure: false,
    changeOrigin: true,
    pathRewrite: {
      ['^/'+targetProxyUrl]: ''
    }
  }
})

module.exports = {
  ...config,
  devServer: {
    port: '3000',
    proxy
  }
}