这应该是一个大多数都经常使用的请求库,由于它能够支持多种配置,跨平台实现,返回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); }); };
内容大体同样,就不说了