上一篇文章,我們簡單介紹了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一下。
參考: