一比一還原axios源碼(一)—— 發起第一個請求
上一篇文章,我們簡單介紹了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程式碼。完整的程式碼大家也可以在//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); }
該章節完整的程式碼在這裡//github.com/zakingWong/zaking-axios/tree/c1。哦對,額外要說的,該章節實現了幾個工具方法,大家可以去lib/utils下查看,寫了注釋,這裡就不多說了。不是主線任務,嘻嘻。
我們也可以直接把項目clone到本地,npm run dev後,在對應的本地地址中查看效果,哦還有,run之前別忘了npm install一下。
參考:

