本身動手實現一個 axios

前言

做爲一名前端er,對於數據請求的第三方工具axios,必定不會陌生,若是仍是有沒有用過,或者不瞭解的小夥伴,這裏給大家準備了貼心的中文文檔 ,聰明的大家一看就會~html

唔,爲了更好的瞭解和學習 axios 封裝思想和實現原理,咱們一塊兒來動手來實現一個簡版的 axios ~前端

前期準備

工欲善其事,必先利其器,咱們在開始咱們的項目以前,必定要作好其相關的準備工做,咱們須要準備的也很簡單,一個 客戶端(client) 方便咱們調試,一個 服務端(server) 作接口測試~node

服務端webpack

服務端我這裏爲了方便調試,直接使用基於 nodejs 實現的 koa 框架,經過 koa-router 來實現接口,參考代碼以下:ios

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

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

//請求中間件,解決跨域
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();
})

//接口測試地址
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());

//啓動服務
app.listen(3000,function () {
    console.log('app is running ~')
})

複製代碼

這裏咱們經過 node app.js 就能夠啓動咱們的服務,若是你在服務端控制檯看到 app is running ~ 說明你的服務已經啓動成功,此時你打開瀏覽器訪問 http://localhost:3000/ ,不出意外你能看到 Hello World 的返回信息,說明服務端這一塊就 配置 ok 了,是否是 so easy~git

客戶端es6

客戶端這塊的話,emm,咱們須要準備一個 html 文件,和 一個 js 文件夾,主要存放咱們要實現的核心代碼~github

html 文件很是簡單,以下web

<!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 的簡版實現</h1>
    </div>
    <script src="./js/main.js"></script>
</body>
</html>
複製代碼

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

要注意的是,因爲咱們的代碼是基於 es6 模塊化開發的,若是直接丟到瀏覽器裏,是沒法識別的,會報錯,不過也不要緊,咱們能夠藉助第三方的打包工具幫咱們搞定這些事~

打包不是咱們主要關注的問題,這裏我就不採用webpack這種工具,給你們推薦一個零配置的打包工具 Parcel ,使用方式也很簡單,在你的客戶端目錄下經過 npm init -y 初始化,經過 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"
  },
複製代碼

這樣,咱們能夠經過 npm run dev 腳本打開咱們的 html 文件,若是大家跟咱們配置同樣,那麼你在瀏覽器的 http://localhost:1234/ 地址會看到 axios 的簡版實現 這幾個字,而且控制檯不會報錯,就證實一切準備 ok 了!!

具體實現

雛形

咱們首先在客戶端的 js 文件夾下建立一個 axios 的文件夾,裏面存放咱們本身實現的 axios 相關代碼。

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

axios的本質是一個類,這裏咱們經過 class 實現,即:

axios.js

class Axios {
    constructor(){
    
    }
}
export default Axios;
複製代碼

經過 index.js 進行 new 初始化,導出 axios 實例,這也是咱們在使用axios中 不須要 new 的緣由~

index.js

import Axios from './Axios'

const axios = new Axios();

export default axios;
複製代碼

此時,咱們只須要在 main.js 經過 import 導入便可

main.js

import axios from './axios'

console.log(axios)
複製代碼

此時整個 axios 雛形就已經完成了~

一個簡單的get請求

咱們先實現一個簡單 axios.get 方法,即經過 axios.get 獲取咱們服務端的響應~

咱們回憶一下咱們平時使用 axios.get 的時候,一般是 axios.get().then 的方式,那麼咱們首先就肯定了咱們的 axios.get 方法返回的是一個 Promise 對象,咱們在 axios.js 中添加這個方法~

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();
        }))
    }
複製代碼

此時咱們在 main.js 調用 get 方法 ,

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

控制檯輸出以下:

對比官方的 axios,咱們少了好比 header 之類的信息,由於官方對請求返回作了二次包裝,這裏咱們只是簡單的json處理,具體的要根據返回的數據類型作不一樣的處理~

默認配置

咱們在使用官方 axios 的,會有不少配置項,包括全局配置,實例配置和請求配置,所以咱們就來看看配置信息這一塊。

咱們在 axios 文件夾下新建一個 config.js ,用於 axios 的默認配置,爲了方便,咱們的默認配置以下:

config.js

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

咱們將默認的配置傳入到咱們的構造函數中,以下:

index.js

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

const axios = new Axios(config);

export default axios;
複製代碼

因此,咱們須要在構造函數中接收一個 config 參數進行處理,即將默認配置寫入到實例中,即:

axios.js

constructor(config){
    //配置
    this.defaults = config;
}
複製代碼

這樣咱們的 get 方法裏請求的 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])
}
......
複製代碼

若是你此時在config.js 中配置 baseURL 那麼,你在axios.get中就能夠省略前面的 baseURL , 由於在請求以前已經幫你拼接完成了~

固然,你也能夠經過 axios.defaults.baseURL = xxx這種方式修改默認配置,都是沒問題的~

實例配置

在使用官方 axios 的時候,咱們能夠經過一個create 方法建立一個axios實例,並傳入配置信息便可,咱們只須要在 index.js 中建立的 axios 添加一個 create 方法便可 。

index.js

axios.create = function (config) {
    return new Axios(config);
}
複製代碼

這樣咱們也能夠經過 create 方法構建一個 axios 實例,它也擁有相應的方法~

可是這麼作存在一個問題,若是咱們建立多個實例,傳入不一樣的 config ,因爲咱們直接在構建的時候 經過 this.defaults = config; 這種方式複製,並無切斷對象的引用關係,所以會致使配置對象會被相互引用,出問題~

所以,咱們須要對其進行 深拷貝 賦值,即 this.defaults = deepClone(config) , 其中 deepClone 時深拷貝函數,這裏再也不贅述~

請求配置

咱們發現官方的 axiosgetpost等請求會有第二個可選參數,也是 config ,即單獨本次請求的配置,若是存在,咱們須要進行配置合併,對於簡單的 baseURLmethod 等這種簡單的配置直接覆蓋,對於headers這種複雜的對象配置,進行對象合併,有點相似 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 是合併兩配置對象的方法,具體實現參考以下:

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~ 如今咱們就能夠經過以下方式進行請求了:

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

能夠看到控制檯輸出跟以前的是同樣的~

細心的小夥伴能夠看到 header 頭已經添加了 token 信息~

攔截器

攔截器主要用於在請求以前或者請求以後可自定義對配置或者響應結果作一系列的處理,axios官方給咱們提供了 use 方法,能夠添加多個攔截器,使用方式以下:

// 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);
    });
複製代碼

那麼,接下來咱們本身來實現這麼一個 use 方法~

首先咱們須要在咱們的 axios 實例上添加一個 interceptors 對象,該對象有 requestresponse 兩個屬性,他們都擁有 use 方法,咱們發現 use 方法的結構都相同,入參爲兩個函數,其實他們是同一個 Interceptor 類的不一樣實例而已。

咱們先來構建 Interceptor 這個類,首先在 axios 文件夾下新建 Interceptor.js 文件,並定義以下:

Interceptor.js

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

這裏,咱們 new 出來的的實例都會擁有 use 方法,而且咱們經過一個 handlers 數組來保存,這樣能夠保證咱們能夠多調用 use 方法,添加多個攔截器~

咱們只需在 Axios.js 中的 constructor 構造函數中初始化便可。

Axios.js

constructor(config){
    //默認配置
    this.defaults = deepClone(config);
    //攔截器
    this.interceptors = {
    	request : new Interceptor() ,
    	response : new Interceptor()
    }
}
複製代碼

這樣儘管咱們已經能夠在咱們的 main.js 中使用 use 方法添加攔截器了,可是仍是沒法正確使用,由於請求這一塊還未進行處理,接下來,咱們須要對咱們以前的 Axios.js 進行改造~

首先,咱們統一封裝一個 request 函數,日後全部的請求都會調用這個方法,入參須要一個 config,返回一個 Promise 對象,咱們在這裏對攔截器進行操做,定義以下:

//request請求
request (config) {
    //配置合併
    let configs = mergeConfig(this.defaults, config);
    //將配置轉成 Promise 對象,鏈式調用和返回 Promise 對象
    let promise = Promise.resolve(configs);
    
    //請求攔截器,遍歷 interceptors.request 裏的處理函數
    let requestHandlers = this.interceptors.request.handlers;
    requestHandlers.forEach(handler => {
    	promise = promise.then(handler.resolvedHandler, handler.rejectedHandler)
    });
    
    //數據請求
    promise = promise.then(this.send)
    
    //相應攔截器,遍歷 interceptors.response 裏的處理函數
    let responseHandlers = this.interceptors.response.handlers;
    responseHandlers.forEach(handler => {
    	promise = promise.then(handler.resolvedHandler, handler.rejectedHandler)
    })
    
    //返回響應信息
    return promise;
}
複製代碼

上面,爲了代碼簡潔,我又將 send 方法提出來,定義跟以前基本一致:

//發送請求
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();
    }))
}
複製代碼

哦對啦,咱們以前的 get 方法也有一點點的不一樣,主要是加入了請求攔截器~

// 發送get請求
get (url, config) {
    config.method = 'get';
    config.url = url;
    return this.request(config);
}
複製代碼

趁熱打鐵,咱們來試試~

這裏我在 main.js 中分別添加了 2 個響應攔截器和請求攔截器:

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

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

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

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

請求攔截器分別打印了請求的配置並將請求的 token 值經行了修改,響應攔截器分別打印了響應信息並將響應添加了 msg 的屬性~

不出意外,你在控制檯能夠看到以下信息,在請求 header 裏看到 token 已經被更改~

大功告成!

總算是有點樣子啦~

結語

至此,咱們本身封裝了一個很是簡單的 axios 的請求庫,因爲篇幅有限,這裏我只是用了最簡單的 get 請求示例,axios源碼中遠不止這些,像一些異常處理、取消請求等的一系列的東西都尚未實現,這裏主要是借鑑其一些思想和實現的思路,我這裏只是牽個頭,剩下的靠大家本身不斷的去完善,動動手老是好的~

文末,附上 git 地址 感興趣的小夥伴能夠參考參考~

相關文章
相關標籤/搜索