拒絕作一個只會用 API 的文檔工程師,本文將會讓你從重複造輪子的過程當中掌握 web 開發相關的基本知識,特別是 XMLHttpRequest。html
又是一篇關於 TypeScript 的分享,年末了,請容許我沉澱一下。上次用 TypeScript 重構 Vconsole 的項目 埋下了對 Axios 源碼解析的梗。因而,此次分享的主題就是 如何從零用 TypeScript 重構 Axios 以及爲何我要這麼作。前端
筆者在用 TypeScript 重複造輪子的時候目的仍是很明確的,不只是爲了用 TypeScript 養成一種好的開發習慣,更重要的是瞭解工具庫關聯的基礎知識。 只有更多地注重基礎知識,才能早日擺脫文檔工程師的困擾。(Ps: 用 TypeScript,也是爲了擺脫前端查文檔的宿命!)node
本次分享包括如下內容:react
項目源碼,分享可能會錯過某些細節實現,須要的能夠看源碼,測試用例基本跑通了。想一想,5w star 的庫,就這樣本身實現了一遍。ios
Axios 是什麼?git
Promise based HTTP client for the browser and node.jsgithub
axios 是基於 Promise 用於瀏覽器和 nodejs 的 HTTP 客戶端,它自己具備如下特性 ( √ 表示本項目具有該特性 ):web
/src/core/dispatchRequest.ts
/src/core/dispatchRequest.ts
這裏主要講解瀏覽器端的 XHR 實現,限於篇幅不會涉及 node 下的 http 。若是你願意一層一層瞭解它,你會發現實現 axios 仍是很簡單的,來一塊兒探索吧!chrome
首先來看下目錄。typescript
目錄與 Axios 基本保持一致,core 是 Axios
類的核心代碼。adapters 是 XHR 核心實現,Cancel 是與 取消請求相關的代碼。helpers 用於放經常使用的工具函數。Karma.conf.js
及 test 目錄與單元測試相關。.travis.yml
用於配置 在線持續集成,另外可在 github 的 README 文件配置構建狀況。
打包工具選用的是 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
複製代碼
運行完 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 總體配置和規範檢測參考以下:
強烈建議開啓 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 寫一個 deepCopy 函數),不想脫離 vscode 編輯器的話,推薦使用 quokka,一款可當即執行腳本的插件。
接着像這樣
({
plugins: 'jsdom-quokka-plugin',
jsdom: { html: `<div id="test">Hello</div>` }
});
const testDiv = document.getElementById('test');
console.log(testDiv.innerHTML);
複製代碼
重構的思路首先是看文檔提供的 API,或者 index.d.ts
聲明文件。 優秀一點的源碼能夠看它的測試用例,通常會提供 API 相關的測試,如 Axios API 測試用例 ,本次分享實現 API 以下:
總得下來就是五類 API,比葫蘆娃還少。有信心了吧,咱們來一個個"送人頭"。
這些 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 實例方法 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 就是,請求的默認配置。
下面咱們來看下默認配置:
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 接受的請求參數都有哪些屬性,如下參數屬性均是可選的。使用 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;
}
複製代碼
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 => {
// 一、判斷是否爲協議形式好比 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 = () => {
// 二、baseURL 處理
return baseURL && !isAbsoluteURL(url) ? combineURLs(baseURL, url) : url;
};
複製代碼
在 axios 中 發送請求時 params 和 data 的區別在於:
params 是添加到 url 的請求字符串中的,用於 get 請求。
data 是添加到請求體(body)中的, 用於 post 請求。
axios 對 params 的處理分爲賦值和序列化(用戶可自定義 paramsSerializer 函數)
helpers 目錄下的 buildURL
文件主要生成完整的 URL 請求地址。
XMLHttpRequest 是經過 send 方法把 data 添加到請求體的。
語法以下:
send();
send(ArrayBuffer data);
send(ArrayBufferView data);
send(Blob data);
send(Document data);
send(DOMString? data);
send(FormData data);
複製代碼
能夠看到 data 有這幾種類型:
但願瞭解 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
}
];
};
複製代碼
axios 提供配置 HTTP 請求的方法:
export interface AxiosRequestConfig {
method?: string;
}
複製代碼
可選配置以下:
接着瞭解下 HTTP 請求
HTTP 定義了一組請求方法, 以代表要對給定資源執行的操做。指示針對給定資源要執行的指望動做. 雖然他們也能夠是名詞, 但這些請求方法有時被稱爲 HTTP 動詞. 每個請求方法都實現了不一樣的語義, 但一些共同的特徵由一組共享:: 例如一個請求方法能夠是 safe, idempotent, 或 cacheable.
safe:說一個 HTTP 方法是安全的,是說這是個不會修改服務器的數據的方法。也就是說,這是一個對服務器只讀操做的方法。這些方法是安全的:GET,HEAD 和 OPTIONS。有些不安全的方法如 PUT 和 DELETE 則不是。
idempotent:一個 HTTP 方法是冪等的,指的是一樣的請求被執行一次與連續執行屢次的效果是同樣的,服務器的狀態也是同樣的。換句話說就是,冪等方法不該該具備反作用(統計用途除外)。在正確實現的條件下,GET,HEAD,PUT 和 DELETE 等方法都是冪等的,而 POST 方法不是。全部的 safe 方法也都是冪等的。
cacheable:可緩存的,響應是可被緩存的 HTTP 響應,它被存儲以供稍後檢索和使用,從而將新的請求保存在服務器。
篇幅有限,看 MDN
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 請求,那這些頭部字段都是瀏覽器自動生成的,好比 host,cookie,user-agent, Accept-Encoding 等。JS 可以控制瀏覽器發起請求,也能在這裏增長一些 header,可是考慮到安全和性能的緣由,對 JS 控制 header 的能力作了一些限制,好比 host 和 cookie, user-agent 等這些字段,JS 是沒法干預的禁止修改的消息首部。關於 HTTP 的知識實在多,這裏簡單談到相關聯的知識。這裏埋下伏筆,後續如有更適合講 HTTP 的例子,再延伸。
接下來的 CSRF,就會修改 headers。
與 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 可以作的事情包括:以你名義發送郵件,發消息,盜取你的帳號,甚至於購買商品,虛擬貨幣轉帳。形成的問題包括:我的隱私泄露以及財產安全。
在他們的釣魚站點,攻擊者能夠經過建立一個 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。
使用 JavaScript 發起 AJAX 請求是限制跨域的。 不能經過一個簡單的 <form>
來發送 JSON
, 因此,經過只接收 JSON,你能夠下降發生上面那種狀況的可能性。
第一種減輕 CSRF 攻擊的方法是禁用 cross-origin requests(跨域請求)。若是你但願容許跨域請求,那麼請只容許 OPTIONS, HEAD, GET
方法,由於他們沒有反作用。不幸的是,這不會阻止上面的請求因爲它沒有使用 JavaScript(所以 CORS 不適用)。
HTTP 頭中有一個 Referer 字段,這個字段用以標明請求來源於哪一個地址。在處理敏感數據請求時,一般來講,Referer 字段應和請求的地址位於同一域名下。這種辦法簡單易行,工做量低,僅須要在關鍵訪問處增長一步校驗。但這種辦法也有其侷限性,因其徹底依賴瀏覽器發送正確的 Referer 字段。雖然 http 協議對此字段的內容有明確的規定,但並沒有法保證來訪的瀏覽器的具體實現,亦沒法保證瀏覽器沒有安全漏洞影響到此字段。而且也存在攻擊者攻擊某些瀏覽器,篡改其 Referer 字段的可能。(PS:可見遵循 web 標準多麼重要)
最終的解決辦法是使用 CSRF tokens。 CSRF tokens 是如何工做的呢?
攻擊者須要經過某種手段獲取你站點的 CSRF token, 他們只能使用 JavaScript 來作。 因此,若是你的站點不支持 CORS, 那麼他們就沒有辦法來獲取 CSRF token, 下降了威脅。
確保 CSRF token 不能經過 AJAX 訪問到!
不要建立一個/CSRF
路由來獲取一個 token, 尤爲不要在這個路由上支持 CORS!
token 須要是不容易被猜到的, 讓它很難被攻擊者嘗試幾回獲得。 它不須要是密碼安全的。 攻擊來自從一個未知的用戶的一次或者兩次的點擊, 而不是來自一臺服務器的暴力攻擊。
這裏有個 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,須要讓後端同窗,敏感的請求不要使用相似 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:先後端相親相愛多重要,因此做爲前端的咱們仍是儘量多瞭解這方面的知識)
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
,在關鍵地方都有註釋!
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'
}
// 攔截器原理:[請求攔截器,發送請求,響應攔截器] 順序執行
// 一、創建一個存放 [ resolve , reject ] 的數組,
// 這裏若是沒有攔截器,則執行發送請求的操做。
// 因爲以後都是 resolve 和 reject 的組合,因此這裏默認 undefined。真是騷操做!
const chain = [ dispatchRequest, undefined ]
// 二、Promise 成功後會往下傳遞參數,因而這裏先傳入合併後的參數,供以後的攔截器使用 (若是有的話)。
let promise: any = Promise.resolve(newConfig)
// 三、又是一波騷操做,完美的運用了數組的方法。咋不用 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)
})
// 四、依次執行 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。