咱們知道,axios是前端一個很是優秀的對於網絡請求的框架,其特色主要是請求方便、功能多(如攔截器)、可擴展性強等。那麼做爲一枚前端開發人員,瞭解並可以使用axios實際上是基礎,深刻了解其實現原理纔是比較重要的,固然,若是能徒手擼一個axios相似的框架出來,那就是至關的不錯了。
這篇文章會從如下幾個大的點來實現一個axios框架:javascript
同時但願你瞭解如下的一些知識點:前端
使用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
看看打印出來的內容: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的時候,能夠有以下的使用狀況:
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';
最終獲得咱們想要的:
在axios中,這三種常見請求的格式大體以下:
get請求:
post請求:
delete請求:
分析三者請求參數的相同點:
都只有一個參數,其參數是字符串類型
都只有一個參數,其參數類型是object
接下來就按照這樣的一個規律去實現這三種請求。先來處理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方法中傳遞的參數。
這裏真正的網絡數據請求模塊與axios模塊是獨立開來的,請求模塊只負責數據的請求,而axios模塊須要負責對數據請求進行處理。
從上面實現的get、post、delete方法中,最後調用的實例的request方法,該方法的主要做用有:
請求頭的處理:這裏有三個地方涉及到請求頭,默認的配置項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中。
這兩個配置對象屬性是一個函數。簡單用法以下:
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.interceptors
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其實不算難,難點在於對各類默認值、參數、請求形式等的處理。
最終附上完整代碼: