TypeScript 重構 Axios 經驗分享

2020-12-13 百家號

拒絕做一個只會用 API 的文檔工程師,本文將會讓你從重複造輪子的過程中掌握 web 開發相關的基本知識,特別是 XMLHttpRequest。

又是一篇關於 TypeScript 的分享,年底了,請允許我沉澱一下。上次用 TypeScript 重構 Vconsole 的項目 埋下了對 Axios 源碼解析的梗。於是,這次分享的主題就是 如何從零用 TypeScript 重構 Axios 以及為什麼我要這麼做

筆者在用 TypeScript 重複造輪子的時候目的還是很明確的,不僅是為了用 TypeScript 養成一種好的開發習慣,更重要的是了解工具庫關聯的基礎知識。只有更多地注重基礎知識,才能早日擺脫文檔工程師的困擾。(Ps: 用 TypeScript,也是為了擺脫前端查文檔的宿命!)

本次分享包括以下內容:

工程簡介 & 開發技巧API 實現XHR,XHR,XHRHTTP,HTTP,HTTP單元測試項目源碼,分享可能會錯過某些細節實現,需要的可以看源碼,測試用例基本跑通了。想想,5w star 的庫,就這樣自己實現了一遍。

工程簡介

Axios 是什麼?

Promise based HTTP client for the browser and node.js

axios 是基於 Promise 用於瀏覽器和 nodejs 的 HTTP 客戶端,它本身具有以下特性 ( √ 表示本項目具備該特性 ):

√ 從瀏覽器創建 XMLHttpRequest => XHR 實現√ 支持 Promise API => XHR 實現√ 攔截請求和響應 => 請求攔截√ 轉換請求和響應數據 => 對應項目目錄/src/core/dispatchRequest.ts√ 取消請求 取消請求√ 自動轉換 JSON 數據 => 對應項目目錄/src/core/dispatchRequest.ts√ 客戶端支持防止 CSRF/XSRF => CSRF× 從 node.js 發出 http 請求這裡主要講解瀏覽器端的 XHR 實現,限於篇幅不會涉及 node 下的 http 。如果你願意一層一層了解它,你會發現實現 axios 還是很簡單的,來一起探索吧!

目錄說明

首先來看下目錄。

目錄與 Axios 基本保持一致,core 是

Axios

類的核心代碼。adapters 是 XHR 核心實現,Cancel 是與 取消請求相關的代碼。helpers 用於放常用的工具函數。

Karma.conf.js

及 test 目錄與單元測試相關。

.travis.yml

用於配置 在線持續集成,另外可在 github 的 README 文件配置構建情況。

Parcel 集成

打包工具選用的是 Parcel,目的是零配置編譯 TypeScript 。入口文件為 src 目錄下的

index.html

,只需在 入口文件裡引入

index.ts

即可完成熱更新,TypeScript 編譯等配置:

<body> <script src="index.ts"></script> </body> 複製代碼

Parcel 相關:

# 全局安裝 yarn global add parcel-bundler # 啟動服務 parcel ./src/index.html # 打包 parcel build ./src/index.ts 複製代碼

vscode 調試

運行完 parcel 命令會啟動一個本地伺服器,可以通過

.vscode

目錄下的

launch.json

配置 Vscode 調試工具。

{ "version": "0.2.0", "configurations": [ { "type": "chrome", "request": "launch", "name": "Lanzar Chrome contra localhost", "url": "http://localhost:1234", "webRoot": "${workspaceRoot}", "sourceMaps": true, "breakOnLoad": true, "sourceMapPathOverrides": { "../*": "${webRoot}/*" } } ] } 複製代碼

配置完成後,可斷點調試,按 F5 即可開始調試。

TypeScript 配置

TypeScript 整體配置和規範檢測參考如下:

tsconfig.jsontslint強烈建議開啟

tslint

,安裝 vscode tslint 插件 並在

.vscode

目錄下的

.setting

配置如下格式:

{ "editor.tabSize": 2, "editor.rulers": [120], "files.trimTrailingWhitespace": true, "files.insertFinalNewline": true, "files.exclude": { "**/.git": true, "**/.DS_Store": true }, "eslint.enable": false, "tslint.autoFixOnSave": true, "typescript.format.enable": true, "typescript.tsdk": "node_modules/typescript/lib" } 複製代碼

如果有安裝 Prettier需注意兩者風格衝突,無論格式化代碼的插件是什麼,我們的目的只有一個,就是 保證代碼格式化風格統一。( 最好遵循 lint 規範 )。

ps:

.vscode

目錄可隨 git 跟蹤進版本管理,這樣可以讓 clone 倉庫的使用者更友好。

另外可以通過,vscode 的 控制面板中的問題 tab迅速查看當前項目問題所在。

TypeScript 代碼片段測試

我們時常會有想要編輯某段測試代碼,又不想在項目裡編寫的需求(比如用 TypeScript 寫一個 deepCopy 函數),不想脫離 vscode 編輯器的話,推薦使用 quokka,一款可立即執行腳本的插件。

如果需要導入其他庫可參考quokka 配置希望引入瀏覽器環境,可在 quokkajs 項目目錄全局安裝jsdom-quokka-plugin插件接著像這樣

({ plugins: 'jsdom-quokka-plugin', jsdom: { html: `<div id="test">Hello</div>` } }); const testDiv = document.getElementById('test'); console.log(testDiv.innerHTML); 複製代碼

API 概覽

重構的思路首先是看文檔提供的 API,或者

index.d.ts

聲明文件。 優秀一點的源碼可以看它的測試用例,一般會提供 API 相關的測試,如 Axios API 測試用例 ,本次分享實現 API 如下:

總得下來就是五類 API,比葫蘆娃還少。有信心了吧,我們來一個個"送人頭"。

Axios 類

這些 API 可以統稱為實例方法,有實例,就肯定有類。所以在講 API 實現之前,先讓我們來看一下 Axios 類。

兩個屬性(defaults,interceptors),一個通用方法( request ,其餘的方法如,get、post、等都是基於 request,只是參數不同 )真的不能再簡單了。

export default class Axios { defaults: AxiosRequestConfig; interceptors: { request: InterceptorManager; response: InterceptorManager; }; request(config: AxiosRequestConfig = {}) { // 請求相關 } // 由 request 延伸出 get 、post 等 } 複製代碼

axios 實例

Axios 庫默認導出的是 Axios 的一個實例 axios,而不是 Axios 類本身。但是,這裡並沒有直接返回 Axios 的實例,而是將 Axios 實例方法 request 的上下文設置為了 Axios。 所以 axios 的類型是 function,不是 object。但由於 function 也是 Object 所以可以設置屬性和方法。於是 axios 既可以表現的像實例,又可以直接函數調用

axios(config)

。具體實現如下:

const createInstance = (defaultConfig: AxiosRequestConfig) => { const context = new Axios(defaultConfig); const instance = Axios.prototype.request.bind(context); extend(instance, Axios.prototype, context); extend(instance, context); return instance; }; axios.create = (instanceConfig: AxiosRequestConfig) => { return createInstance(mergeConfig(axios.defaults, instanceConfig)); }; const axios: AxiosExport = createInstance(defaults); axios.Axios = Axios; export default axios; 複製代碼

axios 還提供了一個 Axios 類的屬性,可供別的類繼承。另外暴露了一個工廠函數,接收一個配置項參數,方便使用者創建多個不同配置的請求實例。

Axios 默認配置

如果不看源碼,我們用一個類,最關心的應該是構造函數,默認設置了什麼屬性,以及我們可以修改哪些屬性。體現在 Axios 就是,請求的默認配置。

下面我們來看下默認配置:

const defaults: AxiosRequestConfig = { headers: headers(), // 請求頭 adapter: getDefaultAdapter(), // XMLHttpRequest 發送請求的具體實現 transformRequest: transformRequest(), // 自定義處理請求相關數據,默認有提供一個修改根據請求的 data 修改 content-type 的方法。 transformResponse: transformResponse(), // 自定義處理響應相關數據,默認提供了一個將 respone 數據轉換為 JSON格式的方法 timeout: 0, xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', validateStatus(status: number) { return status >= 200 && status < 300; } }; 複製代碼

也就是說,如果你用 Axios ,你應該知道它有哪些默認設置。

Axios 傳入配置

先來看下 axios 接受的請求參數都有哪些屬性,以下參數屬性均是可選的。使用 TypeScript 事先定義了這些參數的類型,接下來傳參的時候就可以檢驗傳參的類型是否正確。

export interface AxiosRequestConfig { url?: string; // 請求連結 method?: string; // 請求方法 baseURL?: string; // 請求的基礎連結 xsrfCookieName?: string; // CSRF 相關 xsrfHeaderName?: string; // CSRF 相關 headers?: any; // 請求頭設置 params?: any; // 請求參數 data?: any; // 請求體 timeout?: number; // 超時設置 withCredentials?: boolean; // CSRF 相關 responseType?: XMLHttpRequestResponseType; // 響應類型 paramsSerializer?: (params: any) => string; // url query 參數格式化方法 onUploadProgress?: (progressEvent: any) => void; // 上傳處理函數 onDownloadProgress?: (progressEvent: any) => void; // 下載處理函數 validateStatus?: (status: number) => boolean; adapter?: AxiosAdapter; auth?: any; transformRequest?: AxiosTransformer | AxiosTransformer[]; transformResponse?: AxiosTransformer | AxiosTransformer[]; cancelToken?: CancelToken; } 複製代碼

請求配置

urlmethodbaseURL

export interface AxiosRequestConfig { url?: string; // 請求連結 method?: string; // 請求方法 baseURL?: string; // 請求的基礎連結 } 複製代碼

先來看下相關知識:

url,method 作為 XMLHttpRequest 中 open 方法的參數。

open 語法:xhrReq.open(method, url, async, user, password);

url 是一個 DOMString,表示發送請求的 URL。

注意:將 null | undefined 傳遞給接受 DOMString 的方法或參數時通常會把其 stringifies 為 「null」 | 「undefined」

用原生的 open 方法傳遞如下參數,實際請求 URL 如下:

let xhr = new XMLHttpRequest(); // 假設當前 window.location.host 為 http://localhost:1234 xhr.open('get', ''); // http://localhost:1234/ xhr.open('get', '/'); // href http://localhost:1234/ xhr.open('get', null); // http://localhost:1234/null xhr.open('get', undefined); // http://localhost:1234/undefined 複製代碼

可以看到默認 baseURL 為

window.location.host

類似

http://localhost:1234/undefined

這種 URL 請求成功的情況是存在的。當前端動態傳遞 url 參數時,參數是有可能為

null

undefined

,如果不是通過 response 的狀態碼來響應操作,此時得到的結果就跟預想的不一樣。這讓我想起了,JavaScript 隱式轉換的坑,比比皆是。(此處安利 TypeScript 和 '===' 操作符)

對於這種情況,使用 TypeScript 可以在開發階段規避這些問題。但如果是動態賦值(比如請求返回的結果作為 url 參數時),需要給值判斷下類型,必要時可拋出錯誤或轉換為其他想要的值。

接著來看下 axios url 相關,主要提供了 baseURL 的支持,可以通過

axios.defaults.baseURL

axios({baseURL:'...'})

const isAbsoluteURL = (url: string): boolean => { // 1、判斷是否為協議形式比如 http:// return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url); }; const combineURLs = (baseURL: string, relativeURL: string): string => { return relativeURL ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '') : baseURL; }; const suportBaseURL = () => { // 2、baseURL 處理 return baseURL && !isAbsoluteURL(url) ? combineURLs(baseURL, url) : url; }; 複製代碼

params 與 data

在 axios 中 發送請求時 params 和 data 的區別在於:

params 是添加到 url 的請求字符串中的,用於 get 請求。data 是添加到請求體(body)中的, 用於 post 請求。params

axios 對 params 的處理分為賦值和序列化(用戶可自定義 paramsSerializer 函數)

helpers 目錄下的

buildURL

文件主要生成完整的 URL 請求地址。

data

XMLHttpRequest 是通過 send 方法把 data 添加到請求體的。

語法如下:

send(); send(ArrayBuffer data); send(ArrayBufferView data); send(Blob data); send(Document data); send(DOMString? data); send(FormData data); 複製代碼

可以看到 data 有這幾種類型:

ArrayBufferArrayBufferViewBlobDocumentDOMStringFormData希望了解 data 有哪些類型的可以看這篇

實際使用:

var xhr = new XMLHttpRequest(); xhr.open('GET', '/server', true); xhr.onload = function() { // 請求結束後,在此處寫處理代碼 }; xhr.send(null); // xhr.send('string'); // xhr.send(new Blob()); // xhr.send(new Int8Array()); // xhr.send({ form: 'data' }); // xhr.send(document); 複製代碼

另外,在發送請求即調用 send()方法之前應該根據 data 類型使用 setRequestHeader() 方法設置 Content-Type 頭部來指定數據流的 MIME 類型。

Axios 在

transformRequest

配置項裡有個默認的方法用於修改請求( 可自定義 )。

const transformRequest = () => { return [ (data: any, headers: any) => { // ...根據 data 類型修改對應 headers } ]; }; 複製代碼

HTTP 相關

HTTP 請求方法

axios 提供配置 HTTP 請求的方法:

export interface AxiosRequestConfig { method?: string; } 複製代碼

可選配置如下:

GET:請求一個指定資源的表示形式. 使用 GET 的請求應該只被用於獲取數據.HEAD:HEAD 方法請求一個與 GET 請求的響應相同的響應,但沒有響應體.POST:用於將實體(data)提交到指定的資源,通常導致狀態或伺服器上的副作用的更改.PUT:用請求有效載荷替換目標資源的所有當前表示。DELETE:刪除指定的資源。OPTIONS:用於描述目標資源的通信選項。PATCH:用於對資源應用部分修改。接著了解下 HTTP 請求

HTTP 定義了一組請求方法, 以表明要對給定資源執行的操作。指示針對給定資源要執行的期望動作. 雖然他們也可以是名詞, 但這些請求方法有時被稱為 HTTP 動詞. 每一個請求方法都實現了不同的語義, 但一些共同的特徵由一組共享:: 例如一個請求方法可以是 safe, idempotent, 或 cacheable.

safe:說一個 HTTP 方法是安全的,是說這是個不會修改伺服器的數據的方法。也就是說,這是一個對伺服器只讀操作的方法。這些方法是安全的:GET,HEAD 和 OPTIONS。有些不安全的方法如 PUT 和 DELETE 則不是。idempotent:一個 HTTP 方法是冪等的,指的是同樣的請求被執行一次與連續執行多次的效果是一樣的,伺服器的狀態也是一樣的。換句話說就是,冪等方法不應該具有副作用(統計用途除外)。在正確實現的條件下,GET,HEAD,PUT 和 DELETE 等方法都是冪等的,而 POST 方法不是。所有的 safe 方法也都是冪等的。cacheable:可緩存的,響應是可被緩存的 HTTP 響應,它被存儲以供稍後檢索和使用,從而將新的請求保存在伺服器。篇幅有限,看 MDN

HTTP 請求頭

axios 提供配置 HTTP 請求頭的方法:

export interface AxiosRequestConfig { headers?: any; } 複製代碼

一個請求頭由名稱(不區分大小寫)後跟一個冒號「:」,冒號後跟具體的值(不帶換行符)組成。該值前面的引導空白會被忽略。

請求頭可以被定義為:被用於 http 請求中並且和請求主體無關的那一類 HTTP header。某些請求頭如Accept,Accept-*,If-*``允許執行條件請求。某些請求頭如:Cookie,User-AgentReferer描述了請求本身以確保服務端能返回正確的響應。

並非所有出現在請求中的 http 首部都屬於請求頭,例如在 POST 請求中經常出現的

Content-Length

實際上是一個代表請求主體大小的 entity header,雖然你也可以把它叫做請求頭。

消息頭列表

axios 根據請求方法 設置了不同的

Content-Type

Accpect

請求頭。

設置請求頭

XMLHttpRequest 對象提供的

XMLHttpRequest對象提供的.setRequestHeader()

方法為開發者提供了一個操作這兩種頭部信息的方法,並允許開發者自定義請求頭的頭部信息。

XMLHttpRequest.setRequestHeader() 是設置 HTTP 請求頭部的方法。此方法必須在 open() 方法和 send() 之間調用。如果多次對同一個請求頭賦值,只會生成一個合併了多個值的請求頭。

如果沒有設置 Accept 屬性,則此發送出 send() 的值為此屬性的默認值/。**

安全起見,有些請求頭的值只能由 user agent 設置:forbidden header names 和 forbidden response header names.

默認情況下,當發送 AJAX 請求時,會附帶以下頭部信息:

axios 設置代碼如下:

// 在 adapters 目錄下的 xhr.ts 文件中: if ('setRequestHeader' in requestHeaders) { // 通過 XHR 的 setRequestHeader 方法設置請求頭信息 for (const key in requestHeaders) { if (requestHeaders.hasOwnProperty(key)) { const val = requestHeaders[key]; if ( typeof requestData === 'undefined' && key.toLowerCase() === 'content-type' ) { delete requestHeaders[key]; } else { request.setRequestHeader(key, val); } } } } 複製代碼

至於能不能修改 http header,我的建議是當然不能隨便修改任何欄位。

有一些欄位是絕對不能修改的,比如最重要的 host 欄位,如果沒有 host 值,http1.1 協議會認為這是一個不規範的請求從而直接丟棄。同樣的如果隨便修改這個值,那目的網站也返回不了正確的內容user-agent 也不建議隨便修改,有很多網站是根據這個欄位做內容適配的,比如 PC 和手機肯定是不一樣的內容。有一些欄位能夠修改,比如connectioncache-control等。不會影響你的正常訪問,但有可能會慢一點。還有一些欄位可以刪除,比如你不希望網站記錄你的訪問行為或者歷史信息,你可以刪除 cookie,referfer 等欄位。當然你也可以自定義構造任意你想要的欄位,一般沒什麼影響,除非 header 太長導致內容截斷。通常自定義的欄位都建議 X-開頭。比如 X-test: lance。HTTP 小結

只要是用戶主動輸入網址訪問時發送的 http 請求,那這些頭部欄位都是瀏覽器自動生成的,比如 host,cookie,user-agent, Accept-Encoding 等。JS 能夠控制瀏覽器發起請求,也能在這裡增加一些 header,但是考慮到安全和性能的原因,對 JS 控制 header 的能力做了一些限制,比如 host 和 cookie, user-agent 等這些欄位,JS 是無法幹預的禁止修改的消息首部。關於 HTTP 的知識實在多,這裡簡單談到相關聯的知識。這裡埋下伏筆,後續若有更適合講 HTTP 的例子,再延伸。

接下來的 CSRF,就會修改 headers。

CSRF

與 CSRF 相關的配置屬性有這三個:

export interface AxiosRequestConfig { xsrfCookieName?: string xsrfHeaderName?: string withCredentials?: boolean; } // 默認配置為 { xsrfCookieName: 'XSRF-TOKEN', xsrfHeaderName: 'X-XSRF-TOKEN', withCredentials: false } 複製代碼

那麼,先來簡單了解 CSRF

跨站請求偽造(英語:Cross-site request forgery),也被稱為 one-click attack 或者 session riding,通常縮寫為 CSRF 或者 XSRF, 是一種挾制用戶在當前已登錄的 Web 應用程式上執行非本意的操作的攻擊方法。跟跨網站腳本(XSS)相比,XSS 利用的是用戶對指定網站的信任,CSRF 利用的是網站對用戶網頁瀏覽器的信任。

什麼是 CSRF 攻擊?

你這可以這麼理解 CSRF 攻擊:攻擊者盜用了你的身份,以你的名義發送惡意請求。CSRF 能夠做的事情包括:以你名義發送郵件,發消息,盜取你的帳號,甚至於購買商品,虛擬貨幣轉帳。造成的問題包括:個人隱私洩露以及財產安全。

CSRF 原理

在他們的釣魚站點,攻擊者可以通過創建一個 AJAX 按鈕或者表單來針對你的網站創建一個請求:

<form action="https://my.site.com/me/something-destructive" method="POST"> <button type="submit">Click here for free money!</button> </form> 複製代碼

要完成一次 CSRF 攻擊,受害者必須依次完成兩個步驟:

1.登錄受信任網站 A,並在本地生成 Cookie。

2.在不登出 A 的情況下,訪問危險網站 B。

如果減輕 CSRF 攻擊?

只使用 JSON api

使用 JavaScript 發起 AJAX 請求是限制跨域的。 不能通過一個簡單的

<form>

來發送

JSON

, 所以,通過只接收 JSON,你可以降低發生上面那種情況的可能性。

禁用 CORS

第一種減輕 CSRF 攻擊的方法是禁用 cross-origin requests(跨域請求)。如果你希望允許跨域請求,那麼請只允許

OPTIONS, HEAD, GET

方法,因為他們沒有副作用。不幸的是,這不會阻止上面的請求由於它沒有使用 JavaScript(因此 CORS 不適用)。

檢查 Referer 欄位

HTTP 頭中有一個 Referer 欄位,這個欄位用以標明請求來源於哪個地址。在處理敏感數據請求時,通常來說,Referer 欄位應和請求的地址位於同一域名下。這種辦法簡單易行,工作量低,僅需要在關鍵訪問處增加一步校驗。但這種辦法也有其局限性,因其完全依賴瀏覽器發送正確的 Referer 欄位。雖然 http 協議對此欄位的內容有明確的規定,但並無法保證來訪的瀏覽器的具體實現,亦無法保證瀏覽器沒有安全漏洞影響到此欄位。並且也存在攻擊者攻擊某些瀏覽器,篡改其 Referer 欄位的可能。(PS:可見遵循 web 標準多麼重要)

CSRF Tokens

最終的解決辦法是使用 CSRF tokens。 CSRF tokens 是如何工作的呢?

伺服器發送給客戶端一個 token。客戶端提交的表單中帶著這個 token。如果這個 token 不合法,那麼伺服器拒絕這個請求。攻擊者需要通過某種手段獲取你站點的 CSRF token, 他們只能使用 JavaScript 來做。 所以,如果你的站點不支持 CORS, 那麼他們就沒有辦法來獲取 CSRF token, 降低了威脅。

確保 CSRF token 不能通過 AJAX 訪問到!

不要創建一個

/CSRF

路由來獲取一個 token, 尤其不要在這個路由上支持 CORS!

token 需要是不容易被猜到的, 讓它很難被攻擊者嘗試幾次得到。 它不需要是密碼安全的。 攻擊來自從一個未知的用戶的一次或者兩次的點擊, 而不是來自一臺伺服器的暴力攻擊。

axios 中的 CSRF Tokens

這裡有個

withCredentials

,先來了解下。

XMLHttpRequest.withCredentials 屬性是一個 Boolean 類型,它指示了是否該使用類似 cookies,authorization headers(頭部授權)或者 TLS 客戶端證書這一類資格證書來創建一個跨站點訪問控制(cross-site Access-Control)請求。在同一個站點下使用 withCredentials 屬性是無效的。

如果在發送來自其他域的 XMLHttpRequest 請求之前,未設置 withCredentials 為 true,那麼就不能為它自己的域設置 cookie 值。而通過設置 withCredentials 為 true 獲得的第三方 cookies,將會依舊享受同源策略,因此不能被通過 document.cookie 或者從頭部相應請求的腳本等訪問。

// 在標準瀏覽器環境下 (非 web worker 或者 react-native) 則添加 xsrf 頭 if (isStandardBrowserEnv()) { // 必須在 withCredentials 或 同源的情況,才設置 xsrfHeader 頭 const xsrfValue = (withCredentials || isURLSameOrigin(url)) && xsrfCookieName ? cookies.read(xsrfCookieName) : undefined; if (xsrfValue && xsrfHeaderName) { requestHeaders[xsrfHeaderName] = xsrfValue; } } 複製代碼

CSRF 小結

對於 CSRF,需要讓後端同學,敏感的請求不要使用類似 get 這種冪等的,但是由於 Form 表單發起的 POST 請求並不受 CORS 的限制,因此可以任意地使用其他域的 Cookie 向其他域發送 POST 請求,形成 CSRF 攻擊。

這時,如果有涉及敏感信息的請求,需要跟後端同學配合,進行 XSRF-Token 認證。此時,我們用 axios 請求的時候,就可以通過設置

XMLHttpRequest.withCredentials=true

以及設置

axios({xsrfCookieName:'',xsrfHeaderName:''})

,不使用則會用默認的

XSRF-TOKEN

X-XSRF-TOKEN

(拿這個跟後端配合即可)。

所以,axios 特性中,客戶端支持防止 CSRF/XSRF。只是方便設置 CORF-TOKEN ,關鍵還是要後端同學的接口支持。(PS:前後端相親相愛多重要,所以作為前端的我們還是儘可能多了解這方面的知識

XHR 實現

axios 通過適配器模式,提供了支持 node.js 的 http 以及客戶端的 XMLHttpRequest 的兩張實現,本文主要講解 XHR 實現。

大概的實現邏輯如下:

const xhrAdapter = (config: AxiosRequestConfig): AxiosPromise => { return new Promise((resolve, reject) => { let request: XMLHttpRequest | null = new XMLHttpRequest(); setHeaders(); openXHR(); setXHR(); sendXHR(); }); }; 複製代碼

如果逐行講解,不如錄個教程視頻,建議大家直接看 adapters 目錄下的

xhr.ts

,在關鍵地方都有注釋!

xhrAdapter 接受 config 參數 ( 由默認參數和用戶實例化時傳入參數的合併值,axios 對合併值由做特殊處理。 )設置請求頭,比如根據傳入的參數dataauth,xsrfHeaderName設置對應的 headerssetXHR主要是在request.readyState === 4的時候對響應數據作處理以及錯誤處理最後執行XMLHttpRequest.send方法返回的是一個 Promise 對象,所以支持 Promise 的所有特性。

請求攔截

請求攔截在 axios 應該算是一個比較騷的操作,實現非常簡單。有點像一系列按順序執行的 Promise。

直接看代碼實現:

// interceptors 分為 request 和 response。 interface interceptors { request: InterceptorManager; response: InterceptorManager; } request (config: AxiosRequestConfig = {}) { const { method } = config const newConfig: AxiosRequestConfig = { ...this.defaults, ...config, method: method ? method.toLowerCase() : 'get' } // 攔截器原理:[請求攔截器,發送請求,響應攔截器] 順序執行 // 1、建立一個存放 [ resolve , reject ] 的數組, // 這裡如果沒有攔截器,則執行發送請求的操作。 // 由於之後都是 resolve 和 reject 的組合,所以這裡默認 undefined。真是騷操作! const chain = [ dispatchRequest, undefined ] // 2、Promise 成功後會往下傳遞參數,於是這裡先傳入合併後的參數,供之後的攔截器使用 (如果有的話)。 let promise: any = Promise.resolve(newConfig) // 3、又是一波騷操作,完美的運用了數組的方法。咋不用 reduce 實現 promise 順序執行呢 ? // request 請求攔截器肯定需要 `dispatchRequest` 在前面,於是 [interceptor.fulfilled, interceptor.rejected, dispatchRequest, undefined] this.interceptors.request.forEach((interceptor: Interceptor) => { chain.unshift(interceptor.fulfilled, interceptor.rejected) }) // response 響應攔截器肯定需要在 `dispatchRequest` 後面,於是 [dispatchRequest, undefined,interceptor.fulfilled, interceptor.rejected] this.interceptors.response.forEach((interceptor: Interceptor) => { chain.push(interceptor.fulfilled, interceptor.rejected) }) // 4、依次執行 Promise( fulfilled,rejected ) while (chain.length) { promise = promise.then(chain.shift(), chain.shift()) } return promise } 複製代碼

又是對基礎知識的完美運用,無論是 Promise 還是數組的變異方法都算巧妙運用。

當然,Promise 的順序執行還可以這樣:

function sequenceTasks(tasks) { function recordValue(results, value) { results.push(value); return results; } var pushValue = recordValue.bind(null, []); return tasks.reduce(function(promise, task) { return promise.then(task).then(pushValue); }, Promise.resolve()); } 複製代碼

取消請求

如果不知道 XMLHttpRequest 有 absort 方法,肯定會覺得取消請求這種秀操作的怎麼可能呢!( PS:基礎知識多重要)

const { cancelToken } = config; const request = new XMLHttpRequest(); if (cancelToken) { cancelToken.promise .then(cancel => { if (!request) { return; } request.abort(); reject(cancel); request = null; }) .catch(err => { console.error(err); }); } 複製代碼

至於

CancelToken

就不講了,好奇怪的實現。沒有感悟到原作者的設計真諦!

單元測試

最後到了單元測試的環節,先來看下相關依賴。

用的是 karma,配置如下:

執行命令:

yarn test 複製代碼

本項目是基於

jasmine

來寫測試用例,還是比較簡單的。

karma 會跑 test 目錄下的所有測試用例,感覺測試用例用 TypeScript 來寫,有點難受。因為測試本來就是要讓參數多樣化,然而 TypeScript 事先規定了數據類型。雖然可以使用泛型來解決,但是總覺得有點變扭。

不過,整個測試用例跑下來,代碼強壯了很多。對於這種庫來說,還是很有必要的。如果需要二次重構,基於 TypeScript 和 有覆蓋大部分函數的單元測試支持,應該會容易很多。

總結

感謝能看到這裡的朋友,想必也是 TypeScript 或 Axios 的粉絲,不妨相互認識一下。

還是那句話,TypeScript 確實好用。短時間內就能將 Axios 大致重構了一遍,感興趣的可以跟著一起。老規矩,在分享中不會具體講庫怎麼用 (想必,如果自己擼完這麼一個項目,應該不用去看 API 了吧。) ,更多的是從廣度拓展大家的知識點。如果對某個關鍵詞比較陌生,這就是進步的時候了。比如筆者接下來要去深入涉略 HTTP 了。雖然,感覺目前 TypeScript 的熱度好像好不是很高。好東西,總是那些不容易變的。哈,別到時候打臉了。

我變強了嗎? 不扯了,聽楊宗緯的 "我變了,我沒變" 了。

切記,沒有什麼是看源碼解決不了的 bug。

相關焦點

  • 實戰教學使用 Vue3 重構 Vue2 項目(萬字好文推薦)
    {  "name": "vite-project",  "version": "0.1.0",  "scripts": {    "dev": "vite",    "build": "vite build"  },  "dependencies": {    "core-js":
  • vue中簡單的axios二次封裝
    現在vue時代已經到來,axios 和 fetch都已經開始分別搶佔「請求」這個前端高地。本文將會嘗試著闡述他們之間的區別,並給出自己的一些理解。1、請求配置默認值1)全局的 axios 默認值axios.defaults.baseURL = 'https://api.example.com';axios.defaults.headers.common['Authorization'] = AUTH_TOKEN;axios.defaults.headers.post
  • Vue與axios的完美結合
    如今也是時候來一波技術換新,axios真香~介紹前一陣子我也使用過axios,當時的想法很簡單,axios更專注於數據交互。放棄的理由也很簡單,不熟悉,使用起來不順手。但當我再次去關注axios的時候,才發現它真的魅力四射啊!
  • TypeScript 3.0 正式發布:引入「項目引用」新概念
    這意味著更快的構建可以逐步工作,並支持跨項目導航、編輯和重構。由於 3.0 奠定了基礎並公開了 API,因此任何構建工具都能夠提供這一功能。在 JSX 中支持 defaultProps該特性使得調用者可以通過不需要某些參數來更輕鬆地使用函數。
  • Fetch還是Axios——哪個更適合HTTP請求?
    axios 有一些優勢,比如對XSRF的保護或取消請求。為了能夠使用 axios 庫,我們必須將其安裝並導入到我們的項目中。可以使用CDN,npm或bower安裝 axios。現在,讓我們來看一個簡單的GET方法的語法。
  • Vue開發使用Axios遇到了大坑!
    使用Vue的axios連接部分正常,部分不正常,伺服器狀態碼200,伺服器端控制臺也不報錯。但是頁面請求就是報錯。使用iPhone手機報錯,換華為安卓手機也是一樣的報錯,安卓手機不知道怎麼調試,使用macOS的Safari瀏覽器可以調試iPhone手機瀏覽器,調試報錯,但是不知道原因。就單純報錯,伺服器是沒有任何問題。錯誤代碼是200。
  • fetch和axios接口調用方式的用法
    前端接口調用的幾種方式 ,有原生ajax ,基於jQuery的ajax ,fetch ,axios關於原生ajax ,基於jQuery的ajax都已經簡單介紹過,下面來看看es6接口調用方式fetchfetch接口調用的方式axios是一個基於Promise 用於瀏覽器和 nodejs 的 HTTP
  • 【vue2.0進階】用axios來實現數據請求,簡單易用
    在某個業務場景中,我們需要同時產生以上兩個get請求,並需要等待它們都請求完畢後再做邏輯處理,你也許會想到回調嵌套,但現在你可以使用axios的並發請求API: axios.all()  當這兩個請求都完成的時候會觸發 axios.spread() 函數,res1和res2兩個參數分別代表返回的結果,相當實用的API。
  • Fetch還是Axios,哪個更適合HTTP請求?
    axios 有一些優勢,比如對XSRF的保護或取消請求。為了能夠使用 axios 庫,我們必須將其安裝並導入到我們的項目中。可以使用CDN,npm或bower安裝 axios。現在,讓我們來看一個簡單的GET方法的語法。
  • 1024專屬視頻 | 前端視頻:基礎入門、axios、Promise、mpvue項目實戰
    本視頻教程從基本的HTTP請求協議開始,到封裝XHR定義簡潔版axios;從axios的基本語法使用,到難點技術使用分析,最後進行axios源碼的深入分析,幫助學習者深入掌握axios技術。09.ajax封裝_post請求攜帶參數數據10.ajax封裝_get請求攜帶參數11.ajax封裝_讀取請求結果數據12.ajax封裝_PUT和DELETE請求13.axios的介紹和特點14.axios的文檔說明15.axios使用_發ajax請求16.axios使用_create方法
  • Vue 3.0前的 TypeScript 最佳入門實踐
    根據官方文檔,vue 結合 typescript ,有兩種書寫方式Vue.extendimportVuefrom'vue'constComponent= Vue.extend({// type inference enabled})vue-class-component
  • Vue2.5+ Typescript 引入全面指南
    寫在前面寫這篇文章時的我,Vue使用經驗三個多月,Typescript完全空白,花了大概三個晚上把手頭項目遷移至
  • Spring Boot+Vue|axios異步請求數據的12種操作(上篇)
    組件來完成,axios 是一個基於Promise 用於瀏覽器和 nodejs 的 HTTP 客戶端,可以用在瀏覽器和 node.js 中。Vue 工程中使用 axios,首先要安裝 axios,命令如下所示。然後創建 Vue 工程,手動導入 axios 組件,命令如下所示。Vue 環境搭建好之後,創建 Spring Boot 工程,之後就可以分別完成前後端代碼的開發。
  • 《機戰》快速「機體經驗重構」心得
    首先機體等級必須得達到170級,然後去找「經驗模塊重構系統」NPC(在領取獎品NPC下面那個位置,導航裡也有),找到這個NPC後,選擇要重構的機體,確定後機體等級變為1級,裝備和人物屬性不變,戰鬥力會下降。
  • 打造TypeScript的Visual Studio Code開發環境
    1.2.2 安裝其他Node包新建一個目錄,如:hello-typescript,用剛安裝好的VS Code編輯名為package.json的文件,保存於hello-typescript目錄中。package.json是包描述文件。其中列出了應用所需的各種依賴包、待執行腳本,以及其他一些設置內容。
  • TypeScript vs. ReasonML
    本文的內容基於最近在一些較小的實際項目上使用TypeScript和ReasonML的經驗以及多年的JavaScript經驗。1.   靜態類型的優缺點優點:文檔:對於大多數代碼,記錄參數類型是非常有用的,這樣可以方便區分調用者和被調者。好處遠不止這些。
  • 最詳細從零開始配置 TypeScript 項目的教程
    以下是他的一些不錯的作品,感興趣的可以閱讀:面試分享:兩年工作經驗成功面試阿里 P6 總結[3]如果覺得不錯希望能夠在掘金關注、點讚哦~ 前言 本文是算法與 TypeScript 實現[5]中 TypeScript 項目整體的環境配置過程介紹。
  • 最詳細的從零開始配置 TypeScript 項目的教程
    以下是他的一些不錯的作品,感興趣的可以閱讀:面試分享:兩年工作經驗成功面試阿里 P6 總結[3]如果覺得不錯希望能夠在掘金關注、點讚哦,有機會一起睡覺~ 前言 本文是算法與 TypeScript 實現[5]中 TypeScript 項目整體的環境配置過程介紹。