axios源碼學習到使用

0、寫在前面

先掌握源碼結構再到實際的運行使用中去復盤源碼。就是 源碼—>使用—>源碼 的學習線路。
思維導圖配合文章更清晰


axios.png
0.1 取源碼

源碼取到打開已經打包好的文件 dist/axios.js 看一下,注釋加上空行也就兩千行不到。


git clone https://github.com/axios/axios.git
0.2 入口文件

打開package.json找到入口為index.js



index.js

module.exports = require('./lib/axios');

主要看的就是紅框里這部分內容了。


由上至下大致分析一下文件及文件夾的主要內容。
adapter:適配器,瀏覽器環境用xhr,node環境用http
cancel:取消請求
core:核心代碼
helpers:功能助手
axios.js:聲明定義文件
default.js:默認的配置參數
utils.js:一些小工具函數集合
下面我們就從定義文件開始看起。

1、axios.js

1.1 直接找到聲明語句開始
var axios = createInstance(defaults);

1.1.1 看傳入的默認配置 defaults 定義了哪些東西
adapter:適配器根據運行環境選擇哪種請求方式(瀏覽器用xhr—lib/adapters/xhr.js,node環境用http—lib/adapters/http.js)
transformRequest:請求數據處理方法數組
transformResponse:響應數據處理方法數組
timeout:響應最長返回時間,超過這個時間就取消請求
xsrfCookieName:xsrf在cookie內名稱
xsrfHeaderName:xsrf在HTTP頭內名稱
maxContentLength:允許的響應內容的最大尺寸
maxBodyLength:(僅在node中生效)允許發送HTTP請求內容的最大尺寸
validateStatus:是否 2** 類型的狀態碼
headers:定義了共用的請求頭 common 和各種不同請求方法的請求頭(用了兩個循環完成)

再看 function createInstance(defaultConfig) 里面的語句,也就5行代碼,一行一行看。

1.1.2 返回 Axios 對象

 var context = new Axios(defaultConfig);
  • 打開 core/Axios.js 查看 Axios 所擁有的屬性方法:
    defaults:剛剛的默認配置賦值;
    interceptors:攔截器,包括 request、response 兩個對象;
    request:發送的請求處理,里面主要處理有合并默認配置和自定義配置,確認請求方法,攔截器處理,最后循環執行攔截器至promise對象并返回該對象;
    getUri:獲取請求的完整地址;
    兩個forEach:循環定義HTTP請求方法(delete、get、head、options、post、put、patch),注意前四個與后三個傳參的不同 function(url, config) 和 function(url, data, config)。

  • 另外再看一下攔截器 interceptors 所使用的對象,在 InterceptorManager.js中:
    handlers:攔截處理數組;
    use:增加一個攔截器;
    eject:移除一個攔截器;
    forEach:遍歷處理,將有效的攔截器綁定上處理方法(這個可以在 Axios 對象方法 request 里看攔截器處理那段)。

1.1.3 定義返回 wrap 函數

var instance = bind(Axios.prototype.request, context);

打開 helpers/bind.js 查看 bind 函數,得到這里 instance 是被定義成了一個 wrap 函數,該函數返回值是Axios.prototype.request 方法調用結果,并且 request 內部this指向傳入的值 context,傳入參數為 args ,另外在前面我們看到這個 request 結果是返回 promise 類型對象的。

module.exports = function bind(fn, thisArg) {
  return function wrap() {
    var args = new Array(arguments.length);
    for (var i = 0; i < args.length; i++) {
      args[i] = arguments[i];
    }
    return fn.apply(thisArg, args);
  };
};

1.1.4 這是將 Axios.prototype 定義的 request、getUri以及delete、get、head、options、post、put、patch 都以返回 wrap 函數的方式復制到 intance 上,形同上面的 bind(Axios.prototype.request, context)

utils.extend(instance, Axios.prototype, context);

1.1.5 將創建的 context 對象擴展復制到 instance 上

utils.extend(instance, context);

1.1.6 返回 instance ,即 wrap 函數賦值給變量 axios

return instance;
1.2 將 Axios 暴露出來,允許繼承
axios.Axios = Axios;
1.3 實例函數工廠,用這個可以建立自己的默認配置和攔截器等的實例
axios.create = function create(instanceConfig) {
  return createInstance(mergeConfig(axios.defaults, instanceConfig));
};
1.4 定義取消請求方法

具體使用看下一節 2.4。

axios.Cancel = require('./cancel/Cancel');
axios.CancelToken = require('./cancel/CancelToken');
axios.isCancel = require('./cancel/isCancel');
1.5 定義多請求方法,需要可以與spread配合將返回值數組包裝成多個變量返回值形式

這里結合后面的 2.5 使用更清楚。

axios.all = function all(promises) {
  return Promise.all(promises);
};
axios.spread = require('./helpers/spread');

以上就把整個axios從新建到內部核心動作大致看完了,下面我們通過使用再具體看一下實際使用過程中的運行情況。

2、從使用運行回到源碼學習

2.1 安裝

安裝有兩種方式,一種是包管理,一種是 script 標簽引入,不管是哪種安裝,我們都會得到一個全局變量 axios(包管理使用時需要自己導入定義一下)。

2.2 使用

2.2.1 通過方法名
看一下常用的 get 示例請求

axios.get('/user?ID=12345')
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  })

先看為什么可以直接用 get 這個方法名呢?
這個就是 1.1.4 那節的代碼 utils.extend(instance, Axios.prototype, context); 的作用,把 request、getUri以及delete、get、head、options、post、put、patch 都復制到了 axios 上,所以同樣的道理可以得到以下幾種請求方法,getUri 不是請求方法也不咋用到在這里先忽略。

axios.request(config)

axios.delete(url[, config])

axios.get(url[, config])

axios.head(url[, config])

axios.options(url[, config]])

axios.post(url[, data[, config]])

axios.put(url[, data[, config]])

axios.patch(url[, data[, config]])

現在我們通過上面的示例get請求來看代碼是怎么運行的。

Axios.js 中是通過以下代碼來定義 get 請求的,那么示例代碼中唯一的實參 '/user?ID=12345' 就代表著定義中的形參 url ,并沒有傳入config的實參。

utils.forEach(['delete', 'get', 'head', 'options'], function forEachMethodNoData(method) {
  Axios.prototype[method] = function(url, config) {
    return this.request(utils.merge(config || {}, {
      method: method,
      url: url
    }));
  };
});

定義里返回的是this.request(......)方法,我們之前在 1.1.3 說過這個 this 是指向傳入的 context 的,這個 this.request 就是里面的 request 方法 ,是返回 promise 對象的,所以在示例里我們可以接著 axios.get('/user?ID=12345') 寫 then和catch 方法。

再看 this.request 里對傳入參數的處理。
config || {}:這里我們沒有傳入 congfig 實參,所以 config 是 undefined ,這個結果就是后面的空對象 {}
{method: method,url: url}:method 是定義方法時就賦值的,跟定義的方法同名,url就是我們傳入的 '/user?ID=12345'
utils.merge(.....):這個方法就是把上面的兩個對象合并成一個對象傳入 this.request
程序走到這里合成的這個對象應該如下:

{
  method:"get",
  url:"/user?ID=12345"
}

看下 request 定義代碼,這里的 function request(config) 中的 config 就是上面合成的對象值。

Axios.prototype.request = function request(config) {
 // Allow for axios('example/url'[, config]) a la fetch API
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }
  config = mergeConfig(this.defaults, config);
  // Set config.method
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }
  // Hook up interceptors middleware
  var chain = [dispatchRequest, undefined];
  var promise = Promise.resolve(config);

  this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

  return promise;
};

先略掉開頭一小段,其中有個 config 是否為 string 類型的判斷和請求方法methods的處理,這個主要是 axios('/user?ID=12345') 使用形式的,我們待會說?,F在從 var chain = [dispatchRequest, undefined]; 看起。

這是定義了一個攔截器數組 chain ,并且給了初始值 [dispatchRequest, undefined]。

 var promise = Promise.resolve(config);

定義 promise 對象,方便后面的鏈式調用。


this.interceptors.request.forEach(function unshiftRequestInterceptors(interceptor) {
    chain.unshift(interceptor.fulfilled, interceptor.rejected);
  });

  this.interceptors.response.forEach(function pushResponseInterceptors(interceptor) {
    chain.push(interceptor.fulfilled, interceptor.rejected);
  });

增加請求和響應攔截器,我們沒有另外定義,這里過后 chain 是沒有變化的。


  while (chain.length) {
    promise = promise.then(chain.shift(), chain.shift());
  }

開始調用運行 chain 數據,這里參數為什么是 (chain.shift(), chain.shift()) 呢,可以看下上兩句請求攔截器中(interceptor.fulfilled, interceptor.rejected) 成功與失敗處理函數都是成對添加到數組的,所以這也就解釋了為什么初始化 chain 的時候是 dispatchRequest 和一個undefined 的了。當然是為了保證后續的響應攔截器的成功與失敗處理函數是成對的。
這個 dispatchRequest 定義在 dispatchRequest.js 文件里。我們對照源碼一一分析。

 throwIfCancellationRequested(config);
......
function throwIfCancellationRequested(config) {
  if (config.cancelToken) {
    config.cancelToken.throwIfRequested();
  }
}
.....
CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

在處理請求前先判斷有沒有取消該請求,有了就拋出異常。


 // Ensure headers exist
  config.headers = config.headers || {};

定義賦值請求頭,在get示例我們并沒有定義,所以這里的 config.headers 只有之前 default.js 文件中預先定義的部分,即:

headers:{
  common:{
    'Accept': 'application/json, text/plain, */*'
  },
  delete:{},
  get:{},
  head:{},
  post:{
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  put:{
    'Content-Type': 'application/x-www-form-urlencoded'
  },
  patch:{
    'Content-Type': 'application/x-www-form-urlencoded'
  },
}

 // Transform request data
  config.data = transformData(
    config.data,
    config.headers,
    config.transformRequest
  );

格式化請求數據,但是我們的get示例是沒有請求 data 的,這里經過一圈處理 config.data 為 undefined。


  // Flatten headers
  config.headers = utils.merge(
    config.headers.common || {},
    config.headers[config.method] || {},
    config.headers
  );

將幾個對象合成一個對象賦值給 config.headers。


  utils.forEach(
    ['delete', 'get', 'head', 'post', 'put', 'patch', 'common'],
    function cleanHeaderConfig(method) {
      delete config.headers[method];
    }
  );

刪除默認的 headers 配置


var adapter = config.adapter || defaults.adapter;

賦值適配器,需返回promise對象的,我們沒有定義,就用默認的 default.adapter。


  return adapter(config).then(function onAdapterResolution(response) {
    throwIfCancellationRequested(config);
    // Transform response data
    response.data = transformData(
      response.data,
      response.headers,
      config.transformResponse
    );
    return response;
  }, function onAdapterRejection(reason) {
    if (!isCancel(reason)) {
      throwIfCancellationRequested(config);
      // Transform response data
      if (reason && reason.response) {
        reason.response.data = transformData(
          reason.response.data,
          reason.response.headers,
          config.transformResponse
        );
      }
    }
    return Promise.reject(reason);
  });

終于到這了,config 進入 adapter

adapter: getDefaultAdapter()

function getDefaultAdapter() {
  var adapter;
  if (typeof XMLHttpRequest !== 'undefined') {
    // For browsers use XHR adapter
    adapter = require('./adapters/xhr');
  } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') {
    // For node use HTTP adapter
    adapter = require('./adapters/http');
  }
  return adapter;
}

打開兩個文件看下都是返回的 new Promise(),如果調用過取消請求的方法會執行 function onAdapterRejection(reason) ,我們這里自然進入到了 function onAdapterResolution(response) ,這里就得到帶有響應值的 promise 對象,這就保證了示例get中 axios.get() 后還可以再添加 then 方法。
關于 xhr.js 和 http.js 后續會出專門的文章解釋。

2.2.2 參數調用
我們平時還會用一下的兩方式去調用

axios("/user?ID=12345")

axios({
   url:"/user?ID=12345",
   method:'get"  //post等方法一樣
})

為什么我們可以直接用這種形式調用呢,請看 1.1.3 var instance = bind(Axios.prototype.request, context); 這個在定義的時候就把 request 這個給復制給了 變量本身,所以 axios() 中是直接用的 Axios.prototype.request 方法。

再看Axios.prototype.request中之前略過的幾行代碼,就是為了這種類型的使用做的判斷。

 // Allow for axios('example/url'[, config]) a la fetch API
  if (typeof config === 'string') {
    config = arguments[1] || {};
    config.url = arguments[0];
  } else {
    config = config || {};
  }
  config = mergeConfig(this.defaults, config);
  // Set config.method
  if (config.method) {
    config.method = config.method.toLowerCase();
  } else if (this.defaults.method) {
    config.method = this.defaults.method.toLowerCase();
  } else {
    config.method = 'get';
  }

第一個判斷, axios("/user?ID=12345") 中的config 就是 string 類型。
第二個判斷,axios("/user?ID=12345") 中沒有定義請求方法所以默認get。
經過第二個判斷處理以后我們知道會擴展識別 config 的 method 屬性,也就是說雖然 default.js 里沒有定義到,但是程序處理的時候程序是可以讀取或者建立 method 這個屬性的,所以 axios({ url:"/user?ID=12345"}) 、axios({ url:"/user?ID=12345",method:'post"}) 也都是可以的。

另外中間還有有個 mergeConfig 方法定義在 mergeConfig.js 中,那么在這個文件里,我們看到所有可以設置的配置參數。

  var valueFromConfig2Keys = ['url', 'method', 'data'];
  var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy', 'params'];
  var defaultToConfig2Keys = [
    'baseURL', 'url', 'transformRequest', 'transformResponse', 'paramsSerializer',
    'timeout', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName',
    'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress',
    'maxContentLength', 'maxBodyLength', 'validateStatus', 'maxRedirects', 'httpAgent',
    'httpsAgent', 'cancelToken', 'socketPath', 'responseEncoding'
  ];

這里比較單一自己多看看。

2.3 axios.create()——自己創建 axios 實例

這個在文章 四、vue+ElementUI開發后臺管理模板—方法指令、接口數據 第2大點 獲取接口數據、模擬數據 里實際運用了,這里不再多說。

2.4 axios.CancelToken——取消請求

如果我們瀏覽SAP頁面,在資源大、多或者響應比較慢的頁面在發生跳轉后,前一個頁面的請求還會繼續等待響應,那最好的是到新頁面后,之前的請求應該全部取消,將資源留給新頁面用。

我們先用 GitHub/chizijijiadami/vue-elementui-5 的代碼看下這樣的請求效果,這個是要配合之前用 phpstudy 寫的跨域接口的,具體見文章 四、vue+ElementUI開發后臺管理模板—方法指令、接口數據 底部部分。

獲取代碼后,做如下修改:
src>pages>Index>index.vue

  created() {
    this.getList();
-    //this.getCrossDomainList();
+   this.getCrossDomainList();
  },

src>pages>List>Detail>index.vue

+    created(){
+        console.log('list-detail');
+    }

對phpstudy中建立的網站文件增加一句 sleep(5) 代表延時5秒鐘返回,可以模擬服務器延時情況。


上面這些修改后運行項目,打開首頁后立即切換至詳情頁,在等待數秒后,控制臺如下圖。


頁面已切換,前頁面未完成請求仍然在繼續返回數據

要想實現跳轉至新頁面后,立即取消前面頁面的請求,我們可以做如下操作:
src>data>store>modules>app.js

        system: {
            title: "大米工廠",
+            requestCancel:[]
        },

......

   mutations: {
+        ADD_SYSTEM_REQUEST_CANCEL:(state,c)=>{
+            state.system.requestCancel.push(c)
+        },
+        SET_SYSTEM_REQUEST_CANCEL:state=>{
+            state.system.requestCancel=[]
+        },
    
......
    },
    actions: {
+        addSystemRequestCancel({ commit },c){
+            commit('ADD_SYSTEM_REQUEST_CANCEL',c)
+        },
+        setSystemRequestCancel({ commit }){
+            commit('SET_SYSTEM_REQUEST_CANCEL')
+        },

......
    }
 

src>common>utils>axiosApi.js,給每個請求添加取消方法。

import store from 'data/store'
import axios from 'axios'
import ErrorMessage from './errorMessage'
import { MessageBox } from 'element-ui'
var instance = axios.create({
    baseURL: '',
    timeout: 5000
});

+  const CancelToken = axios.CancelToken;

// 添加請求攔截器
instance.interceptors.request.use(
    // 在發送請求之前做些什么
    config => {
          config.headers['version'] = '1'
+          config.cancelToken = new CancelToken((c) => {
+              store.dispatch('addSystemRequestCancel', c)
+          });
          return config
    },
    error => {
        Promise.reject(error)
    }
);

src>common>routerFilter>index.js,登錄權限信息肯定是不能取消的,這里應該在登錄后實行這種機制。
傳遞取消信息,并且每到一個新頁面前 requestCancel 數組應該清零。

            if (to.path === '/login') {
                next('/')
            } else {
-                next()
+                store.getters.app.system.requestCancel.forEach(cancel => {
+                    cancel('Cancel')
+                })
+                store.dispatch("setSystemRequestCancel").then(() => {
+                    next()
+                })
            }

現在我們再運行切換頁面,看下控制臺在進入 detail 頁之前那個跨域請求被取消報了錯,并且彈出了錯誤提示,這里就要進一步修改,取消請求的情況是不應該彈窗報錯的


src>common>utils>axiosApi.js

    // 對響應錯誤做點什么
    if (!error.response) {
        // 服務器請求失敗時錯誤提示
+        if (error.message !== 'Cancel') {
            MessageBox({
                message: `請求超時${ErrorMessage.API_ERROR_LOAD}`,
                showCancelButton: false,
                confirmButtonText: '確定',
                type: 'error',
                callback() { }
            })
+        }
    }

src>pages>Index>index.vue,這里添加錯誤處理代碼

    getCrossDomainList() {
      api
        .getCrossDomainList()
        .then(res => {
          console.log(res);
        })
+        .catch(() => { });
    }

以上修改完成后我們再切換看看,沒有任何報錯了


這里是axios 取消的一種方式,還有另一種使用方式差不多,自己試試這里就不多說了。

2.5 axios.all()——多請求并發

src>pages>Index>index.vue

  created() {
    // this.getList();
    // this.getCrossDomainList();
+    this.all();
  },

methods:{
......
    all() {
      axios.all([api.getList(), api.getCrossDomainList()]).then(arr => {
        console.log(arr, "arr");
      });
      axios.all([api.getList(), api.getCrossDomainList()]).then(
        axios.spread(function(one, two) {
          console.log(one, two, "spread");
        })
      );
    }
}

運行結果如下圖,就是拆分了返回值數組。還有一點可以看到這個all里面會等請求執行成功以后才一起返回,如果取消了其中一個,就會返回 reject。


感謝閱讀,喜歡的話點個贊吧:)
更多內容請關注后續文章。。。

?著作權歸作者所有,轉載或內容合作請聯系作者
平臺聲明:文章內容(如有圖片或視頻亦包括在內)由作者上傳并發布,文章內容僅代表作者本人觀點,簡書系信息發布平臺,僅提供信息存儲服務。
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市,隨后出現的幾起案子,更是在濱河造成了極大的恐慌,老刑警劉巖,帶你破解...
    沈念sama閱讀 230,527評論 6 544
  • 序言:濱河連續發生了三起死亡事件,死亡現場離奇詭異,居然都是意外死亡,警方通過查閱死者的電腦和手機,發現死者居然都...
    沈念sama閱讀 99,687評論 3 429
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人,你說我怎么就攤上這事?!?“怎么了?”我有些...
    開封第一講書人閱讀 178,640評論 0 383
  • 文/不壞的土叔 我叫張陵,是天一觀的道長。 經常有香客問我,道長,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 63,957評論 1 318
  • 正文 為了忘掉前任,我火速辦了婚禮,結果婚禮上,老公的妹妹穿的比我還像新娘。我一直安慰自己,他們只是感情好,可當我...
    茶點故事閱讀 72,682評論 6 413
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著,像睡著了一般。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發上,一...
    開封第一講書人閱讀 56,011評論 1 329
  • 那天,我揣著相機與錄音,去河邊找鬼。 笑死,一個胖子當著我的面吹牛,可吹牛的內容都是我干的。 我是一名探鬼主播,決...
    沈念sama閱讀 44,009評論 3 449
  • 文/蒼蘭香墨 我猛地睜開眼,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了?” 一聲冷哼從身側響起,我...
    開封第一講書人閱讀 43,183評論 0 290
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎,沒想到半個月后,有當地人在樹林里發現了一具尸體,經...
    沈念sama閱讀 49,714評論 1 336
  • 正文 獨居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內容為張勛視角 年9月15日...
    茶點故事閱讀 41,435評論 3 359
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發現自己被綠了。 大學時的朋友給我發了我未婚夫和他白月光在一起吃飯的照片。...
    茶點故事閱讀 43,665評論 1 374
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內的尸體忽然破棺而出,到底是詐尸還是另有隱情,我是刑警寧澤,帶...
    沈念sama閱讀 39,148評論 5 365
  • 正文 年R本政府宣布,位于F島的核電站,受9級特大地震影響,放射性物質發生泄漏。R本人自食惡果不足惜,卻給世界環境...
    茶點故事閱讀 44,838評論 3 350
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 35,251評論 0 28
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至,卻和暖如春,著一層夾襖步出監牢的瞬間,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 36,588評論 1 295
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人。 一個月前我還...
    沈念sama閱讀 52,379評論 3 400
  • 正文 我出身青樓,卻偏偏與公主長得像,于是被迫代替她去往敵國和親。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 48,627評論 2 380