自己動手實現(xiàn)一個 axios
前言
作為一名前端er黔宛,對于數(shù)據(jù)請求的第三方工具axios
搞坝,一定不會陌生刮刑,如果還是有沒有用過,或者不了解的小伙伴盖喷,這里給你們準備了貼心的中文文檔 爆办,聰明的你們一看就會~
唔,為了更好的了解和學習 axios
封裝思想和實現(xiàn)原理课梳,我們一起來動手來實現(xiàn)一個簡版的 axios
~
前期準備
工欲善其事距辆,必先利其器,我們在開始我們的項目之前暮刃,一定要做好其相關的準備工作跨算,我們需要準備的也很簡單,一個 客戶端(client) 方便我們調試椭懊,一個 服務端(server) 做接口測試~
服務端
服務端我這里為了方便調試诸蚕,直接使用基于 nodejs
實現(xiàn)的 koa
框架,通過 koa-router
來實現(xiàn)接口,參考代碼如下:
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 ~
說明你的服務已經(jīng)啟動成功,此時你打開瀏覽器訪問 http://localhost:3000/
媳板,不出意外你能看到 Hello World
的返回信息桑腮,說明服務端這一塊就 配置 ok 了,是不是 so easy~
客戶端
客戶端這塊的話蛉幸,emm,我們需要準備一個 html
文件丛晦,和 一個 js
文件夾奕纫,主要存放我們要實現(xiàn)的核心代碼~
html
文件非常簡單,如下
<!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 的簡版實現(xiàn)</h1>
</div>
<script src="./js/main.js"></script>
</body>
</html>
其中 main.js
是我們的要使用的js
文件~
要注意的是烫沙,由于我們的代碼是基于 es6
模塊化開發(fā)的匹层,如果直接丟到瀏覽器里,是無法識別的锌蓄,會報錯升筏,不過也沒關系,我們可以借助第三方的打包工具幫我們搞定這些事~
打包不是我們主要關注的問題瘸爽,這里我就不采用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 的簡版實現(xiàn) 這幾個字渗鬼,并且控制臺不會報錯览露,就證明一切準備 ok 了!譬胎!
具體實現(xiàn)
雛形
我們首先在客戶端的 js
文件夾下創(chuàng)建一個 axios
的文件夾差牛,里面存放我們自己實現(xiàn)的 axios
相關代碼。
在 axios
文件夾下新建 index.js
入口文件 和 axios.js
核心js
文件~
axios
的本質是一個類银择,這里我們通過 class
實現(xiàn)多糠,即:
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
雛形就已經(jīng)完成了~
一個簡單的get請求
我們先實現(xiàn)一個簡單 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
處理,具體的要根據(jù)返回的數(shù)據(jù)類型做不同的處理~
默認配置
我們在使用官方 axios
的拍鲤,會有很多配置項贴谎,包括全局配置,實例配置和請求配置季稳,因此我們就來看看配置信息這一塊擅这。
我們在 axios
文件夾下新建一個 config.js
,用于 axios
的默認配置景鼠,為了方便仲翎,我們的默認配置如下:
config.js
export default {
baseURL : '' ,
method : 'get' ,
headers : {
'content-type' : 'application/json'
}
}
我們將默認的配置傳入到我們的構造函數(shù)中,如下:
index.js
import Axios from './Axios'
import config from './config'
const axios = new Axios(config);
export default axios;
所以铛漓,我們需要在構造函數(shù)中接收一個 config
參數(shù)進行處理溯香,即將默認配置寫入到實例中,即:
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
玫坛, 因為在請求之前已經(jīng)幫你拼接完成了~
當然,你也可以通過 axios.defaults.baseURL = xxx
這種方式修改默認配置问顷,都是沒問題的~
實例配置
在使用官方 axios
的時候昂秃,我們可以通過一個create
方法創(chuàng)建一個axios
實例,并傳入配置信息即可杜窄,我們只需要在 index.js
中創(chuàng)建的 axios
添加一個 create
方法即可 肠骆。
index.js
axios.create = function (config) {
return new Axios(config);
}
這樣我們也可以通過 create
方法構建一個 axios
實例,它也擁有相應的方法~
但是這么做存在一個問題塞耕,如果我們創(chuàng)建多個實例蚀腿,傳入不同的 config
,由于我們直接在構建的時候 通過 this.defaults = config;
這種方式復制扫外,并沒有切斷對象的引用關系莉钙,因此會導致配置對象會被相互引用,出問題~
因此筛谚,我們需要對其進行 深拷貝 賦值磁玉,即 this.defaults = deepClone(config)
, 其中 deepClone
時深拷貝函數(shù),這里不再贅述~
請求配置
我們發(fā)現(xiàn)官方的 axios
的get
驾讲、post
等請求會有第二個可選參數(shù)蚊伞,也是 config
席赂,即單獨本次請求的配置,如果存在时迫,我們需要進行配置合并颅停,對于簡單的 baseURL
、method
等這種簡單的配置直接覆蓋掠拳,對于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
是合并兩配置對象的方法喊熟,具體實現(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)在我們就可以通過如下方式進行請求了:
axios.get('/user/info',{
baseURL : 'http://127.0.0.1:3000' ,
headers : {
token : 'x-token-123456'
}
}).then(res=>{
console.log(res);
})
可以看到控制臺輸出跟之前的是一樣的~
細心的小伙伴可以看到 header
頭已經(jīng)添加了 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);
});
那么,接下來我們自己來實現(xiàn)這么一個 use
方法~
首先我們需要在我們的 axios
實例上添加一個 interceptors
對象龙填,該對象有 request
和 response
兩個屬性,他們都擁有 use
方法拐叉,我們發(fā)現(xiàn) use
方法的結構都相同岩遗,入?yún)閮蓚€函數(shù),其實他們是同一個 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
數(shù)組來保存,這樣可以保證我們可以多調用 use
方法笔诵,添加多個攔截器~
我們只需在 Axios.js
中的 constructor
構造函數(shù)中初始化即可返吻。
Axios.js
constructor(config){
//默認配置
this.defaults = deepClone(config);
//攔截器
this.interceptors = {
request : new Interceptor() ,
response : new Interceptor()
}
}
這樣盡管我們已經(jīng)可以在我們的 main.js
中使用 use
方法添加攔截器了,但是還是無法正確使用乎婿,因為請求這一塊還未進行處理测僵,接下來,我們需要對我們之前的 Axios.js
進行改造~
首先谢翎,我們統(tǒng)一封裝一個 request
函數(shù)捍靠,往后所有的請求都會調用這個方法,入?yún)⑿枰粋€ config
森逮,返回一個 Promise
對象榨婆,我們在這里對攔截器進行操作,定義如下:
//request請求
request (config) {
//配置合并
let configs = mergeConfig(this.defaults, config);
//將配置轉成 Promise 對象褒侧,鏈式調用和返回 Promise 對象
let promise = Promise.resolve(configs);
//請求攔截器良风,遍歷 interceptors.request 里的處理函數(shù)
let requestHandlers = this.interceptors.request.handlers;
requestHandlers.forEach(handler => {
promise = promise.then(handler.resolvedHandler, handler.rejectedHandler)
});
//數(shù)據(jù)請求
promise = promise.then(this.send)
//相應攔截器谊迄,遍歷 interceptors.response 里的處理函數(shù)
let responseHandlers = this.interceptors.response.handlers;
responseHandlers.forEach(handler => {
promise = promise.then(handler.resolvedHandler, handler.rejectedHandler)
})
//返回響應信息
return promise;
}
上面,為了代碼簡潔拖吼,我又將 send
方法提出來鳞上,定義跟之前基本一致:
//發(fā)送請求
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
方法也有一點點的不同吊档,主要是加入了請求攔截器~
// 發(fā)送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
值經(jīng)行了修改,響應攔截器分別打印了響應信息并將響應添加了 msg
的屬性~
不出意外怠硼,你在控制臺可以看到如下信息鬼贱,在請求 header
里看到 token
已經(jīng)被更改~
大功告成!
總算是有點樣子啦~
結語
至此香璃,我們自己封裝了一個非常簡單的 axios
的請求庫这难,由于篇幅有限,這里我只是用了最簡單的 get
請求示例葡秒,axios
源碼中遠不止這些姻乓,像一些異常處理、取消請求等的一系列的東西都還沒有實現(xiàn)眯牧,這里主要是借鑒其一些思想和實現(xiàn)的思路蹋岩,我這里只是牽個頭,剩下的靠你們自己不斷的去完善学少,動動手總是好的~
文末剪个,附上 git
地址 感興趣的小伙伴可以參考參考~