拒絕做一個只會用 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-Agent和Referer描述了請求本身以確保服務端能返回正確的響應。
並非所有出現在請求中的 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 和手機肯定是不一樣的內容。有一些欄位能夠修改,比如connection,cache-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 對合併值由做特殊處理。 )設置請求頭,比如根據傳入的參數data,auth,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。