nojsja.gitee.io/blogs 更多內(nèi)容已經(jīng)在個(gè)人博客發(fā)布洛姑,請(qǐng)知悉
Contents
概述
瀏覽器文件操作限制
前端多文件分片上傳的原理和實(shí)現(xiàn)
預(yù)覽
概述
Amazon S3 提供了一個(gè)簡(jiǎn)單 Web 服務(wù)接口,可用于隨時(shí)在 Web 上的任何位置存儲(chǔ)和檢索任何數(shù)量的數(shù)據(jù)甚纲。此服務(wù)讓所有開(kāi)發(fā)人員都能訪問(wèn)同一個(gè)具備高擴(kuò)展性泰演、可靠性、安全性和快速價(jià)廉的數(shù)據(jù)存儲(chǔ)基礎(chǔ)設(shè)施洗搂, Amazon 用它來(lái)運(yùn)行其全球的網(wǎng)站網(wǎng)絡(luò)讯沈。此服務(wù)旨在為開(kāi)發(fā)人員帶來(lái)最大化的規(guī)模效益。
本文主要針對(duì)兼容aws-s3接口的第三方存儲(chǔ)服務(wù)祝迂,在不使用官方sdk的情況下直接使用Restful接口進(jìn)行存儲(chǔ)桶多文件分片上傳睦尽,主要包含瀏覽器端的多文件分片上傳邏輯的Javascript代碼實(shí)現(xiàn)。
瀏覽器文件操作限制
- HTML5新特性
input[type=file]
支持調(diào)用瀏覽器文件訪問(wèn)窗口來(lái)獲取文件數(shù)據(jù)型雳,實(shí)際上JS代碼使用此特性訪問(wèn)本地文件系統(tǒng)后拿到的是一個(gè)指向文件的引用地址当凡,且如果頁(yè)面刷新了那么這個(gè)地址不可復(fù)用,JS代碼并沒(méi)有實(shí)際操作文件本身四啰。前端上傳數(shù)據(jù)時(shí)根據(jù)這個(gè)指向文件的地址把文件的一小塊分片數(shù)據(jù)載入到內(nèi)存并通過(guò)Ajax請(qǐng)求發(fā)送到中間件進(jìn)行處理宁玫。 - 瀏覽器JS代碼沒(méi)有文件系統(tǒng)操作權(quán)限,不能任意存儲(chǔ)和讀取文件柑晒,因此不支持刷新瀏覽器后上傳進(jìn)度斷點(diǎn)恢復(fù)欧瘪,刷新之后斷點(diǎn)恢復(fù)的前提是能拿到文件數(shù)據(jù),但是JS代碼沒(méi)權(quán)限訪問(wèn)之前拿到的文件引用地址匙赞,并且存儲(chǔ)之前上傳過(guò)的文件分片數(shù)據(jù)這一做法也不合理佛掖。
- 相對(duì)于文件上傳,文件下載則完全不可控涌庭,由于文件操作權(quán)限芥被,所以整個(gè)下載文件操作都是由瀏覽器自帶的的下載任務(wù)管理器控制的,沒(méi)有瀏覽器接口能拿到這些下載任務(wù)進(jìn)度坐榆,所以下載任務(wù)進(jìn)度也是不能獲取的拴魄。
前端多文件分片上傳的原理和實(shí)現(xiàn)
完整Github源碼
使用了React16/Webpack4/Mobx狀態(tài)管理庫(kù)
- 支持批量文件分割并行上傳
- 多文件操作:暫停/恢復(fù)/終止/續(xù)傳/重傳
- 自定義上傳任務(wù)數(shù)目、單個(gè)分片大小
運(yùn)行流程圖
主要流程
- cacheFile
前端通過(guò)input組件拿到所有文件地址并緩存起來(lái)。
/**
* [cacheFile 緩存即將注冊(cè)的文件]
*/
@action
cacheFile = (files, bucket) => {
const symbolArr = this.filesCache.map(file => this.getSymbol(file));
const filtedFiles = [];
let uploadingFileFound = false;
files.forEach((file) => {
if (!symbolArr.includes(this.getSymbol(file))) {
if (this.findIsUploading(this.getSymbol(file), bucket)) {
uploadingFileFound = true;
filtedFiles.push(file.name);
} else {
this.filesCache.push(file);
symbolArr.push(this.getSymbol(file));
}
}
});
if (!files.length) openNotification('warning', null, this.lang.lang.noFilCanBeUploaded);
if (uploadingFileFound) openNotification('warning', null, this.lang.lang.uploadingFileReuploadTips + filtedFiles.join(', '));
}
- registry
根據(jù)上一步拿到的文件地址數(shù)組創(chuàng)建多個(gè)Mobx observable對(duì)象跟蹤每個(gè)上傳對(duì)象的基本識(shí)別信息匹中,包括文件名夏漱、文件大小、類型顶捷、分片信息(分片大小和總分片數(shù))挂绰、上傳狀態(tài)信息:uninitial(未初始化)/pending(準(zhǔn)備)/uploading(上傳中)/pause(暫停)/error(錯(cuò)誤)/break(上傳完成)、上傳開(kāi)始時(shí)間服赎、上傳完成時(shí)間葵蒂,為了便于訪問(wèn)這些Mobx observable對(duì)象,建立一個(gè)weakMap存儲(chǔ)file對(duì)象和observable對(duì)象的弱映射關(guān)系重虑。
/**
* [registry 注冊(cè)上傳文件信息]
* @param {[Object]} file [文件對(duì)象]
* @param {[String]} uploadId [文件上傳進(jìn)程id]
* @param {[Object]} state [文件初始化狀態(tài)]
*/
@action registry = (files, region, prefix) => {
let fileObj = null;
this.loading = true;
files.forEach((file) => {
if (this.files.includes(file)) {
return;
}
this.files.push(file);
fileObj = {
name: file.webkitRelativePath || file.name,
prefix: prefix || '',
size: file.size,
type: file.type || mapMimeType((file.webkitRelativePath || file.name).split('.').pop()).type,
state: 'uninitial',
creationTime: '',
completionTime: '',
index: 0,
file,
initialized: false,
partEtags: [],
region,
blockSize: this.blockSize,
total: Math.ceil(file.size / this.blockSize),
activePoint: new Date(),
speed: '0 MB/S',
id: encodeURIComponent(new Date() + file.name + file.type + file.size),
};
this.taskType.uninitial.push(file);
this.taskType.series.push(file);
const obj = observable(fileObj);
if (!this.fileStorage.get(region)) {
this.fileStorage.set(region, [obj]);
} else {
this.fileStorage.get(region).push(obj);
}
this.fileStorageMap.set(file, obj);
});
this.loading = false;
}
- startTasks
獲取文件隊(duì)列中可用于上傳的文件對(duì)象践付,根據(jù)文件狀態(tài)對(duì)其做初始化或切割文件上傳的操作,同時(shí)實(shí)時(shí)修改對(duì)應(yīng)的Mobx observable上傳對(duì)象的元數(shù)據(jù)標(biāo)識(shí)嚎尤,包括當(dāng)前上傳文件的分片索引(單個(gè)文件上傳進(jìn)度=分片索引/總分片數(shù)目)荔仁、已上傳完成的分片etag信息(由服務(wù)器返回伍宦,可用于完成分片上傳時(shí)校驗(yàn)已上傳的所有分片數(shù)據(jù)是否匹配)芽死、當(dāng)前上傳對(duì)象4的上傳狀態(tài)(uninitial/pending/uploading/pause/eror/break)、當(dāng)前上傳對(duì)象的上傳速度(速度=單個(gè)分片大小/單個(gè)分片上傳所用時(shí)間)次洼。
/**
* [startTasks 開(kāi)啟上傳任務(wù)隊(duì)列]
* @param {[String]} region [桶名]
*/
startTasks = (region) => {
// 根據(jù)空閑任務(wù)類型和空閑任務(wù)并發(fā)限制開(kāi)啟空閑任務(wù)
this.refreshTasks(region);
if (this.isUploadListEmpty(region)) return;
const maxLength = this.multiTaskCount - this.taskType.uploading.length;
const taskSeries = [];
for (let i = 0; i < (maxLength) && this.taskType.series[i]; i += 1) {
// const file = this.taskType.series.shift();
const file = this.taskType.series[i];
const storageObject = this.fileStorageMap.get(file);
if (storageObject.state === 'uploading') continue; // 上傳中
if (storageObject.state === 'pause') continue;
taskSeries.push(storageObject);
}
let index;
taskSeries.forEach((storageObject) => {
index = this.taskType.series.indexOf(storageObject.file);
index !== -1 && this.taskType.series.splice(index, 1);
if (this.taskType.uninitial.includes(storageObject.file)) {
this.initRequest(
storageObject.file,
{
bucket: region,
object: storageObject.name,
prefix: storageObject.prefix,
}
).then(({ err, init }) => {
if (!err && init) {
this.upload(storageObject.file, {
bucket: region,
object: storageObject.name,
prefix: storageObject.prefix,
uploadId: storageObject.uploadId,
});
}
});
} else {
this.upload(storageObject.file, {
bucket: region,
object: storageObject.name,
prefix: storageObject.prefix,
uploadId: storageObject.uploadId,
});
}
});
}
- refreshTasks
根據(jù)當(dāng)前設(shè)置的并行上傳任務(wù)數(shù)目和正在上傳的任務(wù)數(shù)目及時(shí)從文件預(yù)備上傳隊(duì)列提取文件放入上傳可調(diào)用文件隊(duì)列关贵。
/* 刷線任務(wù)列表 */
@action
refreshTasks = (region) => {
// 統(tǒng)計(jì)空閑任務(wù)
const storageObject = this.fileStorage.get(region);
if (!storageObject) return;
if (this.taskType.series.length >= this.multiTaskCount) return;
for (let i = 0; i < storageObject.length; i += 1) {
if (this.taskType.series.length === this.multiTaskCount) break;
if (
storageObject[i].index !== storageObject[i].total
&&
(storageObject[i].state === 'pending' || storageObject[i].state === 'uninitial')
&&
!this.taskType.series.includes(storageObject[i])
) {
this.taskType.series.push(storageObject[i]);
}
}
}
- upload & update
根據(jù)當(dāng)前文件對(duì)象的上傳分片索引對(duì)文件進(jìn)行切割并更新索引,然后把切割下來(lái)的數(shù)據(jù)通過(guò)Ajax請(qǐng)求發(fā)送給中間件處理卖毁,中間件發(fā)送到后臺(tái)后返回得到的當(dāng)前分片的etag信息揖曾,前端拿到etag信息并存儲(chǔ)到當(dāng)前上傳對(duì)象分片etag信息數(shù)組里面。
/**
* [upload 分割文件發(fā)起上傳請(qǐng)求]
* @param {[Object]} file [description]
* @param {[Object]} _params [...]
* @param {[String]} _params.bucket [bucket name]
* @param {[String]} _params.object [object name]
* @param {[String]} _params.uploadId [upload id]
*/
@action
upload = (file, _params) => {
const storageObject = this.fileStorageMap.get(file);
let single = false; // 不分片
/* 異常狀態(tài)退出 */
if (!this.isValidUploadingTask(storageObject)) return;
if (storageObject.state === 'pending') {
this.taskType.pending.splice(this.taskType.pending.indexOf(file), 1);
this.taskType.uploading.push(file);
storageObject.state = 'uploading';
}
const num = storageObject.index;
if (num === 0 && file.size <= storageObject.blockSize) {
// 不用分片的情況
single = true;
} else if (num === storageObject.total) {
// 所有分片都已經(jīng)發(fā)出
return;
}
const nextSize = Math.min((num + 1) * storageObject.blockSize, file.size);
const fileData = file.slice(num * storageObject.blockSize, nextSize);
const params = Object.assign(_params, {
partNumber: num + 1,
});
storageObject.activePoint = new Date();
this.uploadRequest({ params, data: fileData, single }).then((rsp) => {
if (rsp.code !== 200) {
openNotification('error', null, (rsp.result.data ? rsp.result.data.Code : this.lang.lang.uploadError));
this.markError(file);
this.startTasks(params.bucket);
return;
}
const { completed, etags } = this.update({
region: params.bucket,
etag: rsp.result.etag,
size: fileData.size,
id: storageObject.id,
index: params.partNumber,
});
if (completed) {
(single ?
() => {
this.complete(file, params.bucket);
} :
(partEtags) => {
this.completeRequest({
bucket: params.bucket,
uploadId: params.uploadId,
object: params.object,
prefix: params.prefix,
partEtags,
}, file);
})(etags);
} else {
this.upload(file, {
bucket: params.bucket,
object: params.object,
uploadId: params.uploadId,
partNumber: params.partNumber,
prefix: params.prefix,
});
}
}).catch((error) => {
this.markError(file);
this.startTasks(params.bucket);
console.log(`${params.bucket}_${params.object} upload error: ${error}`);
});
storageObject.index += 1;
}
- complete
當(dāng)最后一個(gè)分片上傳請(qǐng)求完成返回后亥啦,我們就拿到了服務(wù)端返回的這個(gè)文件的所有分片etag信息炭剪,前端需要校驗(yàn)當(dāng)前上傳對(duì)象etag數(shù)組的長(zhǎng)度是否匹配,數(shù)組內(nèi)每個(gè)etag元素的索引和etag值是否匹配翔脱,校驗(yàn)完成后發(fā)送最后一個(gè)請(qǐng)求到后端進(jìn)行校驗(yàn)和組裝分片奴拦,最終完成一個(gè)文件的分片上傳過(guò)程。
/**
* [completeRequest 完成所有分片數(shù)據(jù)上傳]
* @param {[Object]} _params [...]
* @param {[String]} _params.bucket [bucket name]
* @param {[String]} _params.object [object name]
* @param {[String]} _params.uploadId [upload id]
* @param {[String]} _params.partEtags [upload id]
* @param {[Object]} file [文件對(duì)象]
*/
@action completeRequest = (params, file) => {
postDataPro(
{
...{
...params,
...{
object: params.prefix + params.object,
},
},
partEtags: {
CompleteMultipartUpload: {
Part: params.partEtags.map(info => ({
PartNumber: info.number,
ETag: info.etag,
})),
},
},
},
objectResourceApi.object.completeFragmentUpload
).then((data) => {
this.complete(file, params.bucket);
}).catch((error) => {
this.startTasks(params.bucket);
this.markError(file);
});
}
/**
* [complete 完成上傳]
* @param {[Object]} file [文件對(duì)象]
* @param {[String]} bucket [桶名]
*/
@action
complete = (file, bucket) => {
const index = this.taskType.uploading.indexOf(file);
this.taskType.uploading.splice(index, 1);
this.taskType.break.push(file);
const storageObject = this.fileStorageMap.get(file);
storageObject.completionTime = (new Date().toTimeString()).split(' ')[0];
storageObject.state = 'break';
storageObject.index = storageObject.total;
this.startTasks(bucket);
};
其它操作
暫停文件上傳
將上傳對(duì)象的狀態(tài)從uploading置為pause届吁,然后把該對(duì)象對(duì)應(yīng)的文件從可調(diào)用上傳文件隊(duì)列移除错妖。開(kāi)始暫停的上傳任務(wù)
將上傳對(duì)象的狀態(tài)從pause置為pending,然后把該對(duì)象對(duì)應(yīng)的文件放入可調(diào)用上傳文件隊(duì)列疚沐,等待下一次刷新文件上傳任務(wù)隊(duì)列暂氯。續(xù)傳上傳錯(cuò)誤的任務(wù)
將上傳對(duì)象的狀態(tài)從error置為pending,然后把該對(duì)象對(duì)應(yīng)的文件放入可調(diào)用上傳文件隊(duì)列亮蛔,保持文件的已上傳分片索引記錄痴施,等待下一次刷新文件上傳任務(wù)隊(duì)列,直接調(diào)用上傳函數(shù)進(jìn)行切割并上傳。重傳上傳錯(cuò)誤的任務(wù)
將上傳對(duì)象的狀態(tài)從error置為pending辣吃,然后把該對(duì)象對(duì)應(yīng)的文件放入可調(diào)用上傳文件隊(duì)列锉矢,并將文件已上傳分片索引記錄置為初始狀態(tài),等待下一次刷新文件上傳任務(wù)隊(duì)列齿尽,從文件初始位置重新開(kāi)始切割文件并上傳沽损。
一些關(guān)鍵代碼
- 一個(gè)分片上傳完成后將后臺(tái)返回的etag信息更新到本地的上傳對(duì)象屬性,并判斷此文件是否上傳完成循头。
/**
* [update 更新本地上傳記錄](méi)
* @param {[String]} region [桶名]
* @param {[String]} etag [分片標(biāo)志]
*/
@action
update = ({
region, etag, size, id, index,
}) => {
const target = this.fileStorage.get(region);
for (let i = 0; i < target.length; i += 1) {
if (target[i].id === id) {
target[i].speed = `${(size / 1024 / 1024 / (((new Date() - target[i].activePoint) / 1000))).toFixed(2)} MB/S`;
if (target[i].speed === '0.00 MB/S') {
target[i].speed = `${formatSizeStr(size)}/S`;
}
target[i].partEtags = target[i].partEtags.filter(etagItem => etagItem.number !== index);
target[i].partEtags.push({
number: index,
etag,
});
// 最后一個(gè)分片恰好又暫停的情況
if (index === target[i].total) {
if (target[i].state === 'pause') {
index -= 1;
}
}
// 判斷上傳是否完成
if (target[i].total === 0 || target[i].partEtags.toJS().length === target[i].total) {
return {
completed: true,
etags: target[i].partEtags,
};
}
return {
completed: false,
};
}
}
}
- 在Node.js中間件使用ak/sk預(yù)簽名算法調(diào)用 s3 restful 原生接口
之前預(yù)研的時(shí)候嘗試根據(jù)aws s3-version4簽名文檔里面請(qǐng)求預(yù)簽名算法在使用Node.js中間件進(jìn)行實(shí)現(xiàn)绵估,結(jié)果很容易出現(xiàn)簽名的signature不一致報(bào)錯(cuò)的情況,所以最后在Node.js中間件采用了一個(gè)npm庫(kù)aws4卡骂,用里面的簽名方法對(duì)前端傳過(guò)來(lái)的ak/sk
進(jìn)行url預(yù)簽名国裳,這里給出中間件Request
方法的編寫(xiě)邏輯:
/**
* templateStrTranform [模板字符串轉(zhuǎn)換,s3接口中可能存在一些動(dòng)態(tài)url參數(shù)全跨,比如bucket名和object名缝左,此方法動(dòng)態(tài)替換相關(guān)的字符串]
* params1: {bucket: testBucket, uid: testUid, bucketId: testID}
* params2: /admin/bucket?format=json&bucket={bucket}&uid={uid}&bucket-id={bucketId}
* return: /admin/bucket?format=json&bucket=testBucket&uid=testUid&bucket-id=testID
*
* @author nojsja
* @param {[Object]} varObj [替換變量對(duì)象]
* @param {[String]} templateStr [模板字符串]
* @return {[String]} result [模板字符串]
*/
exports.templateStrTransform = (varObj, templateStr) => {
if (typeof varObj !== 'object' || !templateStr) return templateStr;
for (const attr in varObj) {
if (varObj.hasOwnProperty(attr) && (!Number(attr) && Number(attr) !== 0 )) {
templateStr = templateStr.replace(new RegExp(`{${attr}}`, 'g'), varObj[attr]);
}
}
return templateStr;
};
/**
* api對(duì)象實(shí)例:
listFragmentUpload: {
url: '/{bucket}/{object}?uploadId={uploadId}', // 包含動(dòng)態(tài)模板字符串
method: 'get',
port: '7480',
type: 'xml', // 表明需要將接口返回?cái)?shù)據(jù)進(jìn)行xml -> json轉(zhuǎn)換
reqType: 'xml', // 表明提交參數(shù)是xml格式,需要進(jìn)行 json -> xml轉(zhuǎn)換
}
*
*/
commonApiConfig = (headers, api, data) => {
if (isEnvDev && !isEnvMock) {
return {
url: `http://10.0.9.149:${api.port}${templateStrTransform(data, api.url)}`,
data: paramsObjectParse(data, api.url),
host: `http://10.0.9.149:${api.port}`,
hostname: `http://10.0.9.149`,
ip: '10.0.9.149',
};
} else if(isEnvDev && isEnvMock) {
return {
url: `http://10.0.7.15/mock/63${templateStrTransform(data, api.url)}`,
data: paramsObjectParse(data, api.url),
host: `http://10.0.9.154:${api.port}`,
hostname: `http://10.0.9.154`,
ip: '10.0.9.154',
};
} else {
return {
url: `http://127.0.0.1:${api.port}${templateStrTransform(data, api.url)}`,
data: paramsObjectParse(data, api.url),
host: `http://127.0.0.1:${api.port}`,
hostname: `http://127.0.0.1`,
ip: `127.0.0.1`,
};
}
}
/**
* aws4RequestSign [調(diào)用asw4的sign方法簽名一個(gè)url]
* @param {[Object]} req [Express.js框架路由函數(shù)的req對(duì)象]
* @param {[String]} path [調(diào)用的s3服務(wù)的接口url]
* @param {[Object]} api [自定義的api對(duì)象]
* @param {[Buffer|String]} data [請(qǐng)求body攜帶的參數(shù)]
*/
/**
* Tips: 這里aws4.sign方法依賴node process 中的ak/sk env設(shè)置
* 但是也可以使用sign方法的第二個(gè)options參數(shù)直接傳入ak/sk進(jìn)行顯式調(diào)用浓若,具體請(qǐng)查看此框架的npm文檔
*/
aws4RequestSign = (req, path, api, data) => {
const aws4 = require('aws4');
var opts = {
host: `${(commonApiConfig(req.headers, api, data)).host}`,
path,
url: (commonApiConfig(req.headers, api, data)).hostname,
signQuery: true,
service: process.env.AWS_SERVICE,
region: process.env.AWS_REGION,
method: api.method.toUpperCase(),
headers: {
'Access-Control-Allow-Origin': '*',
},
body: req.body,
data: '',
}
// assumes AWS credentials are available in process.env
aws4.sign(opts)
return opts;
}
/**
* commonRequestAuth [簽名并調(diào)用一個(gè)url]
* @param {[Object]} req [Express.js框架路由函數(shù)的req對(duì)象]
* @param {[String]} path [調(diào)用的s3服務(wù)的接口url]
* @param {[Object]} api [自定義的api對(duì)象]
* @param {[Buffer|String]} data [請(qǐng)求body攜帶的參數(shù)]
*/
const commonRequestAuth = (params, api, req, data) => {
const iAxios = axios.create();
iAxios.defaults.timeout = params['$no_timeout$'] ? 0 : 30e3;
// 使用params對(duì)象轉(zhuǎn)換存在動(dòng)態(tài)變量的url
const parsedUrl = templateStrTransform(params, api.url);
// aws env set
awsEnvRegistry({
key: req.cookies.access_key,
secret: req.cookies.secret_key,
});
// 簽署請(qǐng)求頭
const postData = jsonToXml(data, api.reqType);
const awsOpts = exports.aws4RequestSign(req, parsedUrl, api, params);
return new Promise((resolve, reject) => {
iAxios.request({
baseURL: awsOpts.host,
url: awsOpts.path,
method: awsOpts.method,
headers: getContentType(awsOpts.headers, api.reqType, postData, params._headers),
data: postData,
responseType: api.resType,
}).then((response) => {
// 設(shè)置header返回
if (api.type === 'header') return resolve({
result: response.headers,
code: 200,
});
// 轉(zhuǎn)換xml
if (api.type === 'xml') {
try {
xmlToJson(response.data, api.type, (data) => {
resolve({
result: data,
code: 200,
// data: jsonArrayToString(data),
headers: response.headers,
});
});
} catch (error) {
resolve({
code: 500,
result: global.lang.xml_parse_error,
});
}
}
resolve({
result: response.data,
code: 200,
});
}).catch((error) => {
console.log(error.response.data);
xmlToJson(error.response.data, 'xml', (data) => {
resolve({
code: 600,
result: { headers: error.config, data: data.Error ? data.Error : error.response.data}
})
})
});
});
};
- 在web端使用ak/sk預(yù)簽名算法調(diào)用 s3 restful 原生接口
如果項(xiàng)目需要從web端直連后端s3服務(wù)調(diào)用接口的話渺杉,上面的簽名方法就不能用了,其實(shí)很多時(shí)候直連可以帶來(lái)更好的性能挪钓,比如文件上傳/下載等等是越,不用在中間件做文件轉(zhuǎn)存,其他的接口調(diào)用直連的話也不用中間層做request轉(zhuǎn)發(fā)了碌上。這里推薦一個(gè)能夠進(jìn)行s3請(qǐng)求預(yù)簽名的axios插件aws4-axios倚评,用法如下:
import axios from "axios";
import { aws4Interceptor } from "aws4-axios";
const client = axios.create();
const interceptor = aws4Interceptor({
region: "eu-west-2",
service: "execute-api"
}, {
accessKeyId: '',
secretAccessKey: ''
});
client.interceptors.request.use(interceptor);
// Requests made using Axios will now be signed
client.get("https://example.com/foo").then(res => {
// ...
});