手寫Axios核心原理,再也不怕面試官問我Axios原理
手寫axios核心原理
- 手寫axios核心原理,再也不怕面試官問我axios原理
- 手寫axios核心原理
一、axios簡介
- axios是什么?
- axios有什么特性?(不得不說面試被問到幾次)
二、基本使用方式
三、實現axios和axios.method
四、請求和響應攔截器
- 首先
- 接下來,再執行
- 接下來,再執行
一、axios簡介
axios是什么?
Axios 是一個基于 promise 的 HTTP 庫,可以用在瀏覽器和 node.js 中。
axios有什么特性?(不得不說面試被問到幾次)
- 從瀏覽器中創建 XMLHttpRequests從 node.js
- 創建 http 請求
- 支持 Promise API
- 攔截請求和響應轉換請求數據和響應數據
- 取消請求
- 自動轉換JSON 數據
- 客戶端支持防御 XSRF
實際上,axios可以用在瀏覽器和 node.js 中是因為,它會自動判斷當前環境是什么,如果是瀏覽器,就會基于XMLHttpRequests實現axios。如果是node.js環境,就會基于node內置核心模塊http實現axios簡單來說,axios的基本原理就是
- axios還是屬于 XMLHttpRequest, 因此需要實現一個ajax。或者基于http 。
- 還需要一個promise對象來對結果進行處理。
有什么不理解的或者是建議歡迎評論提出.項目已經放到github.可以的話給個star吧!謝謝 github:https://github.com/Sunny-lucking/howToBuildMyAxios
二、基本使用方式
axios基本使用方式主要有
- axios(config)
- axios.method(url, data , config)
- // index.html文件
- <html>
- <script type="text/javascript" src="./myaxios.js"></script>
- <body>
- <button class="btn">點我發送請求</button>
- <script>
- document.querySelector('.btn').onclick = function() {
- // 分別使用以下方法調用,查看myaxios的效果
- axios.post('/postAxios', {
- name: '小美post'
- }).then(res => {
- console.log('postAxios 成功響應', res);
- })
- axios({
- method: 'post',
- url: '/getAxios'
- }).then(res => {
- console.log('getAxios 成功響應', res);
- })
- }
- </script>
- </body>
- </html>
- </html>
三、實現axios和axios.method
從axios(config)的使用上可以看出導出的axios是一個方法。從axios.method(url, data , config)的使用可以看出導出的axios上或者原型上掛有get,post等方法。
實際上導出的axios就是一個Axios類中的一個方法。
如代碼所以,核心代碼是request。我們把request導出,就可以使用axios(config)這種形式來調用axios了。
- class Axios {
- constructor() {
- }
- request(config) {
- return new Promise(resolve => {
- const {url = '', method = 'get', data = {}} = config;
- // 發送ajax請求
- const xhr = new XMLHttpRequest();
- xhr.open(method, url, true);
- xhr.onload = function() {
- console.log(xhr.responseText)
- resolve(xhr.responseText);
- }
- xhr.send(data);
- })
- }
- }
怎么導出呢?十分簡單,new Axios,獲得axios實例,再獲得實例上的request方法就好了。
- // 最終導出axios的方法,即實例的request方法
- function CreateAxiosFn() {
- let axios = new Axios();
- let req = axios.request.bind(axios);
- return req;
- }
- // 得到最后的全局變量axios
- let axios = CreateAxiosFn();
點擊查看此時的myAxios.js
現在axios實際上就是request方法。
你可能會很疑惑,因為我當初看源碼的時候也很疑惑:干嘛不直接寫個request方法,然后導出呢?非得這樣繞這么大的彎子。別急。后面慢慢就會講到。
現在一個簡單的axios就完成了,我們來引入myAxios.js文件并測試一下可以使用不?
簡單的搭建服務器:
- //server.js
- var express = require('express');
- var app = express();
- //設置允許跨域訪問該服務.
- app.all('*', function (req, res, next) {
- res.header('Access-Control-Allow-Origin', '*');
- res.header('Access-Control-Allow-Headers', 'Content-Type');
- res.header('Access-Control-Allow-Methods', '*');
- res.header('Content-Type', 'application/json;charset=utf-8');
- next();
- });
- app.get('/getTest', function(request, response){
- data = {
- 'FrontEnd':'前端',
- 'Sunny':'陽光'
- };
- response.json(data);
- });
- var server = app.listen(5000, function(){
- console.log("服務器啟動");
- });
- //index.html
- <script type="text/javascript" src="./myAxios.js"></script>
- <body>
- <button class="btn">點我發送請求</button>
- <script>
- document.querySelector('.btn').onclick = function() {
- // 分別使用以下方法調用,查看myaxios的效果
- axios({
- method: 'get',
- url: 'http://localhost:5000/getTest'
- }).then(res => {
- console.log('getAxios 成功響應', res);
- })
- }
- </script>
- </body>
點擊按鈕,看看是否能成功獲得數據。
發現確實成功。
可喜可賀
現在我們來實現下axios.method()的形式。
思路:我們可以再Axios.prototype添加這些方法。而這些方法內部調用request方法即可,如代碼所示:
- // 定義get,post...方法,掛在到Axios原型上
- const methodsArr = ['get', 'delete', 'head', 'options', 'put', 'patch', 'post'];
- methodsArr.forEach(met => {
- Axios.prototype[met] = function() {
- console.log('執行'+met+'方法');
- // 處理單個方法
- if (['get', 'delete', 'head', 'options'].includes(met)) { // 2個參數(url[, config])
- return this.request({
- method: met,
- url: arguments[0],
- ...arguments[1] || {}
- })
- } else { // 3個參數(url[,data[,config]])
- return this.request({
- method: met,
- url: arguments[0],
- data: arguments[1] || {},
- ...arguments[2] || {}
- })
- }
- }
- })
我們通過遍歷methodsArr數組,依次在Axios.prototype添加對應的方法,注意的是'get', 'delete', 'head', 'options'這些方法只接受兩個參數。而其他的可接受三個參數,想一下也知道,get不把參數放body的。
但是,你有沒有發現,我們只是在Axios的prototype上添加對應的方法,我們導出去的可是request方法啊,那怎么辦?簡單,把Axios.prototype上的方法搬運到request上即可。
我們先來實現一個工具方法,實現將b的方法混入a;
- const utils = {
- extend(a,b, context) {
- for(let key in b) {
- if (b.hasOwnProperty(key)) {
- if (typeof b[key] === 'function') {
- a[key] = b[key].bind(context);
- } else {
- a[key] = b[key]
- }
- }
- }
- }
- }
然后我們就可以利用這個方法將Axios.prototype上的方法搬運到request上啦。
我們修改一下之前的CreateAxiosFn方法即可
- function CreateAxiosFn() {
- let axios = new Axios();
- let req = axios.request.bind(axios);
- 增加代碼
- utils.extend(req, Axios.prototype, axios)
- return req;
- }
點擊查看此時的myAxios.js
現在來測試一下能不能使用axios.get()這種形式調用axios。
- <body>
- <button class="btn">點我發送請求</button>
- <script>
- document.querySelector('.btn').onclick = function() {
- axios.get('http://localhost:5000/getTest')
- .then(res => {
- console.log('getAxios 成功響應', res);
- })
- }
- </script>
- </body>
又是意料之中成功。再完成下一個功能之前,先給上目前myAxios.js的完整代碼
- class Axios {
- constructor() {
- }
- request(config) {
- return new Promise(resolve => {
- const {url = '', method = 'get', data = {}} = config;
- // 發送ajax請求
- console.log(config);
- const xhr = new XMLHttpRequest();
- xhr.open(method, url, true);
- xhr.onload = function() {
- console.log(xhr.responseText)
- resolve(xhr.responseText);
- }
- xhr.send(data);
- })
- }
- }
- // 定義get,post...方法,掛在到Axios原型上
- const methodsArr = ['get', 'delete', 'head', 'options', 'put', 'patch', 'post'];
- methodsArr.forEach(met => {
- Axios.prototype[met] = function() {
- console.log('執行'+met+'方法');
- // 處理單個方法
- if (['get', 'delete', 'head', 'options'].includes(met)) { // 2個參數(url[, config])
- return this.request({
- method: met,
- url: arguments[0],
- ...arguments[1] || {}
- })
- } else { // 3個參數(url[,data[,config]])
- return this.request({
- method: met,
- url: arguments[0],
- data: arguments[1] || {},
- ...arguments[2] || {}
- })
- }
- }
- })
- // 工具方法,實現b的方法或屬性混入a;
- // 方法也要混入進去
- const utils = {
- extend(a,b, context) {
- for(let key in b) {
- if (b.hasOwnProperty(key)) {
- if (typeof b[key] === 'function') {
- a[key] = b[key].bind(context);
- } else {
- a[key] = b[key]
- }
- }
- }
- }
- }
- // 最終導出axios的方法-》即實例的request方法
- function CreateAxiosFn() {
- let axios = new Axios();
- let req = axios.request.bind(axios);
- // 混入方法, 處理axios的request方法,使之擁有get,post...方法
- utils.extend(req, Axios.prototype, axios)
- return req;
- }
- // 得到最后的全局變量axios
- let axios = CreateAxiosFn();
四、請求和響應攔截器
我們先看下攔截器的使用
- // 添加請求攔截器
- axios.interceptors.request.use(function (config) {
- // 在發送請求之前做些什么
- return config;
- }, function (error) {
- // 對請求錯誤做些什么
- return Promise.reject(error);
- });
- // 添加響應攔截器
- axios.interceptors.response.use(function (response) {
- // 對響應數據做點什么
- return response;
- }, function (error) {
- // 對響應錯誤做點什么
- return Promise.reject(error);
- });
攔截器是什么意思呢?其實就是在我們發送一個請求的時候會先執行請求攔截器的代碼,然后再真正地執行我們發送的請求,這個過程會對config,也就是我們發送請求時傳送的參數進行一些操作。
而當接收響應的時候,會先執行響應攔截器的代碼,然后再把響應的數據返回來,這個過程會對response,也就是響應的數據進行一系列操作。
怎么實現呢?需要明確的是攔截器也是一個類,管理響應和請求。因此我們先實現攔截器
- class InterceptorsManage {
- constructor() {
- this.handlers = [];
- }
- use(fullfield, rejected) {
- this.handlers.push({
- fullfield,
- rejected
- })
- }
- }
我們是用這個語句axios.interceptors.response.use和axios.interceptors.request.use,來觸發攔截器執行use方法的。
說明axios上有一個響應攔截器和一個請求攔截器。那怎么實現Axios呢?看代碼
- class Axios {
- constructor() {
- 新增代碼
- this.interceptors = {
- request: new InterceptorsManage,
- response: new InterceptorsManage
- }
- }
- request(config) {
- return new Promise(resolve => {
- const {url = '', method = 'get', data = {}} = config;
- // 發送ajax請求
- console.log(config);
- const xhr = new XMLHttpRequest();
- xhr.open(method, url, true);
- xhr.onload = function() {
- console.log(xhr.responseText)
- resolve(xhr.responseText);
- };
- xhr.send(data);
- })
- }
- }
可見,axios實例上有一個對象interceptors。這個對象有兩個攔截器,一個用來處理請求,一個用來處理響應。
所以,我們執行語句axios.interceptors.response.use和axios.interceptors.request.use的時候,實現獲取axios實例上的interceptors對象,然后再獲取response或request攔截器,再執行對應的攔截器的use方法。
而執行use方法,會把我們傳入的回調函數push到攔截器的handlers數組里。
到這里你有沒有發現一個問題。這個interceptors對象是Axios上的啊,我們導出的是request方法啊(欸?好熟悉的問題,上面提到過哈哈哈~~~額)。處理方法跟上面處理的方式一樣,都是把Axios上的方法和屬性搬到request過去,也就是遍歷Axios實例上的方法,得以將interceptors對象掛載到request上。
所以只要更改下CreateAxiosFn方法即可。
- function CreateAxiosFn() {
- let axios = new Axios();
- let req = axios.request.bind(axios);
- // 混入方法, 處理axios的request方法,使之擁有get,post...方法
- utils.extend(req, Axios.prototype, axios)
- 新增代碼
- utils.extend(req, axios)
- return req;
- }
好了,現在request也有了interceptors對象,那么什么時候拿interceptors對象中的handler之前保存的回調函數出來執行。
沒錯,就是我們發送請求的時候,會先獲取request攔截器的handlers的方法來執行。再執行我們發送的請求,然后獲取response攔截器的handlers的方法來執行。
因此,我們要修改之前所寫的request方法 之前是這樣的。
- request(config) {
- return new Promise(resolve => {
- const {url = '', method = 'get', data = {}} = config;
- // 發送ajax請求
- console.log(config);
- const xhr = new XMLHttpRequest();
- xhr.open(method, url, true);
- xhr.onload = function() {
- console.log(xhr.responseText)
- resolve(xhr.responseText);
- };
- xhr.send(data);
- })
- }
但是現在request里不僅要執行發送ajax請求,還要執行攔截器handlers中的回調函數。所以,最好下就是將執行ajax的請求封裝成一個方法
- request(config) {
- this.sendAjax(config)
- }
- sendAjax(config){
- return new Promise(resolve => {
- const {url = '', method = 'get', data = {}} = config;
- // 發送ajax請求
- console.log(config);
- const xhr = new XMLHttpRequest();
- xhr.open(method, url, true);
- xhr.onload = function() {
- console.log(xhr.responseText)
- resolve(xhr.responseText);
- };
- xhr.send(data);
- })
- }
好了,現在我們要獲得handlers中的回調
- request(config) {
- // 攔截器和請求組裝隊列
- let chain = [this.sendAjax.bind(this), undefined] // 成對出現的,失敗回調暫時不處理
- // 請求攔截
- this.interceptors.request.handlers.forEach(interceptor => {
- chain.unshift(interceptor.fullfield, interceptor.rejected)
- })
- // 響應攔截
- this.interceptors.response.handlers.forEach(interceptor => {
- chain.push(interceptor.fullfield, interceptor.rejected)
- })
- // 執行隊列,每次執行一對,并給promise賦最新的值
- let promise = Promise.resolve(config);
- while(chain.length > 0) {
- promise = promise.then(chain.shift(), chain.shift())
- }
- return promise;
- }
我們先把sendAjax請求和undefined放進了chain數組里,再把請求攔截器的handlers的成對回調放到chain數組頭部。再把響應攔截器的handlers的承兌回調反倒chain數組的尾部。
然后再 逐漸取數 chain數組的成對回調執行。
- promise = promise.then(chain.shift(), chain.shift())
這一句,實際上就是不斷將config從上一個promise傳遞到下一個promise,期間可能回調config做出一些修改。什么意思?我們結合一個例子來講解一下
首先攔截器是這樣使用的
- // 添加請求攔截器
- axios.interceptors.request.use(function (config) {
- // 在發送請求之前做些什么
- return config;
- }, function (error) {
- // 對請求錯誤做些什么
- return Promise.reject(error);
- });
- // 添加響應攔截器
- axios.interceptors.response.use(function (response) {
- // 對響應數據做點什么
- return response;
- }, function (error) {
- // 對響應錯誤做點什么
- return Promise.reject(error);
- });
然后執行request的時候。chain數組的數據是這樣的
- chain = [
- function (config) {
- // 在發送請求之前做些什么
- return config;
- },
- function (error) {
- // 對請求錯誤做些什么
- return Promise.reject(error);
- }
- this.sendAjax.bind(this),
- undefined,
- function (response) {
- // 對響應數據做點什么
- return response;
- },
- function (error) {
- // 對響應錯誤做點什么
- return Promise.reject(error);
- }
- ]
首先
執行第一次promise.then(chain.shift(), chain.shift()),即
- promise.then(
- function (config) {
- // 在發送請求之前做些什么
- return config;
- },
- function (error) {
- // 對請求錯誤做些什么
- return Promise.reject(error);
- }
- )
一般情況,promise是resolved狀態,是執行成功回調的,也就是執行
- function (config) {
- // 在發送請求之前做些什么
- return config;
- },
promise.then是要返回一個新的promise對象的。為了區分,在這里,我會把這個新的promise對象叫做第一個新的promise對象 這個第一個新的promise對象會把
- function (config) {
- // 在發送請求之前做些什么
- return config;
- },
的執行結果傳入resolve函數中
- resolve(config)
使得這個返回的第一個新的promise對象的狀態為resovled,而且第一個新的promise對象的data為config。
這里需要對Promise的原理足夠理解。所以我前一篇文章寫的是手寫Promise核心原理,再也不怕面試官問我Promise原理,你可以去看看
接下來,再執行
- promise.then(
- sendAjax(config)
- ,
- undefined
- )
注意:這里的promise是 上面提到的第一個新的promise對象。
而promise.then這個的執行又會返回第二個新的promise對象。
因為這里promise.then中的promise也就是第一個新的promise對象的狀態是resolved的,所以會執行sendAjax()。而且會取出第一個新的promise對象的data 作為config轉入sendAjax()。
當sendAjax執行完,就會返回一個response。這個response就會保存在第二個新的promise對象的data中。
接下來,再執行
- promise.then(
- function (response) {
- // 對響應數據做點什么
- return response;
- },
- function (error) {
- // 對響應錯誤做點什么
- return Promise.reject(error);
- }
- )
同樣,會把第二個新的promise對象的data取出來作為response參數傳入
- function (response) {
- // 對響應數據做點什么
- return response;
- },
飯后返回一個promise對象,這個promise對象的data保存了這個函數的執行結果,也就是返回值response。
然后通過return promise;
把這個promise返回了。咦?是怎么取出promise的data的。我們看看我們平常事怎么獲得響應數據的
- axios.get('http://localhost:5000/getTest')
- .then(res => {
- console.log('getAxios 成功響應', res);
- })
在then里接收響應數據。所以原理跟上面一樣,將返回的promise的data作為res參數了。
現在看看我們的myAxios完整代碼吧,好有個全面的了解
- class InterceptorsManage {
- constructor() {
- this.handlers = [];
- }
- use(fullfield, rejected) {
- this.handlers.push({
- fullfield,
- rejected
- })
- }
- }
- class Axios {
- constructor() {
- this.interceptors = {
- request: new InterceptorsManage,
- response: new InterceptorsManage
- }
- }
- request(config) {
- // 攔截器和請求組裝隊列
- let chain = [this.sendAjax.bind(this), undefined] // 成對出現的,失敗回調暫時不處理
- // 請求攔截
- this.interceptors.request.handlers.forEach(interceptor => {
- chain.unshift(interceptor.fullfield, interceptor.rejected)
- })
- // 響應攔截
- this.interceptors.response.handlers.forEach(interceptor => {
- chain.push(interceptor.fullfield, interceptor.rejected)
- })
- // 執行隊列,每次執行一對,并給promise賦最新的值
- let promise = Promise.resolve(config);
- while(chain.length > 0) {
- promise = promise.then(chain.shift(), chain.shift())
- }
- return promise;
- }
- sendAjax(){
- return new Promise(resolve => {
- const {url = '', method = 'get', data = {}} = config;
- // 發送ajax請求
- console.log(config);
- const xhr = new XMLHttpRequest();
- xhr.open(method, url, true);
- xhr.onload = function() {
- console.log(xhr.responseText)
- resolve(xhr.responseText);
- };
- xhr.send(data);
- })
- }
- }
- // 定義get,post...方法,掛在到Axios原型上
- const methodsArr = ['get', 'delete', 'head', 'options', 'put', 'patch', 'post'];
- methodsArr.forEach(met => {
- Axios.prototype[met] = function() {
- console.log('執行'+met+'方法');
- // 處理單個方法
- if (['get', 'delete', 'head', 'options'].includes(met)) { // 2個參數(url[, config])
- return this.request({
- method: met,
- url: arguments[0],
- ...arguments[1] || {}
- })
- } else { // 3個參數(url[,data[,config]])
- return this.request({
- method: met,
- url: arguments[0],
- data: arguments[1] || {},
- ...arguments[2] || {}
- })
- }
- }
- })
- // 工具方法,實現b的方法混入a;
- // 方法也要混入進去
- const utils = {
- extend(a,b, context) {
- for(let key in b) {
- if (b.hasOwnProperty(key)) {
- if (typeof b[key] === 'function') {
- a[key] = b[key].bind(context);
- } else {
- a[key] = b[key]
- }
- }
- }
- }
- }
- // 最終導出axios的方法-》即實例的request方法
- function CreateAxiosFn() {
- let axios = new Axios();
- let req = axios.request.bind(axios);
- // 混入方法, 處理axios的request方法,使之擁有get,post...方法
- utils.extend(req, Axios.prototype, axios)
- return req;
- }
- // 得到最后的全局變量axios
- let axios = CreateAxiosFn();
來測試下攔截器功能是否正常
- <script type="text/javascript" src="./myAxios.js"></script>
- <body>
- <button class="btn">點我發送請求</button>
- <script>
- // 添加請求攔截器
- axios.interceptors.request.use(function (config) {
- // 在發送請求之前做些什么
- config.method = "get";
- console.log("被我請求攔截器攔截了,哈哈:",config);
- return config;
- }, function (error) {
- // 對請求錯誤做些什么
- return Promise.reject(error);
- });
- // 添加響應攔截器
- axios.interceptors.response.use(function (response) {
- // 對響應數據做點什么
- console.log("被我響應攔截攔截了,哈哈 ");
- response = {message:"響應數據被我替換了,啊哈哈哈"}
- return response;
- }, function (error) {
- // 對響應錯誤做點什么
- console.log("錯了嗎");
- return Promise.reject(error);
- });
- document.querySelector('.btn').onclick = function() {
- // 分別使用以下方法調用,查看myaxios的效果
- axios({
- url: 'http://localhost:5000/getTest'
- }).then(res => {
- console.log('response', res);
- })
- }
- </script>
- </body>
攔截成功!!!!!