JavaScript相关面试题3:1.JavaScript中如何取消请求;2.实现大型文件上传;3.async/await怎么进行错误处理

简介: 进度条数据分块进度数据利用 axios 中的 onUploadProgress 配置项获取数据,通过使用computed 根据分块进度数据的变化自动自动计算当前文件的总进度.// 总进度条

文章目录

JavaScript中如何取消请求

怎么实现大型文件上传?

async/await怎么进行错误处理?

JavaScript中如何取消请求

众所周知,JavaScript 实现异步请求就靠浏览器提供的两个 API —— XMLHttpRequest 和 Fetch。我们平常用的较多的是 Promise 请求库 axios,它基于 XMLHttpRequest。

取消 XMLHttpRequest 请求

当请求已经发送了,可以使用 XMLHttpRequest.abort() 方法取消发送,代码示例如下:


const xhr = new XMLHttpRequest();

xhr.open('GET', '<http://127.0.0.1:3000/api/get>', true);

xhr.send();

setTimeout(() => {

    xhr.abort();

}, 1000);

1

2

3

4

5

6

取消请求,readyState 会变成 XMLHttpRequest.UNSENT(0);请求的 xhr.status 会被设为 0 ;


取消 Fetch 请求

取消 Fetch 请求,需要用到 AbortController API。我们可以构造一个 controller 实例:const controller = new AbortController() , controller 它有一个只读属性 AbortController.signal,可以作为参数传入到 fetch 中,用于将控制器与获取请求相关联;


const controller = new AbortController();

void (async function () {

   const response = await fetch('<http://127.0.0.1:3000/api/get>', {

       signal: controller.signal,

   });

   const data = await response.json();

})();


setTimeout(() => {

   controller.abort();

}, 1000);

1

2

3

4

5

6

7

8

9

10

11

取消 aixos 请求

axios 同样支持 AbortController


const controller = new AbortController();

const API_URL = '<http://127.0.0.1:3000/api/get>';

void (async function () {

   const response = await axios.get(API_URL, {

       signal: controller.signal,

   });

   const { data } = response;

})();

setTimeout(() => {

   controller.abort();

}, 1000);

1

2

3

4

5

6

7

8

9

10

11

怎么实现大型文件上传?

大文件快速上传的方案,其实就是将文件变小,也就是通过 压缩文件资源 或者 文件资源分块 后再上传。


谁负责资源分块?谁负责资源整合?

前端负责分块,服务端负责整合


前端怎么对资源进行分块?

首先是选择上传的文件资源,接着就可以得到对应的文件对象 File,而 File.prototype.slice 方法可以实现资源的分块,当然可以用Blob.prototype.slice 方法,因为 Blob.prototype.slice === File.prototype.slice.


服务端怎么知道什么时候要整合资源?如何保证资源整合的有序性?

由于前端会将资源分块,然后单独发送请求,也就是说,原来 1 个文件对应 1 个上传请求,现在可能会变成 1 个文件对应 n 个上传请求,所以前端可以基于 Promise.all 将这多个接口整合,上传完成在发送一个合并的请求,通知服务端进行合并

合并时可通过 nodejs 中的读写流(readStream/writeStream),将所有切片的流通过管道(pipe)输入最终文件的流中。

在发送请求资源时,前端会定好每个文件对应的序号,并将当前分块、序号以及文件 hash 等信息一起发送给服务端,服务端在进行合并时,通过序号进行依次合并即可。


如果某个分块的上传请求失败了,怎么办?

一旦服务端某个上传请求失败,会返回当前分块失败的信息,其中会包含文件名称、文件 hash、分块大小以及分块序号等,前端拿到这些信息后可以进行重传,同时考虑此时是否需要将 Promise.all 替换为 Promise.allSettled 更方便.


创建项目

通过 pnpm create vite 创建项目

请求模块

src/request.js

该文件就是针对 axios 进行简单的封装,如下


import axios from "axios";


const baseURL = 'http://localhost:3001';


export const uploadFile = (url, formData, onUploadProgress = () => { }) => {

 return axios({

   method: 'post',

   url,

   baseURL,

   headers: {

     'Content-Type': 'multipart/form-data'

   },

   data: formData,

   onUploadProgress

 });

}


export const mergeChunks = (url, data) => {

 return axios({

   method: 'post',

   url,

   baseURL,

   headers: {

     'Content-Type': 'application/json'

   },

   data

 });

}


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

文件资源分块

根据 DefualtChunkSize = 5 * 1024 * 1024 ,即 5 MB ,来对文件进行资源分块进行计算,通过 spark-md5 根据文件内容计算出文件的 hash 值,方便做其他优化,比如:当 hash 值不变时,服务端没有必要重复读写文件等.


const getFileChunk = (file, chunkSize = DefualtChunkSize) => {

 return new Promise((resovle) => {

   let blobSlice = File.prototype.slice || File.prototype.mozSlice || File.prototype.webkitSlice,

     chunks = Math.ceil(file.size / chunkSize),

     currentChunk = 0,

     spark = new SparkMD5.ArrayBuffer(),

     fileReader = new FileReader();


   fileReader.onload = function (e) {

     console.log('read chunk nr', currentChunk + 1, 'of');


     const chunk = e.target.result;

     spark.append(chunk);

     currentChunk++;


     if (currentChunk < chunks) {

       loadNext();

     } else {

       let fileHash = spark.end();

       console.info('finished computed hash', fileHash);

       resovle({ fileHash });

     }

   };


   fileReader.onerror = function () {

     console.warn('oops, something went wrong.');

   };


   function loadNext() {

     let start = currentChunk * chunkSize,

       end = ((start + chunkSize) >= file.size) ? file.size : start + chunkSize;

     let chunk = blobSlice.call(file, start, end);

     fileChunkList.value.push({ chunk, size: chunk.size, name: currFile.value.name });

     fileReader.readAsArrayBuffer(chunk);

   }


   loadNext();

 });

}


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

发送上传请求和合并请求

通过 Promise.all 方法整合所以分块的上传请求,在所有分块资源上传完毕后,在 then 中发送合并请求.


const uploadChunks = (fileHash) => {

 const requests = fileChunkList.value.map((item, index) => {

   const formData = new FormData();

   formData.append(`${currFile.value.name}-${fileHash}-${index}`, item.chunk);

   formData.append("filename", currFile.value.name);

   formData.append("hash", `${fileHash}-${index}`);

   formData.append("fileHash", fileHash);

   return uploadFile('/upload', formData, onUploadProgress(item));

 });


 Promise.all(requests).then(() => {

   mergeChunks('/mergeChunks', { size: DefualtChunkSize, filename: currFile.value.name });

 });

}

1

2

3

4

5

6

7

8

9

10

11

12

13

14

进度条数据

分块进度数据利用 axios 中的 onUploadProgress 配置项获取数据,通过使用computed 根据分块进度数据的变化自动自动计算当前文件的总进度.


// 总进度条

const totalPercentage = computed(() => {

 if (!fileChunkList.value.length) return 0;

 const loaded = fileChunkList.value

   .map(item => item.size * item.percentage)

   .reduce((curr, next) => curr + next);

 return parseInt((loaded / currFile.value.size).toFixed(2));

})


// 分块进度条

const onUploadProgress = (item) => (e) => {

 item.percentage = parseInt(String((e.loaded / e.total) * 100));

}

1

2

3

4

5

6

7

8

9

10

11

12

13

async/await怎么进行错误处理?

一般情况下 async/await 在错误处理方面,主要使用 try/catch


const fetchData = () => {

   return new Promise((resolve, reject) => {

       setTimeout(() => {

           resolve('fetch data is me')

       }, 1000)

   })

}


(async () => {

   try {

       const data = await fetchData()

       console.log('data is ->', data)

   } catch(err) {

       console.log('err is ->', err)

   }

})()


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

有多个异步操作,需要对每个异步返回的 error 错误状态进行不同的处理,以下是示例代码


const fetchDataA = () => {

   return new Promise((resolve, reject) => {

       setTimeout(() => {

           resolve('fetch data is A')

       }, 1000)

   })

}


const fetchDataB = () => {

   return new Promise((resolve, reject) => {

       setTimeout(() => {

           resolve('fetch data is B')

       }, 1000)

   })

}


const fetchDataC = () => {

   return new Promise((resolve, reject) => {

       setTimeout(() => {

           resolve('fetch data is C')

       }, 1000)

   })

}


(async () => {

   try {

       const dataA = await fetchDataA()

       console.log('dataA is ->', dataA)

   } catch(err) {

       console.log('err is ->', err)

   }


   try {

       const dataB = await fetchDataB()

       console.log('dataB is ->', dataB)

   } catch(err) {

       console.log('err is ->', err)

   }


   try {

       const dataC = await fetchDataC()

       console.log('dataC is ->', dataC)

   } catch(err) {

       console.log('err is ->', err)

   }

})()


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

42

43

44

45

46

这样写代码里充斥着 try/catch,这时可能会想到只用一个 try/catch。


(async () => {

   try {

       const dataA = await fetchDataA()

       console.log('dataA is ->', dataA)

       const dataB = await fetchDataB()

       console.log('dataB is ->', dataB)

       const dataC = await fetchDataC()

       console.log('dataC is ->', dataC)

   } catch(err) {

       console.log('err is ->', err)

       // 难道要定义 err 类型,然后判断吗??

       /**

        * if (err.type === 'dataA') {

        *  console.log('dataA err is', err)

        * }

        * ......

        * */

   }

})()


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

如果是这样写只会增加编码的复杂度,而且要多写代码,这个时候就应该想想怎么优雅的解决,async/await 本质就是 promise 的语法糖,既然是 promise 那么就可以使用 then 函数了


(async () => {

   const fetchData = () => {

       return new Promise((resolve, reject) => {

           setTimeout(() => {

               resolve('fetch data is me')

           }, 1000)

       })

   }


   const data = await fetchData().then(data => data ).catch(err => err)

   console.log(data)

})()

1

2

3

4

5

6

7

8

9

10

11

12

在上面写法中,如果 fetchData 返回 resolve 正确结果时,data 是我们要的结果,如果是 reject 了,发生错误了,那么 data 是错误结果,这显然是行不通的,再对其完善。


(async () => {

   const fetchData = () => {

       return new Promise((resolve, reject) => {

           setTimeout(() => {

               resolve('fetch data is me')

           }, 1000)

       })

   }


   const [err, data] = await fetchData().then(data => [null, data] ).catch(err => [err, null])

   console.log('err', err)

   console.log('data', data)

   // err null

   // data fetch data is me

})()

1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

这样是不是好很多了呢,不能每个 await 都写这么长,写着也不方便也不优雅,再优化一下


(async () => {

   const fetchData = () => {

       return new Promise((resolve, reject) => {

           setTimeout(() => {

               resolve('fetch data is me')

           }, 1000)

       })

   }


   // 抽离成公共方法

   const awaitWrap = (promise) => {

       return promise

           .then(data => [null, data])

           .catch(err => [err, null])

   }


   const [err, data] = await awaitWrap(fetchData())

   console.log('err', err)

   console.log('data', data)

   // err null

   // data fetch data is me

})()


1

2

3

4

5

6

7

8

9

10

11

12

13

14

15

16

17

18

19

20

21

22

将对 await 处理的方法抽离成公共的方法,在使用 await 调用 awaitWrap 这样的方法是不是更优雅了呢。如果使用 typescript 实现大概是这个样子


function awaitWrap<T, U = any>(promise: Promise<T>): Promise<[U | null, T | null]> {

   return promise

       .then<[null, T]>((data: T) => [null, data])

       .catch<[U, null]>(err => [err, null])

}


目录
相关文章
|
15天前
sd.js 2.0封装:更加简化请求传参内容(逐步废弃、逐渐日落2024.01.02)
sd.js 2.0封装:更加简化请求传参内容(逐步废弃、逐渐日落2024.01.02)
|
17天前
|
前端开发 JavaScript 网络协议
前端最常见的JS面试题大全
【4月更文挑战第3天】前端最常见的JS面试题大全
35 5
|
1月前
|
JavaScript 前端开发 Java
springboot从控制器请求至页面时js失效的解决方法
springboot从控制器请求至页面时js失效的解决方法
15 0
springboot从控制器请求至页面时js失效的解决方法
|
1月前
|
JSON JavaScript 前端开发
解决js中Long类型数据在请求与响应过程精度丢失问题(springboot项目中)
解决js中Long类型数据在请求与响应过程精度丢失问题(springboot项目中)
35 0
|
1月前
|
JavaScript 前端开发
springboot+layui从控制器请求至页面时js失效的解决方法
springboot+layui从控制器请求至页面时js失效的解决方法
15 0
|
1月前
|
前端开发 JavaScript 开发者
JavaScript 中的异步编程:Promise 和 Async/Await
在现代的 JavaScript 开发中,异步编程是至关重要的。本文将介绍 JavaScript 中的异步编程概念,重点讨论 Promise 和 Async/Await 这两种常见的处理异步操作的方法。通过本文的阐述,读者将能够更好地理解和应用这些技术,提高自己在 JavaScript 开发中处理异步任务的能力。
|
1天前
|
前端开发 JavaScript 编译器
深入解析JavaScript中的异步编程:Promises与async/await的使用与原理
【4月更文挑战第22天】本文深入解析JavaScript异步编程,重点讨论Promises和async/await。Promises用于管理异步操作,有pending、fulfilled和rejected三种状态。通过.then()和.catch()处理结果,但可能导致回调地狱。async/await是ES2017的语法糖,使异步编程更直观,类似同步代码,通过事件循环和微任务队列实现。两者各有优势,适用于不同场景,能有效提升代码可读性和维护性。
|
7天前
|
JavaScript 前端开发 测试技术
「一劳永逸」送你21道高频JavaScript手写面试题(上)
「一劳永逸」送你21道高频JavaScript手写面试题
32 0
|
30天前
|
JavaScript 前端开发
js开发:请解释什么是ES6的async/await,以及它如何解决回调地狱问题。
ES6的`async/await`是基于Promise的异步编程工具,能以同步风格编写异步代码,提高代码可读性。它缓解了回调地狱问题,通过将异步操作封装为Promise,避免回调嵌套。错误处理更直观,类似同步的try...catch。
|
1月前
|
设计模式 JavaScript 前端开发
最常见的26个JavaScript面试题和答案
最常见的26个JavaScript面试题和答案
43 1