ÿÿ深入淺出 axios(二):XMLHttpRequestã€CancelToken | Alex Liu

深入淺出 axios(二):XMLHttpRequestã€CancelToken

• 25 min read

axios 是一個 Promise based çš„ Http 請求工具,他å¯ä»¥é‹è¡Œåœ¨ç€è¦½å™¨ç’°å¢ƒèˆ‡ Node.js 中。相信在 AJAX 技術被廣泛應用的今日,ç¨å¾®æœ‰ä¸€é»žç¶“é©—çš„æ§æ²¹é–€å°ä»–一定都ä¸é™Œç”Ÿã€‚ä¸Šç¯‡å° axios 的核心程å¼è¨­è¨ˆåšäº†å‰–æžï¼Œé€™ç¯‡è¦ä¾†å¾ž axios çš„é©é…器(adapter,介é¢ï¼‰çš„設計èªè­˜ XMLHttpRequest 物件,並且了解 axios å–æ¶ˆè«‹æ±‚的設計。那就就讓我們一起看下去å§ï¼

å‰è¨€

本篇的 axios 版本為 0.21.0

這是一個系列的分享,é è¨ˆæœƒæœ‰å…©ç¯‡ï¼Œæœ¬æ–‡æ˜¯è©²ç³»åˆ—的第二篇。

上一篇我們了解了é è¨­å°Žå…¥çš„ axios 其實是一個 function,並且利用了在 JavaScript 中 function 的本質也是一個物件的特性,在該 function 上掛上了å„種方法æä¾›ä½¿ç”¨è€…æ“作;å¦å¤–也了解了核心的 Axios 類別設計與負責攔截器(Interceptor)管ç†çš„ InterceptorManager 類別設計。

這一篇我們è¦çœ‹çš„æ˜¯ axios 如何應用 XMLHttpRequest,以åŠç•¶æˆ‘們喿¶ˆäº†ä¸€å€‹è«‹æ±‚ axios åšäº†ä»€éº¼æ“作。

在本文當中會æåˆ°ä»¥ä¸‹é€™äº›å…§å®¹ï¼š

  • 從 axios 的角度èªè­˜ XMLHttpRequest。
  • axios å–æ¶ˆè«‹æ±‚çš„ CancelToken 類別設計。

axios 坿‡‰ç”¨åœ¨ã€Œç€è¦½å™¨ç’°å¢ƒã€èˆ‡ã€ŒNode.jsã€ç’°å¢ƒä¸­ã€‚在ç€è¦½å™¨ç’°å¢ƒä¸‹ä½¿ç”¨äº† XMLHttpRequest 而在 Node.js 環境則使用了 http 模組。由於目å‰å·¥ä½œä¸Šçš„使用經驗還是以ç€è¦½å™¨ç«¯ç‚ºä¸»ï¼Œå› æ­¤æœ¬ç³»åˆ—æš«æ™‚ä¹Ÿåªæœƒé‡å°ç€è¦½å™¨ç«¯çš„功能åšç ”究,分享。

從 axios 的角度èªè­˜ XMLHttpRequest

axios 如果是在ç€è¦½å™¨ç’°å¢ƒé‹è¡Œï¼ŒHTTP 請求部分會使用 XMLHttpRequest 這個 Web APIã€‚ä¸€èˆ¬æƒ…æ³æˆ‘們其實比較少有機會直接æ“作 XMLHttpRequest,但åªè¦ç‰½æ¶‰åˆ°éœ€è¦åš HTTP 請求,許多相關工具的底層還是會é€éŽä»–來æ“作,除了本系列的 axios 外,åƒåœ¨ jQuery 中æ“作 AJAX 請求,其實底層也是使用了 XMLHttpRequest。

䏋颿œƒä»¥æ®µè½çš„æ–¹å¼ç€è¦½ axios 使用了那些 XMLHttpRequest 屬性與方法,åšäº†å“ªäº›äº‹æƒ…。ä¸éŽåœ¨é–‹å§‹å‰æˆ‘們需è¦å…ˆçŸ¥é“幾個會在段è½ä¸­å‡ºç¾çš„變數å稱與æ„義

module.exports = function xhrAdapter(config) {
  return new Promise(function dispatchXhrRequest(resolve, reject) {
    var requestData = config.data;
    var requestHeaders = config.headers;

    // 接下來都會在這裡é¢è™•ç†è™•ç†
  })
}

resolve() 與 reject(),就是建立 Promise 物件時傳入的 executor æœƒæŽ¥åˆ°çš„å…©å€‹åƒæ•¸ï¼›requestData å°±æ˜¯æˆ‘å€‘ç™¼é€ PUTã€POST 之類的方法時帶的 config.data,requestHeaders 則是 config.headers。

XMLHttpRequest 基本使用方å¼

先來了解一下 XMLHttpRequest 類型基本使用方å¼ï¼š

GET

var request = new XMLHttpRequest();
request.open('GET', '/my/url', true);

request.onreadystatechange = function() {
  if (this.readyState === 4) {
    if (this.status >= 200 && this.status < 400) {
      // Success!
      var data = JSON.parse(this.responseText);
    } else {
      // Error :(
    }
  }
};

request.send();
request = null;

POST

var request = new XMLHttpRequest();
request.open('POST', '/my/url', true);
request.setRequestHeader('Content-Type', 'application/x-www-form-urlencoded; charset=UTF-8');
request.send(data);

這是從 You might not need jQuery 上擷å–的範例。從範例å¯ä»¥å¾—知基本用法,我們必須é€éŽå»ºæ§‹å¼ä¾†åˆå§‹åŒ–一個 XMLHttpRequest 物件;é€éŽ open() åˆå§‹åŒ–請求;最後é€éŽ send() é€å‡ºè«‹æ±‚。

如果是 POST 請求,則å¯ä»¥å°‡è¦å‚³é€çš„資料(body)作為 send() çš„åƒæ•¸å‚³å…¥ã€‚

å¦å¤–如果是éžåŒæ­¥è«‹æ±‚,則å¯ä»¥è¨­ç½®äº‹ä»¶è™•ç†å™¨ï¼ˆEventHandler):onreadystatechange。當 readyState 發生改變時調用指定的 function 確èªç‹€æ…‹ï¼Œå–得回應的資料。

補充 如果去看 You might not need jQuery 這個網站上的範例,å¯èƒ½æœƒçœ‹è¦‹ä»–䏿˜¯ä½¿ç”¨ onreadystatechange 這個事件處ç†å™¨ï¼Œè€Œæ˜¯é¸ç”¨ onload,並且ä¸éœ€è¦ç‰¹åˆ¥ç¢ºèª readyState 的值。如果è¦çœ‹åˆ°è·Ÿä¸Šè¿°ä¸€æ¨£çš„範例,需è¦å°‡ 「What's the oldest version of IE you need to support?ã€é€™å€‹é¸é …設定為 9 或是 8。 根據 MDN ä»¥åŠ Can I use... 的資料都顯示,onload 事件處ç†å™¨æ˜¯åœ¨ IE 9 ä¹‹å¾Œæ‰æ”¯æ´çš„。

瞭解了基本使用方å¼ï¼Œèˆ‡åœ¨å»ºç«‹ XMLHttpRequest ç‰©ä»¶å¾Œï¼Œå°±è¦æº–備來åˆå§‹åŒ–請求了。

åˆå§‹åŒ–請求

建立 XMLHttpRequest 物件後,需è¦é€éŽ XMLHttpRequest.prototype.open() åˆå§‹åŒ–請求,其語法與型別定義如下:

語法:

XMLHttpRequest.open(method, url[, async[, user[, password]]])

型別:

open(method: string, url: string): void;
open(method: string, url: string, async: boolean, username?: string | null, password?: string | null): void;

在 axios 中,會將 config(請求的設定與é è¨­è¨­å®šåˆä½µå¾Œï¼‰ä¸­çš„屬性解æžå‡ºéœ€è¦çš„åƒæ•¸ï¼š

var fullPath = buildFullPath(config.baseURL, config.url);
request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true);

這有個奇怪的點å¯ä»¥è¨Žè«–:HTTP Method 的大å°å¯«

在這裡會將傳入的 method 轉為大寫後帶入,所以我們在使用 axios 時ä¸è«– method 給大å°å¯«éƒ½æ˜¯å¯ä»¥çš„,忽大忽å°ä¹Ÿå¯ä»¥ï¼ˆåªæ˜¯å¾ˆå¥‡æ€ªè€Œå·²ï¼‰ã€‚

看到這裡其實我有很大的疑惑,在 Axios.prototype.request() 中,有特別將 method 全部轉為å°å¯«ï¼Œä½†åœ¨é€™è£¡å»åˆçµ±ä¸€è½‰ç‚ºå¤§å¯«ï¼Œé€™æ¨£æ˜Žé¡¯å¤šåšäº†ä¸€æ¬¡å·¥ï¼ä½†é€ä¸€ç€è¦½ git 紀錄後發ç¾åœ¨é€™å…©å€‹åœ°æ–¹å…¶å¯¦åŽŸæœ¬éƒ½æ˜¯æ²’æœ‰è½‰æ›å¤§å°å¯«çš„,而是分別在後來的兩個 PR 中加上的

  • PR #30 - Delete fails in IE8/IE9, the verb needs to be capitalized.
    在 open() 方法中統一轉大寫是為了解決 ActiveXObject 在 IE8/IE9 環境必須使用大寫的å•題。雖然 axios 早已沒有使用 ActiveXObject 物件,但這轉æ›ä¾ç„¶ç•™äº†ä¸‹ä¾†ã€‚
    ä¸éŽå¦å¤–在 whatwg - fetch 2.2.1. Methods 中有æåˆ°ï¼š

    To normalize a method, if it is a byte-case-insensitive match for DELETE, GET, HEAD, OPTIONS, POST, or PUT, byte-uppercase it.


    Using patch is highly likely to result in a 405 Method Not Allowed. PATCH is much more likely to succeed.


    看起來 DELETEã€GETã€HEADã€OPTIONSã€POSTã€æˆ– PUT 使用大å°å¯«éƒ½å¯ä»¥ï¼Œä½†é‡åˆ°åˆ°ä½¿ç”¨ patch 有極高的å¯èƒ½æœƒç™¼ç”ŸéŒ¯èª¤ï¼Œä½¿ç”¨ PATCH 更有å¯èƒ½æˆåŠŸï¼ˆæˆ‘çš„ç¶“é©—æ˜¯ PATCH 一定è¦å¤§å¯«ï¼‰ã€‚
  • PR #912 與 #930 - Convert the method parameter to lowercase
    å¦å¤–在 Axios.prototype.request() å°‡ method 全部轉為å°å¯«å‰‡æ˜¯èªªä»¥ä¸‹ç‹€æ³æœƒç™¼ç”ŸéŒ¯èª¤ï¼š
    axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded';
    
    return axios({
      method: 'POST',
      url: url
    });
    

    é€™è£¡ä¹‹å‰æ²’有特別æåˆ°ï¼Œåœ¨ Axios.prototype.request() ä¸­å‘¼å« dispatchRequest 這個方法到使用é©é…å™¨ç™¼å‡ºè«‹æ±‚å‰æœƒå°‡ config.headers.commonã€config.headers[config.method] || {}ã€config.headers åˆä½µæˆä¸€å€‹æ–°ç‰©ä»¶ 賦值回 config.headers 中:
    // dispatchRequest.js
    
    // Flatten headers
    config.headers = utils.merge(
      config.headers.common || {},
      config.headers[config.method] || {},
      config.headers
    );
    

    ä¾ç…§å‰é¢çš„範例,在åˆä½µä¹‹å‰ method 如果沒有統一轉為å°å¯«ï¼Œåœ¨é€™è£¡å°±ç„¡æ³•å–得正確的值而造æˆéŒ¯èª¤ã€‚

設定超時(timeout)

在 XMLHttpRequest 中å¯ä»¥åœ¨æ¯å€‹è«‹æ±‚中設定該請求的時間(毫秒)é™åˆ¶ï¼Œè¨­å®š 0 則表示沒有超時é™åˆ¶ï¼Œå¦‚果在時間é™åˆ¶å…§æ²’有å–得回應則拋出錯誤。

// Set the request timeout in MS
request.timeout = config.timeout;

如果因為請求超時拋出錯誤,å¯ä»¥ç›£è½ timeout 事件,或設定 ontimeout 事件處ç†å™¨å°éŒ¯èª¤çš„後續åšè™•ç†ã€‚

設定éžåŒæ­¥è³‡æ–™éŸ¿æ‡‰è™•ç†

// Listen for ready state
request.onreadystatechange = function handleLoad() {
  if (!request || request.readyState !== 4) {
    return;
  }

  // The request errored out and we didn't get a response, this will be
  // handled by onerror instead
  // With one exception: request that using file: protocol, most browsers
  // will return status as 0 even though it's a successful request
  if (request.status === 0 && !(request.responseURL && request.responseURL.indexOf('file:') === 0)) {
    return;
  }

  var responseHeaders = 'getAllResponseHeaders' in request ? parseHeaders(request.getAllResponseHeaders()) : null;
  var responseData = !config.responseType || config.responseType === 'text' ? request.responseText : request.response;
  var response = {
    data: responseData,
    status: request.status,
    statusText: request.statusText,
    headers: responseHeaders,
    config: config,
    request: request
  };

  settle(resolve, reject, response);

  // Clean up request
  request = null;
};

// Add responseType to request if needed
if (config.responseType) {
  try {
    request.responseType = config.responseType;
  } catch (e) {
    if (config.responseType !== 'json') {
      throw e;
    }
  }
}

ç¢ºèªæ˜¯å¦å®Œæˆï¼Œonreadystatechange 與 readyState

因為 onreadystatechange 是æ¯ç•¶ readyState 有更動時就會被呼å«ï¼Œæ‰€ä»¥éœ€éœ€è¦äº†è§£ readyState åŠå…¶å«ç¾©ï¼Œå¦‚下:

值狀態說明
0UNSENTXMLHttpRequest 物件已經被åˆå§‹åŒ–ï¼Œä½†é‚„æ²’æœ‰å‘¼å« open() 方法。
1OPENEDopen() 已經被呼å«ï¼Œå†æ¬¡æœŸé–“å¯ä½¿ç”¨ setRequestHeader() 設定 Request Header,並å¯å‘¼å« send() 方法來發é€è«‹æ±‚。
2HEADERS_RECEIVEDsend() 方法已被呼å«ï¼Œä¸¦ä¸”已接收到 Response Header
3LOADING正在接收資料中。
4DONE請求æ“作完æˆã€‚

由此å¯çŸ¥ï¼Œç•¶ç”±ç•¶è«‹æ±‚çµæŸå¾Œï¼ŒreadyState 的值為 4ï¼Œåœ¨é€™ä¹‹å¤–çš„å…¶ä»–å€¼éƒ½ä¸æ˜¯æˆ‘們è¦çš„,å¯ä»¥ç›´æŽ¥è·³å‡ºã€‚

產生資料

確èªã€Œè«‹æ±‚æ“作完æˆã€å¾Œç¶“éŽæ•´ç†å°±å¹¾ä¹Žæœƒæ˜¯æˆ‘們平常接收到的資料。

由原始碼å¯ä»¥å¾—知,如果我們沒有設定 config.responseType(é è¨­ç‚º '')或是 config.responseType 設定為 'text',會å–用 XMLHttpRequest.prototype.responseText,其餘都會使用 XMLHttpRequest.prototype.response 的值,這個值ç€è¦½å™¨æœƒä¾æ“šæˆ‘å€‘è¨­å®šçš„ XMLHttpRequest.prototype.responseType 幫我們轉æ›ã€‚

補充:

注æ„到 XMLHttpRequest.prototype.responseText 這個屬性,如果在åˆå§‹åŒ–時設定 responseType 「ä¸ç‚ºã€'' 或是 'text' 的話,ç€è¦½å™¨æœƒä¸Ÿå‡ºä¸€å€‹éŒ¯èª¤ï¼Œåƒé€™æ¨£ï¼š

Uncaught DOMException: Failed to read the 'responseText' property from 'XMLHttpRequest': The value is only accessible if the > object's 'responseType' is '' or 'text' (was 'json').

最後在 settle() 中會å†é©—è­‰ response.status 的值是å¦åœ¨ 200 至 300 之間(é è¨­ï¼‰ä¾†æ±ºå®šè¦ resolve 承諾還是 reject。

設定錯誤處ç†

在目å‰çš„ axios 中,有å°ä¸‰ç¨®ç‹€æ³åšéŒ¯èª¤çš„後續處ç†ï¼Œä¸€å€‹æ˜¯å‰é¢æåˆ°çš„æ“æ™‚éŒ¯èª¤è™•ç†ï¼šontimeoutï¼Œå…¶ä»–åˆ†åˆ¥æ˜¯å–æ¶ˆè«‹æ±‚çš„ onabort以åŠç¶²è·¯ç™¼ç”ŸéŒ¯èª¤çš„ onerror。

request.onabort = function handleAbort() {
  if (!request) {
    return;
  }

  reject(createError('Request aborted', config, 'ECONNABORTED', request));

  request = null;
};

request.onerror = function handleError() {
  reject(createError('Network Error', config, null, request));

  request = null;
};

request.ontimeout = function handleTimeout() {
  var timeoutErrorMessage = 'timeout of ' + config.timeout + 'ms exceeded';
  if (config.timeoutErrorMessage) {
    timeoutErrorMessage = config.timeoutErrorMessage;
  }
  reject(createError(timeoutErrorMessage, config, 'ECONNABORTED', request));

  request = null;
};

在æ¯å€‹éŒ¯éŒ¯èª¤è™•ç†ä¸­éƒ½æœƒ reject 該錯誤,並且帶入錯誤訊æ¯ã€‚å¦å¤–å¯ä»¥å…ˆæ³¨æ„到在這裡的 onabort 是有執行 reject() çš„ï¼Œå¾Œé¢æœƒå†æåˆ°ã€‚

最後這裡å¯ä»¥çœ‹åˆ°æ–‡ä»¶ä¸Šæ²’有æåˆ°çš„ï¼šã€Œè¶…æ™‚éŒ¯èª¤è¨Šæ¯æ˜¯å¯ä»¥è‡ªå®šç¾©çš„ã€ã€‚(這就是深入閱讀原始碼的å°ç¢ºå¹¸ï¼‰

防止 XSRF(CSRF) 攻擊

在 axios 請求中有 3 個屬性在這裡會用到:

  • withCredentials:布林值,設定 XMLHttpRequest.prototype.withCredentials。這個部分設定了,當為跨域請求,是å¦è¦å¤¾å¸¶æ†‘證資料。如果跨域並設定為 true,那在 Server çš„ Response Header è¦åŠ ä¸Š Access-Control-Allow-Origin,並且必須指定來æºä¸å¾—為 *。
  • xsrfCookieName:如果 withCredentials 為 true,axios 會根據指定的 xsrfCookieName 去 cookie 中å–得值,並帶入 Request Header 中
  • xsrfHeaderName:如果 xsrfCookieName 在 cookie 中有å–得值,則把該值設定到 requestHeaders[config.xsrfHeaderName] 中,é€çµ¦å¾Œç«¯é©—證。
// Add xsrf header
// This is only done if running in a standard browser environment.
// Specifically not if we're in a web worker, or react-native.
if (utils.isStandardBrowserEnv()) {
  var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName 
    ? cookies.read(config.xsrfCookieName) 
    : undefined;

  if (xsrfValue) {
    requestHeaders[config.xsrfHeaderName] = xsrfValue;
  }
}

if (!utils.isUndefined(config.withCredentials)) {
  request.withCredentials = !!config.withCredentials;
}

關於 XSRF 攻擊究竟是什麼,推薦閱讀由胡立(huli)寫的:讓我們來談談 CSRF。

設定 HTTP Authorization

if (config.auth) {
  var username = config.auth.username || '';
  var password = config.auth.password ? unescape(encodeURIComponent(config.auth.password)) : '';
  requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password);
}

這部分在原始碼中其實是在åˆå§‹åŒ–å‰ï¼Œä¸éŽç‚ºäº†æ•´é«”æµæš¢æ€§ï¼Œæˆ‘把它放在這裡。但這個部份有沒有需è¦ä½¿ç”¨éœ€è¦è·Ÿå¾Œç«¯è¨Žè«–,至於沒有設定 config.authï¼Œå°±å®Œå…¨ä¸æœƒåŸ·è¡Œé€™ä¸€æ®µå›‰ï¼

設定 Request Header

在發起請求時我們å¯ä»¥åœ¨ c‡Áx>onfig.headers 設定我們è¦å‚³é€çš„ Request Headers,在這裡就會將我們希望傳é€çš„屬性寫進請求的 Header 中。而如果è¦è¨­å®š XMLHttpRequest çš„ Request Header 則需è¦ä½¿ç”¨ XMLHttpRequest.prototype.setRequestHeader() 方法。

語法:

XMLHttpRequest.setRequestHeader(header, value)

型別:

setRequestHeader(name: string, value: string): void;

這部分很單純åªè¦æŠŠ config.headers 中的屬性與值é€éŽé€™å€‹æ–¹æ³•設定到請求當中就好了,å¦å¤–如果請求沒有帶 data 則會把 Content-Type 這個 Request Headers 刪除。

// Add headers to the request
if ('setRequestHeader' in request) {
  utils.forEach(requestHeaders, function setRequestHeader(val, key) {
    if (typeof requestData === 'undefined' && key.toLowerCase() === 'content-type') {
      // Remove Content-Type if data is undefined
      delete requestHeaders[key];
    } else {
      // Otherwise add header to the request
      request.setRequestHeader(key, val);
    }
  });
}

支æ´ä¸Šå‚³èˆ‡ä¸‹è¼‰é€²åº¦

如果是有檔案下載或上傳進度顯示需求,å¯ä»¥é€éŽè¨­å®š config.onDownloadProgress 與 config.onUploadProgress 傳入自定義的事件監è½å™¨ä¾†è™•ç†ã€‚

// Handle progress if needed
if (typeof config.onDownloadProgress === 'function') {
  request.addEventListener('progress', config.onDownloadProgress);
}

// Not all browsers support upload events
if (typeof config.onUploadProgress === 'function' && request.upload) {
  request.upload.addEventListener('progress', config.onUploadProgress);
}

如果想知é“è¦æ€Žéº¼æ‡‰ç”¨ï¼Œé€™è£¡æœ‰ä¸€å€‹ç¯„例å¯ä»¥åƒè€ƒï¼šaxios 檔案下載進度範例

å–æ¶ˆè«‹æ±‚

在 XMLHttpRequest 物件中,我們å¯ä»¥é€éŽ XMLHttpRequest.prototype.abort() 來喿¶ˆä¸€å€‹é‚„在等待資料的請求。而 axios å‰‡åŒ…è£æˆä½¿ç”¨ axios.CancelTokn 這個類別來æ“ä½œã€‚é—œæ–¼é€™å€‹é¡žåˆ¥åœ¨å¾Œé¢æœƒä¾†è¨Žè«–。

å¯èƒ½æœ‰äººæ²’用éŽï¼Œæˆ‘們è¦å…ˆçŸ¥é“在一種在 axios å–æ¶ˆè«‹æ±‚的方å¼ï¼š

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // An executor function receives a cancel function as a parameter
    cancel = c;
  })
});

// cancel the request
cancel();

在這個建構出來的 CancelToken 實例中有一個 CancelToken.prototype.promise 的 Promise 物件,在執行 cancel() 後會 resolve(CancelToken 實例內部的 resolve)這個承諾。

接著來看到在 axios 中é‡å°å–消請求後會åšä»€éº¼ï¼

if (config.cancelToken) {
  // Handle cancellation
  config.cancelToken.promise.then(function onCanceled(cancel) {
    if (!request) {
      return;
    }

    request.abort();
    reject(cancel);
    // Clean up request
    request = null;
  });
}

在接到 resolve å–æ¶ˆè«‹æ±‚後會執行 XMLHttpRequest.prototype.abort(),這裡就會觸發 onabort 事件處ç†å™¨ä¸¦æŽ¥è‘—執行 reject()。

還記得å‰é¢ç‰¹åˆ¥è«‹å¤§å®¶è¨˜å¾— onabort 事件處ç†å™¨ä¸­ä¹ŸæœƒåŸ·è¡Œ reject() 嗎?

request.onabort = function handleAbort() {
  if (!request) {
    return;
  }

  reject(createError('Request aborted', config, 'ECONNABORTED', request));

  request = null;
};

在執行 XMLHttpRequest.prototype.abort() 後,如果有設定 onabort 事件處ç†å™¨ï¼Œæœƒå…ˆè·‘完事件處ç†å™¨æ‰å¾€ä¸‹èµ°ï¼Œä¹Ÿå°±æ˜¯èªª request.abort() 到 reject(cancel) 之間其實已經跑éŽä¸€æ¬¡ reject(createError('Request aborted', config, 'ECONNABORTED', request)),所以ç†è«–上 reject(cancel); 執行了也沒有任何效果,我們ä¸èƒ½ reject() 兩次。

但å¯èƒ½æ˜¯å› ç‚ºé€™å…©æ®µæ˜¯ä¸åŒäººè™•ç†çš„,因此這裡出ç¾äº†ä¸€é»žé»žå°ç‘•ç–µå§ï¼é›–然有人æå‡ºéŽç›¸é—œç–‘惑但目å‰ä»ç„¶æ²’有回應。

é€å‡ºè«‹æ±‚

ç¶“éŽä¸Šé¢å„種設定,終於走到最後è¦é€å‡ºè«‹æ±‚了,我們需è¦ç”¨ XMLHttpRequest.prototype.open() 來é€å‡ºè«‹æ±‚,他的定語法語與型別定義如下:

語法:

XMLHttpRequest.send(body)

型別:

send(body?: Document | BodyInit | null): void

所以我們åªè¦å°‡è¦å‚³é€çš„資料(body)帶入就å¯ä»¥äº†ï¼Œaxios 的原始碼:

if (!requestData) {
  requestData = null;
}

// Send the request
request.send(requestData);

ä¸éŽåˆä¸€å€‹å¥‡æ€ªçš„æ˜¯ï¼Œç‚ºä»€éº¼å¦‚果當 requestData 為空時(å¯èƒ½ç‚º '', 0, undefined ...等)è¦ç‰¹åœ°å°‡å®ƒè¨­å®šç‚º null 呢?

追查後發ç¾é€™æ˜¯åœ¨ React Native 上 Android å¹³å°çš„一個蟲å­ã€‚我把修復該錯誤的 PR 附在這裡(#PR 1487 - fix 'Network Error' in react native android)。

XMLHttpRequest 部分å°çµèªž

到這裡,我們已經看完了 axios 使用 XMLHttpRequest 的部分。但關於 XMLHttpRequest 還有超多細節å¯ä»¥æ·±å…¥è‘—墨,這裡æåˆ°çš„çœŸçš„åªæ˜¯å†°å±±ä¸€è§’角,有興趣的å¯ä»¥ç¹¼çºŒæ·±å…¥ç ”究。

axios å–æ¶ˆè«‹æ±‚çš„ CancelToken 類別設計

å–æ¶ˆè«‹æ±‚我覺得是一個蠻有趣的功能,也是我入å‘研究 axios 的起頭原因之一。這邊會先了解他的用法,接著了解 CancelToken 類別設計。

å–æ¶ˆè«‹æ±‚的用法

é€™å€‹éƒ¨åˆ†å€‘å…ˆä¾†çœ‹çœ‹å®˜æ–¹æ–‡ä»¶å–æ¶ˆè«‹æ±‚的範例:

const CancelToken = axios.CancelToken;
let cancel;

axios.get('/user/12345', {
  cancelToken: new CancelToken(function executor(c) {
    // An executor function receives a cancel function as a parameter
    cancel = c;
  })
});

// cancel the request
cancel();

或是

const CancelToken = axios.CancelToken;
const source = CancelToken.source();

axios.get('/user/12345', {
  cancelToken: source.token
}).catch(function (thrown) {
  if (axios.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});

axios.post('/user/12345', {
  name: 'new name'
}, {
  cancelToken: source.token
})

// cancel the request (the message parameter is optional)
source.cancel('Operation canceled by the user.');

æ•´ç†ä¸€ä¸‹ç”¨æ³•:

  • 使用 new CancelToken() 建立一個 CancelToken 實例,這個類別建構時接收一個 executor function,這個 function 會接到一個 cancel(範例中為 c) çš„ funciotn,å¯ä»¥å°‡å…¶å­˜èµ·ä¾†ï¼Œå¦‚æžœè¦å–消請求則執行這個 function,並å¯ä»¥å¸¶å…¥å–消的訊æ¯ã€‚
  • 使用 CancelToken.source() 回傳一個 source 物件,並把 source.token 屬性傳給 config.cancelToken,如果è¦å–消請求則執行 source.cancel() 一樣å¯ä»¥å¸¶å…¥å–消的訊æ¯ã€‚
  • 在 axios 上æä¾›äº†ä¸€å€‹ isCancel 的方法,給使用者確èªé€™æ¬¡çš„éŒ¯èª¤æ˜¯ä¸æ˜¯ç”±å–消請求拋出的。

有了這些基礎資訊,就å¯ä»¥è©¦è‘—來實作看看囉ï¼

CancelToken 類別設計

收先我們需è¦ä¸€å€‹ CancelToken çš„é¡žåˆ¥ï¼Œä¸Šé¢æœ‰ä¸€å€‹promise 屬性,而建構 CancelToken 需è¦å‚³å…¥ä¸€å€‹ executor function ç•¶åƒæ•¸å‚³å…¥ï¼Œè€Œé€™å€‹ function å‰‡åˆæœƒæ”¶åˆ°ä¸€å€‹å–消用的 function。所以程å¼ç¢¼çš„部分會åƒé€™æ¨£ï¼š

function CancelToken(executor) {
  if (typeof executor !== 'function') {
    throw new TypeError('executor must be a function.');
  }

  var resolvePromise;
  this.promise = new Promise(function promiseExecutor(resolve) {
    resolvePromise = resolve;
  });

  executor(resolvePromise);
}

CancelToken.prototype.promise 部分是一個 Promise 物件,我們需è¦å…ˆæŠŠä»–çš„ resolve() 存起來,傳給使用者。如果使用者è¦å–消請求,就å¯ä»¥åŸ·è¡Œè©² resolve()

å¦‚æžœå–æ¶ˆï¼Œå‰‡æ‹‹å‡ºéŒ¯èª¤

接著我們需è¦ä¸€å€‹æ–¹æ³•,æä¾›åœ¨å¹¾å€‹æ™‚機點檢查是å¦å·²ç¶“å–æ¶ˆè«‹æ±‚ï¼Œå¦‚æžœå–æ¶ˆäº†å°±æ‹‹å‡ºä¸€å€‹éŒ¯èª¤ï¼Œè€Œè‘—個喿¶ˆçš„æ™‚機點則是看 CancelToken.prototype.reason 是å¦å·²ç¶“è¢«è³¦äºˆå–æ¶ˆçš„ç†ç”±ã€‚

所以上é¢çš„ CancelToken 會這樣調整

function CancelToken(executor) {
  // ç•¥

  var token = this;
  executor(function cancel(message) {
    if (token.reason) {
      return;
    }

    token.reason = new Cancel(message);
    resolvePromise(token.reason);
  });
}

有了上é¢çš„調整,就å¯ä»¥ä¾†æ™‚作檢查機制 CancelToken.prototype.throwIfRequested()ï¼Œåœ¨è£¡é¢æˆ‘們åªè¦åˆ¤æ–· CancelToken.prototype.reason 有沒有值就知é“è©²è«‹æ±‚æœ‰æ²’æœ‰è¢«å–æ¶ˆã€‚

CancelToken.prototype.throwIfRequested = function throwIfRequested() {
  if (this.reason) {
    throw this.reason;
  }
};

到這裡 CancelToken 類別還缺一個功能,å‰é¢æåˆ°æˆ‘們也å¯ä»¥ç”¨ CancelToken.source() å–得一個 source 物件,並且將 source.token 傳給 config.token 以åŠè¦å–消時就執行 source.cancel。

其實 CancelToken.source é€™å€‹éœæ…‹æ–¹æ³•本質上還是建構了一個 CancelToken å¯¦ä¾‹ï¼Œä¸¦å°‡å»ºç«‹çš„å¯¦ä¾‹èˆ‡å–æ¶ˆçš„ function 包æˆç‰©ä»¶å›žå‚³ã€‚

CancelToken.source = function source() {
  var cancel;
  var token = new CancelToken(function executor(c) {
    cancel = c;
  });
  return {
    token: token,  // CancelToken 實例
    cancel: cancel // å–æ¶ˆçš„ function
  };
};

看完兩種方法的設計方å¼ï¼Œåœ¨æ‡‰ç”¨ä¸Šå¯ä»¥è‡ªè¡Œé¸æ“‡å–œæ­¡çš„ç”¨æ³•ï¼Œåæ­£æœ¬è³ªä¸Šæ˜¯åœ¨åšåŒä¸€ä»¶äº‹æƒ…。

Cancel 類別與 isCancel()

在 CancelToken 類別中,我們呼å«äº† cancel() ä¸¦å‚³å…¥äº†ä¸€æ®µå–æ¶ˆçš„ message,而這個 message 會當作 Cancel é¡žåˆ¥å»ºæ§‹æ™‚çš„åƒæ•¸ã€‚

function Cancel(message) {
  this.message = message;
}

Cancel.prototype.toString = function toString() {
  return 'Cancel' + (this.message ? ': ' + this.message : '');
};

module.exports = Cancel;

這邊å¯ä»¥çœ‹åˆ° Cancel 類別上還有一個 toString() 的方法,這邊å¯ä»¥å°å°ç§‘普一個觀念:物件上或物件的原型éŠä¸Šå¦‚果有 toString() 這個方法,當該物件轉字串後就會是這個方法的回傳值。

例如:

const obj1 = {
  key: 'value'
}

const obj2 = {
  toString() {
    return '這是一個字串'
  }
}

console.log(`${obj1}`)
console.log(`${obj2}`)

因為在轉字串的éŽç¨‹ä¸­ï¼Œå¯¦éš›ä¸Šå°±æ˜¯å‘¼å«äº† toString() 這個方法,所以輸出分別會是:

[object Object]
這是一個字串

由此å¯çŸ¥ï¼Œç•¶æŽ¥åˆ°å–消請求的錯誤,我們å¯ä»¥é€™æ¨£ä½¿ç”¨ console.log('Request canceled' + thrown.message) å°±å¯ä»¥å°‡æˆ‘å€‘å‚³å…¥çš„éŒ¯èª¤è¨Šæ¯æŽ¥åœ¨ Request canceled 後é¢å›‰ï¼

最後關於 isCancel() 的實作則是先在 Cancel 的實例上新增一個 __CANCEL__ 的屬性。

Cancel.prototype.__CANCEL__ = true;

é€™æ¨£ç•¶å‘¼å« isCancel() ä»–åªè¦ç¢ºèªå‚³å…¥çš„物件 __CANCEL__ æ˜¯å¦æœ‰å€¼å³å¯ï¼š

module.exports = function isCancel(value) {
  return !!(value && value.__CANCEL__);
};

æ•´å€‹å–æ¶ˆè«‹æ±‚的設計就差ä¸å¤šæ˜¯é€™æ¨£ã€‚

在一些教學中會æåˆ°ä½¿ç”¨å–æ¶ˆè«‹æ±‚çš„æ™‚æ©Ÿï¼Œåƒæ˜¯ç•¶æœ‰é‡è¤‡çš„è«‹æ±‚å…ˆå–æ¶ˆä¸Šä¸€æ¬¡çš„請求å†åŽ»ç™¼æ–°çš„è«‹æ±‚ï¼Œä»¥æ¸›å°‘å¾Œç«¯çš„è² æ“”ã€‚ä½†åœ¨è·Ÿå¾Œç«¯å·¥ç¨‹å¸«èŠéŽé€™å€‹å•題後發ç¾ï¼Œå°±ç®—å‰ç«¯æœ‰ç™¼å‡ºå–æ¶ˆè«‹æ±‚ï¼Œä½†å¦‚æžœå¾Œç«¯æ²’æœ‰å¯¦ä½œé€™ä¸€æ®µï¼Œä½ ä»¥ç‚ºå–æ¶ˆäº†ï¼Ÿä½†å…¶å¯¦å¾Œç«¯é‚„是處ç†äº†ä¸€å€‹å®Œæ•´çš„è«‹æ±‚ï¼Œä½ ä»¥ç‚ºå–æ¶ˆäº†åˆªé™¤çš„請求,後端其實還是執行了刪除。

çµèªž

本篇中,我們花了很大的篇幅é€éŽ axios 的原始碼èªè­˜äº† XMLHttpRequset 這個類別,知é“如何åˆå§‹åŒ–請求ã€è¨­å®šç´°ç¯€ä»¥åŠé€å‡ºè«‹æ±‚。具有比較疑惑的部分也從éŽåŽ»çš„ git 紀錄中,找到貢ç»äº†è€…é‡åˆ°çš„å•題與解決方å¼ã€‚

æœ€å¾Œæˆ‘å€‘å¿«é€Ÿåœ°çœ‹éŽ axios å–æ¶ˆè«‹æ±‚的設計,也æåˆ°ä»»ä½•物件在字串化的éŽç¨‹ä¸­å…¶å¯¦éƒ½æ˜¯å‘¼å«äº†åŽŸåž‹éŠçš„ toString() æ–¹æ³•ï¼Œæœ€å¾Œä¹Ÿåˆ†äº«äº†æˆ‘å°æ–¼å–消請求的一些看法。

在 axios 的設計éžå¸¸å½ˆæ€§ï¼Œå¾ˆå¤šæ±è¥¿éƒ½æ˜¯å¯ä»¥æ›æˆè‡ªå·±æƒ³è¦çš„的,例如你覺得 XMLHttpRequest å¤ªè€æ´¾äº†ï¼Œæƒ³ç”¨åˆé…·åˆç‚«çš„ fetch,就å¯ä»¥è‡ªå·±åŒ…好以 fetch 為基底的é©é…器設定在 config.adapter 上;希望ä¸è«–狀態碼 400ã€500 都ä¸è¦æ‹‹å‡ºéŒ¯èª¤ï¼Œä¹Ÿå¯ä»¥è‡ªå·±å¯«é©—è­‰é‚輯到 config.validateStatus,彈性真的éžå¸¸å¤§ã€‚這些內容都在原始碼的 default.js 中,有興趣å¯ä»¥åŽ»äº†è§£äº†è§£ã€‚

åƒè€ƒè³‡æ–™

è«‹æˆ‘å–æ¯å’–å•¡

如果這裡的內容有幫助到你的話,一æ¯å’–å•¡å°±æ˜¯å°æˆ‘最大的鼓勵。

è«‹æˆ‘å–æ¯å’–å•¡
ÿÿÿÿ