npm package: @jysd/taro-umi-request
Description: A request tool based on fetch.
Author: puwei
Latest version: 3.0.2

English | [简体中文](./README_zh-CN.md)

# umi-request

The network request library, based on fetch encapsulation, combines the features of fetch and axios to provide developers with a unified api call method, simplifying usage, and providing common functions such as caching, timeout, character encoding processing, and error handling.

[![NPM version](https://img.shields.io/npm/v/umi-request.svg?style=flat)](https://npmjs.org/package/umi-request)
[![Build Status](https://img.shields.io/travis/umijs/umi-request.svg?style=flat)](https://travis-ci.org/umijs/umi-request)
[![NPM downloads](http://img.shields.io/npm/dm/umi-request.svg?style=flat)](https://npmjs.org/package/umi-request)

--------------------

## Supported features

- url parameter is automatically serialized
- post data submission method is simplified
- response return processing simplification
- api timeout support
- api request cache support
- support for processing gbk
- request and response interceptor support like axios
- unified error handling
- middleware support
- cancel request support like axios
- make http request from node.js

## umi-request vs fetch vs axios

| Features | umi-request | fetch | axios |
| :---------- | :-------------- | :-------------- | :-------------- |
| implementation | Browser native support | Browser native support | XMLHttpRequest |
| size | 9k | 4k (polyfill) | 14k |
| query simplification | ✅ | ❌ | ✅ |
| post simplification | ✅ | ❌ | ❌ |
| timeout | ✅ | ❌ | ✅ |
| cache | ✅ | ❌ | ❌ |
| error Check | ✅ | ❌ | ❌ |
| error Handling | ✅ | ❌ | ✅ |
| interceptor | ✅ | ❌ | ✅ |
| prefix | ✅ | ❌ | ❌ |
| suffix | ✅ | ❌ | ❌ |
| processing gbk | ✅ | ❌ | ❌ |
| middleware | ✅ | ❌ | ❌ |
| cancel request | ✅ | ❌ | ✅ |

For more discussion, refer to [Traditional Ajax is dead, Fetch eternal life](https://github.com/camsong/blog/issues/2) If you have good suggestions and needs, please mention [issue](https://github.com/umijs/umi/issues)

## TODO Welcome pr

- [x] Test case coverage 85%+
- [x] write a document
- [x] CI integration
- [x] release configuration
- [x] typescript

## Installation

```
npm install --save umi-request
```

## Example
Performing a ```GET``` request

``` javascript
import request from 'umi-request';

request.get('/api/v1/xxx?id=1')
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

// use options.params
request.get('/api/v1/xxx', {
    params: {
      id: 1
    }
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });
```

Performing a ```POST``` request

``` javascript
request.post('/api/v1/user', {
    data: {
      name: 'Mike'
    }
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });
```

## umi-request API

Requests can be made by passing relevant options to ```umi-request```

**umi-request(url[, options])**

```javascript
import request from 'umi-request';

request('/api/v1/xxx', {
    method: 'get',
    params: { id: 1 }
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

request('/api/v1/user', {
    method: 'post',
    data: {
      name: 'Mike'
    }
  })
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });

```

## Request method aliases

For convenience umi-request have been provided for all supported methods.

**request.get(url[, options])**

**request.post(url[, options])**

**request.delete(url[, options])**

**request.put(url[, options])**

**request.patch(url[, options])**

**request.head(url[, options])**

**request.options(url[, options])**

## Creating an instance

You can use ```extend({[options]})``` to create a new instance of umi-request.

**extend([options])**

``` javascript
import { extend } from 'umi-request';

const request = extend({
  prefix: '/api/v1',
  timeout: 1000,
  headers: {
    'Content-Type': 'multipart/form-data'
  }
});

request.get('/user')
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  });
```

Create an instance of umi-request in NodeJS enviroment

```javascript
const umi = require('umi-request');
const extendRequest = umi.extend({ timeout: 10000 })

extendRequest('/api/user')
  .then(res => {
    console.log(res);
  })
  .catch(err => {
    console.log(err);
  });
```

The available instance methods are list below. The specified options will be merge with the instance options.

**request.get(url[, options])**

**request.post(url[, options])**

**request.delete(url[, options])**

**request.put(url[, options])**

**request.patch(url[, options])**

**request.head(url[, options])**

**request.options(url[, options])**

More umi-request cases can see [antd-pro](https://github.com/umijs/ant-design-pro/blob/master/src/utils/request.js)

## request options

| Parameter | Description | Type | Optional Value | Default |
| :--- | :--- | :--- | :--- | :--- |
| method | request method | string | get , post , put ... | get |
| params | url request parameters | object or URLSearchParams | -- | -- |
| data | Submitted data | any | -- | -- |
| headers | fetch original parameters | object | -- | {} |
| timeout | timeout, default millisecond, write with caution | number | -- | -- |
| prefix | prefix, generally used to override the uniform settings prefix | string | -- | -- |
| suffix | suffix, such as some scenes api need to be unified .json | string | -- |
| credentials | fetch request with cookies | string | -- | credentials: 'same-origin' |
| useCache | Whether to use caching (only support browser environment) | boolean | -- | false |
| validateCache | cache strategy function | (url, options) => boolean | -- | only get request to cache |
| ttl | Cache duration, 0 is not expired | number | -- | 60000 |
| maxCache | Maximum number of caches | number | -- | 0(Infinity) |
| requestType | post request data type | string | json , form | json |
| parseResponse | response processing simplification | boolean | -- | true |
| charset | character set | string | utf8 , gbk | utf8 |
| responseType | How to parse the returned data | string | json , text , blob , formData ... | json , text |
| throwErrIfParseFail | throw error when JSON parse fail and responseType is 'json' | boolean | -- | false |
| getResponse | Whether to get the source response, the result will wrap a layer | boolean | -- | fasle |
| errorHandler | exception handling, or override unified exception handling | function(error) | -- |
| cancelToken | Token to cancel request | CancelToken.token | -- | -- |

The other parameters of fetch are valid. See [fetch documentation](https://github.github.io/fetch/)

## extend options Initialize default parameters, support all of the above

| Parameter | Description | Type | Optional Value | Default |
| :--- | :--- | :--- | :--- | :--- |
| method | request method | string | get , post , put ... | get |
| params | url request parameters | object | -- | -- |
| data | Submitted data | any | -- | -- |
| ... |

``` javascript
{
  // 'method' is the request method to be used when making the request
  method: 'get', // default

  // 'params' are the URL parameters to be sent with request
  // Must be a plain object or a URLSearchParams object
  params: { id: 1 },

  // 'paramSerializer' is a function in charge of serializing 'params'. ( be aware of 'params' was merged by extends's 'params' and request's 'params' and URLSearchParams will be transform to plain object. )
  paramsSerializer: function (params) {
    return Qs.stringify(params, { arrayFormat: 'brackets' })
  },

  // 'data' 作为请求主体被发送的数据
  // 适用于这些请求方法 'PUT', 'POST', 和 'PATCH'
  // 必须是以下类型之一：
  // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
  // - 浏览器专属：FormData, File, Blob
  // - Node 专属： Stream

  // 'data' is the data to be sent as the request body
  // Only applicable for request methods 'PUT', 'POST', and 'PATCH'
  // Must be of one of the following types:
  // 1. string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams
  // 2. Browser only: FormData, File, Blob
  // 3. Node only: Stream
  data: { name: 'Mike' },

  // 'headers' are custom headers to be sent
  headers: { 'Content-Type': 'multipart/form-data' },

  // 'timeout' specifies the number of milliseconds before the request times out.
  // If the request takes longer than 'timeout', request will be aborted and throw RequestError.
  timeout: 1000,

  // ’prefix‘ used to set URL's prefix
  // ( e.g. request('/user/save', { prefix: '/api/v1' }) => request('/api/v1/user/save') )
  prefix: '',

  // ’suffix‘ used to set URL's suffix
  // ( e.g. request('/api/v1/user/save', { suffix: '.json'}) => request('/api/v1/user/save.json') )
  suffix: '',

  // 'credentials' indicates whether the user agent should send cookies from the other domain in the case of cross-origin requests.
  // omit: Never send or receive cookies.
  // same-origin: Send user credentials (cookies, basic http auth, etc..) if the URL is on the same origin as the calling script. This is the default value.
  // include: Always send user credentials (cookies, basic http auth, etc..), even for cross-origin calls.
  credentials: 'same-origin', // default

  // ’useCache‘ The GET request would be cache in ttl milliseconds when 'useCache' is true.
  // The cache key would be 'url + params + method'.
  useCache: false, // default

  // 'ttl' cache duration（milliseconds），0 is infinity
  ttl: 60000,

  // 'maxCache' are the max number of requests to be cached, 0 means infinity.
  maxCache: 0,

  // According to http protocal, request of GET used to get data from server, it's necessary to cache response data when server data update not frequently. We provide 'validateCache'
  // for some cases that need to cache data with other method reqeust.
  validateCache: (url, options) => { return options.method.toLowerCase() === 'get' },


  // 'requestType' umi-request will add headers and body according to the 'requestType' when the type of data is object or array.
  // 1. requestType === 'json' :(default )
  // options.headers = {
  //   Accept: 'application/json',
  //   'Content-Type': 'application/json;charset=UTF-8',
  //   ...options.headers,
  // }
  // options.body = JSON.stringify(data)
  // 
  // 2. requestType === 'form':
  // options.headers = {
  //   Accept: 'application/json',
  //   'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8',
  //   ...options.headers,
  // };
  // options.body = query-string.stringify(data);
  // 
  // 3. other requestType
  // options.headers = {
  //   Accept: 'application/json',
  //   ...options.headers,
  // };
  // options.body = data;
  requestType: 'json', // default

  // 'parseResponse' whether processing response 
  parseResponse: true, // default

  // 'charset' This parameter can be used when the server returns gbk to avoid garbled characters.(parseResponse should set to true)
  charset: 'gbk',

  // 'responseType': how to processing response.(parseResponse should be true)
  // The default value is 'json', would processing response by Response.text().then( d => JSON.parse(d) )
  // Other responseType (text, blob, arrayBuffer, formData), would processing response by Response[responseType]()
  responseType: 'json', // default

  // 'throwErrIfParseFail': whether throw error or not when JSON parse data fail and responseType is 'json'.
  throwErrIfParseFail: false, // default

  // 'getResponse': if you need the origin Response, set true and will return { data, response }.
  getResponse: false,// default

  // 'errorHandler' error handle entry.
  errorHandler: function(error) { /* 异常处理 */ },

  // 'cancelToken' the token of cancel request.
  cancelToken: null,
}
```

### Extend Options
Sometimes we need to update options after **extend** a request instance, umi-request provide **extendOptions** for users to update options:

```javascript
const request = extend({ timeout: 1000, params: { a: '1' }})
// default options is: { timeout: 1000, params: { a: '1' }}

request.extendOptions({ timeout: 3000, params: { b: '2' }})
// after extendOptions: { timeout: 3000, params: { a: '1', b: '2' }}
```

## Response Schema

The response for a request contains the following information.

``` javascript
{
  // 'data' is the response that was provided by the server
  data: {},

  // 'status' is the HTTP status code from the server response
  status: 200,

  // 'statusText' is the HTTP status message from the server response
  statusText: 'OK',

  // 'headers' the headers that the server responded with
  // All header names are lower cased
  headers: {},
}
```

When options.getResponse === false, the response schema would be 'data'

``` javascript
request.get('/api/v1/xxx', { getResponse: false })
  .then(function(data) {
    console.log(data);
  })
```

When options.getResponse === true ，the response schema would be { data, response }

``` javascript
request.get('/api/v1/xxx', { getResponse: true })
  .then(function({ data, response }) {
    console.log(data);
    console.log(response.status);
    console.log(response.statusText);
    console.log(response.headers);
  })

```

You can get Response from ```error``` object in errorHandler or request.catch.

## Error handling

```javascript
import request, { extend } from 'umi-request';

const errorHandler = function (error) {
  const codeMap = {
    '021': 'An error has occurred',
    '022': 'It’s a big mistake,',
    // ....
  };
  if (error.response) {
    // The request was made and the server responded with a status code
    // that falls out of the range of 2xx
    console.log(error.response.status);
    console.log(error.response.headers);
    console.log(error.data);
    console.log(error.request);
    console.log(codeMap[error.data.status])
  } else {
    // The request was made but no response was received or error occurs when setting up the request.
    console.log(error.message);
  }

  throw error; // If throw. The error will continue to be thrown.
  
  // return {some: 'data'}; If return, return the value as a return. If you don't write it is equivalent to return undefined, you can judge whether the response has a value when processing the result.
  // return {some: 'data'}; 
}

// 1. Unified processing
const extendRequest = extend({ errorHandler });

// 2. Separate special treatment
// If unified processing is configured, but an api needs special handling. When requested, the errorHandler is passed as a parameter.
request('/api/v1/xxx', { errorHandler });


// 3. not configure errorHandler, the response will be directly treated as promise, and it will be caught.
request('/api/v1/xxx')
.then(function (response) {
  console.log(response);
})
.catch(function (error) {
  return errorHandler(error);
})
```

## Middleware

Expressive HTTP middleware framework for node.js. For development to enhance before and after request. Support create instance, global, core middlewares.

**Instance Middleware (default)** request.use(fn) Different instances's instance middleware are independence.
**Global Middleware** request.use(fn, { global: true }) Different instances share global middlewares.
**Core Middleware** request.use(fn, { core: true }) Used to expand request core.

request.use(fn[, options])

### params

fn params

* ctx(Object)：context, content request and response
* next(Function)：function to call the next middleware

options params

* global(boolean): whether global， higher priority than core
* core(boolean): whether core

### example

1. same type of middlewares

``` javascript
import request, { extend } from 'umi-request';
request.use(async (ctx, next) => {
  console.log('a1');
  await next();
  console.log('a2');
})
request.use(async (ctx, next) => {
  console.log('b1');
  await next();
  console.log('b2');
})

const data = await request('/api/v1/a');
```

order of middlewares be called:

```shell
a1 -> b1 -> response -> b2 -> a2
```

2. Defferent type of middlewares

``` javascript
request.use( async (ctx, next) => {
  console.log('instanceA1');
  await next();
  console.log('instanceA2');
})
request.use( async (ctx, next) => {
  console.log('instanceB1');
  await next();
  console.log('instanceB2');
})
request.use( async (ctx, next) => {
  console.log('globalA1');
  await next();
  console.log('globalA2');
}, { global: true })
request.use( async (ctx, next) => {
  console.log('coreA1');
  await next();
  console.log('coreA2');
}, { core: true })
```

order of middlewares be called:

```shell
instanceA1 -> instanceB1 -> globalA1 -> coreA1 -> coreA2 -> globalA2 -> instanceB2 -> instanceA2
```

3. Enhance request

``` javascript
request.use(async (ctx, next) => {
  const { req } = ctx;
  const { url, options } = req;

  if ( url.indexOf('/api') !== 0 ) {
    ctx.req.url = `/api/v1/${url}`;
  }
  ctx.req.options = {
    ...options,
    foo: 'foo'
  };

  await next();

  const { res } = ctx;
  const { success = false } = res; 
  if (!success) {
    // ...
  }
})

```

4. Use core middleware to expand request core.

``` javascript

request.use(async (ctx, next) => {
  const { req } = ctx;
  const { url, options } = req;
  const { __umiRequestCoreType__ = 'normal' } = options;
  
  // __umiRequestCoreType__ use to identificat request core
  // when value is 'normal' , use umi-request 's fetch request core
  if ( __umiRequestCoreType__ === 'normal') {
    await next();
    return;
  }

  // when value is not normal, use your request func. 
  const response = getResponseByOtherWay();

  ctx.res = response;

  await next();
  return;
}, { core: true });


request('/api/v1/rpc', {
  __umiRequestCoreType__: 'rpc',
  parseResponse: false,
})
  .then(function (response) {
    console.log(response);
  })
  .catch(function (error) {
    console.log(error);
  })

```

## Interceptor

You can intercept requests or responses before they are handled by then or catch.

1. global Interceptor

``` javascript
// request interceptor, change url or options.
request.interceptors.request.use((url, options) => {
  return (
    {
      url: `${url}&interceptors=yes`,
      options: { ...options, interceptors: true },
    }
  );
});

// Same as the last one
request.interceptors.request.use((url, options) => {
  return (
    {
      url: `${url}&interceptors=yes`,
      options: { ...options, interceptors: true },
    }
  );
}, { global: true });

// response interceptor, chagne response
request.interceptors.response.use((response, options) => {
  response.headers.append('interceptors', 'yes yo');
  return response;
});

// handling error in response interceptor
request.interceptors.response.use((response) => {
  const codeMaps = {
    502: '网关错误。',
    503: '服务不可用，服务器暂时过载或维护。',
    504: '网关超时。',
  };
  message.error(codeMaps[response.status]);
  return response;
});

// clone response in response interceptor
request.interceptors.response.use(async (response) => {
  const data = await response.clone().json();
  if(data && data.NOT_LOGIN) {
    location.href = '登录url';
  }
  return response;
})
```

1. instance Interceptor

``` javascript
// Global interceptors are used `request` instance method directly
request.interceptors.request.use((url, options) => {
  return {
    url: `${url}&interceptors=yes`,
    options: { ...options, interceptors: true },
  };
}, { global: false }); // second paramet defaults { global: true }

function createClient(baseUrl) {
  const request = extend({
    prefix: baseUrl
  });
  return request;
}

const clientA = createClient('/api');
const clientB = createClient('/api');
// Independent instance Interceptor
clientA.interceptors.request.use((url, options) => {
  return {
    url: `${url}&interceptors=clientA`,
    options,
  };
}, { global: false });

clientB.interceptors.request.use((url, options) => {
  return {
    url: `${url}&interceptors=clientB`,
    options,
  };
}, { global: false });
```

## Cancel request

1. You can cancel a request using a cancel token.

```javascript
import Request from 'umi-request';

const CancelToken = Request.CancelToken;
const { token, cancel } = CancelToken.source();

Request.get('/api/cancel', {
  cancelToken: token
}).catch(function(thrown) {
  if (Request.isCancel(thrown)) {
    console.log('Request canceled', thrown.message);
  } else {
    // handle error
  }
});

Request.post('/api/cancel', {
  name: 'hello world'
}, {
  cancelToken: token
})

// cancel request (the message parameter is optional)
cancel('Operation canceled by the user.');
```

2. You can also create a cancel token by passing an executor function to the CancelToken constructor:
```javascript
import Request from 'umi-request';

const CancelToken = Request.CancelToken;
let cancel;

Request.get('/api/cancel', {
  cancelToken: new CancelToken(function executor(c) {
    cancel = c;
  })
});

// cancel request
cancel();
```

## Cases

### How to get Response Headers

Use **Headers.get()** (more detail see [MDN 文档](https://developer.mozilla.org/zh-CN/docs/Web/API/Headers/get))

``` javascript
request('/api/v1/some/api', { getResponse: true })
.then(({ data, response}) => {
  response.headers.get('Content-Type');
})
```

If want to get a custem header, you need to set [Access-Control-Expose-Headers](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Expose-Headers) on server.

### File upload

Use FormData() contructor，the browser will add rerequest header  ```"Content-Type: multipart/form-data"``` automatically, developer don't need to add request header **Content-Type**

``` javascript
const formData = new FormData();
formData.append('file', file);
request('/api/v1/some/api', { method:'post', data: formData });
```

The Access-Control-Expose-Headers response header indicates which headers can be exposed as part of the response by listing their names.[Access-Control-Expose-Headers](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Expose-Headers)

## Development and debugging

- npm install
- npm run dev
- npm link
- Then go to the project you are testing to execute npm link umi-request
- Introduced and used

## Questions & Suggestions

Please open an issue [here](https://github.com/umijs/umi/issues?q=is%3Aissue+is%3Aopen+sort%3Aupdated-desc).

## Code Contributors

- @clock157
- @yesmeck
- @yutingzhao1991

## LICENSE

MIT