axios 內(nèi)置了 2 個適配器(截止到 v1.6.8 版本)[8]:xhr.js 和 http.js。
圖片
顧名思義,xhr.js 是針對瀏覽器環(huán)境提供的 XMLHttpRequest 封裝的;http.js 則是針對 Node 端的 http/https 模塊進行封裝的。
不久前,我們詳細講解了瀏覽器端的實現(xiàn),本文就來看看 Node 環(huán)境又是如何實現(xiàn)的。
老規(guī)矩,在介紹實現(xiàn)之前,先看看 axios 在瀏覽器器環(huán)境的使用。
首先創(chuàng)建項目,安裝 axios 依賴:
mdir axios-demoscd axios-demosnpm initnpm install axios# 使用 VS Code 打開當前目錄code .
寫一個測試文件 index.js:
// index.jsconst axios = require('axios')axios.get('https://httpstat.us/200') .then(res => { console.log('res >>>>', res) })
執(zhí)行文件:
node --watch index.js
注意:--watch[9] 是 Node.js 在 v16.19.0 版本引入的實驗特性,在 v22.0.0 已轉(zhuǎn)為正式特性。
打印出來結(jié)果類似:
Restarting 'index.js'res >>>> { status: 200, statusText: 'OK' headers: Object [AxiosHeaders] {} config: {} request: <ref *1> ClientRequest {} data: { code: 200, description: 'OK' }}Completed running 'index.js'
修改 Index.js 文件內(nèi)容保存:
const axios = require('axios')axios.get('https://httpstat.us/404') .catch(err => { console.log('err >>>>', err) })
打印結(jié)果類似:
Restarting 'index.js'err >>>> AxiosError: Request failed with status code 404 { code: 'ERR_BAD_REQUEST', config: {} request: <ref *1> ClientRequest {} response: { status: 404, statusText: 'Not Found', data: { code: 404, description: 'Not Found' } }}
以上我們就算講完了 axios 在 Node 端的簡單使用,這就是 axios 好處所在,統(tǒng)一的使用體驗,免去了我們在跨平臺的學習成本,提升了開發(fā)體驗。
接下來就來看看 axios 的 Node 端實現(xiàn)。源代碼位于 lib/adapters/http.js[10] 下。
// /v1.6.8/lib/adapters/http.js#L160export default isHttpAdapterSupported && function httpAdapter(config) {/* ... */}
Node 端發(fā)出的請求最終都是交由 httpAdapter(config) 函數(shù)處理的,其核心實現(xiàn)如下:
import http from 'http';import https from 'https';export default isHttpAdapterSupported && function httpAdapter(config) { // 1) return wrapAsync(async function dispatchHttpRequest(resolve, reject, onDone) { // 2) let {data, lookup, family} = config; const {responseType, responseEncoding} = config; const method = config.method.toUpperCase(); // Parse url const fullPath = buildFullPath(config.baseURL, config.url); const parsed = new URL(fullPath, 'http://localhost'); const headers = AxiosHeaders.from(config.headers).normalize(); 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(new AxiosError( 'Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream', AxiosError.ERR_BAD_REQUEST, config )); } } const options = { path, method: method, headers: headers.toJSON(), agents: { http: config.httpAgent, https: config.httpsAgent }, auth, protocol, family, beforeRedirect: dispatchBeforeRedirect, beforeRedirects: {} }; // 3) let transport; const isHttpsRequest = /https:?/.test(options.protocol); if (config.maxRedirects === 0) { transport = isHttpsRequest ? https : http; } // Create the request req = transport.request(options, function handleResponse(res) { // ... } // 4) // Handle errors req.on('error', function handleRequestError(err) { // @todo remove // if (req.aborted && err.code !== AxiosError.ERR_FR_TOO_MANY_REDIRECTS) return; reject(AxiosError.from(err, null, config, req)); }); // 5) // Handle request timeout if (config.timeout) { req.setTimeout(timeout, function handleRequestTimeout() { if (isDone) return; let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded'; const transitional = config.transitional || transitionalDefaults; if (config.timeoutErrorMessage) { timeoutErrorMessage = config.timeoutErrorMessage; } reject(new AxiosError( timeoutErrorMessage, transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED, config, req )); abort(); }); } // 6) // Send the request if (utils.isStream(data)) { let ended = false; let errored = false; data.on('end', () => { ended = true; }); data.once('error', err => { errored = true; req.destroy(err); }); data.on('close', () => { if (!ended && !errored) { abort(new CanceledError('Request stream has been aborted', config, req)); } }); data.pipe(req); } else { req.end(data); } }
是有點長,但大概瀏覽一遍就行,后面會詳細講。實現(xiàn)主要有 6 部分:
大概介紹了之后,我們再深入每一步具體學習一下。
首先,httpAdapter(config) 內(nèi)部的實現(xiàn)是經(jīng)過 wrapAsync 包裝函數(shù)返回的。
// /v1.6.8/lib/adapters/http.js#L122-L145const wrapAsync = (asyncExecutor) => { return new Promise((resolve, reject) => { let onDone; let isDone; const done = (value, isRejected) => { if (isDone) return; isDone = true; onDone && onDone(value, isRejected); } const _resolve = (value) => { done(value); resolve(value); }; const _reject = (reason) => { done(reason, true); reject(reason); } asyncExecutor(_resolve, _reject, (onDoneHandler) => (onDone = onDoneHandler)).catch(_reject); })};
調(diào)用 wrapAsync 函數(shù)會返回一個 Promise 對象,除了跟原生 Promise 構(gòu)造函數(shù)一樣會返回 resolve、reject 之外,還額外拓展了一個 onDone 參數(shù),確保 Promise 狀態(tài)改變后,總是會調(diào)用 onDone。
在處理好返回值后,接下來要做的就是組裝請求參數(shù)了,請求參數(shù)最終會交由 http.request(options)[11]/https.request(options)[12] 處理,因此需要符合其類型定義。
在理解 options 參數(shù)之前,先了解一下 http 模塊的請求案例。
const http = require('node:http');const options = { hostname: 'www.google.com', port: 80, path: '/upload', method: 'POST', headers: { 'Content-Type': 'application/json', 'Content-Length': Buffer.byteLength(postData), },};const req = http.request(options, (res) => { console.log(`STATUS: ${res.statusCode}`); console.log(`HEADERS: ${JSON.stringify(res.headers)}`); res.setEncoding('utf8'); res.on('data', (chunk) => { console.log(`BODY: ${chunk}`); }); res.on('end', () => { console.log('No more data in response.'); });});req.on('error', (e) => { console.error(`problem with request: ${e.message}`);});req.end(JSON.stringify({ 'msg': 'Hello World!',}));
以上,我們向 http://www.google.com/upload 發(fā)起了一個 POST 請求(https 請求與此類次)。
值得注意的是,請求參數(shù) options 中并不包含請求體數(shù)據(jù),請求體數(shù)據(jù)最終是以 req.end(data) 發(fā)動出去的,這一點跟 XMLHttpRequest 實例的做法類似。
再來看看 axios 中關(guān)于這塊請求參數(shù)的組裝邏輯。
首先,使用 .baseURL 和 .url 參數(shù)解析出跟 URL 相關(guān)數(shù)據(jù)。
/v1.6.8/lib/adapters/http.js#L221// Parse urlconst fullPath = buildFullPath(config.baseURL, config.url);const parsed = new URL(fullPath, 'http://localhost');const protocol = parsed.protocol || supportedProtocols[0];
不支持的請求協(xié)議會報錯。
// /v1.6.8/lib/platform/node/index.js#L11protocols: [ 'http', 'https', 'file', 'data' ]// /v1.6.8/lib/adapters/http.js#L44const supportedProtocols = platform.protocols.map(protocol => { return protocol + ':';});// /v1.6.8/lib/adapters/http.js#L265-L271if (supportedProtocols.indexOf(protocol) === -1) { return reject(new AxiosError( 'Unsupported protocol ' + protocol, AxiosError.ERR_BAD_REQUEST, config ));}
錯誤 CODE 是 ERR_BAD_REQUEST,類似 4xx 錯誤。
接下來,將 headers 參數(shù)轉(zhuǎn)成 AxiosHeaders 實例。
// /v1.6.8/lib/adapters/http.js#L273const headers = AxiosHeaders.from(config.headers).normalize();
最后,處理下請求體數(shù)據(jù) config.data。
// /v1.6.8/lib/adapters/http.js#L287-L326// support for spec compliant FormData objectsif (utils.isSpecCompliantForm(data)) { const userBoundary = headers.getContentType(/boundary=([-_/w/d]{10,70})/i); data = formDataToStream(data, (formHeaders) => { headers.set(formHeaders); }, { tag: `axios-${VERSION}-boundary`, boundary: userBoundary && userBoundary[1] || undefined }); // support for https://www.npmjs.com/package/form-data api} else if (utils.isFormData(data) && utils.isFunction(data.getHeaders)) { headers.set(data.getHeaders()); if (!headers.hasContentLength()) { try { const knownLength = await util.promisify(data.getLength).call(data); Number.isFinite(knownLength) && knownLength >= 0 && headers.setContentLength(knownLength); /*eslint no-empty:0*/ } catch (e) { } }} else if (utils.isBlob(data)) { data.size && headers.setContentType(data.type || 'application/octet-stream'); headers.setContentLength(data.size || 0); data = stream.Readable.from(readBlob(data));} else 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(new AxiosError( 'Data after transformation must be a string, an ArrayBuffer, a Buffer, or a Stream', AxiosError.ERR_BAD_REQUEST, config )); }
axios 會針對傳入的不同類型的 config.data 做統(tǒng)一處理,最終不是處理成 Stream 就是處理成 Buffer。
不過,當傳入的 data 是對象時,在調(diào)用 httpAdapter(config) 之前,會先經(jīng)過 transformRequest() 函數(shù)處理成字符串。
// /v1.6.8/lib/defaults/index.js#L91-L94if (isObjectPayload || hasJSONContentType ) { headers.setContentType('application/json', false); return stringifySafely(data);}
針對這個場景,data 會進入到下面的處理邏輯,將字符串處理成 Buffer。
// /v1.6.8/lib/adapters/http.js#L287-L326if (utils.isString(data)) { data = Buffer.from(data, 'utf-8');}
然后,獲得請求路徑 path。
// /v1.6.8/lib/adapters/http.js#L384C4-L397C1try { path = buildURL( parsed.pathname + parsed.search, config.params, config.paramsSerializer ).replace(/^/?/, '');} catch (err) { // ...}
最后,組裝 options 參數(shù)。
// /v1.6.8/lib/adapters/http.js#L403C1-L413C7const options = { path, method: method, headers: headers.toJSON(), agents: { http: config.httpAgent, https: config.httpsAgent }, auth, protocol, family, beforeRedirect: dispatchBeforeRedirect, beforeRedirects: {}};
再看創(chuàng)建請求環(huán)節(jié)。
首先,是獲得請求實例。
import followRedirects from 'follow-redirects';const {http: httpFollow, https: httpsFollow} = followRedirects;// /v1.6.8/lib/adapters/http.js#L426-L441let transport;const isHttpsRequest = isHttps.test(options.protocol);options.agent = isHttpsRequest ? config.httpsAgent : config.httpAgent;if (config.transport) { transport = config.transport;} else if (config.maxRedirects === 0) { transport = isHttpsRequest ? https : http;} else { if (config.maxRedirects) { options.maxRedirects = config.maxRedirects; } if (config.beforeRedirect) { options.beforeRedirects.config = config.beforeRedirect; } transport = isHttpsRequest ? httpsFollow : httpFollow;}
如上所示,你可以通過 config.transport 傳入,但通常不會這么做。否則,axios 內(nèi)部會根據(jù)你是否傳入 config.maxRedirects(默認 undefined) 決定使用原生 http/https 模塊還是 follow-redirects 包里提供的 http/https 方法。
如果沒有傳入 config.maxRedirects,axios 默認會使用 follow-redirects 包里提供的 http/https 方法發(fā)起請求,它的用法跟原生 http/https 模塊一樣,這里甚至可以只使用 follow-redirects 就夠了。
下面就是創(chuàng)建請求了。
// Create the requestreq = transport.request(options, function handleResponse(res) {}
我們在 handleResponse 回調(diào)函數(shù)里處理返回數(shù)據(jù) res。
function request(options: RequestOptions | string | URL, callback?: (res: IncomingMessage) => void): ClientRequest;function request( url: string | URL, options: RequestOptions, callback?: (res: IncomingMessage) => void,): ClientRequest;
根據(jù)定義,我們知道 res 是 IncomingMessage 類型,繼承自 stream.Readable[13],是一種可讀的 Stream。
const readable = getReadableStreamSomehow();readable.on('data', (chunk) => { console.log(`Received ${chunk.length} bytes of data.`);});
res 的處理我們會放到處理請求一節(jié)講述,下面就是發(fā)出請求了。
這部分代碼比較簡單,而數(shù)據(jù)體也是在這里傳入的。
// /v1.6.8/lib/adapters/http.js#L658C5-L681C6// Send the requestif (utils.isStream(data)) { let ended = false; let errored = false; data.on('end', () => { ended = true; }); data.once('error', err => { errored = true; req.destroy(err); }); data.on('close', () => { if (!ended && !errored) { abort(new CanceledError('Request stream has been aborted', config, req)); } }); data.pipe(req);} else { req.end(data);}
如果你的請求體是 Buffer 類型的,那么直接傳入 req.end(data) 即可,否則(Stream 類型)則需要以管道形式傳遞給 req。
接著創(chuàng)建請求一節(jié),下面開始分析請求的處理。
Node.js 部分的請求處理,比處理 XMLHttpRequest 稍微復雜一些。你要在 2 個地方做監(jiān)聽處理。
首先,用 res/responseStream 上已有的信息組裝響應(yīng)數(shù)據(jù) response。
// /v1.6.8/lib/adapters/http.js#L478// decompress the response body transparently if requiredlet responseStream = res;// return the last request in case of redirectsconst lastRequest = res.req || req;const response = { status: res.statusCode, statusText: res.statusMessage, headers: new AxiosHeaders(res.headers), config, request: lastRequest};
這是不完整的,因為我們還沒有設(shè)置 response.data。
// /v1.6.8/lib/adapters/http.js#L535C7-L538C15if (responseType === 'stream') { response.data = responseStream; settle(resolve, reject, response);} else { // ...}
如果用戶需要的是響應(yīng)類型是 stream,那么一切就變得簡單了,直接將數(shù)據(jù)都給 settle 函數(shù)即可。
// /v1.6.8/lib/core/settle.jsexport default function settle(resolve, reject, response) { const validateStatus = response.config.validateStatus; if (!response.status || !validateStatus || validateStatus(response.status)) { resolve(response); } else { reject(new AxiosError( 'Request failed with status code ' + response.status, [AxiosError.ERR_BAD_REQUEST, AxiosError.ERR_BAD_RESPONSE][Math.floor(response.status / 100) - 4], response.config, response.request, response )); }}
settle 函數(shù)會根據(jù)傳入的 response.status 和 config.validateStatus() 決定請求是成功(resolve)還是失?。╮eject)。
當然,如果需要的響應(yīng)類型不是 stream,就監(jiān)聽 responseStream 對象上的事件,處理請求結(jié)果。
// /v1.6.8/lib/adapters/http.js#L538C1-L591C8} else { const responseBuffer = []; let totalResponseBytes = 0; // 1) responseStream.on('data', function handleStreamData(chunk) { responseBuffer.push(chunk); totalResponseBytes += chunk.length; // make sure the content length is not over the maxContentLength if specified if (config.maxContentLength > -1 && totalResponseBytes > config.maxContentLength) { // stream.destroy() emit aborted event before calling reject() on Node.js v16 rejected = true; responseStream.destroy(); reject(new AxiosError('maxContentLength size of ' + config.maxContentLength + ' exceeded', AxiosError.ERR_BAD_RESPONSE, config, lastRequest)); } }); // 2) responseStream.on('aborted', function handlerStreamAborted() { if (rejected) { return; } const err = new AxiosError( 'maxContentLength size of ' + config.maxContentLength + ' exceeded', AxiosError.ERR_BAD_RESPONSE, config, lastRequest ); responseStream.destroy(err); reject(err); }); // 3) responseStream.on('error', function handleStreamError(err) { if (req.destroyed) return; reject(AxiosError.from(err, null, config, lastRequest)); }); // 4) responseStream.on('end', function handleStreamEnd() { try { let responseData = responseBuffer.length === 1 ? responseBuffer[0] : Buffer.concat(responseBuffer); if (responseType !== 'arraybuffer') { responseData = responseData.toString(responseEncoding); if (!responseEncoding || responseEncoding === 'utf8') { responseData = utils.stripBOM(responseData); } } response.data = responseData; } catch (err) { return reject(AxiosError.from(err, null, config, response.request, response)); } settle(resolve, reject, response); });}
responseStream 上會監(jiān)聽 4 個事件。
以上,我們完成了對響應(yīng)數(shù)據(jù)的監(jiān)聽。我們再來看看,對請求實例 req 的監(jiān)聽。
// /v1.6.8/lib/adapters/http.js#L606// Handle errorsreq.on('error', function handleRequestError(err) { // @todo remove // if (req.aborted && err.code !== AxiosError.ERR_FR_TOO_MANY_REDIRECTS) return; reject(AxiosError.from(err, null, config, req));});// /v1.6.8/lib/adapters/http.js#L619// Handle request timeoutif (config.timeout) { req.setTimeout(timeout, function handleRequestTimeout() { if (isDone) return; let timeoutErrorMessage = config.timeout ? 'timeout of ' + config.timeout + 'ms exceeded' : 'timeout exceeded'; const transitional = config.transitional || transitionalDefaults; if (config.timeoutErrorMessage) { timeoutErrorMessage = config.timeoutErrorMessage; } reject(new AxiosError( timeoutErrorMessage, transitional.clarifyTimeoutError ? AxiosError.ETIMEDOUT : AxiosError.ECONNABORTED, config, req )); abort(); });}
一共監(jiān)聽了 2 個事件:
以上,我們就完成了請求處理的所有內(nèi)容。可以發(fā)現(xiàn),Node 端處理請求的邏輯會比瀏覽器端稍微復雜一些:你需要同時監(jiān)聽請求實例以及響應(yīng)流數(shù)據(jù)上的事件,確保整個請求過程被完整監(jiān)聽。
本文主要帶大家學習了 axios 的 Node 端實現(xiàn)。
相比較于瀏覽器端要稍微復雜一些,不僅是因為我們要考慮請求可能的最大跳轉(zhuǎn)(maxRedirects),還要同時監(jiān)聽請求實例以及響應(yīng)流數(shù)據(jù)上的事件,確保整個請求過程被完整監(jiān)聽。
[1]axios 是如何實現(xiàn)取消請求的?: https://juejin.cn/post/7359444013894811689
[2]你知道嗎?axios 請求是 JSON 響應(yīng)優(yōu)先的: https://juejin.cn/post/7359580605320036415
[3]axios 跨端架構(gòu)是如何實現(xiàn)的?: https://juejin.cn/post/7362119848660451391
[4]axios 攔截器機制是如何實現(xiàn)的?: https://juejin.cn/post/7363545737874161703
[5]axios 瀏覽器端請求是如何實現(xiàn)的?: https://juejin.cn/post/7363928569028821029
[6]axios 對外出口API是如何設(shè)計的?: https://juejin.cn/post/7364614337371308071
[7]axios 中是如何處理異常的?: https://juejin.cn/post/7369951085194739775
[8]axios 內(nèi)置了 2 個適配器(截止到 v1.6.8 版本): https://github.com/axios/axios/tree/v1.6.8/lib/adapters
[9]--watch: https://nodejs.org/api/cli.html#--watch
[10]lib/adapters/http.js: https://github.com/axios/axios/blob/v1.6.8/lib/adapters/http.js
[11]http.request(options): https://nodejs.org/docs/latest/api/http.html#httprequestoptions-callback
[12]https.request(options): https://nodejs.org/docs/latest/api/https.html#httpsrequestoptions-callback
[13]stream.Readable: https://nodejs.org/docs/latest/api/stream.html#class-streamreadable
本文鏈接:http://www.tebozhan.com/showinfo-26-92115-0.htmlAxios Node 端請求是如何實現(xiàn)的?
聲明:本網(wǎng)頁內(nèi)容旨在傳播知識,若有侵權(quán)等問題請及時與本網(wǎng)聯(lián)系,我們將在第一時間刪除處理。郵件:2376512515@qq.com