English | [简体中文](./README_zh-CN.md)

# wodax-umi-request

**Base on 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/wodax-umi-request.svg?style=flat)](https://npmjs.org/package/wodax-umi-request)
[![NPM downloads](http://img.shields.io/npm/dm/wodax-umi-request.svg?style=flat)](https://npmjs.org/package/wodax-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

## wodax-umi-request vs fetch vs axios

| Features | wodax-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 wodax-umi-request
```

## Example
Performing a ```GET``` request

``` javascript
import request from 'wodax-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);
  });
```

## wodax-umi-request API
Requests can be made by passing relevant options to ```wodax-umi-request```

**wodax-umi-request(url[, options])**
```javascript
import request from 'wodax-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 wodax-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 wodax-umi-request.

**extend([options])**

``` javascript
import { extend } from 'wodax-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 wodax-umi-request in NodeJS enviroment

```javascript
const umi = require('wodax-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 wodax-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 |
| 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'.
  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,

  // 'requestType' wodax-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,
  }
```



## 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 'wodax-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 'wodax-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:
```
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:
```
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 wodax-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.

``` javascript
// request interceptor, change url or options.
request.interceptors.request.use((url, options) => {
  return (
    {
      url: `${url}&interceptors=yes`,
      options: { ...options, interceptors: 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;
})
```

## Cancel request
1. You can cancel a request using a cancel token.
```javascript
import Request from 'wodax-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 'wodax-umi-request';

const CancelToken = Request.CancelToken;
let cancel;
 
Request.get('/api/cancel', {
  cancelToken: new CancelToken(function executor(c) {
    cancel = c;
  })
});
 
// cancel request
cancel();
```

## FAQ
### How to get Response Headers
use **Headers.get()** to get information from Response Headers. ( more detail see [MDN doc](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.



## 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');
})
```

### 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 wodax-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).


## LICENSE

MIT
