手寫Axios核心原理
Axios是一個基于promise的HTTP庫,它能夠自動判斷當前環境,自由切換在瀏覽器和 node.js環境中。如果是瀏覽器,就會基于XMLHttpRequests實現;如果是node環境,就會基于node內置核心http模塊實現。同時,它還用promise處理了響應結果,避免陷入回調地獄中去。
不僅如此,Axios還可以攔截請求和響應、轉化請求數據和響應數據、中斷請求、自動轉換JSON數據、客戶端支持防御XSRF等。如此眾多好用的功能,快來一起看看它是如何實現的吧!
1.基本使用
axios基本使用方式主要有:
- axios(config)
- axios.method(url,data,config)
- // 發送 POST 請求
- axios({
- method: 'post',
- url: '/user/12345',
- data: {
- username: 'Web前端嚴選',
- age: 2
- }
- });
- // GET請求ID參數
- axios.get('/user?ID=12345')
- .then(function (response) {
- console.log(response);
- })
- .catch(function (error) {
- console.log(error);
- });
2.實現axios
從axios(config)的使用上可以看出導出的axios是一個方法,從axios.get()的使用上可以看出導出的axios原型上會有get,post,put,delete等方法。
由分析可知,axios實際上是Axios類中的一個方法。我們可以先寫一個request方法來實現主要的請求功能,這樣就能使用axios(config)形式來調用了。
- class Axios{
- constructor(){
- }
- request(config){
- return new Promise((resolve) => {
- const {url='',data={},method='get'} = config; //解構傳參
- const xhr = new XMLHttpRequest; //創建請求對象
- xhr.open(method,url,true);
- xhr.onreadystatechange = () => {
- if(xhr.readyState == 4 && xhr.status == 200){
- resolve(xhr.responseText);
- //異步請求返回后將Promise轉為成功態并將結果導出
- }
- }
- xhr.send(JSON.stringfy(data));
- })
- }
- }
- function CreateAxiosFn(){
- let axios = new Axios;
- let req = axios.request.bind(axios);
- return req;
- }
- let axios = CreateAxiosFn();
然后搭建一個簡易服務端代碼,以測試請求的效果:
- const express = require('express')
- let 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('/getInfo', function(request, response){
- let data = {
- 'username':'前端嚴選',
- 'age':'2'
- };
- response.json(data);
- });
- app.listen(3000, function(){
- console.log("服務器啟動");
- });
啟動服務后,在頁面中測試請求是否成功:
- <button onclick="getMsg()">點擊</button>
- <script src="./axios.js"></script>
- <script>
- function getMsg(){
- axios({
- method: 'get',
- url: 'http://localhost:3000/getInfo'
- }).then(res => {
- console.log(res);
- })
- }
- </script>
點擊按鈕后,可以看到請求成功并獲取到數據。
3.原型上的方法
接下來實現以axios.method()形式的方法。
通過axios.get(),axios.post(),axios.put()等方法可以看出它們都是Axios.prototype上的方法,這些方法調用內部的request方法即可:
- const methodsArr = ['get','post','put','delete','head','options','patch','head'];
- methodsArr.forEach(method => {
- Axios.prototype[method] = function(){
- return this.request({
- method: method,
- ...arguments[0]
- })
- }
- })
arguments的第一個參數包含url,data等信息,直接解構它的第一個元素即可
還需要實現一個工具方法,用來將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方法,使之擁有get,post等方法
- function CreateAxiosFn(){
- let axios = new Axios;
- let req = axios.request.bind(axios);
- //新增如下代碼
- utils.extend(req, Axios.prototype, axios)
- return req;
- }
再來測試一下post的請求:
- axios.post({
- url: 'http://localhost:3000/postTest',
- data: {
- a: 1,
- b: 2
- }
- }).then(res => {
- console.log(res);
- })
可以看到正確返回結果了。
4.攔截器
先來看看攔截器的使用:
- // 請求攔截
- 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);
- });
攔截器,顧名思義就是在請求之前和響應之前,對真正要執行的操作數據攔截住進行一些處理。
那么如何實現呢,首先攔截器也是一個類,用于管理響應和請求。
- class InterceptorsManage{
- constructor(){
- this.handlers = [];
- }
- use(onFulField,onRejected){
- //將成功的回調和失敗的回調都存放到隊列中
- this.handlers.push({
- onFulField,
- onRejected
- })
- }
- }
axios.interceptors.response.use和axios.interceptors.request.use來定義請求和響應的攔截方法。
這說明axios上有響應攔截器和請求攔截器,那么如何在axios上實現呢:
- class Axios{
- constructor(){
- this.interceptors = {
- request: new InterceptorsManage,
- response: new InterceptorsManage
- }
- }
- //....
- }
在Axios的構造函數中新增interceptors屬性,然后定義request和response屬性用于處理請求和響應。
執行use方法時,會把傳入的回調函數放到handlers數組中。
這時再回看使用方式,axios.interceptors.request.use方法是綁在axios實例上的,因此同樣需要把Axios上的屬性和方法轉移到request上,將interceptors對象掛載到request方法上。
- function CreateAxiosFn() {
- let axios = new Axios();
- let req = axios.request.bind(axios);
- utils.extend(req, Axios.prototype, axios)
- //新增如下代碼
- utils.extend(req, axios)
- return req;
- }
但是現在request不僅要執行請求的發送,還要執行攔截器中handler的回調函數,因此還需要把request方法進行一下改造:
- request(config){
- //攔截器和請求的隊列
- let chain = [this.sendAjax.bind(this),undefined];
- //請求的攔截
- this.interceptors.request.handlers.forEach(interceptor => {
- chain.unshift(interceptor.onFulField,interceptor.onRejected);
- })
- //響應的攔截
- this.interceptors.response.handlers.forEach(interceptor => {
- chain.push(interceptor.onFulField,interceptor.onRejected)
- })
- let promise = Promise.resolve(config);
- while(chain.length > 0){
- //從頭部開始依次執行請求的攔截、真正的請求、響應的攔截
- promise = promise.then(chain.shift(),chain.shift());
- }
- return promise;
- }
- sendAjax(config){
- return new Promise((resolve) => {
- const {url='',method='get',data={}} = config;
- const xhr = new XMLHttpRequest();
- xhr.open(method,url,true);
- xhr.onreadystatechange = () => {
- if(xhr.readyState == 4 && xhr.status == 200){
- resolve(xhr.responseText)
- }
- }
- xhr.send(JSON.stringify(data));
- })
- }
最后執行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執行為:
- promise.then(
- function (config) {
- return config;
- },
- function (error) {
- return Promise.reject(error);
- }
- )
請求時,執行為:
- promise.then(
- this.sendAjax.bind(this),
- undefined,
- )
響應后,執行為:
- promise.then(
- function (response) {
- return response;
- },
- function (error) {
- return Promise.reject(error);
- }
- )
這時我們測試一下攔截器的使用:
- function getMsg(){
- axios.interceptors.request.use((config) => {
- console.log('請求攔截:',config);
- return config;
- },err => {
- return Promise.reject(err)
- })
- axios.interceptors.response.use(response => {
- response = {
- message: '響應數據替換',
- data: response
- }
- return response
- },err => {
- console.log(err,'響應錯誤')
- return Promise.reject(err)
- })
- axios.get({
- url: 'http://localhost:3000/getTest',
- }).then(res => {
- console.log(res);
- })
- }
可以在控制臺中看到攔截處理的打印輸出,證明攔截成功!
5.總結
Axios天然支持Promise的性能讓其方便對異步進行處理,同時又利用了Promise對請求進行了攔截,使得用戶可以在請求過程中添加更多的功能,對請求的中斷能自如操作。它的思想既清新樸實又不落入俗套,具有很好的借鑒意義。
看完這篇文章,你了解了Axios的核心原理了嗎?
本文轉載自微信公眾號「Web前端嚴選」,可以通過以下二維碼關注。轉載本文請聯系Web前端嚴選公眾號。