TypeScript 重構 Axios 經驗分享

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

又是一篇關於 TypeScript 的分享,年末了,請容許我沉澱一下。上次用 TypeScript 重構 Vconsole 的項目 埋下了對 Axios 源碼解析的梗。因而,此次分享的主題就是 如何從零用 TypeScript 重構 Axios 以及爲何我要這麼作前端

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

本次分享包括如下內容:react

  • 工程簡介 & 開發技巧
  • API 實現
  • XHR,XHR,XHR
  • HTTP,HTTP,HTTP
  • 單元測試

項目源碼,分享可能會錯過某些細節實現,須要的能夠看源碼,測試用例基本跑通了。想一想,5w star 的庫,就這樣本身實現了一遍。ios

工程簡介

Axios 是什麼?git

Promise based HTTP client for the browser and node.jsgithub

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

  • √ 從瀏覽器建立 XMLHttpRequest => XHR 實現
  • √ 支持 Promise API => XHR 實現
  • √ 攔截請求和響應 => 請求攔截
  • √ 轉換請求和響應數據 => 對應項目目錄 /src/core/dispatchRequest.ts
  • √ 取消請求 取消請求
  • √ 自動轉換 JSON 數據 => 對應項目目錄 /src/core/dispatchRequest.ts
  • √ 客戶端支持防止 CSRF/XSRF => CSRF
  • × 從 node.js 發出 http 請求

這裏主要講解瀏覽器端的 XHR 實現,限於篇幅不會涉及 node 下的 http 。若是你願意一層一層瞭解它,你會發現實現 axios 仍是很簡單的,來一塊兒探索吧!chrome

目錄說明

首先來看下目錄。typescript

目錄與 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 總體配置和規範檢測參考以下:

強烈建議開啓 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,一款可當即執行腳本的插件。

接着像這樣

({
  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;
}
複製代碼

請求配置

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

先來看下相關知識:

url,method 做爲 XMLHttpRequestopen 方法的參數。

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 參數時,參數是有可能爲 nullundefined ,若是不是經過 response 的狀態碼來響應操做,此時獲得的結果就跟預想的不同。這讓我想起了,JavaScript 隱式轉換的坑,比比皆是。(此處安利 TypeScript 和 '===' 操做符)

對於這種狀況,使用 TypeScript 能夠在開發階段規避這些問題。但若是是動態賦值(好比請求返回的結果做爲 url 參數時),須要給值判斷下類型,必要時可拋出錯誤或轉換爲其餘想要的值。

接着來看下 axios url 相關,主要提供了 baseURL 的支持,能夠經過 axios.defaults.baseURLaxios({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;
};
複製代碼

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 有這幾種類型:

  • ArrayBuffer
  • ArrayBufferView
  • Blob
  • Document
  • DOMString
  • FormData

但願瞭解 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-TypeAccpect 請求頭。

設置請求頭

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 是如何工做的呢?

  1. 服務器發送給客戶端一個 token。
  2. 客戶端提交的表單中帶着這個 token。
  3. 若是這個 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-TOKENX-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 ,在關鍵地方都有註釋!

  1. xhrAdapter 接受 config 參數 ( 由默認參數和用戶實例化時傳入參數的合併值,axios 對合並值由作特殊處理。 )
  2. 設置請求頭,好比根據傳入的參數 dataauth,xsrfHeaderName 設置對應的 headers
  3. setXHR 主要是在 request.readyState === 4 的時候對響應數據做處理以及錯誤處理
  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。

參考

相關文章
相關標籤/搜索