寫在前面:
距離2021年 還有兩個月~11月份的開始,決定勤奮一波
關於axios
axios 不用更多的介紹,vue官方的推薦是使用axios,vue-resource淡出框架的依賴,在我們進行項目開發和搭建的時候,axios是我們連接和后端接口的橋梁,當然選擇axios,自然是其的一些特點,能夠更好的滿足我們日常的工作;
能學習到什么呢
通過本篇文章,你將大概了解到axios創建實例過程、配置合並流程以及原理,還有進行請求的流程;深入了解到axios的響應攔截器原理,支持多平台(瀏覽器和node)環境下使用原理、請求響應參數的設置原理、整個的執行過程~嘻嘻,有些地方大家簡單大概了解,還是需要看下源碼的執行原理滴;
進入axios
想要了解axios的原理,可以從其特點出發;
進入原理探究過程(搓手手)
首先自行下載axios的包,查看其目錄結構
在這個里面有兩個助手包,一個helpers、一個是utils,這兩個包都是axios的工具包,helpers是服務於axios,utils更加廣泛的工具,可以在其他的插件中進行引入使用,我們每個插件其實都會自定義自己的工具包~
配置階段
axios的配置共分為3種,全局配置、實例配置、請求配置;這三個配置和我們進行的操作息息相關;
全局配置
全局配置,更加明確的說其實是axios里面給我們提供的一個默認的選項,當我們導入axios的包的時候,其實就已經進行了默認選項的配置;
默認配置項如下:
而這些全局配置選項也正是我們在進行實例化的時候,可以進行自定義配置的,主要的屬性包括;
- transformRequest :Array 允許在向服務器前發送信息的時候,統一處理請求信息默認配置選項功能:
- 序列化請求參數
- 為不同類型的請求參數添加請求頭
-
transformResponse :Array 則是對響應數據的操作處理
- timeout 、headers等默認的配置
- 默認配置如圖所示
實例配置
實例配置其實是我們針對自己項目進行的個性化的配置,在導入axios時候,就給我們提供了一個create的函數,而這個函數的作用其實是創建一個新的實例,並返回給我們;應用於整個項目配置
在一個項目中每個接口都有共同的配置,或者幾個接口有共同的表現形式,每個實例化的axios都有自己的配置,通過全局配置進行初始化,或者合並成一個新的配置項目實例化配置一般是我們自己配置的,我們在項目中可能包含多個模塊,項目的接口名字不一致,因此需要配置不同的axios的實例信息,這樣我們配置moduleA和moduleB的實例配置,每次返回的新的實例不會互相影響;
create的方法
axios.create = function create(instanceConfig) { // console.log("實例配置",mergeConfig(axios.defaults, instanceConfig)) return createInstance(mergeConfig(axios.defaults, instanceConfig)); };
//定義創建的axios function createInstance(defaultConfig) { var context = new Axios(defaultConfig); // instance 綁定axios的默認數據 instance 返回wrap函數 var instance = bind(Axios.prototype.request, context); // console.log(instance.prototype) // 復制Axios的原型到擴展到實例中 utils.extend(instance, Axios.prototype, context); //將Axios的屬性擴展到實例上 utils.extend(instance, context); //instance 進行復制 return instance; }
這塊兒其實有點難以理解 ,就是我們創建實例時候,直接new Axios就可以了,為什么還需要進行包裝呢??
先來看bind方法 ,bind的方法其實很好理解,就是將Axios原型方法上的request方法綁定在context的上下文中,返回一個wrap的方法
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); }; };
》》 utils.extend(instance, Axios.prototype, context); 這個的意思又是什么呢
function extend(a, b, thisArg) { // console.log("參數b",b) forEach(b, function assignValue(val, key) { if (thisArg && typeof val === 'function') { a[key] = bind(val, thisArg); } else { a[key] = val; } }); return a; }
迷糊人員?這里其實是將Axios上原型上的方法復制到我們的instance的實例上,這樣的操作?保持迷惑?
最后一個的extend utils.extend(instance, context); 是將剛剛創建的Axios的實例,復制到instance上,整個創建實例包含的步驟有:
- 將b的屬性內容復制給a
- 此時將Axios原型上的方法復制到instance中
- 最后把axios實例復制給instance 形成一個真正的instance
查閱資料,說這樣的操作是為了更好的使用axios,如果我們只是返回一個new Axios的實例,那么我們在進行調用的方式是比較單一的,這樣子配置了后;調用的方式就多了起來
axios({config}).then()
axios.get('url').then()
這種的實現方式,利用了拷貝繼承,打印instance的構造函數;
請求配置
同一個實例會有一些公用的配置項目,如baseUrl,但是很多時候,不同的請求具體的配置是不一樣的,如url、method等,所以在請求的時候需要傳入的配置與實例配置進行合並;
請求的時候,也進行了相關的配置項目,這樣形成了三個配置項,主要采用后配置后優先的原則,優先級順序:請求配置>實例配置>全局配置;
這個時候便涉及到了合並的問題;主要涉及的配置合並
配置合並主要在/lib/core/mergeConfig.js中進行

module.exports = function mergeConfig(config1, config2) { // eslint-disable-next-line no-param-reassign config2 = config2 || {}; var config = {}; var valueFromConfig2Keys = ['url', 'method', 'params', 'data']; //需要進行深拷貝的屬性 var mergeDeepPropertiesKeys = ['headers', 'auth', 'proxy']; //默認的配置項目key var defaultToConfig2Keys = [ 'baseURL', 'url', 'transformRequest', 'transformResponse', 'paramsSerializer', 'timeout', 'withCredentials', 'adapter', 'responseType', 'xsrfCookieName', 'xsrfHeaderName', 'onUploadProgress', 'onDownloadProgress', 'maxContentLength', 'validateStatus', 'maxRedirects', 'httpAgent', 'httpsAgent', 'cancelToken', 'socketPath' ]; //將傳入的配置項目內容先賦值到config中 utils.forEach(valueFromConfig2Keys, function valueFromConfig2(prop) { if (typeof config2[prop] !== 'undefined') { config[prop] = config2[prop]; } }); // 遍歷需要進行深拷貝的屬性 /** * config2中的如果是對象 則進行深拷貝 * 如果不是則直接進行賦值,如果該屬性對應的值, * 則直接進行拷貝config1的內容 */ utils.forEach(mergeDeepPropertiesKeys, function mergeDeepProperties(prop) { if (utils.isObject(config2[prop])) { config[prop] = utils.deepMerge(config1[prop], config2[prop]); } else if (typeof config2[prop] !== 'undefined') { config[prop] = config2[prop]; } else if (utils.isObject(config1[prop])) { config[prop] = utils.deepMerge(config1[prop]); } else if (typeof config1[prop] !== 'undefined') { config[prop] = config1[prop]; } }); //defaultToConfig2Keys 一些配置 config2的內容存在則使用config2的,不存在使用config1的內容 utils.forEach(defaultToConfig2Keys, function defaultToConfig2(prop) { if (typeof config2[prop] !== 'undefined') { config[prop] = config2[prop]; } else if (typeof config1[prop] !== 'undefined') { config[prop] = config1[prop]; } }); //請求的一些key var axiosKeys = valueFromConfig2Keys .concat(mergeDeepPropertiesKeys) .concat(defaultToConfig2Keys); //其他的配置key 傳入的key的值 var otherKeys = Object .keys(config2) .filter(function filterAxiosKeys(key) { return axiosKeys.indexOf(key) === -1; }); // 將config2中的自定義key的值 存入config中 utils.forEach(otherKeys, function otherKeysDefaultToConfig2(prop) { if (typeof config2[prop] !== 'undefined') { config[prop] = config2[prop]; } else if (typeof config1[prop] !== 'undefined') { config[prop] = config1[prop]; } }); //返回config return config; };
mergeConfig中主要涉及到三個主要key值的合並;
-
valueFromConfig2Keys 是在請求的時候要進行添加的項目內容
-
mergeDeepPropertiesKeys 需要神拷貝的key,比如headers、proxy等(主要是對象)
-
defaultToConfig2Keys 淺拷貝字符串
合並的規則如下:
- 進行key遍歷,如果config2中某個key存在內容,則取config2的內容
- 三個key遍歷完成后,進行屬性的合並,因為config2中有些屬性是我們自己自定義設置的,將自定義的設置增加到config中
整體配置就完成了~
創建請求
配置已經完成,接下來就是創建請求了;創建請求其實很重要起的作用的就是我們剛剛創建的實例Axios
封裝了幾乎是http所支持的所有請求方法, 主要的一個作用就是
- 合並請求配置和實例配置
- 規整化請求方法信息
- 收集請求攔截器和響應攔截器
- 進行發送請求
- 返回當前的promise
我們進行的請求方法主要存放文件在axios/lib/core/dispatchRequest.js中
執行transformRequest方法,對數據進行相關操作,獲取到要進行傳入的data
根據請求方法配置headers 此時有的確定的方法 刪除無用的headers
根據獲取的請求適配器傳入config進行請求調用 以瀏覽器為例:會創建XmlRequest的對象進行請求和發送數據;Xhr的過程

執行完成后,進行回調后的對數據操作的方法;
這樣整個執行的流程就完畢了,
攔截器
攔截器作用:當我們發送數據的時候,能夠攔截到發送內容並進行更改內容,適用於統一性的發送信息,比如token、自定義的請求頭,通過統一封裝的request函數為每個請求添加統一的信息;
存問問題:
后期如果需要為某些 GET 請求設置緩存時間或者控制某些請求的調用頻率的話,我們就需要不斷修改 request 函數來擴展對應的功能。此時,如果在考慮對響應進行統一處理的話,我們的 request 函數將變得越來越龐大,也越來越難維護
而axios也給我們提供了一種方法,分為請求攔截器和響應攔截器
請求攔截器:該類攔截器的作用是在請求發送前統一執行某些操作,比如在請求頭中添加 token 字段。
響應攔截器:該類攔截器的作用是在接收到服務器響應后統一執行某些操作,比如發現響應狀態碼為 9998 時,自動跳轉到登錄頁
Axios 的作用是用於發送 HTTP 請求,而請求攔截器和響應攔截器的本質都是一個實現特定功能的函數,攔截器的過程主要可以分為三個步驟
- 任務配置收集攔截器
- 任務編排(按照順序進行存儲)時的調度
- 按照攔截器的順序進行執行的攔截器
進入任務收集
我們在進行配置攔截器的使用,使用了實例上的interceptors的request的屬性,進入源碼中發現,在Axios的實例上,存在request的屬性,是INterceptorManager的實例。調用use的方法時候,其實就是調用了InterceptorManager中的方法
InterceptorManager.prototype.use = function use(fulfilled, rejected) { this.handlers.push({ fulfilled: fulfilled, rejected: rejected }); return this.handlers.length - 1; };
發現其實利用了一個handlers進行收集我們的攔截方法,fullfilled是成功,reject是失敗,
響應攔截器的原理也是一樣 ,都是利用handlers進行收集 ,
任務編排
到這里,我們請求攔截器和響應攔截器的任務配置已經收集完畢,接下來就是任務的編排過程;我們必須要保證,請求攔截器在我們請求之前,響應攔截器在實際請求之后執行;這里便引入了異步的promise方法,利用promise 中then的方法進行調用
//請求攔截的中間件 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); });
先存儲當前的實際請求,然后將請求攔截器放置在請求隊列的前面,響應攔截器放置在響應攔截器的后面;這樣便實際形成了一個數據的隊列
該隊列以請求函數作為區分,前面部分分別分別存儲請求成功時候函數和請求攔截失敗時候函數,后面響應攔截函數;
任務執行
到這里任務開始執行,
//當響應攔截器還存在的時候,壓入的promis中 去請求 while (chain.length) { promise = promise.then(chain.shift(), chain.shift()); }
利用了promise的串行、順序執行的特點,Promise 的執行串原理 因此需要每一個請求攔截器的resolve中返回config 供下一個promise函數使用;
適配器;
axios不僅能夠在vue、react等項目中使用,還能夠應用在node環境中,其主要是瀏覽器環境和node環境,支持不同的平台,自然是進行了請求適配~
在我們的全局默認配置中存在一個adapter的屬性,該屬性就是在引入實例或者創建新的實例的時候進行了適配器的選擇,主要的代碼如下:
//選擇哪一個請求器 如果當前存在xmlHttpRequest的話 就去請求這個否則將調用node的http模塊的訪問 function getDefaultAdapter() { var adapter; if (typeof XMLHttpRequest !== 'undefined') { ///通過判斷XMLHttpRequest是否存在,來判斷是否是瀏覽器環境 adapter = require('axios/lib/adapters/xhr'); } else if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { // 不再瀏覽器環境將用node環境 adapter = require('axios/lib/adapters/http'); } return adapter; }
Axios 同時支持瀏覽器和 Node.js 環境,對於瀏覽器環境來說,我們可以通過 XMLHttpRequest 或 fetch API 來發送 HTTP 請求,而對於 Node.js 環境來說,我們可以通過 Node.js 內置的 http 或 https 模塊來發送 HTTP 請求。
默認適配器:http
取消請求實現
進行取消請求設置
在請求的發送中:
axios.CancelToken.source()
創建了一個 CancelToken 實例給 token, CancelToken 的參數是一個函數,將函數參數再賦值給 cancel
將 { token: token,cancel: cancel } 作為新對象返回
CancelToken 具體做了什么 ?
創建了一個 Promise , 同時保存 Promise resolve 的具體實現
執行上一步傳遞的函數 A ,並將 取消操作的具體實現函數 作為參數傳遞給 A ,A 將其賦值給 cancel 傳遞給用戶
取消操作是執行了 Promise.resolve,同時將用戶設定的 message 封裝后作為結果返回給 then 的實現
其實都是進行了promise上的操作流程;
其他
axios中還有其他的功能:如錯誤處理機制
進行雙重cookie預防Csrf攻擊;
整體流程依賴
核心模塊依賴類圖,發送和任務攔截收集均在Axios中完成,整個過程依賴Promise的then方法保證串行、 順序執行;
整個的活動圖如圖所示:
axios主要配置使用
統一status攔截
axios給我們提供了攔截函數,如果正在項目中所有的場景接口都是一樣的,對統一的狀態碼進行處理;此時我們在攔截函數中,需要進行控制操作;
if(err.response.status){ const errCode = err.response.status const msg = err.response.message const errorMsgMap = { 400: '錯誤請求', 401: '請檢查用戶名和密碼', 403: '身份過期請重新登錄', 404: '請求錯誤,未找到該資源', 408: '請求超時', 500: '服務器端出錯', 501: '網絡未實現', 502: '網絡錯誤', 503: '服務不可用', 504: '網絡超時', 'other':'未知錯誤' } var message = errorMsgMap[errCode] ? errorMsgMap[errCode] :errorMsgMap['other'] ; err.message = message Toast.show({content:message}) }
這樣管理方便
鑒權:
鑒權是驗證用戶是否擁有權限訪問系統;傳統的鑒權是通過密碼來驗證的。這種方式的前提是,每個獲得密碼的用戶都已經被授權。在建立用戶時,就為此用戶分配一個密碼,用戶的密碼可以由管理員指定,也可以由用戶自行申請。這種方式的弱點十分明顯:一旦密碼被偷或用戶遺失密碼,情況就會十分麻煩,需要管理員對用戶密碼進行重新修改,而修改密碼之前還要人工驗證用戶的合法身份。
常用的鑒權方法:
在前后端分離的項目中,jwt方式比較多;
- 客戶端使用用戶名和密碼登錄
- 服務端收到請求,校驗用戶名和密碼
- 返回客戶端一個token
- 客戶端收到token 進行存儲
- 客戶端請求服務端接口 攜帶token信息
- 服務端收到請求 驗證token內容
服務端生成token 此時利用node的三方插件 ,前端收到token時候進行存儲使用 ,發送的時候進行在請求攔截時候,使用
服務端接收和校驗,前端根據返回的響應狀態碼進行操作
服務端校驗標准
- 請求信息中不存在token,則直接進行返回登錄狀態
- 存在token,解析token,解析生成uid ,校驗uid是否存在
- 不存在可能是因為token過期 直接返回登錄狀態
除了axios的鑒權,vue-router也可以進行鑒權操作,不過還是依賴服務端返回的用戶角色等信息~
寫到結尾:
不知不覺,又重新回顧了自己學習過的內容,其實有的時候感覺自己寫不出來,但是了解了思想和寫法,在后續使用中想到這種實現模式;