自己動(dòng)手實(shí)現(xiàn)一個(gè) axios

自己動(dòng)手實(shí)現(xiàn)一個(gè) axios

前言

作為一名前端er,對(duì)于數(shù)據(jù)請(qǐng)求的第三方工具axios,一定不會(huì)陌生,如果還是有沒(méi)有用過(guò),或者不了解的小伙伴,這里給你們準(zhǔn)備了貼心的中文文檔 ,聰明的你們一看就會(huì)~

唔,為了更好的了解和學(xué)習(xí) axios 封裝思想和實(shí)現(xiàn)原理,我們一起來(lái)動(dòng)手來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)版的 axios ~

前期準(zhǔn)備

工欲善其事,必先利其器,我們?cè)陂_始我們的項(xiàng)目之前,一定要做好其相關(guān)的準(zhǔn)備工作,我們需要準(zhǔn)備的也很簡(jiǎn)單,一個(gè) 客戶端(client) 方便我們調(diào)試,一個(gè) 服務(wù)端(server) 做接口測(cè)試~

服務(wù)端

服務(wù)端我這里為了方便調(diào)試,直接使用基于 nodejs 實(shí)現(xiàn)的 koa 框架,通過(guò) koa-router 來(lái)實(shí)現(xiàn)接口,參考代碼如下:

const Koa = require('koa');
const KoaRouter = require('koa-router')

//app 實(shí)例
const app = new Koa();
//router 實(shí)例
const router = new KoaRouter();

//請(qǐng)求中間件,解決跨域
app.use(async (ctx,next)=>{
    ctx.set('Access-Control-Allow-Origin', '*');
    ctx.set('Access-Control-Allow-Headers', 'content-type,token,accept');
    ctx.set('Access-Control-Allow-Methods', 'POST,GET,OPTIONS');
    ctx.set("Content-Type", "application/json")
    ctx.set('Access-Control-Max-Age', 10)
    //處理 options
    if (ctx.request.method.toLowerCase() === 'options'){
        ctx.response.status = 200;
        ctx.body = '';
    } else await next();
})

//接口測(cè)試地址
router.get('/',async  ctx=>{
    ctx.body = {
        data : 'Hello World'
    }
})

router.get('/user/info',async ctx =>{
    ctx.body = {
        name : 'Chris' ,
        msg : 'Hello World'
    }
})

app.use(router.routes());

//啟動(dòng)服務(wù)
app.listen(3000,function () {
    console.log('app is running ~')
})

這里我們通過(guò) node app.js 就可以啟動(dòng)我們的服務(wù),如果你在服務(wù)端控制臺(tái)看到 app is running ~ 說(shuō)明你的服務(wù)已經(jīng)啟動(dòng)成功,此時(shí)你打開瀏覽器訪問(wèn) http://localhost:3000/ ,不出意外你能看到 Hello World 的返回信息,說(shuō)明服務(wù)端這一塊就 配置 ok 了,是不是 so easy~

客戶端

客戶端這塊的話,emm,我們需要準(zhǔn)備一個(gè) html 文件,和 一個(gè) js 文件夾,主要存放我們要實(shí)現(xiàn)的核心代碼~

html 文件非常簡(jiǎn)單,如下

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
    <title>axios-demo</title>
</head>
<body>
    <div class="">
        <h1>axios 的簡(jiǎn)版實(shí)現(xiàn)</h1>
    </div>
    <script src="./js/main.js"></script>
</body>
</html>

其中 main.js 是我們的要使用的js文件~

要注意的是,由于我們的代碼是基于 es6 模塊化開發(fā)的,如果直接丟到瀏覽器里,是無(wú)法識(shí)別的,會(huì)報(bào)錯(cuò),不過(guò)也沒(méi)關(guān)系,我們可以借助第三方的打包工具幫我們搞定這些事~

打包不是我們主要關(guān)注的問(wèn)題,這里我就不采用webpack這種工具,給大家推薦一個(gè)零配置的打包工具 Parcel ,使用方式也很簡(jiǎn)單,在你的客戶端目錄下通過(guò) npm init -y 初始化,通過(guò) npm install parcel-bundler --save-dev 安裝 Parcel ,然后在你的 package.json 文件中添加如下腳本:

 "scripts": {
    "test": "echo \"Error: no test specified\" && exit 1",
    "dev": "parcel ./*.html",
    "build": "parcel build ./*.html"
  },

這樣,我們可以通過(guò) npm run dev 腳本打開我們的 html 文件,如果你們跟我們配置一樣,那么你在瀏覽器的 http://localhost:1234/ 地址會(huì)看到 axios 的簡(jiǎn)版實(shí)現(xiàn) 這幾個(gè)字,并且控制臺(tái)不會(huì)報(bào)錯(cuò),就證明一切準(zhǔn)備 ok 了?。?/p>

具體實(shí)現(xiàn)

雛形

我們首先在客戶端的 js 文件夾下創(chuàng)建一個(gè) axios 的文件夾,里面存放我們自己實(shí)現(xiàn)的 axios 相關(guān)代碼。

axios 文件夾下新建 index.js 入口文件 和 axios.js 核心js文件~

axios的本質(zhì)是一個(gè)類,這里我們通過(guò) class 實(shí)現(xiàn),即:

axios.js

class Axios {
    constructor(){
    
    }
}
export default Axios;

通過(guò) index.js 進(jìn)行 new 初始化,導(dǎo)出 axios 實(shí)例,這也是我們?cè)谑褂?code>axios中 不需要 new 的原因~

index.js

import Axios from './Axios'

const axios = new Axios();

export default axios;

此時(shí),我們只需要在 main.js 通過(guò) import 導(dǎo)入即可

main.js

import axios from './axios'

console.log(axios)

此時(shí)整個(gè) axios 雛形就已經(jīng)完成了~

一個(gè)簡(jiǎn)單的get請(qǐng)求

我們先實(shí)現(xiàn)一個(gè)簡(jiǎn)單 axios.get 方法,即通過(guò) axios.get 獲取我們服務(wù)端的響應(yīng)~

我們回憶一下我們平時(shí)使用 axios.get 的時(shí)候,通常是 axios.get().then 的方式,那么我們首先就確定了我們的 axios.get 方法返回的是一個(gè) Promise 對(duì)象,我們?cè)?axios.js 中添加這個(gè)方法~

    get(url){
        return new Promise((resolve => {
            let xhr = new XMLHttpRequest();
            
            xhr.onload = function() {
                resolve({
                    data: JSON.parse(xhr.responseText),
                    status: xhr.status,
                    statusText: xhr.statusText
                });
            }
            
            xhr.open( 'get', url , true );
            xhr.send();
        }))
    }

此時(shí)我們?cè)?main.js 調(diào)用 get 方法 ,

axios.get('http://127.0.0.1:3000/user/info').then(res=>{
    console.log(res);
})

控制臺(tái)輸出如下:

對(duì)比官方的 axios,我們少了比如 header 之類的信息,因?yàn)楣俜綄?duì)請(qǐng)求返回做了二次包裝,這里我們只是簡(jiǎn)單的json處理,具體的要根據(jù)返回的數(shù)據(jù)類型做不同的處理~

默認(rèn)配置

我們?cè)谑褂霉俜?axios 的,會(huì)有很多配置項(xiàng),包括全局配置,實(shí)例配置和請(qǐng)求配置,因此我們就來(lái)看看配置信息這一塊。

我們?cè)?axios 文件夾下新建一個(gè) config.js ,用于 axios 的默認(rèn)配置,為了方便,我們的默認(rèn)配置如下:

config.js

export default {
    baseURL : '' ,
    method : 'get' ,
    headers : {
        'content-type' : 'application/json'
    }
}

我們將默認(rèn)的配置傳入到我們的構(gòu)造函數(shù)中,如下:

index.js

import Axios from './Axios'
import config from './config'

const axios = new Axios(config);

export default axios;

所以,我們需要在構(gòu)造函數(shù)中接收一個(gè) config 參數(shù)進(jìn)行處理,即將默認(rèn)配置寫入到實(shí)例中,即:

axios.js

constructor(config){
    //配置
    this.defaults = config;
}

這樣我們的 get 方法里請(qǐng)求的 url 就可以改寫成 :

this.defaults.baseURL += url
......
xhr.open( 'get', this.defaults.baseURL , true );
//添加header頭
for(let key in configs.headers){
    xhr.setRequestHeader(key,configs.headers[key])
}
......

如果你此時(shí)在config.js 中配置 baseURL 那么,你在axios.get中就可以省略前面的 baseURL , 因?yàn)樵谡?qǐng)求之前已經(jīng)幫你拼接完成了~

當(dāng)然,你也可以通過(guò) axios.defaults.baseURL = xxx這種方式修改默認(rèn)配置,都是沒(méi)問(wèn)題的~

實(shí)例配置

在使用官方 axios 的時(shí)候,我們可以通過(guò)一個(gè)create 方法創(chuàng)建一個(gè)axios實(shí)例,并傳入配置信息即可,我們只需要在 index.js 中創(chuàng)建的 axios 添加一個(gè) create 方法即可 。

index.js

axios.create = function (config) {
    return new Axios(config);
}

這樣我們也可以通過(guò) create 方法構(gòu)建一個(gè) axios 實(shí)例,它也擁有相應(yīng)的方法~

但是這么做存在一個(gè)問(wèn)題,如果我們創(chuàng)建多個(gè)實(shí)例,傳入不同的 config ,由于我們直接在構(gòu)建的時(shí)候 通過(guò) this.defaults = config; 這種方式復(fù)制,并沒(méi)有切斷對(duì)象的引用關(guān)系,因此會(huì)導(dǎo)致配置對(duì)象會(huì)被相互引用,出問(wèn)題~

因此,我們需要對(duì)其進(jìn)行 深拷貝 賦值,即 this.defaults = deepClone(config) , 其中 deepClone 時(shí)深拷貝函數(shù),這里不再贅述~

請(qǐng)求配置

我們發(fā)現(xiàn)官方的 axiosget、post等請(qǐng)求會(huì)有第二個(gè)可選參數(shù),也是 config ,即單獨(dú)本次請(qǐng)求的配置,如果存在,我們需要進(jìn)行配置合并,對(duì)于簡(jiǎn)單的 baseURL、method 等這種簡(jiǎn)單的配置直接覆蓋,對(duì)于headers這種復(fù)雜的對(duì)象配置,進(jìn)行對(duì)象合并,有點(diǎn)類似 Object.assign 方法~

所以,我們更改我們的 get 方法如下:

get(url,config){

    let configs = mergeConfig(this.defaults,config);

    return new Promise((resolve => {
        let xhr = new XMLHttpRequest();
    
        xhr.onload = function() {
            resolve({
                data: JSON.parse(xhr.responseText),
                status: xhr.status,
                statusText: xhr.statusText
            });
        }
    
        xhr.open( 'get', configs.baseURL + url , true );
        //添加header頭
        for(let key in configs.headers){
            xhr.setRequestHeader(key,configs.headers[key])
        }
        xhr.send();
    }))
}

其中 mergeConfig 是合并兩配置對(duì)象的方法,具體實(shí)現(xiàn)參考如下:

function mergeConfig (obj1, obj2) {
    let target = deepClone(obj1),
        source = deepClone(obj2);
    
    return Object.keys(source).reduce((t,k)=>{
        if(['url','baseURL','method'].includes(k)){
            t[k] = source[k]
        }
        if(['headers'].includes(k)){
            t[k] = Object.assign({},source[k],t[k])
        }
        return t;
    },target)
}

ok~ 現(xiàn)在我們就可以通過(guò)如下方式進(jìn)行請(qǐng)求了:

axios.get('/user/info',{
    baseURL : 'http://127.0.0.1:3000' ,
    headers : {
        token : 'x-token-123456'
    }
}).then(res=>{
    console.log(res);
})

可以看到控制臺(tái)輸出跟之前的是一樣的~

細(xì)心的小伙伴可以看到 header 頭已經(jīng)添加了 token 信息~

攔截器

攔截器主要用于在請(qǐng)求之前或者請(qǐng)求之后可自定義對(duì)配置或者響應(yīng)結(jié)果做一系列的處理,axios官方給我們提供了 use 方法,可以添加多個(gè)攔截器,使用方式如下:

// Add a request interceptor
axios.interceptors.request.use(function (config) {
        // Do something before request is sent
        return config;
    }, function (error) {
        // Do something with request error
        return Promise.reject(error);
    });
 
// Add a response interceptor
axios.interceptors.response.use(function (response) {
        // Do something with response data
        return response;
    }, function (error) {
        // Do something with response error
        return Promise.reject(error);
    });

那么,接下來(lái)我們自己來(lái)實(shí)現(xiàn)這么一個(gè) use 方法~

首先我們需要在我們的 axios 實(shí)例上添加一個(gè) interceptors 對(duì)象,該對(duì)象有 requestresponse 兩個(gè)屬性,他們都擁有 use 方法,我們發(fā)現(xiàn) use 方法的結(jié)構(gòu)都相同,入?yún)閮蓚€(gè)函數(shù),其實(shí)他們是同一個(gè) Interceptor 類的不同實(shí)例而已。

我們先來(lái)構(gòu)建 Interceptor 這個(gè)類,首先在 axios 文件夾下新建 Interceptor.js 文件,并定義如下:

Interceptor.js

export default class Interceptor {
    
    constructor() {
        this.handlers = [];
    }
    
    use( resolvedHandler, rejectedHandler ) {
        this.handlers.push({
            resolvedHandler,
            rejectedHandler
        });
    }
}

這里,我們 new 出來(lái)的的實(shí)例都會(huì)擁有 use 方法,并且我們通過(guò)一個(gè) handlers 數(shù)組來(lái)保存,這樣可以保證我們可以多調(diào)用 use 方法,添加多個(gè)攔截器~

我們只需在 Axios.js 中的 constructor 構(gòu)造函數(shù)中初始化即可。

Axios.js

constructor(config){
    //默認(rèn)配置
    this.defaults = deepClone(config);
    //攔截器
    this.interceptors = {
        request : new Interceptor() ,
        response : new Interceptor()
    }
}

這樣盡管我們已經(jīng)可以在我們的 main.js 中使用 use 方法添加攔截器了,但是還是無(wú)法正確使用,因?yàn)檎?qǐng)求這一塊還未進(jìn)行處理,接下來(lái),我們需要對(duì)我們之前的 Axios.js 進(jìn)行改造~

首先,我們統(tǒng)一封裝一個(gè) request 函數(shù),往后所有的請(qǐng)求都會(huì)調(diào)用這個(gè)方法,入?yún)⑿枰粋€(gè) config,返回一個(gè) Promise 對(duì)象,我們?cè)谶@里對(duì)攔截器進(jìn)行操作,定義如下:

//request請(qǐng)求
request (config) {
    //配置合并
    let configs = mergeConfig(this.defaults, config);
    //將配置轉(zhuǎn)成 Promise 對(duì)象,鏈?zhǔn)秸{(diào)用和返回 Promise 對(duì)象
    let promise = Promise.resolve(configs);
    
    //請(qǐng)求攔截器,遍歷 interceptors.request 里的處理函數(shù)
    let requestHandlers = this.interceptors.request.handlers;
    requestHandlers.forEach(handler => {
        promise = promise.then(handler.resolvedHandler, handler.rejectedHandler)
    });
    
    //數(shù)據(jù)請(qǐng)求
    promise = promise.then(this.send)
    
    //相應(yīng)攔截器,遍歷 interceptors.response 里的處理函數(shù)
    let responseHandlers = this.interceptors.response.handlers;
    responseHandlers.forEach(handler => {
        promise = promise.then(handler.resolvedHandler, handler.rejectedHandler)
    })
    
    //返回響應(yīng)信息
    return promise;
}

上面,為了代碼簡(jiǎn)潔,我又將 send 方法提出來(lái),定義跟之前基本一致:

//發(fā)送請(qǐng)求
send (configs) {
    return new Promise((resolve => {
        let xhr = new XMLHttpRequest();
    
        xhr.onload = function () {
            resolve({
                data: JSON.parse(xhr.responseText),
                status: xhr.status,
                statusText: xhr.statusText
            });
        }
        xhr.open(configs.method, configs.baseURL + configs.url, true);
    
        //添加header頭
        for ( let key in configs.headers ) {
            xhr.setRequestHeader(key, configs.headers[key])
        }
    
        xhr.send();
    }))
}

哦對(duì)啦,我們之前的 get 方法也有一點(diǎn)點(diǎn)的不同,主要是加入了請(qǐng)求攔截器~

// 發(fā)送get請(qǐng)求
get (url, config) {
    config.method = 'get';
    config.url = url;
    return this.request(config);
}

趁熱打鐵,我們來(lái)試試~

這里我在 main.js 中分別添加了 2 個(gè)響應(yīng)攔截器和請(qǐng)求攔截器:

//請(qǐng)求攔截器
axios.interceptors.request.use(config=>{
    console.log('請(qǐng)求配置信息:',config);
    return config
})

axios.interceptors.request.use(config=>{
    config.headers.token = 'x-token-654321';
    return config
})

//響應(yīng)攔截器
axios.interceptors.response.use(res=>{
    console.log('請(qǐng)求響應(yīng)信息',res)
    return res;
})

axios.interceptors.response.use(res=>{
    res.msg = 'request is ok ~';
    return res;
})

請(qǐng)求攔截器分別打印了請(qǐng)求的配置并將請(qǐng)求的 token 值經(jīng)行了修改,響應(yīng)攔截器分別打印了響應(yīng)信息并將響應(yīng)添加了 msg 的屬性~

不出意外,你在控制臺(tái)可以看到如下信息,在請(qǐng)求 header 里看到 token 已經(jīng)被更改~

大功告成!

總算是有點(diǎn)樣子啦~

結(jié)語(yǔ)

至此,我們自己封裝了一個(gè)非常簡(jiǎn)單的 axios 的請(qǐng)求庫(kù),由于篇幅有限,這里我只是用了最簡(jiǎn)單的 get 請(qǐng)求示例,axios源碼中遠(yuǎn)不止這些,像一些異常處理、取消請(qǐng)求等的一系列的東西都還沒(méi)有實(shí)現(xiàn),這里主要是借鑒其一些思想和實(shí)現(xiàn)的思路,我這里只是牽個(gè)頭,剩下的靠你們自己不斷的去完善,動(dòng)動(dòng)手總是好的~

文末,附上 git 地址 感興趣的小伙伴可以參考參考~

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時(shí)請(qǐng)結(jié)合常識(shí)與多方信息審慎甄別。
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

  • Axios是近幾年非?;鸬腍TTP請(qǐng)求庫(kù),官網(wǎng)上介紹Axios 是一個(gè)基于 promise 的 HTTP 庫(kù),可以...
    milletmi閱讀 3,615評(píng)論 0 9
  • Axios是一個(gè)基于Promise的HTTP請(qǐng)求庫(kù),可以用在瀏覽器和Node.js中。平時(shí)在Vue項(xiàng)目中,經(jīng)常使用...
    多啦斯基周閱讀 919評(píng)論 0 0
  • 概述 在前端開發(fā)過(guò)程中,我們經(jīng)常會(huì)遇到需要發(fā)送異步請(qǐng)求的情況。而使用一個(gè)功能齊全,接口完善的HTTP請(qǐng)求庫(kù),能夠在...
    grain先森閱讀 1,818評(píng)論 0 4
  • axios 基于 Promise 的 HTTP 請(qǐng)求客戶端,可同時(shí)在瀏覽器和 node.js 中使用 功能特性 在...
    Yanghc閱讀 3,743評(píng)論 0 7
  • 簡(jiǎn)介 Axios 是一個(gè)基于 promise 的 HTTP 庫(kù),可以用在瀏覽器和 node.js 中。本文先從瀏覽...
    microkof閱讀 10,486評(píng)論 0 4

友情鏈接更多精彩內(nèi)容