Home Reference Source

src/request.js

import defaultsDeep from 'lodash/defaultsDeep'
import isFunction from 'lodash/isFunction'
import isPlainObject from 'lodash/isPlainObject'
import isEmpty from 'lodash/isEmpty'
import forEach from 'lodash/forEach'
import assign from 'lodash/assign'
import URI from 'urijs'
import { G, M, K } from './config'

/**
 * @typedef {Object} Request
 * @property {XMLHttpRequest} xhr AJAX 对象
 * @property {Function} cancel 取消函数
 */

let settings = {}

/**
 * 设置默认配置
 *
 * @param {Object} [options={}] 配置
 */
export function configure (options = {}) {
  settings = defaultsDeep({}, options, settings)
}

/**
 * 上传,执行 POST 请求 XMLHttpRequest
 *
 * @param {String} url 请求地址
 * @param {Object} data 提交数据
 * @param {Object} [options] 配置
 * @param {Function} callback 回调函数
 * @return {Request} 返回一个请求对象
 */
export function upload (url, data, options, callback) {
  return request('POST', url, data, options, callback)
}

/**
 * 执行 GET 请求 XMLHttpRequest
 *
 * @param {String} url 请求地址
 * @param {Object} data 提交数据
 * @param {Object} [options] 配置
 * @param {Function} callback 回调函数
 * @return {Request} 返回一个请求对象
 */
export function post (url, data, options, callback) {
  return request('POST', url, data, options, callback)
}

/**
 * 请求 XMLHttpRequest
 *
 * @param {string} [method='POST'] 提交方法
 * @param {String} url 请求地址
 * @param {Object} data 提交数据,若请求方法为 GET,则数据将转换成请求地址的 query
 * @param {Object} [options={}] 请求配置
 * @param {Function} callback 回调函数
 * @return {Request} 返回一个请求对象
 */
export function request (method = 'POST', url, data, options = {}, callback) {
  if (arguments.length < 3) {
    return request('POST', method, {}, {}, url)
  }

  if (arguments.length < 4) {
    return request(method, url, {}, {}, data)
  }

  if (arguments.length < 5) {
    return request(method, url, data, {}, options)
  }

  if (!isFunction(callback)) {
    throw new TypeError('Callback is not provided or not be a function')
  }

  method = method.toUpperCase()
  options = defaultsDeep(options, settings)

  let xhr = new window.XMLHttpRequest()

  let xhrComplete = function () {
    xhr.onerror = null
    xhr.onreadystatechange = null
    xhr = undefined
  }

  xhr.onerror = (error) => {
    if (xhr.aborted === true) {
      return
    }

    xhr.errorFlag = true
    callback(error)
    xhrComplete()
  }

  xhr.onreadystatechange = () => {
    if (xhr.errorFlag === true || xhr.aborted === true) {
      return
    }

    if (xhr.readyState === 4) {
      let parsedData

      if (!xhr.responseText) {
        callback(new Error('Response text is empty'))
        xhrComplete()
        return
      }

      try {
        parsedData = JSON.parse(xhr.responseText)
      } catch (error) {
        callback(new Error(`Reponse data is invalid JSON\n${xhr.responseText}`))
        xhrComplete()
        return
      }

      xhr.status === 200
        ? callback(null, parsedData)
        : callback(new Error(parsedData))

      xhrComplete()
    }
  }

  if (isFunction(options.progress)) {
    let startDatetime = Date.now()

    xhr.upload.addEventListener('progress', (event) => {
      if (event.lengthComputable) {
        let nowDatetime = Date.now()
        let spendTime = nowDatetime - startDatetime
        let size = event.loaded
        let time = spendTime / 1000
        let speed = size / time || 0
        let description = `${speed.toFixed(2)}Byte/s`

        if (speed > G) {
          description = `${(speed / G).toFixed(2)}Gb/s`
        } else if (speed > M) {
          description = `${(speed / M).toFixed(2)}Mb/s`
        } else if (speed > K) {
          description = `${(speed / K).toFixed(2)}Kb/s`
        }

        event.during = time
        event.speed = speed
        event.speedDescription = description

        options.progress.call(xhr, event)
      }
    }, false)
  }

  let isGetMethod = method === 'GET' && isPlainObject(data)
  if (isGetMethod) {
    if (isEmpty(data)) {
      xhr.open(method, url, true)
    } else {
      let uri = new URI(url)
      let params = URI.parseParameters(uri.query())

      params = assign(params, data)
      uri.query(params)

      xhr.open(method, uri.href(), true)
    }
  } else {
    xhr.open(method, url, true)
  }

  /**
   * 必须在 xhr.open 后才能配置
   */
  xhr.withCredentials = !!options.withCredentials
  forEach(options.headers, (header, name) => xhr.setRequestHeader(name, header))

  if (isGetMethod) {
    xhr.send(null)
  } else {
    if (isPlainObject(data)) {
      let formData = new window.FormData()
      forEach(data, (value, key) => formData.append(key, value))
      xhr.send(formData)
    } else {
      xhr.send(data || null)
    }
  }

  let cancel = function () {
    if (xhr) {
      xhr.readyState !== 4 && xhr.abort()
      xhr.aborted = true

      callback(new Error('Request is canceled'))
    }
  }

  return { cancel, xhr }
}