一比一還原axios原始碼(一)—— 發起第一個請求

Zaking發表於2022-03-15

  上一篇文章,我們簡單介紹了XMLHttpRequest及其他可以發起AJAX請求的API,那部分大家有興趣可以自己去擴充套件學習。另外,簡單介紹了怎麼去讀以及我會怎麼寫這個系列的文章,那麼下面就開始真正的axios原始碼實現,跟緊我的步伐,你會發現其實閱讀原始碼並不是一件很複雜的事情。另外,我在上一篇概要中附上的連結,大家一定要去看,至少要了解一下XMLHttpRequest的相關屬性和方法都有哪些,因為接下來的核心內容,其實都是基於此的。

  那麼先來看看我們今天要來實現的內容有哪些,首先第一部分,我會建立一個本地的server服務,實現這部分的程式碼,以供我們在實現axios的過程中可以用來驗證程式碼以做測試,另外,會實現簡單的axios的get請求。

  下面我們就先來看看server的程式碼是什麼樣的。 

一、編寫server程式碼

  首先,我們在examples資料夾下建立webpack.config.js和server.js檔案,是server部分的核心程式碼,其中webpack比較簡單,程式碼如下:

const fs = require("fs");
const path = require("path");
const webpack = require("webpack");

module.exports = {
  mode: "development",
  entry: fs.readdirSync(__dirname).reduce((entries, dir) => {
    const fullDir = path.join(__dirname, dir);
    const entry = path.join(fullDir, "app.js");
    if (fs.statSync(fullDir).isDirectory() && fs.existsSync(entry)) {
      entries[dir] = ["webpack-hot-middleware/client", entry];
    }
    return entries;
  }, {}),
  output: {
    path: path.join(__dirname, "__build__"),
    filename: "[name].js",
    publicPath: "/__build__/",
  },
  module: {},
  resolve: {
    extensions: [".js"],
  },
  plugins: [
    new webpack.HotModuleReplacementPlugin(),
    new webpack.NoEmitOnErrorsPlugin(),
  ],
};

  看上面的程式碼,核心就是讀取examples目錄下的所有檔案,然後生成多頁應用。其中if判斷的是如果是資料夾並且該資料夾存在,那麼則會加入熱更新的依賴。這些都比較容易理解。其次,是server.js。

  通過express生成一個伺服器,並讀取webpack.config.js的配置檔案, express通過webpack-dev-middleware外掛來讀取webpack的配置檔案,最後通過

app.use(express.static(__dirname));

  這行程式碼,讀取根目錄下的index.html作為訪問伺服器的跟路由頁面。再然後通過下面的程式碼註冊每一個example的路由,這裡的路由,是後端路由,代表著可訪問的介面地址:

function registerC1Router() {
  router.get("/c1/get", function (req, res) {
    res.json({
      msg: `hello world`,
    });
  });
}

  在後面的開發過程中,會根據章節增加examples的demo程式碼。完整的程式碼大家也可以在https://github.com/zakingWong/zaking-axios/tree/c1這裡檢視。

 

二、發起ajax請求

  接下來,我們要看如何實現axios中的一個api,我們先看下axios的官方文件:

  這是axios從伺服器獲取一個圖片的方法,發起了get請求,需要一個url,那麼我們今天就來實現紅框中的部分,通過程式碼的實現,來發起一個真正的請求。 

  接下來,我們在lib資料夾裡建立一個adapters資料夾,在adapters資料夾下建立一個xhr檔案,這是我們真正的XMLHttpRequest的程式碼,xhr檔案的程式碼這樣寫:

export default function xhrAdapter(config) {
    var request = new XMLHttpRequest();

    request.open(config.method.toUpperCase(), config.url, true);

    request.send(config.data);
}

  很簡單,實際上就是開啟一個XMLHttpRequest請求。然後在lib下的axios檔案中引入並呼叫即可。這樣,我們就完成了axios原始碼的實現,好了,本系列到此結束。哈哈哈,開個玩笑。

1、完善url引數

  OK,經過上面的程式碼,我們已經可以發起get請求了,但是還有個問題沒有解決,就是params引數的傳遞,axios可以傳遞params後拼在url的請求後面。那麼,我們也來實現一下。axios通過一個buildURL方法來輔助處理url後攜帶的引數,那麼我們也照著抄唄。

  首先,我們在lib下建立一個helpers資料夾,這個資料夾是用來放一堆一堆的輔助處理的方法的,在這個資料夾下,我們建立一個名字叫buildURL的檔案。然後,我們一口氣把目前所需要的檔案建立完吧,接下來就可以專注的寫程式碼了,我們再在lib資料夾下建立一個utils檔案,也就是用來存放一些工具方法,我們稍後會用到的時候再來抄,哦不,再來寫。

  下面……激動人心的時刻到了,但是我們還不能開始寫程式碼,我們先來看看。我們需要處理params的場景有哪些:

最常見的普通使用方法:

axios({
  method: "get",
  url: "/c1/get",
  params: {
    a: 1,
    b: 2,
  },
});

  上面這種場景是最常見的,我們希望可以把params物件拼在url後面,變成這樣:"/c1/get?a=1&b=2"即可。

引數值為陣列:

axios({
  method: "get",
  url: "/c1/get",
  params: {
    a: [1, 2, 3, 4],
  },
});

  上面的程式碼,params引數物件的值a是一個陣列,我們希望它的url可以變成這樣:"/c1/get?a[]=1&a[]=2&a[]=3&a[]=4",就好了。

引數值為物件:

// params的引數的值為物件的情況
axios({
  method: "get",
  url: "/c1/get",
  params: {
    a: {
      b: 1,
    },
  },
});

   我們希望是這樣的url:"/c1/get?a=%7B%22b%22:%221%22%7D"。這裡的%7B就是“{”,%22就是“"”,%7D就是“}”,也就是字元encode後的結果。

引數值為Date型別:

// params的引數的值為Date型別
const date = new Date();

axios({
  method: "get",
  url: "/c1/get",
  params: {
    date,
  },
});

  它在url上就會變成這樣:"/c1/get?date=2019-04-01T05:55:39.030Z"date 後面拼接的是 date.toISOString() 的結果。

特殊字元的支援:

// 支援特殊字元
axios({
  method: "get",
  url: "/c1/get",
  params: {
    a: "@:$, ",
  },
});

  我們希望可以支援@:$,[],這些字元@符號,:冒號,,逗號,空格,中括號[],$美元符,希望這些特殊符號不會被encode,要注意,這裡的空格會被轉換成+號。url就是這樣的:"/c1/get?foo=@:$+"。

忽略空值

// 忽略空值
axios({
  method: "get",
  url: "/c1/get",
  params: {
    a: 1,
    b: null,
    c: undefined,
  },
});

  所以上面的請求程式碼,url就是這樣:"/c1/get?a=1"。

丟棄URL中的hash標記

// 丟棄URL中的hash標記
axios({
  method: "get",
  url: "/c1/get#fuckhash",
  params: {
    a: 1,
    b: null,
    c: undefined,
  },
});

  誒?這個跟上面的url表現是一樣的:"/c1/get?a=1"。

保留URL中已存在的引數

// 保留URL中已存在的引數
axios({
  method: "get",
  url: "/c1/get#fuckhash?m=12&md=23",
  params: {
    a: 1,
    b: null,
    c: undefined,
  },
});

  這個呢就是這樣的:"/c1/get?m=12&md=23&a=1"。

  OK,終於,我們分析完了所有的情況,下面就要開始抄寫buildURL程式碼了。

export default function buildURL(url, params, paramsSerializer) {
  // 如果沒有params的引數的話,直接返回url即可
  if (!params) return url;
  // 首先啊,由於我們引入了可以自定義轉換的邏輯,所以這裡我們先判斷一下
  let serializedParams; // 這個變數就是轉換後的url引數
  if (paramsSerializer) {
    serializedParams = paramsSerializer(params);
  } else if (utils.isURLSearchParams(params)) {
    serializedParams = params.toString();
  } else {
    // 如果即沒有自定義的轉換方法,又不是一個URLSearchParams物件,那麼就走預設的轉換邏輯
    // 先宣告一個儲存變數
    let parts = [];
    // 這裡用了一個自定義的迴圈方法
    utils.forEach(params, function serialize(val, key) {
      // 這個跟我們說好的場景一致,如果沒有值,就不管它了。
      if (val === null || typeof val === "undefined") {
        return;
      }

      // 判斷val是否是個陣列,如果是陣列的話,那麼key要變化一下,這個我們們之前的需求也說過,
      // 如果不是的話,把它變成陣列,方便後面統一迴圈處理
      if (utils.isArray(val)) {
        key = key + "[]";
      } else {
        val = [val];
      }

      utils.forEach(val, function parseValue(v) {
        // 這個也說了,日期的話要處理下
        if (utils.isDate(v)) {
          v = v.toISOString();
          // 如果是個物件的話,那直接stringify就好
        } else if (utils.isObject(v)) {
          v = JSON.stringify(v);
        }
        // 陣列裡的樣子就是這樣的["a=1","v=2"]醬紫。
        parts.push(encode(key) + "=" + encode(v));
      });
    });
    // parts裡面的引數都放完了,我們隔一下
    serializedParams = parts.join("&");
    // 此時的serializedParams就是這樣的"a=1&v=2"了
  }

  // 上面,我們根據不同的條件(自定義轉換,URLSearchParams,預設)處理好了searchParams
  // 下面,我們要處理hash
  // 這個邏輯很簡單,就是有hash的時候,只留下hash前的地址
  if (serializedParams) {
    console.log(url, "url");

    var hashmarkIndex = url.indexOf("#");
    // 要注意的是,如果hash存在,並且還存在引數,那麼hash後面的引數也會被視為hash的一部分
    if (hashmarkIndex !== -1) {
      url = url.slice(0, hashmarkIndex);
    }
    console.log(url, "url");
    // 判斷下url有沒有引數,根據不同條件分割searchParams
    url += (url.indexOf("?") === -1 ? "?" : "&") + serializedParams;
  }

  return url;
}

  首先啊,上面的程式碼都詳細的寫了註釋。包括大家也可以去gitHub上看原始碼,好吧,跟axios一模一樣,沒有幾乎,唉。。畢竟是抄的嘛。。。我在簡單說下邏輯,首先,根據傳入的引數判斷要對params如何處理。如果既不存在自定義的轉換方法又不是URLSearchParams物件,那麼就會進入到我們自己的邏輯裡。

  自己的邏輯裡,用到了一個自定義的工具forEach方法,這個方法不多說,大家自己去原始碼的註釋裡看,迴圈的時候會判斷下,這個key要是沒有可使用的值就拋棄掉。那如果是陣列,就轉換一下key,如果不是,就把值變成一個陣列,因為後面,我們要迴圈這個key的值,這塊很重要,我們不僅要迴圈整個params物件,因為可能存在params中的值也是資料的情況,所以,還要迴圈遍歷在params中的某個key的值是陣列的情況。這樣,如果值是陣列的話,就會拼湊一個一個的key,剛好,之前我們把不是陣列的也變成陣列裡,就可以單純對陣列進行迴圈處理。

  剩下的就比較簡單,對Date和Object做一下特殊的處理,並且剔除hash。這裡針對hash尤其要說一下,如果hash和searchParams同時存在,那麼會連帶一起拋棄掉的。比如www.baidu.com#query?a=1&b=2,那麼處理後就只剩下www.baidu.com了。其他的就沒了。

  我們在xhrAdapter方法中加上buildURL,然後,就可以檢視結果了。

import buildURL from "../helpers/buildURL";
export default function xhrAdapter(config) {
  var request = new XMLHttpRequest();

  request.open(
    config.method.toUpperCase(),
    buildURL(config.url, config.params, config.paramsSerializer),
    true
  );

  request.send(config.data);
}

  該章節完整的程式碼在這裡https://github.com/zakingWong/zaking-axios/tree/c1。哦對,額外要說的,該章節實現了幾個工具方法,大家可以去lib/utils下檢視,寫了註釋,這裡就不多說了。不是主線任務,嘻嘻。

  我們也可以直接把專案clone到本地,npm run dev後,在對應的本地地址中檢視效果,哦還有,run之前別忘了npm install一下。 

 

參考:

  1. https://developer.mozilla.org/zh-CN/docs/Web/API/URLSearchParams/URLSearchParams
  2. https://zhuanlan.zhihu.com/p/29581070

相關文章