重磅!帶你從零實現一個axios網絡請求框架

咱們知道,axios是前端一個很是優秀的對於網絡請求的框架,其特色主要是請求方便、功能多(如攔截器)、可擴展性強等。那麼做爲一枚前端開發人員,瞭解並可以使用axios實際上是基礎,深刻了解其實現原理纔是比較重要的,固然,若是能徒手擼一個axios相似的框架出來,那就是至關的不錯了。

這篇文章會從如下幾個大的點來實現一個axios框架:javascript

  1. axios的本質是什麼?
  2. axios默認值、參數的實現
  3. 常見請求方式:get、post、delete在axios中的實現
  4. 真正請求(XMLHttpRequest)的實現
  5. axios攔截器的實現
  6. 打包發佈

同時但願你瞭解如下的一些知識點:前端

  • webpack
  • axios框架的基本使用
  • Es6的Proxy、Class等
  • XMLHttpRequest
  • http協議...等

axios的本質是什麼

使用axios框架的時候,咱們大部分狀況都是以模塊的形式引入進行使用,如:java

import axios from 'axios'

發出一個get請求,如:node

axios.get('/user')或axios('/user')

從中能夠看出,axios的本質就是一個函數,那麼就先來實現一個axios的雛形。(本篇文章實現利用es6的calss去實現)。webpack

axios.jsios

class Axios{
    constructor(){

    }
}

export default new Axios();

index.jsgit

import axios from './axios'

這裏毫無疑問能夠看出,axios如今只是一個Axios的實例,並非一個函數,那麼要怎樣將axios變成一個函數?這就須要兩個知識點:Axios的構造函數是能夠進行返回值操做、利用ES6的Proxy。更進一步的Axios類的代碼以下:es6

class Axios{
    constructor(){
        return new Proxy(function(){},{
            apply(fn,thisArg,agrs){

            }
        })
    }
}

export default new Axios();

這樣咱們獲得的一個Axios實例axios就是一個函數了。這裏簡單提一下,Proxy在進行函數處理的使用,apply是頗有用的,使用它咱們就能夠對一個函數進行代理處理了。github

axios的本質是函數

看看打印出來的內容:web

圖片描述

接下來,要怎樣纔可以實現axios('/user')或axios.get('/user')這樣的效果呢?,咱們知道了axios是一個函數,那麼給一個函數增長一個屬性(也是函數),就可以解決這個問題了。先來簡單看下函數式的寫法:

圖片描述

繼續完善Axios類中的代碼:

class Axios{
    constructor(){
        const _this = this;
        return new Proxy(function(){},{
            apply(fn,thisArg,agrs){
                
            },
            get(fn,key){
                return _this[key];
            },
            set(fn,key,val){
                _this[key] = val;
                return true;
            }
        })
    }
    get(){
        console.log('get request');
    }
}

export default new Axios();

這樣就實現了axios('/user')或axios.get('/user')這樣的效果了。

axios默認值、默認參數的實現

咱們知道,在使用axios的時候,能夠有以下的使用狀況:

import Axios from 'axios'

let axios1 = Axios.create({
    baseUrl:'http://www.xxx.com',
    headers:{
        common:{
            a:'12',
            b:{
                c:{

                }
            }
        }
    }
})
let axios2 = Axios.create({
    baseUrl:"http://www.yyy.com",

})

axios1.default.baseUrl = 'http://www.xxx.com';
 axios2.default.baseUrl= "http://www.yyy.com";
 Axios.default.headers.common.a = '123';

也就是經過axios的create方法來建立不一樣的axios實例,那接下來就來實現一下這個create方法

首先須要一個默認配置項,好比咱們在使用axios('/user')來進行請求的時候,其實默認的請求方式是get、默認配置裏面還有baseUrl、headers等基本默認信息。那麼就須要抽離出來成爲一個獨立的模塊(設計模式中的單一職責)default.js

export default {
    method:'get',
    bserUrl:'',
    headers:{
        common:{
           'X-Request-By':'XMLHttpRequest'
        },
        get:{

        },
        post:{

        }
    }
}

並在axios.js中引入

import defaultOptions from './default'

接下來就是實現create這個方法以及類Axios和實例aixos的default配置屬性。

let axios1 = Axios.create({
    baseUrl:'http://www.xxx.com',
    headers:{
        common:{
            a:'12',
            b:{
                c:{

                }
            }
        }
    }
})

該方法的參數是一個對象。當咱們像上面這樣配置的時候,就須要將默認配置項中的部分進行覆蓋,也就是須要對對象進行合併操做。首先抽取一個模塊,名字叫utils.js,主要主要是封裝各類工具子模塊(參數類型判斷、對象合併、對象克隆)。部分代碼以下:

圖片描述
並在類Axios所在模塊中引入,便於後續使用。

最終create方法實現以下

Axios.create = Axios.prototype.create = function(options){
    let axios = new Axios();
    //拷貝一份默認配置
    let res = cloneObj(defaultOptions);
    merge(res,options);
    console.log('res:',res);
    axios.default = res;
    return axios;
}

export default Axios.create();

當咱們經過下面代碼去使用的時候。

import Axios from './axios'

let axios1 = Axios.create({
    baseUrl:'http://www.xxx.com',
    headers:{
        common:{
            a:'12',
            b:{
                c:{
                    
                }
            }
        }
    }
})

let axios2 = Axios.create({})
axios2.default.headers.common.test = 'test';

最終獲得咱們想要的:

圖片描述

常見請求方式:get、post、delete在axios中的實現

在axios中,這三種常見請求的格式大體以下:

get請求:

  • axios.get(url)
  • axios.get(url,{params:{},headers:{}})
  • axios.get({url,params:{},headers:{}})

post請求:

  • axios.post(url)
  • axios.post(url,{a:12,b:13})
  • axios.post(url,{a:12,b:13},{params:{},headers:{}})
  • axios.post({url,params:{},headers:{},data})

delete請求:

  • axios.delete(url)
  • axios.delete(url,{headers:{},params:{}})
  • axios.delete({url,headers:{},params:{}})

分析三者請求參數的相同點

  1. 都只有一個參數,其參數是字符串類型

    • axois.get(url)
    • axios.post(url)
    • axios.delete(url)
  2. 都只有一個參數,其參數類型是object

    • axios.get({url,params:{},headers:{}})
    • axios.post({url,params:{},headers:{},data})
    • axios.delete({url,headers:{},params:{}})

接下來就按照這樣的一個規律去實現這三種請求。先來處理get請求的方式:

get(...agrs){
    let options;
    if(agrs.length===1 && typeof agrs[0] ==='string'){//axois.get(url)
        options = {
            method:'get',
            url:agrs[0]
        }

    }else if(agrs.length === 1 && agrs[0] instanceof Object){//axios.get({url,params:{},headers:{}}) 
        options = {
            ...agrs[0],
            method:'get'
        }
    }else if(agrs.length === 2 && typeof agrs[0] ==='string'){//axios.get(url,{params:{},headers:{}})
        options={
            method:'get',
            url:agrs[0],
            ...agrs[1]
        }
    }else{
        assert(false,`arguments invalidate!`)
    }
    console.log('get options:',options);
}

測試:

圖片描述

圖片描述

那麼post與delete的處理方式也相似,它們由不少相同點,因此提取到同一個方法中進行處理。

/**
 * 預處理方法參數
 * @param {*} methdoType 
 * @param {*} args 
 */
_preprocessArgs(methdoType,args){
    let options;
    if(args.length===1 && typeof args[0] === 'string'){
        options = {
            method:methdoType,
            url:args[0]
        }
    }else if(args.length===1 && args[0].constructor === Object) {
        options = {
            ...args[0],
            method:methdoType
        }
    }else{
        return undefined
    }
    return options;
}

最終實現的get、post、delete部分代碼以下:

圖片描述

這裏還有一種特殊須要處理的,axios('/user')、axios('/user',{})、axios({url,xxx})這三種狀況,這就須要藉助Proxy第二個參數中的apply方法。該方法的第一個參數就是axios這個方法,第三個參數就是axios方法中傳遞的參數。

圖片描述

真正請求(XMLHttpRequest)的實現

這裏真正的網絡數據請求模塊與axios模塊是獨立開來的,請求模塊只負責數據的請求,而axios模塊須要負責對數據請求進行處理

從上面實現的get、post、delete方法中,最後調用的實例的request方法,該方法的主要做用有:

  • 請求頭的處理
  • 參數的檢測
  • 正式調用請求數據
  • 變換請求transformRequest、transformResponse的處理

請求頭的處理:這裏有三個地方涉及到請求頭,默認的配置項defaultOptions(axios(this).default)、get/post/delete請求中配置的、options中配置的,其優先級是defaultOptions(axios(this).default).default<get/post/delete<options。

圖片描述

參數的檢測:

//參數檢測
 checkOptions(options);
    
function checkOptions(options){
    assert(options,'options is required!');
    assert(options.url,`not found url!`);
    assert(typeof options.url === 'string',`the type of url must be string!`);
    assert(options.url,`not found method!`);
    assert(typeof options.method === 'string',`the type of method must be string!`);
}

正式調用請求數據:這裏須要實現XMLHttpRequest模塊request.js,並準備些Mock數據進行測試。

request.js模塊代碼以下:

export default function request(options) {
    let xhr = new XMLHttpRequest();
    xhr.open(options.method,options.url,true);
    for(let key in options.headers){
        xhr.setRequestHeader(encodeURIComponent(key),encodeURIComponent(options.headers[key]));
    }
    xhr.send(options.data);
    return new Promise((resolve,reject) => {
        xhr.onreadystatechange = function () {
            if(xhr.readyState===4){
                if(xhr.status>=200 && xhr.status<300){
                    resolve(xhr);
                }else{
                    reject(xhr);
                }
            }
        }
    })
}

調用狀況以下:

圖片描述

Mock數據以下:

src/data/test.json:

{
    "skill":"javascript",
    "name":"Darkcode"
}

請求方式以下:

import axios from './axios'

axios.get('../datas/test.json').then((res) => {
    console.log('返回的數據是:',res);
})

發現url這裏有問題:

圖片描述

也就上圖中對url拼接這裏出了問題。這裏有了nodejs內置url模塊來解決這個問題:

options.url = urlLibrary.resolve(options.baseUrl,options.url);

axios.get(url).then(),能夠知道get方法返回的是一個promise,因此在最開始處理實現的get、post、delete等地方,須要進行類Axios request方法,該方法返回一個promise。

圖片描述

request方法的完善代碼以下:

圖片描述

這時候,請求就成功了,能夠看到代碼:

axios.get('../datas/test.json').then((res) => {
    console.log('返回的數據是:',res);
})

打印出來的就是一個xhr對象。

圖片描述

發現這返回來的數據與使用真正的axios框架中的返回值不填同樣,真正使用axios返回的數據應該是以下:

{
    status:200,
    statusText:'ok',
    data:{},
    ...
}

這時候就須要對請求到的數據進行進一步處理了,鑑於請求返回的模式:返回成功、返回失敗,抽取成單獨的模塊進行處理

請求成功模塊:response.js,對數據進行封裝

export default function (xhr) {
    let arr = xhr.getAllResponseHeaders().split('\r\n')
    let headers = {};
    arr.forEach((str) =>{
        if(!str){
            return;
        }
        const [name,value] = str.split(": ");
        headers[name] = value;
    })
    return{
        ok:true,
        status:xhr.status,
        statusText:xhr.statusText,
        data:xhr.response,
        xhr,
        headers
    }
}

請求失敗的模塊:error.js

export default function(xhr){
    return {
        ok:false,
        status:xhr.status,
        statusText:xhr.statusText,
        data:xhr.response,
    }
}

接下來在axios.js模塊中引入這兩個模塊,並針對請求request模塊部分的代碼進行處理。

import response from './response'
    import err from './error'
    
    //發出真正的請求
    return new Promise((resolve,reject) => {
        request(options).then((xhr) => {
            let res = response(xhr);
            resolve(res)
        },(xhr) => {
            let error = err(xhr);
            reject(error);
        });
    })

在看一下獲得的數據狀況:

圖片描述

是否是發現data裏面是字符串形式,而不是咱們常見的json對象形式,😔,接着搞。

變換請求transformRequest、transformResponse的處理

在axios中。

  • transformRequest:負責向服務器發送請求前針對數據進行處理
  • transformResponse:負責服務器返回數據後針對數據進行處理。

這兩個配置對象屬性是一個函數。簡單用法以下:

axios.create({
    transformRequest:function (config) {
        //do something
        return config;
    },
    transformResponse:function (res) {
        ////do something
        return JSON.parse(res);
    }
})

那麼要解決上面返回的data的值是字符串的問題,就很簡單了。直接在默認配置模塊中進行配置:

圖片描述

並在真正請求以前,和請求以後對數據作處理:

const {transformRequest,transformResponse} = options;
    options = transformRequest(options);
    checkOptions(options);
    //發出真正的請求
    return new Promise((resolve,reject) => {
        request(options).then((xhr) => {
            let res = response(xhr);
            res.data = transformResponse(res.data);
            resolve(res)
        },(xhr) => {
            let error = err(xhr);
            reject(error);
        });
    })

這下數據就徹底正確了。

圖片描述

變換一下請求方式:

import axios from './axios'

axios.default.headers.common.auth = 'xxxx';

axios('../datas/test.json').then((res) => {
    console.log('返回的數據是:',res);
})

圖片描述

也是很OK的。

axios攔截器的實現

axios攔截器:axios.interceptors

  • axios.interceptors.request.use(config => {config})
  • axios.interceptors.response.use(response => { response})

axios的攔截器的功能有點相似上面提到的transformRequest、transformResponse的功能,但又有區別,攔截器的功能更增強大,不只能夠針對數據進行處理,還能夠針對實際業務進行功能的處理等。

從寫法上來看,用法大體同樣。分別針對請求前數據、請求後的數據進行攔截處理。這裏也是須要抽取成一個獨立模塊:interceptors.js

export default class Interceptors{
    constructor(){
        this._list =[]
    }
    use(fn){
        this._list.push(fn);
    }
    list(){
        return this._list;
    }
}

而後在Axios構造函數中初始化interceptors對象屬性,包含require、response兩個屬性:

圖片描述

接下來在request方法中進行處理。

圖片描述

測試一下使用狀況:

import axios from './axios'

axios.default.headers.common.auth = 'xxxx';
axios.interceptors.request.use(function(config){
    config.headers.abc = '11'
    return config;
})
axios('../datas/test.json').then((res) => {
    console.log('response info is:',res);
})

圖片描述

圖片描述

再測試一個錯誤的請求:

axios('../datas/test1.json').then((res) => {
    console.log('response info is:',res);
},(err) => {
    console.log('error info:',err);
})

圖片描述

打包發佈

每每在一個庫或者框架開發測試完後,須要打包發佈給他人使用,接下來就是對已完成的axios進行打包。

npm run build

圖片描述

至於發佈操做,一般都會選擇發佈到npmjs上,這裏就不作一一的操做了。很簡單。

到此。從0實現一個axios其實不算難,難點在於對各類默認值、參數、請求形式等的處理。

最終附上完整代碼:

https://github.com/huangche00...

相關文章
相關標籤/搜索