這應該是一個大多數都經常使用的請求庫,由於它能夠支持多種配置,跨平臺實現,返回promise進行鏈式調用.徹底過一遍源碼能夠提高本身對請求庫的理解知識node
axios源碼系列(一) --- 目錄結構和工具函數
axios源碼系列(二) --- 適配器內部
axios源碼系列(三) --- 默認配置和取消請求
axios源碼系列(四) --- Axios和dispatchRequest與攔截器react
核心是nodejs的http(s).request方法進行請求ios
var utils = require('./../utils'); var settle = require('./../core/settle'); var buildFullPath = require('../core/buildFullPath'); var buildURL = require('./../helpers/buildURL'); var http = require('http'); var https = require('https'); var httpFollow = require('follow-redirects').http; var httpsFollow = require('follow-redirects').https; var url = require('url'); var zlib = require('zlib'); var pkg = require('./../../package.json'); var createError = require('../core/createError'); var enhanceError = require('../core/enhanceError'); var isHttps = /https:?/;
咱們先看看裏面引用的一些庫做用git
庫 | 做用 |
---|---|
http | http請求庫 |
https | https請求庫 |
follow-redirects | 替代nodejs的http和https模塊,自動跟隨重定向。 |
url | 解析和格式化url |
zlib | 簡單,同步壓縮或解壓node.js buffers. |
裏面還有其餘的內置模塊github
var createError = require('./createError'); /** * Resolve or reject a Promise based on response status. * * @param {Function} resolve A function that resolves the promise. * @param {Function} reject A function that rejects the promise. * @param {object} response The response. */ module.exports = function settle(resolve, reject, response) { var validateStatus = response.config.validateStatus; if (!validateStatus || validateStatus(response.status)) { resolve(response); } else { reject(createError( 'Request failed with status code ' + response.status, response.config, null, response.request, response )); } };
在獲得響應請求的基礎上,決定返回成功或者失敗的Promise態,當失敗的時候會建立自定義錯誤web
var enhanceError = require('./enhanceError'); /** * Create an Error with the specified message, config, error code, request and response. * * @param {string} message The error message. * @param {Object} config The config. * @param {string} [code] The error code (for example, 'ECONNABORTED'). * @param {Object} [request] The request. * @param {Object} [response] The response. * @returns {Error} The created error. */ module.exports = function createError(message, config, code, request, response) { var error = new Error(message); return enhanceError(error, config, code, request, response); };
有點中轉站的意思,只負責建立錯誤對象,至於修改的任務則交給enhanceError
函數算法
/** * Update an Error with the specified config, error code, and response. * * @param {Error} error The error to update. * @param {Object} config The config. * @param {string} [code] The error code (for example, 'ECONNABORTED'). * @param {Object} [request] The request. * @param {Object} [response] The response. * @returns {Error} The error. */ module.exports = function enhanceError(error, config, code, request, response) { error.config = config; if (code) { error.code = code; } error.request = request; error.response = response; error.isAxiosError = true; error.toJSON = function() { return { // Standard message: this.message, name: this.name, // Microsoft description: this.description, number: this.number, // Mozilla fileName: this.fileName, lineNumber: this.lineNumber, columnNumber: this.columnNumber, stack: this.stack, // Axios config: this.config, code: this.code }; }; return error; };
返回包裝傳入的錯誤對象,儘量多得信息賦值到錯誤對象上,其中包括部分瀏覽器才提供的錯誤信息json
/*eslint consistent-return:0*/ module.exports = function httpAdapter(config) { return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) { var resolve = function resolve(value) { resolvePromise(value); }; var reject = function reject(value) { rejectPromise(value); }; var data = config.data; var headers = config.headers; // Set User-Agent (required by some servers) // Only set header if it hasn't been set in config // See https://github.com/axios/axios/issues/69 if (!headers['User-Agent'] && !headers['user-agent']) { headers['User-Agent'] = 'axios/' + pkg.version; } // ---省略部分代碼--- }); };
傳入請求的配置信息,返回新的Promise,而且將改變狀態的觸發函數賦值到變量,當請求頭不包含User-Agent
或者user-agent
的時候默認賦值axios的特有信息,pkg
是axios的package.json
.axios
/*eslint consistent-return:0*/ module.exports = function httpAdapter(config) { return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) { // ---省略部分代碼--- if (data && !utils.isStream(data)) { if (Buffer.isBuffer(data)) { // Nothing to do... } else if (utils.isArrayBuffer(data)) { data = Buffer.from(new Uint8Array(data)); } else if (utils.isString(data)) { data = Buffer.from(data, 'utf-8'); } else { return reject(createError( 'Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream', config )); } // Add Content-Length header if data exists headers['Content-Length'] = data.length; } // ---省略部分代碼--- }); };
在有data數據而且不爲流的狀況下,根據data
的數據類型作對應轉換並設置Content-Length
爲data
的長度,都不符合的狀況下返回錯誤態Promise而且中斷流程.segmentfault
/*eslint consistent-return:0*/ module.exports = function httpAdapter(config) { return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) { // ---省略部分代碼--- // HTTP basic authentication var auth = undefined; if (config.auth) { var username = config.auth.username || ''; var password = config.auth.password || ''; auth = username + ':' + password; } // Parse url var fullPath = buildFullPath(config.baseURL, config.url); var parsed = url.parse(fullPath); var protocol = parsed.protocol || 'http:'; if (!auth && parsed.auth) { var urlAuth = parsed.auth.split(':'); var urlUsername = urlAuth[0] || ''; var urlPassword = urlAuth[1] || ''; auth = urlUsername + ':' + urlPassword; } if (auth) { delete headers.Authorization; } var isHttpsRequest = isHttps.test(protocol); var agent = isHttpsRequest ? config.httpsAgent : config.httpAgent; var options = { path: buildURL(parsed.path, config.params, config.paramsSerializer).replace(/^\?/, ''), method: config.method.toUpperCase(), headers: headers, agent: agent, agents: { httpsAgent: config.httpsAgent, httpAgent: config.httpAgent }, auth: auth }; if (config.socketPath) { options.socketPath = config.socketPath; } else { options.hostname = parsed.hostname; options.port = parsed.port; } // ---省略部分代碼--- }); };
auth
,則拼接用戶名和密碼,能夠爲空;auth
信息的狀況下要刪除Authorization
頭,即"用戶名+冒號+密碼"用BASE64算法加密後的字符串;https
協議下獲取配置的httpsAgent
信息,不然拿httpAgent
信息;options
中;socket
路徑或者hostname
和port
;HTTP Authorization請求標頭包含用於向服務器認證用戶代理的憑證,一般在服務器響應401 Unauthorized狀態和WWW-Authenticate標題後。
當服務器收到請求的時候,當設置了須要驗證信息,若是請求頭帶有Authorization,會檢查裏面的內容是否在用戶列表中
若是請求頭帶有Authorization
,會檢查裏面的內容是否在用戶列表中
401
狀態碼,瀏覽器會彈出對話框讓用戶輸入帳號密碼上面還提到幾個方法,分別是
var isAbsoluteURL = require('../helpers/isAbsoluteURL'); var combineURLs = require('../helpers/combineURLs'); /** * Creates a new URL by combining the baseURL with the requestedURL, * only when the requestedURL is not already an absolute URL. * If the requestURL is absolute, this function returns the requestedURL untouched. * * @param {string} baseURL The base URL * @param {string} requestedURL Absolute or relative URL to combine * @returns {string} The combined full path */ module.exports = function buildFullPath(baseURL, requestedURL) { if (baseURL && !isAbsoluteURL(requestedURL)) { return combineURLs(baseURL, requestedURL); } return requestedURL; };
返回完整且規範的絕對地址
/** * Determines whether the specified URL is absolute * * @param {string} url The URL to test * @returns {boolean} True if the specified URL is absolute, otherwise false */ module.exports = function isAbsoluteURL(url) { // A URL is considered absolute if it begins with "<scheme>://" or "//" (protocol-relative URL). // RFC 3986 defines scheme name as a sequence of characters beginning with a letter and followed // by any combination of letters, digits, plus, period, or hyphen. return /^([a-z][a-z\d\+\-\.]*:)?\/\//i.test(url); };
正則判斷是否絕對路徑
/** * Creates a new URL by combining the specified URLs * * @param {string} baseURL The base URL * @param {string} relativeURL The relative URL * @returns {string} The combined URL */ module.exports = function combineURLs(baseURL, relativeURL) { return relativeURL ? baseURL.replace(/\/+$/, '') + '/' + relativeURL.replace(/^\/+/, '') : baseURL; };
拼裝而且規範合併地址
var utils = require('./../utils'); function encode(val) { return encodeURIComponent(val). replace(/%40/gi, '@'). replace(/%3A/gi, ':'). replace(/%24/g, '$'). replace(/%2C/gi, ','). replace(/%20/g, '+'). replace(/%5B/gi, '['). replace(/%5D/gi, ']'); } /** * Build a URL by appending params to the end * * @param {string} url The base of the url (e.g., http://www.google.com) * @param {object} [params] The params to be appended * @returns {string} The formatted url */ module.exports = function buildURL(url, params, paramsSerializer) { /*eslint no-param-reassign:0*/ if (!params) { return url; } var serializedParams; if (paramsSerializer) { serializedParams = paramsSerializer(params); } else if (utils.isURLSearchParams(params)) { serializedParams = params.toString(); } else { var parts = []; utils.forEach(params, function serialize(val, key) { if (val === null || typeof val === 'undefined') { return; } if (utils.isArray(val)) { key = key + '[]'; } else { val = [val]; } utils.forEach(val, function parseValue(v) { if (utils.isDate(v)) { v = v.toISOString(); } else if (utils.isObject(v)) { v = JSON.stringify(v); } parts.push(encode(key) + '=' + encode(v)); }); }); serializedParams = parts.join('&'); } if (serializedParams) { var hashmarkIndex = url.indexOf('#'); if (hashmarkIndex !== -1) { url = url.slice(0, hashmarkIndex); } url += (url.indexOf('?') === -1 ? '?' : '&') + serializedParams; } return url; };
咱們能夠看到除了引入工具模塊以後自己模塊還自定義了一個encode
方法
buildURL
裏面還有一個paramsSerializer
入參,在註釋上沒看到,可是能夠猜想到應該是一個參數序列化的方法
第一步設置serializedParams
的流程上有三個分支條件:
paramsSerializer
方法則直接用來處理params
參數params
是URLSearchParams
對象就直接調用toString
方法utils.forEach
,根據類型在回調函數作一層轉換,最終輸出一份&
拼接的字符串參數第二步若是上面能獲得serializedParams
的狀況,根據url規則拼接上去
第三步返回拼接後的url或者原始url
接下來看下面源碼
/*eslint consistent-return:0*/ module.exports = function httpAdapter(config) { return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) { // ---省略部分代碼--- var proxy = config.proxy; // 若是沒有傳遞代理參數的話會默認配置 if (!proxy && proxy !== false) { // 協議名後拼接字符串,表明代理的環境變量名 var proxyEnv = protocol.slice(0, -1) + '_proxy'; // 代理地址 var proxyUrl = process.env[proxyEnv] || process.env[proxyEnv.toUpperCase()]; if (proxyUrl) { // 解析代理地址 var parsedProxyUrl = url.parse(proxyUrl); // no_proxy環境變量 var noProxyEnv = process.env.no_proxy || process.env.NO_PROXY; var shouldProxy = true; if (noProxyEnv) { // 返回分割而且清除空格後的數組 var noProxy = noProxyEnv.split(',').map(function trim(s) { return s.trim(); }); // 是否應該代理 shouldProxy = !noProxy.some(function proxyMatch(proxyElement) { // 不存在返回false if (!proxyElement) { return false; } // 通配符返回true if (proxyElement === '*') { return true; } // 判斷proxyElement與請求url的域名是否相等 if (proxyElement[0] === '.' && parsed.hostname.substr(parsed.hostname.length - proxyElement.length) === proxyElement) { return true; } return parsed.hostname === proxyElement; }); } // 拼裝代理配置 if (shouldProxy) { proxy = { host: parsedProxyUrl.hostname, port: parsedProxyUrl.port }; if (parsedProxyUrl.auth) { var proxyUrlAuth = parsedProxyUrl.auth.split(':'); proxy.auth = { username: proxyUrlAuth[0], password: proxyUrlAuth[1] }; } } } } // 若是有代理配置,添加到options if (proxy) { options.hostname = proxy.host; options.host = proxy.host; options.headers.host = parsed.hostname + (parsed.port ? ':' + parsed.port : ''); options.port = proxy.port; options.path = protocol + '//' + parsed.hostname + (parsed.port ? ':' + parsed.port : '') + options.path; // Basic proxy authorization if (proxy.auth) { var base64 = Buffer.from(proxy.auth.username + ':' + proxy.auth.password, 'utf8').toString('base64'); options.headers['Proxy-Authorization'] = 'Basic ' + base64; } } // ---省略部分代碼--- }); };
這塊代碼屬於定義代理服務器,示例以下
proxy: { host: '127.0.0.1', port: 9000, auth: { username: 'mikeymike', password: 'rapunz3l' } },
具體作了什麼能夠一目瞭然
/*eslint consistent-return:0*/ module.exports = function httpAdapter(config) { return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) { // ---省略部分代碼--- var transport; // 是否https代理 var isHttpsProxy = isHttpsRequest && (proxy ? isHttps.test(proxy.protocol) : true); // 若是配置了直接使用 if (config.transport) { transport = config.transport; } else if (config.maxRedirects === 0) { // 最大重定向次數爲0判斷使用https模塊仍是http模塊 transport = isHttpsProxy ? https : http; } else { // 若是容許重定向 if (config.maxRedirects) { options.maxRedirects = config.maxRedirects; } // 直接判斷使用https重定向模塊仍是http重定向模塊 transport = isHttpsProxy ? httpsFollow : httpFollow; } // 若是設置了長度而且大於-1則添加到options上 if (config.maxContentLength && config.maxContentLength > -1) { options.maxBodyLength = config.maxContentLength; } // ---省略部分代碼--- }); };
根據協議決定使用對應的請求庫,而且設定最大重定向次數和請求內容長度
/*eslint consistent-return:0*/ module.exports = function httpAdapter(config) { return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) { // ---省略部分代碼--- // Create the request var req = transport.request(options, function handleResponse(res) { // 若是終止則中斷流程 if (req.aborted) return; // uncompress the response body transparently if required var stream = res; switch (res.headers['content-encoding']) { /*eslint default-case:0*/ case 'gzip': case 'compress': case 'deflate': // add the unzipper to the body stream processing pipeline stream = (res.statusCode === 204) ? stream : stream.pipe(zlib.createUnzip()); // remove the content-encoding in order to not confuse downstream operations delete res.headers['content-encoding']; break; } // return the last request in case of redirects var lastRequest = res.req || req; var response = { status: res.statusCode, statusText: res.statusMessage, headers: res.headers, config: config, request: lastRequest }; if (config.responseType === 'stream') { response.data = stream; settle(resolve, reject, response); } else { var responseBuffer = []; stream.on('data', function handleStreamData(chunk) { responseBuffer.push(chunk); // make sure the content length is not over the maxContentLength if specified if (config.maxContentLength > -1 && Buffer.concat(responseBuffer).length > config.maxContentLength) { stream.destroy(); reject(createError('maxContentLength size of ' + config.maxContentLength + ' exceeded', config, null, lastRequest)); } }); stream.on('error', function handleStreamError(err) { if (req.aborted) return; reject(enhanceError(err, config, null, lastRequest)); }); stream.on('end', function handleStreamEnd() { var responseData = Buffer.concat(responseBuffer); if (config.responseType !== 'arraybuffer') { responseData = responseData.toString(config.responseEncoding); } response.data = responseData; settle(resolve, reject, response); }); } }); // ---省略部分代碼--- }); };
主要作了幾件事:
若是帶有壓縮指示的content-encoding
,根據狀態碼是否204
決定需不須要進行壓縮,而後刪除頭避免混淆後續操做
response
對象根據responseType
決定怎麼解析響應數據,而後更新response
:
stream
事件解析/*eslint consistent-return:0*/ module.exports = function httpAdapter(config) { return new Promise(function dispatchHttpRequest(resolvePromise, rejectPromise) { // ---省略部分代碼--- // Handle errors req.on('error', function handleRequestError(err) { if (req.aborted) return; reject(enhanceError(err, config, null, req)); }); // Handle request timeout if (config.timeout) { // Sometime, the response will be very slow, and does not respond, the connect event will be block by event loop system. // And timer callback will be fired, and abort() will be invoked before connection, then get "socket hang up" and code ECONNRESET. // At this time, if we have a large number of request, nodejs will hang up some socket on background. and the number will up and up. // And then these socket which be hang up will devoring CPU little by little. // ClientRequest.setTimeout will be fired on the specify milliseconds, and can make sure that abort() will be fired after connect. req.setTimeout(config.timeout, function handleRequestTimeout() { req.abort(); reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', req)); }); } if (config.cancelToken) { // Handle cancellation config.cancelToken.promise.then(function onCanceled(cancel) { if (req.aborted) return; req.abort(); reject(cancel); }); } // Send the request if (utils.isStream(data)) { data.on('error', function handleStreamError(err) { reject(enhanceError(err, config, null, req)); }).pipe(req); } else { req.end(data); } }); };
對請求失敗,超時,取消作對應操做處理,若是data自己是steam類型則直接監聽
http adapter源碼至此爲止了
核心是瀏覽器的XMLHttpRequest對象
由於大多數方法都已經在http adapter講過了,因此這裏能夠快速過一下
var utils = require('./../utils'); var settle = require('./../core/settle'); var buildURL = require('./../helpers/buildURL'); var buildFullPath = require('../core/buildFullPath'); var parseHeaders = require('./../helpers/parseHeaders'); var isURLSameOrigin = require('./../helpers/isURLSameOrigin'); var createError = require('../core/createError'); module.exports = function xhrAdapter(config) { return new Promise(function dispatchXhrRequest(resolve, reject) { var requestData = config.data; var requestHeaders = config.headers; // 若是是FormData對象刪除Content-Type讓瀏覽器自定義 if (utils.isFormData(requestData)) { delete requestHeaders['Content-Type']; // Let the browser set it } // 本身封裝最原始的請求 var request = new XMLHttpRequest(); // HTTP basic authentication if (config.auth) { var username = config.auth.username || ''; var password = config.auth.password || ''; requestHeaders.Authorization = 'Basic ' + btoa(username + ':' + password); } var fullPath = buildFullPath(config.baseURL, config.url); request.open(config.method.toUpperCase(), buildURL(fullPath, config.params, config.paramsSerializer), true); // Set the request timeout in MS request.timeout = config.timeout; // 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; } // Prepare the response 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; }; // Handle browser request cancellation (as opposed to a manual cancellation) request.onabort = function handleAbort() { if (!request) { return; } reject(createError('Request aborted', config, 'ECONNABORTED', request)); // Clean up request request = null; }; // Handle low level network errors request.onerror = function handleError() { // Real errors are hidden from us by the browser // onerror should only fire if it's a network error reject(createError('Network Error', config, null, request)); // Clean up request request = null; }; // Handle timeout request.ontimeout = function handleTimeout() { reject(createError('timeout of ' + config.timeout + 'ms exceeded', config, 'ECONNABORTED', request)); // Clean up request request = null; }; // 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 cookies = require('./../helpers/cookies'); // Add xsrf header var xsrfValue = (config.withCredentials || isURLSameOrigin(fullPath)) && config.xsrfCookieName ? cookies.read(config.xsrfCookieName) : undefined; if (xsrfValue) { requestHeaders[config.xsrfHeaderName] = xsrfValue; } } // 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); } }); } // Add withCredentials to request if needed if (config.withCredentials) { request.withCredentials = true; } // Add responseType to request if needed if (config.responseType) { try { request.responseType = config.responseType; } catch (e) { // Expected DOMException thrown by browsers not compatible XMLHttpRequest Level 2. // But, this can be suppressed for 'json' type as it can be parsed by default 'transformResponse' function. if (config.responseType !== 'json') { throw e; } } } // 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); } if (config.cancelToken) { // Handle cancellation config.cancelToken.promise.then(function onCanceled(cancel) { if (!request) { return; } request.abort(); reject(cancel); // Clean up request request = null; }); } if (requestData === undefined) { requestData = null; } // Send the request request.send(requestData); }); };
內容大體同樣,就不說了