8-2、ts-axios其他功能实现

1、上传和下载进度监控
  • 需求
    有些时候,当我们上传文件或者是请求一个大体积数据的时候,希望知道实时的进度,甚至可以基于此做一个进度条提示。
    我们希望给axios的请求配置提供onDownloadProgress和onUploadProgress函数属性,用户可以通过这2个函数实现对上传进度和下载进度的监控。
axios.get('/more/get',{
  onDownloadProgress(progressEvent) {
    // 监听下载进度
  }
})

axios.post('/more/post',{
  onUploadProgress(progressEvent) {
    // 监听上传进度
  }
})

xhr对象提供了一个progress事件,我们可以监听此事件对数据的下载进度做监控;另外,xhr.upload对象也提供了progress事件,哦我们呢可以基于此对上传事件做监控。

  • 代码实现
    首先修改接口定义:
interface AxiosRequestConfig {
  // ...
  onDownloadProgress?: (e: ProgressEvent) => void
  onUploadProgress?: (e: ProgressEvent) => void
}

接着,在发送请求前,给xhr对象添加属性

export default function xhr(config: AxiosRequestConfig): AxiosPromise {
  return new Promise((resolve, reject) => {
    const {
      // ...
      onDownloadProgress,
      onUploadProgress
    } = config
    if (onDownloadProgress) {
      request.onprogress = onDownloadProgress
    }
    if (onUploadProgress) {
      request.upload.onprogress = onUploadProgress
    }
  })
}

另外,如果请求的数据是FormData类型,我们应该主动删除请求headers中的Content-Type字段,让浏览器自动根据请求数据设置Content-Type。比如,当我们通过FormData上传文件的时候,浏览器会把请求headers中的Content-Type设置为multipart/form-data。

我们先添加一个判断FormData的方法:

function isFormData(val: any): boolean {
  return typeof val !== 'undefined' && val instanceof FormData
}

然后添加相关逻辑
xhr.ts

if (isFormData(data)) {
  delete headers['Content-Type']
}

我们发现,xhr函数内部随着需求越来越多,代码也越来越臃肿,我们可以把逻辑梳理一下,在代码内部做一层封装优化:

import { AxiosRequestConfig, AxiosPromise, AxiosResponse } from "../types";
import { parseHeaders } from "../helpers/headers";
import { transformResponse } from "../helpers/data";
import { createError } from "../helpers/error";
import transform from './transform'
import { isURLSameOrigin } from "../helpers/url";
import cookie from "../helpers/cookie";
import { isFormData } from "../helpers/util";

export default function xhr(config: AxiosRequestConfig): AxiosPromise {
  return new Promise((resolve, reject) => {
    // method和data可能没有, 所以给出默认值
    const {
      url,
      method = 'get',
      data = null,
      headers,
      responseType,
      timeout,
      cancelToken,
      withCredentials,
      xsrfCookieName,
      xsrfHeaderName,
      onDownloadProgress,
      onUploadProgress
    } = config
    const request = new XMLHttpRequest()
    request.open(method.toUpperCase(), url!)
    configureRequest()
    addEvents()
    processHeaders()
    processCancel()
    request.send(data)

    function configureRequest(): void {
      if (responseType) {
        request.responseType = responseType
      }
      if (timeout) {
        request.timeout = timeout
      }
      if (withCredentials) {
        request.withCredentials = withCredentials
      }
    }

    function addEvents(): void {
      if (onDownloadProgress) {
        request.onprogress = onDownloadProgress
      }
      if (onUploadProgress) {
        request.upload.onprogress = onUploadProgress
      }
      request.onerror = function handleError() {
        reject(createError('Network Error', config, null, request))
      }
      request.onreadystatechange = function handleLoad() {
        // request.readyState不是4,说明请求还未返回
        if (request.readyState !== 4) {
          return
        }
        // 在请求未返回的时候,status为0,
        // 当XMLHttpRequest出错的时候,status也为0
        // 所以status为0的时候,不做处理
        if (request.status === 0) {
          return
        }
        const responseHeaders = parseHeaders(request.getAllResponseHeaders())
        const responseData = responseType && responseType !== 'text' ? request.response : request.responseText
        const response: AxiosResponse = {
          data: transform(responseData, responseHeaders, config.transformResponse),
          headers: responseHeaders,
          status: request.status,
          statusText: request.statusText,
          config,
          request
        }
        handleResponse(response)
      }
      request.ontimeout = function handleTimeout() {
        reject(createError(`Timeout of ${timeout}ms exceeded`, config, 'ECONNABORTED', request))
      }
    }

    function processHeaders(): void {
      if ((withCredentials || isURLSameOrigin(url!)) && xsrfCookieName) {
        const xsrfValue = cookie.read(xsrfCookieName)
        headers[xsrfHeaderName!] = xsrfValue
      }
      if (isFormData(data)) {
        delete headers['Content-Type']
      }
      Object.keys(headers).forEach((name) => {
        request.setRequestHeader(name, headers[name])
      })
    }

    function processCancel(): void {
      if (cancelToken) {
        cancelToken.promise.then(reason => {
          request.abort()
          reject(reason)
        })
      }
    }

    function handleResponse(response: AxiosResponse) {
      if (response.status >= 200 && response.status < 300) {
        resolve(response)
      } else {
        reject(createError(`Request failed width status code ${response.status}`, config, null, request, response))
      }
    }
  })
}

可以看到,整个流程分为7步:

  • 创建一个request实例
  • 执行request.open方法初始化
  • 执行configureRequest配置request对象
  • 执行addEvents给request添加事件处理函数
  • 执行processHeaders处理请求headers
  • 执行processCancel处理请求取消逻辑
  • 执行request.send方法发送请求
    这样分割可以使代码逻辑更清晰。

demo

// server
var multipart = require('connect-multiparty');
const path = require('path')
var multipartMiddleware = multipart({
  uploadDir: path.resolve(__dirname, '../upload-file')
});
router.post('/upload', multipartMiddleware,  (req, res) => {
  console.log(req.body, req.files)
  res.send('upload success')
})

// client

import axios, { Canceler } from '../../src/index'
const instance = axios.create()
const url = 'http://p1.meituan.net/dpmerchantimage/32874e71-23df-4bd9-84a3-4530eb582848.jpg%40740w_2048h_0e_1l%7Cwatermark%3D1%26%26r%3D1%26p%3D9%26x%3D2%26y%3D2%26relative%3D1%26o%3D20'
const downloadEl = document.getElementById('download')
downloadEl.addEventListener('click', e => {
  instance.get(url, {
    onDownloadProgress: (e) => {
      console.log('onDownloadProgress', e)
    }
  })
})
const uploadEl = document.getElementById('upload')
uploadEl.addEventListener('click', (e) => {
  const data = new FormData()
  const fileEl = document.getElementById('file') as HTMLInputElement
  if (fileEl.files) {
    console.log(fileEl.files)
    data.append('file', fileEl.files[0])
    instance.post('/api/upload', data, {
      onUploadProgress: (e) => {
        console.log('onUploadProgress', e)
      }
    }).then(res => {
      console.log(res)
    })
  }
})

//html
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>demo2</title>
</head>
<body>
  <a>demo2</a>
  <button type="button" id="download">download</button>
  <form>
    <input type="file" id="file"/>
    <button type="button" id="upload">upload</button>
  </form>
  <script src="/dist/demo2.js"></script>
</body>
</html>

这样,我们可以在上传和下载的时候,监听到进度变化。

©著作权归作者所有,转载或内容合作请联系作者
平台声明:文章内容(如有图片或视频亦包括在内)由作者上传并发布,文章内容仅代表作者本人观点,简书系信息发布平台,仅提供信息存储服务。

推荐阅读更多精彩内容

  • Ajax和XMLHttpRequest 我们通常将Ajax等同于XMLHttpRequest,但细究起来它们两个是...
    changxiaonan阅读 2,313评论 0 2
  •   2005 年,Jesse James Garrett 发表了一篇在线文章,题为“Ajax: A new App...
    霜天晓阅读 905评论 0 1
  • # Ajax标签(空格分隔): 笔记整理---[TOC]### 从输入网址开始:- 在学习ajax之前,你应该先了...
    V8阅读 289评论 1 0
  • ajax作为前端开发必需的基础能力之一,你可能会使用它,但并不一定懂得其原理,以及更深入的服务器通信相关的知识。在...
    萧玄辞阅读 848评论 0 0
  • 1、XMLHttpRequest 对象 在浏览器中创建XHR 对象 1.1 XHR 的用法 在使用XHR 对象时,...
    shanruopeng阅读 597评论 0 1