我們知道,axios是前端一個非常優秀的對於網絡請求的框架,其特點主要是請求方便、功能多(如攔截器)、可擴展性強等。那麼作為一枚前端開發人員,瞭解並能夠使用axios其實是基礎,深入瞭解其實現原理才是比較重要的,當然,如果能徒手擼一個axios類似的框架出來,那就是相當的不錯了。
這篇文章會從以下幾個大的點來實現一個axios框架:
- axios的本質是什麼?
- axios默認值、參數的實現
- 常見請求方式:get、post、delete在axios中的實現
- 真正請求(XMLHttpRequest)的實現
- axios攔截器的實現
- 打包發佈
同時希望你瞭解以下的一些知識點:
- webpack
- axios框架的基本使用
- Es6的Proxy、Class等
- XMLHttpRequest
- http協議...等
axios的本質是什麼
使用axios框架的時候,我們大部分情況都是以模塊的形式引入進行使用,如:
import axios from 'axios'
發出一個get請求,如:
axios.get('/user')或axios('/user')
從中可以看出,axios的本質就是一個函數,那麼就先來實現一個axios的雛形。(本篇文章實現利用es6的calss去實現)。
axios.js
class Axios{
constructor(){
}
}
export default new Axios();
index.js
import axios from './axios'
這裏毫無疑問可以看出,axios現在只是一個Axios的實例,並不是一個函數,那麼要怎樣將axios變成一個函數?這就需要兩個知識點:Axios的構造函數是可以進行返回值操作、利用ES6的Proxy。更進一步的Axios類的代碼如下:
class Axios{
constructor(){
return new Proxy(function(){},{
apply(fn,thisArg,agrs){
}
})
}
}
export default new Axios();
這樣我們得到的一個Axios實例axios就是一個函數了。這裏簡單提一下,Proxy在進行函數處理的使用,apply是很有用的,使用它我們就可以對一個函數進行代理處理了。
看看打印出來的內容:
接下來,要怎樣才能夠實現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:{}})
分析三者請求參數的相同點:
-
都只有一個參數,其參數是字符串類型
- axois.get(url)
- axios.post(url)
- axios.delete(url)
-
都只有一個參數,其參數類型是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...