From a056cc101f489d1dbe87d1dccd8f2f99e1b669a0 Mon Sep 17 00:00:00 2001 From: clock157 Date: Sat, 16 Feb 2019 19:07:16 +0800 Subject: [PATCH 01/94] doc: add doc vs axios and english readme. --- README.md | 301 +++++++++++++++++++++++++++--------------------- README_zh-CN.md | 267 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 434 insertions(+), 134 deletions(-) create mode 100644 README_zh-CN.md diff --git a/README.md b/README.md index ea639b0..0e5d322 100644 --- a/README.md +++ b/README.md @@ -1,6 +1,8 @@ +English | [简体中文](./README_zh-CN.md) + # umi-request -网络请求库,基于 fetch 封装, 旨在为开发者提供一个统一的api调用方式, 简化使用, 并提供诸如缓存, 超时, 字符编码处理, 错误处理等常用功能. +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][npm-image]][npm-url] [![build status][travis-image]][travis-url] @@ -12,223 +14,254 @@ -------------------- -## 支持的功能 -- url 参数自动序列化 -- post 数据提交方式简化 -- response 返回处理简化 -- api 超时支持 -- api 请求缓存支持 -- 支持处理 gbk -- 类 axios 的 request 和 response 拦截器(interceptors)支持 -- 统一的错误处理方式 - -## TODO 欢迎pr -- [ ] rpc支持 -- [x] 测试用例覆盖85%+ -- [x] 写文档 -- [x] CI集成 -- [x] 发布配置 +## 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 for class axios +- unified error handling + +## 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 | ✅ | ❎ | ❎ | +| quick Support | ✅ | ❓ | ❓ | + +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-request/issues) + +## TODO Welcome pr + +- [ ] rpc support +- [x] Test case coverage 85%+ +- [x] write a document +- [x] CI integration +- [x] release configuration - [x] typescript -## 安装 +## Installation + `npm install umi-request --save` -## request options 参数 -| 参数 | 说明 | 类型 | 可选值 | 默认值 | -| --- | --- | --- | --- | --- | -| method | 请求方式 | string | get , post , put ... | get | -| params | url请求参数 | object | -- | -- | -| charset | 字符集 | string | utf8 , gbk | utf8 | -| requestType | post请求时数据类型 | string | json , form | json | -| data | 提交的数据 | any | -- | -- | -| responseType | 如何解析返回的数据 | string | json , text , blob , formData ... | json , text | -| getResponse | 是否获取源response, 返回结果将包裹一层 | boolean | -- | fasle | -| timeout | 超时时长, 默认毫秒, 写操作慎用 | number | -- | -- | -| useCache | 是否使用缓存 | boolean | -- | false | -| ttl | 缓存时长, 0 为不过期 | number | -- | 60000 | -| prefix | 前缀, 一般用于覆盖统一设置的prefix | string | -- | -- | -| suffix | 后缀, 比如某些场景 api 需要统一加 .json | string | -- | -- | -| errorHandler | 异常处理, 或者覆盖统一的异常处理 | function(error) | -- | -| headers | fetch 原有参数 | object | -- | {} | - -fetch原其他参数有效, 详见[fetch文档](https://github.github.io/fetch/) - -## extend options 初始化默认参数, 支持以上所有 -| 参数 | 说明 | 类型 | 默认值 | -| --- | --- | --- | --- | -| maxCache | 最大缓存数 | number | 不限 | -| prefix | 默认url前缀 | string | -- | -| errorHandler | 默认异常处理 | function(error) | -- | -| headers | 默认headers | object | {} | -| params | 默认带上的query参数 | object | {} | +## request options + +| Parameter | Description | Type | Optional Value | Default | +| :--- | :--- | :--- | :--- | :--- | +| method | request method | string | get , post , put ... | get | +| params | url request parameters | object | -- | -- | +| charset | character set | string | utf8 , gbk | utf8 | +| requestType | post request data type | string | json , form | json | +| data | Submitted data | any | -- | -- | +| responseType | How to parse the returned data | string | json , text , blob , formData ... | json , text | +| getResponse | Whether to get the source response, the result will wrap a layer | boolean | -- | fasle | +| timeout | timeout, default millisecond, write with caution | number | -- | -- | +| useCache | Whether to use caching | boolean | -- | false | +| ttl | Cache duration, 0 is not expired | number | -- | 60000 | +| prefix | prefix, generally used to override the uniform settings prefix | string | -- | -- | +| suffix | suffix, such as some scenes api need to be unified .json | string | -- | +| errorHandler | exception handling, or override unified exception handling | function(error) | -- | +| headers | fetch original parameters | object | -- | {} | + +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 | Default | +| :--- | :--- | :--- | :--- | +| maxCache | Maximum number of caches | number | Any | +| prefix | default url prefix | string | -- | +| errorHandler | default exception handling | function(error) | -- | +| headers | default headers | object | {} | +| params | default with the query parameter | object | {} | | ... | +## Use -## 使用 -```javascript -// request 是默认实例可直接使用, extend为可配置方法, 传入一系列默认参数, 返回一个新的request实例, 用法与request一致. -import request, { extend } from 'umi-request'; +> request can be used in a simple package and can be used with reference to [antd-pro](https://github.com/umijs/ant-design-pro/blob/master/src/utils/request.js) -const extendedRequest = extend({ - maxCache: 10, // 最大缓存个数, 超出后会自动清掉按时间最开始的一个. - prefix: '/api/v1', // prefix - suffix: '.json', // suffix - errorHandler: (error) => { - // 集中处理错误 - }, - headers: { - some: 'header' // 统一的headers - }, - params: { - hello: 'world' // 每个请求都要带上的query参数 - } +```javascript +// request is the default instance that can be used directly, extend is a configurable method, passing a series of default parameters, returning a new request instance, usage is consistent with the request. +import { extend } from 'umi-request'; + +const request = extend({ +  maxCache: 10, // The maximum number of caches. When it is exceeded, it will automatically clear the first one according to the time. +  prefix: '/api/v1', // prefix +  suffix: '.json', // suffix +  errorHandler: (error) => { +    // Centralized processing error +  }, +  headers: { +    Some: 'header' // unified headers +  }, +  params: { +    Hello: 'world' // the query parameter to be included with each request +  } }); -extendedRequest('/some/api'); +request('/some/api'); -// 支持语法糖 如: request.get request.post ... +// Support syntax sugar such as: request.get request.post ... request.post('/api/v1/some/api', { data: {foo: 'bar'}}); -// 请求一个api, 没有method参数默认为get +// request an api, no method parameter defaults to get request('/api/v1/some/api').then(res => { - console.log(res); +  console.log(res); }).catch(err => { - console.log(err); +  console.log(err); }); -// url参数序列化 +// url parameter serialization request('/api/v1/some/api', { params: {foo: 'bar'} }); -// post 数据提交简化 -// 当data为object时, 默认requestType: 'json'可不写, header会自动带上 application/json +// post data submission simplification +// When data is object, the default requestType: 'json' can not write, header will automatically bring application / json request('/api/v1/some/api', { method:'post', data: {foo: 'bar'} }); -// requestType: 'form', header会自动带上 application/x-www-form-urlencoded +// requestType: 'form', header will automatically bring application/x-www-form-urlencoded request('/api/v1/some/api', { method:'post', requestType: 'form', data: {foo: 'bar'} }); -// reponseType: 'blob', 如何处理返回的数据, 默认情况下 text 和 json 都不用加. 如blob 或 formData 之类需要加 +// reponseType: 'blob', how to handle the returned data, by default text and json are not added. Such as blob or formData need to add request('/api/v1/some/api', { reponseType: 'blob' }); -// 提交其他数据, 如文本, 上传文件等, requestType不填, 手动添加对应header. +// Submit other data, such as text, upload files, etc., requestType is not filled, manually add the corresponding header. request('/api/v1/some/api', { method:'post', data: 'some data', headers: { 'Content-Type': 'multipart/form-data'} }); -// 默认返回的就是数据体, 如果需要源response来扩展, 可用getResponse参数. 返回结果会多套一层 +// The default is to return the data body, if you need the source response to expand, you can use the getResponse parameter. The result will be a set of layers request('/api/v1/some/api', { getResponse: true }).then({data, response} => { - console.log(data, response); +  console.log(data, response); }); -// 超时 单位毫秒, 但是超时后客户端虽然返回超时, 但api请求不会断开, 写操作慎用. +// Timeout in milliseconds, but after the timeout, although the client returns a timeout, but the api request will not be disconnected, the write operation is used with caution. request('/api/v1/some/api', { timeout: 3000 }); -// 使用缓存, 只有get时有效. 单位毫秒, 不加ttl默认60s, ttl=0不过期. cache key为url+params组合 +// Use the cache, only valid when get. Units of milliseconds, do not add ttl default 60s, ttl = 0 does not expire. cache key for url + params combination request('/api/v1/some/api', { params: { hello: 'world' }, useCache: true, ttl: 10000 }); -// 当服务端返回的是gbk时可用这个参数, 避免得到乱码 +// This parameter can be used when the server returns gbk to avoid garbled characters. request('/api/v1/some/api', { charset: 'gbk' }); -// request拦截器, 改变url 或 options. +// request interceptor, change url or options. request.interceptors.request.use((url, options) => { - return ( - { - url: `${url}&interceptors=yes`, - options: { ...options, interceptors: true }, - } - ); +  return ( +    { +      url: `${url}&interceptors=yes`, +      options: { ...options, interceptors: true }, +    } +  ); }); -// response拦截器, 处理response +// response interceptor, handling response request.interceptors.response.use((response, options) => { - response.headers.append('interceptors', 'yes yo'); - return response; +  response.headers.append('interceptors', 'yes yo'); +  return response; }); ``` -## 错误处理 +## Error handling + ```javascript import request, { extend } from 'umi-request'; /** - * 这里介绍四种处理方式 - */ + * Here are four ways to deal with + */ /** - * 1. 统一处理 - * 常用于错误码较规范的项目中, 集中处理错误. - */ + * 1. Unified processing + * Commonly used in projects where the error code is more standardized, and the error is handled centrally. + */ const codeMap = { - '021': '发生错误啦', - '022': '发生大大大大错误啦', - ... +  '021': 'An error has occurred', +  '022': 'It’s a big mistake,' +  ... }; const errorHandler = (error) => { - const { response, data } = error; - message.error(codeMap[data.errorCode]); +  const { response, data } = error; +  message.error(codeMap[data.errorCode]); - throw error; // 如果throw. 错误将继续抛出. - // return {some: 'data'}; 如果return, 将值作为返回. 不写则相当于return undefined, 在处理结果时判断response是否有值即可. +  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. } const extendRequest = extend({ - prefix: server.url, - errorHandler +  prefix: server.url, +  errorHandler }); -const response = await extendRequest('/some/api'); // 将自动处理错误, 不用catch. 如果throw了需要catch. +const response = await extendRequest('/some/api'); // will automatically handle the error, no catch. If throw needs to catch. if (response) { - // do something +  // do something } /** -* 2. 单独特殊处理 -* 如果配置了统一处理, 但某个api需要特殊处理. 则在请求时, 将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. */ const response = await extendRequest('/some/api', { - method: 'get', - errorHandler: (error) => { - // do something - } +  method: 'get', +  errorHandler: (error) => { +    // do something +  } }); /** - *3. 不配置 errorHandler, 将reponse直接当promise处理, 自己catch. - */ + * 3. not configure errorHandler, the response will be directly treated as promise, and it will be caught. + */ try { - const response = await request('/some/api'); +  const response = await request('/some/api'); } catch (error) { - // do something +  // do something } /** -*4. 基于response interceptors +* 4. Based on response interceptors */ request.interceptors.response.use((response) => { - const codeMaps = { - 502: '网关错误。', - 503: '服务不可用,服务器暂时过载或维护。', - 504: '网关超时。', - }; - message.error(codeMaps[response.status]); - return response; +  const codeMaps = { +    502: 'Gateway error. ', +    503: 'The service is unavailable, the server is temporarily overloaded or maintained. ', +    504: 'The gateway timed out. ', +  }; +  message.error(codeMaps[response.status]); +  return response; }); /** -*5. 对于状态码实际是 200 的错误 +* 5. For the status code is actually 200 errors */ request.interceptors.response.use(async (response) => { - const data = await response.clone().json(); - if(data && data.NOT_LOGIN) { - location.href = '登录url'; - } - return response; +  const data = await response.clone().json(); +  if(data && data.NOT_LOGIN) { +    location.href = 'login url'; +  } +  return response; }) ``` -## 开发和调试 +## Development and debugging + - npm install - npm run dev - npm link -- 然后到你测试的项目中执行 npm link umi-request -- 引入并使用 +- Then go to the project you are testing to execute npm link umi-request +- Introduced and used + +## Code Contributors -## 代码贡献者 - @clock157 - @yesmeck +- @yutingzhao1991 diff --git a/README_zh-CN.md b/README_zh-CN.md new file mode 100644 index 0000000..1b82151 --- /dev/null +++ b/README_zh-CN.md @@ -0,0 +1,267 @@ +[English](./README.md) | 简体中文 + +# umi-request + +网络请求库,基于 fetch 封装, 兼具 fetch 与 axios 的特点, 旨在为开发者提供一个统一的api调用方式, 简化使用, 并提供诸如缓存, 超时, 字符编码处理, 错误处理等常用功能. + +[![NPM version][npm-image]][npm-url] +[![build status][travis-image]][travis-url] + +[npm-image]: https://img.shields.io/npm/v/umi-request.svg?style=flat-square +[npm-url]: https://npmjs.org/package/umi-request +[travis-image]: https://img.shields.io/travis/umijs/umi-request.svg?style=flat-square +[travis-url]: https://travis-ci.org/umijs/umi-request.svg?branch=master + +-------------------- + +## 支持的功能 + +- url 参数自动序列化 +- post 数据提交方式简化 +- response 返回处理简化 +- api 超时支持 +- api 请求缓存支持 +- 支持处理 gbk +- 类 axios 的 request 和 response 拦截器(interceptors)支持 +- 统一的错误处理方式 + +## 与 fetch, axios 异同 + +| 特性 | umi-request | fetch | axios | +| :---------- | :-------------- | :-------------- | :-------------- | +| 实现 | 浏览器原生支持 | 浏览器原生支持 | XMLHttpRequest | +| 大小 | 9k | 4k (polyfill) | 14k | +| query 简化 | ✅ | ❎ | ✅ | +| post 简化 | ✅ | ❎ | ❎ | +| 超时 | ✅ | ❎ | ✅ | +| 缓存 | ✅ | ❎ | ❎ | +| 错误检查 | ✅ | ❎ | ❎ | +| 错误处理 | ✅ | ❎ | ✅ | +| 拦截器 | ✅ | ❎ | ✅ | +| 前缀 | ✅ | ❎ | ❎ | +| 后缀 | ✅ | ❎ | ❎ | +| 处理 gbk | ✅ | ❎ | ❎ | +| 快速支持 | ✅ | ❓ | ❓ | + +更多讨论参考[传统 Ajax 已死,Fetch 永生](https://github.com/camsong/blog/issues/2), 如果你有好的建议和需求, 请提 [issue](https://github.com/umijs/umi-request/issues) + +## TODO 欢迎pr + +- [ ] rpc支持 +- [x] 测试用例覆盖85%+ +- [x] 写文档 +- [x] CI集成 +- [x] 发布配置 +- [x] typescript + +## 安装 + +`npm install umi-request --save` + +## request options 参数 + +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +| :--- | :--- | :--- | :--- | :--- | +| method | 请求方式 | string | get , post , put ... | get | +| params | url请求参数 | object | -- | -- | +| charset | 字符集 | string | utf8 , gbk | utf8 | +| requestType | post请求时数据类型 | string | json , form | json | +| data | 提交的数据 | any | -- | -- | +| responseType | 如何解析返回的数据 | string | json , text , blob , formData ... | json , text | +| getResponse | 是否获取源response, 返回结果将包裹一层 | boolean | -- | fasle | +| timeout | 超时时长, 默认毫秒, 写操作慎用 | number | -- | -- | +| useCache | 是否使用缓存 | boolean | -- | false | +| ttl | 缓存时长, 0 为不过期 | number | -- | 60000 | +| prefix | 前缀, 一般用于覆盖统一设置的prefix | string | -- | -- | +| suffix | 后缀, 比如某些场景 api 需要统一加 .json | string | -- | -- | +| errorHandler | 异常处理, 或者覆盖统一的异常处理 | function(error) | -- | +| headers | fetch 原有参数 | object | -- | {} | + +fetch原其他参数有效, 详见[fetch文档](https://github.github.io/fetch/) + +## extend options 初始化默认参数, 支持以上所有 + +| 参数 | 说明 | 类型 | 默认值 | +| :--- | :--- | :--- | :--- | +| maxCache | 最大缓存数 | number | 不限 | +| prefix | 默认url前缀 | string | -- | +| errorHandler | 默认异常处理 | function(error) | -- | +| headers | 默认headers | object | {} | +| params | 默认带上的query参数 | object | {} | +| ... | + +## 使用 + +> request 可以进行一层简单封装后再使用, 可参考 [antd-pro](https://github.com/umijs/ant-design-pro/blob/master/src/utils/request.js) + +```javascript +// request 是默认实例可直接使用, extend为可配置方法, 传入一系列默认参数, 返回一个新的request实例, 用法与request一致. +import { extend } from 'umi-request'; + +const request = extend({ + maxCache: 10, // 最大缓存个数, 超出后会自动清掉按时间最开始的一个. + prefix: '/api/v1', // prefix + suffix: '.json', // suffix + errorHandler: (error) => { + // 集中处理错误 + }, + headers: { + some: 'header' // 统一的headers + }, + params: { + hello: 'world' // 每个请求都要带上的query参数 + } +}); +request('/some/api'); + +// 支持语法糖 如: request.get request.post ... +request.post('/api/v1/some/api', { data: {foo: 'bar'}}); + +// 请求一个api, 没有method参数默认为get +request('/api/v1/some/api').then(res => { + console.log(res); +}).catch(err => { + console.log(err); +}); + +// url参数序列化 +request('/api/v1/some/api', { params: {foo: 'bar'} }); + +// post 数据提交简化 +// 当data为object时, 默认requestType: 'json'可不写, header会自动带上 application/json +request('/api/v1/some/api', { method:'post', data: {foo: 'bar'} }); + +// requestType: 'form', header会自动带上 application/x-www-form-urlencoded +request('/api/v1/some/api', { method:'post', requestType: 'form', data: {foo: 'bar'} }); + +// reponseType: 'blob', 如何处理返回的数据, 默认情况下 text 和 json 都不用加. 如blob 或 formData 之类需要加 +request('/api/v1/some/api', { reponseType: 'blob' }); + +// 提交其他数据, 如文本, 上传文件等, requestType不填, 手动添加对应header. +request('/api/v1/some/api', { method:'post', data: 'some data', headers: { 'Content-Type': 'multipart/form-data'} }); + +// 默认返回的就是数据体, 如果需要源response来扩展, 可用getResponse参数. 返回结果会多套一层 +request('/api/v1/some/api', { getResponse: true }).then({data, response} => { + console.log(data, response); +}); + +// 超时 单位毫秒, 但是超时后客户端虽然返回超时, 但api请求不会断开, 写操作慎用. +request('/api/v1/some/api', { timeout: 3000 }); + +// 使用缓存, 只有get时有效. 单位毫秒, 不加ttl默认60s, ttl=0不过期. cache key为url+params组合 +request('/api/v1/some/api', { params: { hello: 'world' }, useCache: true, ttl: 10000 }); + +// 当服务端返回的是gbk时可用这个参数, 避免得到乱码 +request('/api/v1/some/api', { charset: 'gbk' }); + +// request拦截器, 改变url 或 options. +request.interceptors.request.use((url, options) => { + return ( + { + url: `${url}&interceptors=yes`, + options: { ...options, interceptors: true }, + } + ); +}); + +// response拦截器, 处理response +request.interceptors.response.use((response, options) => { + response.headers.append('interceptors', 'yes yo'); + return response; +}); +``` + +## 错误处理 + +```javascript +import request, { extend } from 'umi-request'; +/** + * 这里介绍四种处理方式 + */ +/** + * 1. 统一处理 + * 常用于错误码较规范的项目中, 集中处理错误. + */ + +const codeMap = { + '021': '发生错误啦', + '022': '发生大大大大错误啦', + ... +}; + +const errorHandler = (error) => { + const { response, data } = error; + message.error(codeMap[data.errorCode]); + + throw error; // 如果throw. 错误将继续抛出. + // return {some: 'data'}; 如果return, 将值作为返回. 不写则相当于return undefined, 在处理结果时判断response是否有值即可. +} + +const extendRequest = extend({ + prefix: server.url, + errorHandler +}); + +const response = await extendRequest('/some/api'); // 将自动处理错误, 不用catch. 如果throw了需要catch. +if (response) { + // do something +} + +/** +* 2. 单独特殊处理 +* 如果配置了统一处理, 但某个api需要特殊处理. 则在请求时, 将errorHandler作为参数传入. +*/ +const response = await extendRequest('/some/api', { + method: 'get', + errorHandler: (error) => { + // do something + } +}); + +/** + * 3. 不配置 errorHandler, 将reponse直接当promise处理, 自己catch. + */ +try { + const response = await request('/some/api'); +} catch (error) { + // do something +} + +/** +* 4. 基于response interceptors +*/ +request.interceptors.response.use((response) => { + const codeMaps = { + 502: '网关错误。', + 503: '服务不可用,服务器暂时过载或维护。', + 504: '网关超时。', + }; + message.error(codeMaps[response.status]); + return response; +}); + +/** +* 5. 对于状态码实际是 200 的错误 +*/ +request.interceptors.response.use(async (response) => { + const data = await response.clone().json(); + if(data && data.NOT_LOGIN) { + location.href = '登录url'; + } + return response; +}) +``` + +## 开发和调试 + +- npm install +- npm run dev +- npm link +- 然后到你测试的项目中执行 npm link umi-request +- 引入并使用 + +## 代码贡献者 + +- @clock157 +- @yesmeck +- @yutingzhao1991 From 3a43d6577678cf8f4088a375b21346940d509ae1 Mon Sep 17 00:00:00 2001 From: clock157 Date: Sat, 16 Feb 2019 19:25:40 +0800 Subject: [PATCH 02/94] refactor: use umi-plugin-library to bundle --- .babelrc | 15 ------------- .umirc.js | 10 +++++++++ package.json | 33 +++++++++------------------ rollup.config.js | 58 ------------------------------------------------ 4 files changed, 20 insertions(+), 96 deletions(-) delete mode 100644 .babelrc create mode 100644 .umirc.js delete mode 100644 rollup.config.js diff --git a/.babelrc b/.babelrc deleted file mode 100644 index 978cda5..0000000 --- a/.babelrc +++ /dev/null @@ -1,15 +0,0 @@ -{ - "plugins": ["@babel/plugin-transform-runtime"], - "presets": [ - ["@babel/env", { - "targets": { - "browsers": ["last 2 versions", "IE 10"] - } - }] - ], - "env": { - "test": { - "presets": [["@babel/env"]] - } - } -} diff --git a/.umirc.js b/.umirc.js new file mode 100644 index 0000000..2ffb4ed --- /dev/null +++ b/.umirc.js @@ -0,0 +1,10 @@ +export default { + plugins: [ + [ + "umi-plugin-library", + { + umd: false + } + ] + ] +}; diff --git a/package.json b/package.json index 6302db9..dfd4c5a 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "name": "umi-request", "version": "1.0.3", - "description": "网络请求库,基于 fetch封装, 旨在为开发者提供一个统一的api调用方式, 简化使用, 并提供诸如缓存, 超时, 字符编码处理, 错误处理等常用功能.", + "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", "repository": { @@ -10,11 +10,11 @@ }, "bugs": "https://github.com/umijs/umi-request/issues", "scripts": { - "dev": "cross-env NODE_ENV=development rollup -c -w", - "build": "cross-env NODE_ENV=production rollup -c", - "test": "cross-env NODE_ENV=test jest", - "test:watch": "cross-env NODE_ENV=test jest --watch", - "test:cover": "cross-env NODE_ENV=test jest --coverage", + "dev": "umi lib build --w", + "build": "umi lib build", + "test": "umi-test", + "test:watch": "umi-test --watch", + "test:cover": "umi-test --coverage", "lint": "eslint src", "ci": "npm run lint && npm run test:cover", "coveralls": "cat ./coverage/lcov.info | coveralls", @@ -29,16 +29,7 @@ }, "license": "MIT", "devDependencies": { - "@babel/cli": "^7.0.0", - "@babel/core": "^7.0.0", - "@babel/plugin-transform-runtime": "^7.0.0", - "@babel/preset-env": "^7.0.0", - "@babel/runtime": "^7.0.0", - "babel-core": "^7.0.0-bridge.0", - "babel-eslint": "^9.0.0", - "babel-jest": "^23.4.2", "create-test-server": "2.3.1", - "cross-env": "^5.2.0", "debug": "^4.1.0", "eslint": "^5.5.0", "eslint-config-airbnb-base": "^13.1.0", @@ -49,14 +40,10 @@ "jest": "^23.5.0", "lint-staged": "^8.1.0", "prettier": "^1.15.3", - "rollup": "^0.68.0", - "rollup-plugin-babel": "^4.0.3", - "rollup-plugin-commonjs": "^9.1.6", - "rollup-plugin-eslint": "^5.0.0", - "rollup-plugin-node-resolve": "^3.4.0", - "rollup-plugin-terser": "^3.0.0", - "rollup-plugin-uglify": "^5.0.0", - "typescript": "^3.0.3" + "typescript": "^3.0.3", + "umi": "^2.5.0", + "umi-plugin-library": "^1.1.6", + "umi-test": "^1.4.0" }, "dependencies": { "query-string": "^6.0.0", diff --git a/rollup.config.js b/rollup.config.js deleted file mode 100644 index ccdc53d..0000000 --- a/rollup.config.js +++ /dev/null @@ -1,58 +0,0 @@ -import nodeResolve from "rollup-plugin-node-resolve"; -import babel from "rollup-plugin-babel"; -import commonjs from "rollup-plugin-commonjs"; -import { eslint } from "rollup-plugin-eslint"; -import { terser } from "rollup-plugin-terser"; -import { uglify } from "rollup-plugin-uglify"; -import pkg from "./package.json"; - -const env = process.env.NODE_ENV; - -const outputOpts = { - name: "umi-request", - exports: "named", - globals: { - "query-string": "queryString" - } -}; - -const config = { - input: "src/index.js", - plugins: [ - nodeResolve(), - babel({ - exclude: "node_modules/**", - runtimeHelpers: true - }), - commonjs(), - eslint({ - include: "./src" - }) - ], - external: ["query-string", "whatwg-fetch"] -}; - -/** - * 这样分开写是由于 terser 一个 Bug. https://github.com/TrySound/rollup-plugin-terser/issues/5 - * 同时 umd 包用 uglify 是为了避免打包进 es6 语法, 导致 IE 等出错. - * */ -export default [ - { - output: { - ...outputOpts, - format: "umd", - file: pkg.main - }, - ...config, - plugins: [...config.plugins, ...(env === "production" ? [uglify()] : [])] - }, - { - output: { - ...outputOpts, - format: "es", - file: pkg.module - }, - ...config, - plugins: [...config.plugins, ...(env === "production" ? [terser()] : [])] - } -]; From c3cc27a42a8d2167ecf2cda705cc0b74cb8c0542 Mon Sep 17 00:00:00 2001 From: clock157 Date: Sat, 23 Feb 2019 21:31:57 +0800 Subject: [PATCH 03/94] feat: use umi-lint --- .prettierrc | 5 +++++ package.json | 20 +++++--------------- {src => types}/index.d.ts | 13 ++++++++----- 3 files changed, 18 insertions(+), 20 deletions(-) create mode 100644 .prettierrc rename {src => types}/index.d.ts (95%) diff --git a/.prettierrc b/.prettierrc new file mode 100644 index 0000000..32922af --- /dev/null +++ b/.prettierrc @@ -0,0 +1,5 @@ +{ + "printWidth": 120, + "trailingComma": "es5", + "singleQuote": true +} diff --git a/package.json b/package.json index dfd4c5a..0ffda56 100644 --- a/package.json +++ b/package.json @@ -15,13 +15,13 @@ "test": "umi-test", "test:watch": "umi-test --watch", "test:cover": "umi-test --coverage", - "lint": "eslint src", + "lint": "umi-lint src", "ci": "npm run lint && npm run test:cover", "coveralls": "cat ./coverage/lcov.info | coveralls", "prepublishOnly": "rm -rf ./dist && npm run lint && npm run test && npm run build", "pub": "npm publish", "pub-beta": "npm publish --tag beta", - "precommit": "lint-staged" + "precommit": "umi-lint --staged --prettier --fix" }, "author": { "name": "puwei", @@ -31,17 +31,14 @@ "devDependencies": { "create-test-server": "2.3.1", "debug": "^4.1.0", - "eslint": "^5.5.0", "eslint-config-airbnb-base": "^13.1.0", "eslint-config-prettier": "^3.3.0", "eslint-plugin-import": "^2.14.0", - "husky": "^0.14.3", "iconv-lite": "^0.4.24", "jest": "^23.5.0", - "lint-staged": "^8.1.0", - "prettier": "^1.15.3", "typescript": "^3.0.3", "umi": "^2.5.0", + "umi-lint": "^1.0.0-alpha.1", "umi-plugin-library": "^1.1.6", "umi-test": "^1.4.0" }, @@ -54,14 +51,7 @@ "package.json", "README.md", "CHANGELOG.md", - "src/index.d.ts" + "types/index.d.ts" ], - "types": "src/index.d.ts", - "lint-staged": { - "*.js": [ - "prettier --write --singleQuote=true --trailingComma=all --printWidth=120", - "eslint --fix", - "git add" - ] - } + "types": "types/index.d.ts" } diff --git a/src/index.d.ts b/types/index.d.ts similarity index 95% rename from src/index.d.ts rename to types/index.d.ts index b7e75a5..1ff00c5 100644 --- a/src/index.d.ts +++ b/types/index.d.ts @@ -42,9 +42,12 @@ export type RequestResponse = { response: Response; }; -export type RequestInterceptor = (url: string, options: RequestOptionsInit) => { - url?: string, - options?: RequestOptionsInit, +export type RequestInterceptor = ( + url: string, + options: RequestOptionsInit +) => { + url?: string; + options?: RequestOptionsInit; }; export type ResponseInterceptor = (response: Response, options: RequestOptionsInit) => Response; @@ -64,8 +67,8 @@ export interface RequestMethod { }; response: { use: (handler: ResponseInterceptor) => void; - } - } + }; + }; } export interface ExtendOnlyOptions { From 8f22cf4b42437a642dd79f1f7dfd07adcfe78b99 Mon Sep 17 00:00:00 2001 From: Wei Zhu Date: Wed, 27 Feb 2019 12:06:37 +0800 Subject: [PATCH 04/94] fix error type close #25 --- src/index.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/index.d.ts b/src/index.d.ts index b7e75a5..5cb4f9d 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,3 +1,5 @@ +import { ResponseError } from "./utils"; + export type ResponseType = 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData'; /** @@ -24,7 +26,7 @@ export interface RequestOptionsInit extends RequestInit { useCache?: boolean; ttl?: number; timeout?: number; - errorHandler?: (error: Error) => void; + errorHandler?: (error: ResponseError) => void; prefix?: string; suffix?: string; } From df647e9fd4d803b896c218d4eb0c7cd2086f2560 Mon Sep 17 00:00:00 2001 From: Ray Tsang Date: Sun, 24 Mar 2019 22:45:39 -0400 Subject: [PATCH 05/94] Update defaultInterceptor.js send data for patch method --- src/defaultInterceptor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/defaultInterceptor.js b/src/defaultInterceptor.js index 42b1c7c..297b2a7 100644 --- a/src/defaultInterceptor.js +++ b/src/defaultInterceptor.js @@ -14,7 +14,7 @@ export default (url, originOptions = {}) => { // 默认get, 兼容method大小写 let method = options.method || "get"; method = method.toLowerCase(); - if (method === "post" || method === "put") { + if (method === "post" || method === "put" || method === "patch") { // requestType 简写默认值为 json const { requestType = "json", data } = options; // 数据使用类axios的新字段data, 避免引用后影响旧代码, 如将body stringify多次 From b7ddc5f95d9b2ab5c752abe7531566b7dd4de632 Mon Sep 17 00:00:00 2001 From: clock157 Date: Mon, 25 Mar 2019 19:04:41 +0800 Subject: [PATCH 06/94] publish --- CHANGELOG.MD | 6 ++++++ package.json | 2 +- 2 files changed, 7 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 07a8d5b..9a216b7 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,3 +1,9 @@ +## 1.0.5 +* feat: #32 send data for patch + +## 1.0.4 +* fix: #25 error type + ## 1.0.3 * fix: #7 throw error when method is undefined diff --git a/package.json b/package.json index dfd4c5a..dada012 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.0.3", + "version": "1.0.5", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From fba794eac99075b8bae4b56b1725eb5de545ba4e Mon Sep 17 00:00:00 2001 From: zven21 Date: Mon, 1 Apr 2019 10:49:15 +0800 Subject: [PATCH 07/94] add credentials desc. --- README.md | 1 + README_zh-CN.md | 5 +++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 0e5d322..19e5c77 100644 --- a/README.md +++ b/README.md @@ -76,6 +76,7 @@ For more discussion, refer to [Traditional Ajax is dead, Fetch eternal life](htt | suffix | suffix, such as some scenes api need to be unified .json | string | -- | | errorHandler | exception handling, or override unified exception handling | function(error) | -- | | headers | fetch original parameters | object | -- | {} | +| credentials | fetch request with cookies | string | -- | credentials: 'include' | The other parameters of fetch are valid. See [fetch documentation](https://github.github.io/fetch/) diff --git a/README_zh-CN.md b/README_zh-CN.md index 1b82151..1b8cc4b 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -75,7 +75,8 @@ | prefix | 前缀, 一般用于覆盖统一设置的prefix | string | -- | -- | | suffix | 后缀, 比如某些场景 api 需要统一加 .json | string | -- | -- | | errorHandler | 异常处理, 或者覆盖统一的异常处理 | function(error) | -- | -| headers | fetch 原有参数 | object | -- | {} | +| headers | fetch 原有参数 | object | -- | {} | +| credentials | fetch 请求包含 cookies 信息 | object | -- | credentials: 'include' | fetch原其他参数有效, 详见[fetch文档](https://github.github.io/fetch/) @@ -193,7 +194,7 @@ const errorHandler = (error) => { const { response, data } = error; message.error(codeMap[data.errorCode]); - throw error; // 如果throw. 错误将继续抛出. + throw error; // 如果throw. 错误将继续抛出. // return {some: 'data'}; 如果return, 将值作为返回. 不写则相当于return undefined, 在处理结果时判断response是否有值即可. } From 66caba378a46f5a9af933c0abf0e32c269532fd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=B0=8F=E8=81=AA?= Date: Tue, 2 Apr 2019 11:26:40 +0800 Subject: [PATCH 08/94] Update index.d.ts --- src/index.d.ts | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/src/index.d.ts b/src/index.d.ts index 5cb4f9d..a145b71 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -1,7 +1,9 @@ -import { ResponseError } from "./utils"; - export type ResponseType = 'json' | 'text' | 'blob' | 'arrayBuffer' | 'formData'; - +export interface ResponseError extends Error { + name: string; + data: D; + response: Response; +} /** * 增加的参数 * @param {string} requestType post类型, 用来简化写content-Type, 默认json From e3c9f3ccfb8db514691f35617ffa04a54e08b8ca Mon Sep 17 00:00:00 2001 From: handylee Date: Wed, 10 Apr 2019 11:56:09 +0800 Subject: [PATCH 09/94] =?UTF-8?q?fix:=20index.d.ts=20=E7=BC=BA=E5=B0=91=20?= =?UTF-8?q?patch=20=E6=96=B9=E6=B3=95=E5=AE=9A=E4=B9=89?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/index.d.ts b/src/index.d.ts index a145b71..5e57e39 100644 --- a/src/index.d.ts +++ b/src/index.d.ts @@ -61,6 +61,7 @@ export interface RequestMethod { post: RequestMethod; delete: RequestMethod; put: RequestMethod; + patch: RequestMethod; rpc: RequestMethod; interceptors: { request: { From 14e12e498de8ec168a2694356a630c4a84dc43c9 Mon Sep 17 00:00:00 2001 From: clock157 Date: Thu, 11 Apr 2019 13:32:56 +0800 Subject: [PATCH 10/94] publish 1.0.6 --- CHANGELOG.MD | 3 +++ package.json | 2 +- 2 files changed, 4 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 9a216b7..7a818a4 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,3 +1,6 @@ +## 1.0.6 +* fix: index.d.ts 缺少 patch 方法定义 @handycode + ## 1.0.5 * feat: #32 send data for patch diff --git a/package.json b/package.json index dada012..3a9e2df 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.0.5", + "version": "1.0.6", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From d08636a360c7d196a215c68cb44535260250b44f Mon Sep 17 00:00:00 2001 From: xchunzhao Date: Wed, 17 Apr 2019 13:44:43 +0800 Subject: [PATCH 11/94] feat: Send data using delete --- src/defaultInterceptor.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/defaultInterceptor.js b/src/defaultInterceptor.js index 297b2a7..a7571a4 100644 --- a/src/defaultInterceptor.js +++ b/src/defaultInterceptor.js @@ -14,7 +14,7 @@ export default (url, originOptions = {}) => { // 默认get, 兼容method大小写 let method = options.method || "get"; method = method.toLowerCase(); - if (method === "post" || method === "put" || method === "patch") { + if (method === "post" || method === "put" || method === "patch" || method === 'delete') { // requestType 简写默认值为 json const { requestType = "json", data } = options; // 数据使用类axios的新字段data, 避免引用后影响旧代码, 如将body stringify多次 From c85c29d19aa6f5f533a3d0a453570b8465b79add Mon Sep 17 00:00:00 2001 From: xchunzhao Date: Thu, 18 Apr 2019 11:44:00 +0800 Subject: [PATCH 12/94] test: Add delete method test case --- test/index.test.js | 338 +++++++++++++++++++++++---------------------- 1 file changed, 175 insertions(+), 163 deletions(-) diff --git a/test/index.test.js b/test/index.test.js index 4874d1d..9b50e82 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,15 +1,15 @@ -import createTestServer from "create-test-server"; -import iconv from "iconv-lite"; -import request, { extend } from "../src/index"; -import { MapCache } from "../src/utils"; -const debug = require("debug")("afx-request:test"); +import createTestServer from 'create-test-server'; +import iconv from 'iconv-lite'; +import request, { extend } from '../src/index'; +import { MapCache } from '../src/utils'; +const debug = require('debug')('afx-request:test'); const writeData = (data, res) => { - res.setHeader("access-control-allow-origin", "*"); + res.setHeader('access-control-allow-origin', '*'); res.send(data); }; -describe("test fetch:", () => { +describe('test fetch:', () => { let server; beforeAll(async () => { @@ -19,227 +19,227 @@ describe("test fetch:", () => { const prefix = api => `${server.url}${api}`; // 测试超时 - it("test timeout", async () => { - server.get("/test/timeout", (req, res) => { + it('test timeout', async () => { + server.get('/test/timeout', (req, res) => { setTimeout(() => { - writeData("ok", res); + writeData('ok', res); }, 1000); }); // 第一次超时前返回 - let response = await request(prefix("/test/timeout"), { + let response = await request(prefix('/test/timeout'), { timeout: 1200, - getResponse: true + getResponse: true, }); expect(response.response.ok).toBe(true); // 第二次超时异常 try { - response = await request(prefix("/test/timeout"), { timeout: 800 }); + response = await request(prefix('/test/timeout'), { timeout: 800 }); } catch (error) { - expect(error.name).toBe("RequestError"); + expect(error.name).toBe('RequestError'); } }, 5000); // 测试请求类型 - it("test requestType", async () => { - server.post("/test/requestType", (req, res) => { + it('test requestType', async () => { + server.post('/test/requestType', (req, res) => { writeData(req.body, res); }); - let response = await request(prefix("/test/requestType"), { - method: "post", - requestType: "form", + let response = await request(prefix('/test/requestType'), { + method: 'post', + requestType: 'form', data: { - hello: "world" - } + hello: 'world', + }, }); - expect(response.hello).toBe("world"); + expect(response.hello).toBe('world'); - response = await request(prefix("/test/requestType"), { - method: "post", - requestType: "json", + response = await request(prefix('/test/requestType'), { + method: 'post', + requestType: 'json', data: { - hello: "world2" - } + hello: 'world2', + }, }); - expect(response.hello).toBe("world2"); + expect(response.hello).toBe('world2'); - response = await request(prefix("/test/requestType"), { - method: "post", - data: "hehe" + response = await request(prefix('/test/requestType'), { + method: 'post', + data: 'hehe', }); - expect(response).toBe("hehe"); + expect(response).toBe('hehe'); }, 5000); // 测试返回类型 #TODO 更多类型 - it("test responseType", async () => { - server.post("/test/responseType", (req, res) => { + it('test responseType', async () => { + server.post('/test/responseType', (req, res) => { writeData(req.body, res); }); - server.get("/test/responseType", (req, res) => { - if (req.query.type === "blob") { - const data = new Blob(["aaaaa"]); + server.get('/test/responseType', (req, res) => { + if (req.query.type === 'blob') { + const data = new Blob(['aaaaa']); writeData(data, res); } else { writeData(req.body, res); } }); - let response = await request(prefix("/test/responseType"), { - method: "post", - responseType: "json", - data: { a: 11 } + let response = await request(prefix('/test/responseType'), { + method: 'post', + responseType: 'json', + data: { a: 11 }, }); expect(response.a).toBe(11); try { - response = await request(prefix("/test/responseType"), { - responseType: "other", - params: { type: "blob" } + response = await request(prefix('/test/responseType'), { + responseType: 'other', + params: { type: 'blob' }, }); } catch (error) { - expect(error.message).toBe("responseType not support"); + expect(error.message).toBe('responseType not support'); } }, 5000); // 测试拼接参数 - it("test queryParams", async () => { - server.get("/test/queryParams", (req, res) => { + it('test queryParams', async () => { + server.get('/test/queryParams', (req, res) => { writeData(req.query, res); }); - let response = await request(prefix("/test/queryParams"), { + let response = await request(prefix('/test/queryParams'), { params: { - hello: "world3", - wang: "hou" - } + hello: 'world3', + wang: 'hou', + }, }); - expect(response.wang).toBe("hou"); + expect(response.wang).toBe('hou'); }, 5000); // 测试缓存 - it("test cache", async () => { - server.get("/test/cache", (req, res) => { + it('test cache', async () => { + server.get('/test/cache', (req, res) => { writeData(req.query, res); }); - server.get("/test/cache2", (req, res) => { + server.get('/test/cache2', (req, res) => { writeData(req.query, res); }); - server.get("/test/cache3", (req, res) => { + server.get('/test/cache3', (req, res) => { writeData(req.query, res); }); const extendRequest = extend({ maxCache: 2, prefix: server.url, - headers: { Connection: "keep-alive" } + headers: { Connection: 'keep-alive' }, }); // 第一次写入缓存 - let response = await extendRequest("/test/cache", { + let response = await extendRequest('/test/cache', { params: { - hello: "world3", - wang: "hou" + hello: 'world3', + wang: 'hou', }, useCache: true, - ttl: 5000 + ttl: 5000, }); // 第二次读取缓存 - response = await extendRequest("/test/cache", { + response = await extendRequest('/test/cache', { params: { - hello: "world3", - wang: "hou" + hello: 'world3', + wang: 'hou', }, useCache: true, ttl: 5000, - getResponse: true + getResponse: true, }); expect(response.response.useCache).toBe(true); // 模拟参数不一致, 读取失败 - response = await extendRequest("/test/cache2", { + response = await extendRequest('/test/cache2', { params: { - hello: "world3", - wang: "hou" + hello: 'world3', + wang: 'hou', }, useCache: true, ttl: 5000, - getResponse: true + getResponse: true, }); expect(response.response.useCache).toBe(false); // 模拟写入第三次, 第一个将被删掉 - response = await extendRequest("/test/cache3", { + response = await extendRequest('/test/cache3', { params: { - hello: "world3", - wang: "hou" + hello: 'world3', + wang: 'hou', }, useCache: true, - ttl: 5000 + ttl: 5000, }); // 读取第一个缓存, 将读取失败 - response = await extendRequest("/test/cache", { + response = await extendRequest('/test/cache', { params: { - hello: "world3", - wang: "hou" + hello: 'world3', + wang: 'hou', }, useCache: true, ttl: 5000, - getResponse: true + getResponse: true, }); expect(response.response.useCache).toBe(false); }, 10000); // 测试异常捕获 - it("test exception", async () => { - server.get("/test/exception", (req, res) => { - res.setHeader("access-control-allow-origin", "*"); + it('test exception', async () => { + server.get('/test/exception', (req, res) => { + res.setHeader('access-control-allow-origin', '*'); res.status(401); res.send({ hello: 11 }); }); // 测试访问一个不存在的网址 try { - let response = await request(prefix("/test/exception"), { + let response = await request(prefix('/test/exception'), { params: { - hello: "world3", - wang: "hou" - } + hello: 'world3', + wang: 'hou', + }, }); } catch (error) { - expect(error.name).toBe("ResponseError"); + expect(error.name).toBe('ResponseError'); expect(error.response.status).toBe(401); } }, 6000); // 测试字符集 gbk支持 https://yuque.antfin-inc.com/zhizheng.ck/me_and_world/rfaldm - it("test charset", async () => { - server.get("/test/charset", (req, res) => { - res.setHeader("access-control-allow-origin", "*"); - res.setHeader("Content-Type", "text/html; charset=gbk"); - writeData(iconv.encode("我是乱码?", "gbk"), res); + it('test charset', async () => { + server.get('/test/charset', (req, res) => { + res.setHeader('access-control-allow-origin', '*'); + res.setHeader('Content-Type', 'text/html; charset=gbk'); + writeData(iconv.encode('我是乱码?', 'gbk'), res); }); - let response = await request(prefix("/test/charset"), { charset: "gbk" }); - expect(response).toBe("我是乱码?"); + let response = await request(prefix('/test/charset'), { charset: 'gbk' }); + expect(response).toBe('我是乱码?'); }, 6000); // 测试错误处理方法 - it("test errorHandler", async () => { - server.get("/test/errorHandler", (req, res) => { - res.setHeader("access-control-allow-origin", "*"); + it('test errorHandler', async () => { + server.get('/test/errorHandler', (req, res) => { + res.setHeader('access-control-allow-origin', '*'); res.status(401); - res.send({ errorCode: "021", errorMsg: "some thing wrong" }); + res.send({ errorCode: '021', errorMsg: 'some thing wrong' }); }); const codeMap = { - "021": "发生错误啦", - "022": "发生大大大大错误啦" + '021': '发生错误啦', + '022': '发生大大大大错误啦', }; const errorHandler = error => { @@ -254,66 +254,78 @@ describe("test fetch:", () => { const extendRequest = extend({ prefix: server.url, - errorHandler + errorHandler, }); try { - let response = await extendRequest.get("/test/errorHandler"); + let response = await extendRequest.get('/test/errorHandler'); } catch (error) { - expect(error).toBe("发生错误啦"); + expect(error).toBe('发生错误啦'); } try { - let response = await extendRequest.get("/test/errorHandler", { + let response = await extendRequest.get('/test/errorHandler', { errorHandler: error => { - return "返回数据"; - } + return '返回数据'; + }, }); - expect(response).toBe("返回数据"); - response = await extendRequest.get("/test/errorHandler", { + expect(response).toBe('返回数据'); + response = await extendRequest.get('/test/errorHandler', { errorHandler: error => { - throw "统一错误处理被覆盖啦"; - } + throw '统一错误处理被覆盖啦'; + }, }); // throw response; } catch (error) { - expect(error).toBe("统一错误处理被覆盖啦"); + expect(error).toBe('统一错误处理被覆盖啦'); } }, 6000); - it("test prefix and suffix", async () => { - server.get("/prefix/api/hello", (req, res) => { + it('test prefix and suffix', async () => { + server.get('/prefix/api/hello', (req, res) => { writeData({ success: true }, res); }); - server.get("/api/hello.json", (req, res) => { + server.get('/api/hello.json', (req, res) => { writeData({ success: true }, res); }); - let response = await request("/hello", { - prefix: `${server.url}/prefix/api` + let response = await request('/hello', { + prefix: `${server.url}/prefix/api`, }); expect(response.success).toBe(true); - response = await request(prefix("/api/hello"), { - suffix: ".json", - params: { hello: "world" } + response = await request(prefix('/api/hello'), { + suffix: '.json', + params: { hello: 'world' }, }); expect(response.success).toBe(true); }); - it("test array json", async () => { - server.post("/api/array/json", (req, res) => { + it('test array json', async () => { + server.post('/api/array/json', (req, res) => { + writeData({ data: req.body }, res); + }); + + //server.delete throw error: Cross origin http://localhost forbidden + server.all('/api/array/json/delete', (req, res) => { writeData({ data: req.body }, res); }); - let response = await request(prefix("/api/array/json"), { - method: "post", - data: ["hello", { world: "two" }] + let response = await request(prefix('/api/array/json'), { + method: 'post', + data: ['hello', { world: 'two' }], }); - expect(response.data[0]).toBe("hello"); - expect(response.data[1].world).toBe("two"); + expect(response.data[0]).toBe('hello'); + expect(response.data[1].world).toBe('two'); + + response = await request(prefix('/api/array/json/delete'), { + method: 'delete', + data: ['1', '2'], + }); + expect(response.data[0]).toBe('1'); + expect(response.data[1]).toBe('2'); }); afterAll(() => { @@ -322,53 +334,53 @@ describe("test fetch:", () => { }); // 测试rpc #TODO -describe("test rpc:", () => { - it("test hello", () => { - expect(request.rpc("wang").hello).toBe("wang"); +describe('test rpc:', () => { + it('test hello', () => { + expect(request.rpc('wang').hello).toBe('wang'); }); }); // 测试工具函数 -describe("test utils:", () => { - it("test cache:", done => { +describe('test utils:', () => { + it('test cache:', done => { const mapCache = new MapCache({ maxCache: 3 }); // 设置读取 - const key = { some: "one" }; - mapCache.set(key, { hello: "world1" }, 1000); - expect(mapCache.get(key).hello).toBe("world1"); + const key = { some: 'one' }; + mapCache.set(key, { hello: 'world1' }, 1000); + expect(mapCache.get(key).hello).toBe('world1'); setTimeout(() => { expect(mapCache.get(key)).toBe(undefined); done(); }, 1001); // 删除 - const key2 = { other: "two" }; - mapCache.set(key2, { hello: "world1" }, 10000); + const key2 = { other: 'two' }; + mapCache.set(key2, { hello: 'world1' }, 10000); mapCache.delete(key2); expect(mapCache.get(key2)).toBe(undefined); // 清除 - const key3 = { other: "three" }; - mapCache.set(key3, { hello: "world1" }, 10000); + const key3 = { other: 'three' }; + mapCache.set(key3, { hello: 'world1' }, 10000); mapCache.clear(); expect(mapCache.get(key3)).toBe(undefined); // 测试超过最大数 - mapCache.set("max1", { what: "ok" }, 10000); - mapCache.set("max1", { what: "ok1" }, 10000); - mapCache.set("max2", { what: "ok2" }, 10000); - mapCache.set("max3", { what: "ok3" }, 10000); - expect(mapCache.get("max1").what).toBe("ok1"); - mapCache.set("max4", { what: "ok4" }, 10000); - expect(mapCache.get("max1")).toBe(undefined); - mapCache.set("max5", { what: "ok5" }); - mapCache.set("max6", { what: "ok6" }, 0); + mapCache.set('max1', { what: 'ok' }, 10000); + mapCache.set('max1', { what: 'ok1' }, 10000); + mapCache.set('max2', { what: 'ok2' }, 10000); + mapCache.set('max3', { what: 'ok3' }, 10000); + expect(mapCache.get('max1').what).toBe('ok1'); + mapCache.set('max4', { what: 'ok4' }, 10000); + expect(mapCache.get('max1')).toBe(undefined); + mapCache.set('max5', { what: 'ok5' }); + mapCache.set('max6', { what: 'ok6' }, 0); }, 3000); }); // 测试fetch lib -describe("test fetch lib:", () => { +describe('test fetch lib:', () => { let server; beforeAll(async () => { @@ -377,8 +389,8 @@ describe("test fetch lib:", () => { const prefix = api => `${server.url}${api}`; - it("test interceptors", async () => { - server.get("/test/interceptors", (req, res) => { + it('test interceptors', async () => { + server.get('/test/interceptors', (req, res) => { writeData(req.query, res); }); @@ -396,50 +408,50 @@ describe("test fetch lib:", () => { debug(url, options); return { url: `${url}?interceptors=yes`, - options: { ...options, interceptors: true } + options: { ...options, interceptors: true }, }; }); // response拦截器, 修改一个header request.interceptors.response.use((res, options) => { - res.headers.append("interceptors", "yes yo"); + res.headers.append('interceptors', 'yes yo'); return res; }); - let response = await request(prefix("/test/interceptors"), { + let response = await request(prefix('/test/interceptors'), { timeout: 1200, - getResponse: true + getResponse: true, }); - expect(response.data.interceptors).toBe("yes"); - expect(response.response.headers.get("interceptors")).toBe("yes yo"); + expect(response.data.interceptors).toBe('yes'); + expect(response.response.headers.get('interceptors')).toBe('yes yo'); // 测试乱写 try { request({ hello: 1 }); } catch (error) { - expect(error.message).toBe("url MUST be a string"); + expect(error.message).toBe('url MUST be a string'); } }); - it("modify request data", async () => { - server.post("/test/post/interceptors", (req, res) => { + it('modify request data', async () => { + server.post('/test/post/interceptors', (req, res) => { writeData(req.body, res); }); request.interceptors.request.use((url, options) => { - if (options.method.toLowerCase() === "post") { + if (options.method.toLowerCase() === 'post') { options.data = { ...options.data, - foo: "foo" + foo: 'foo', }; } return { url, options }; }); - let data = await request(prefix("/test/post/interceptors"), { - method: "post", - data: { bar: "bar" } + let data = await request(prefix('/test/post/interceptors'), { + method: 'post', + data: { bar: 'bar' }, }); - expect(data.foo).toBe("foo"); + expect(data.foo).toBe('foo'); }); afterAll(() => { From 7c0e92b2bae2823cc5aa11b96f0948473fec423c Mon Sep 17 00:00:00 2001 From: clock157 Date: Sat, 20 Apr 2019 10:53:12 +0800 Subject: [PATCH 13/94] refactor: use umi-lint format code --- src/defaultInterceptor.js | 34 +++++++++++++++++----------------- src/index.js | 6 +++--- src/lib/fetch.js | 14 +++++++------- src/request.js | 17 ++++++++--------- src/utils.js | 6 +++--- src/wrapped-fetch.js | 31 ++++++++++++------------------- test/index.test.js | 27 +++++++++++---------------- 7 files changed, 61 insertions(+), 74 deletions(-) diff --git a/src/defaultInterceptor.js b/src/defaultInterceptor.js index a7571a4..8865c16 100644 --- a/src/defaultInterceptor.js +++ b/src/defaultInterceptor.js @@ -1,4 +1,4 @@ -import { stringify } from "query-string"; +import { stringify } from 'query-string'; /** * 注册request拦截器 * get 和 post 参数简化 @@ -12,35 +12,35 @@ import { stringify } from "query-string"; export default (url, originOptions = {}) => { const options = { ...originOptions }; // 默认get, 兼容method大小写 - let method = options.method || "get"; + let method = options.method || 'get'; method = method.toLowerCase(); - if (method === "post" || method === "put" || method === "patch" || method === 'delete') { + if (method === 'post' || method === 'put' || method === 'patch' || method === 'delete') { // requestType 简写默认值为 json - const { requestType = "json", data } = options; + const { requestType = 'json', data } = options; // 数据使用类axios的新字段data, 避免引用后影响旧代码, 如将body stringify多次 if (data) { const dataType = Object.prototype.toString.call(data); - if (dataType === "[object Object]" || dataType === "[object Array]") { - if (requestType === "json") { + if (dataType === '[object Object]' || dataType === '[object Array]') { + if (requestType === 'json') { options.headers = { - Accept: "application/json", - "Content-Type": "application/json;charset=UTF-8", - ...options.headers + Accept: 'application/json', + 'Content-Type': 'application/json;charset=UTF-8', + ...options.headers, }; options.body = JSON.stringify(data); - } else if (requestType === "form") { + } else if (requestType === 'form') { options.headers = { - Accept: "application/json", - "Content-Type": "application/x-www-form-urlencoded;charset=UTF-8", - ...options.headers + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + ...options.headers, }; options.body = stringify(data); } } else { // 其他 requestType 自定义header options.headers = { - Accept: "application/json", - ...options.headers + Accept: 'application/json', + ...options.headers, }; options.body = data; } @@ -49,12 +49,12 @@ export default (url, originOptions = {}) => { // 支持类似axios 参数自动拼装, 其他method也可用, 不冲突. if (options.params && Object.keys(options.params).length > 0) { - const str = url.indexOf("?") !== -1 ? "&" : "?"; + const str = url.indexOf('?') !== -1 ? '&' : '?'; url = `${url}${str}${stringify(options.params)}`; } return { url, - options + options, }; }; diff --git a/src/index.js b/src/index.js index c5e938b..ad1f675 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ -import fetch from "./lib/fetch"; -import request, { extend } from "./request"; -import { RequestError, ResponseError } from "./utils"; +import fetch from './lib/fetch'; +import request, { extend } from './request'; +import { RequestError, ResponseError } from './utils'; export { fetch, extend, RequestError, ResponseError }; diff --git a/src/lib/fetch.js b/src/lib/fetch.js index 4390b07..a885249 100644 --- a/src/lib/fetch.js +++ b/src/lib/fetch.js @@ -1,11 +1,11 @@ -import "whatwg-fetch"; -import defaultInterceptor from "../defaultInterceptor"; +import 'whatwg-fetch'; +import defaultInterceptor from '../defaultInterceptor'; const requestInterceptors = []; export const responseInterceptors = []; function fetch(url, options = {}) { - if (typeof url !== "string") throw new Error("url MUST be a string"); + if (typeof url !== 'string') throw new Error('url MUST be a string'); // 执行 request 的拦截器 requestInterceptors.concat([defaultInterceptor]).forEach(handler => { @@ -15,7 +15,7 @@ function fetch(url, options = {}) { }); // 将 method 改为大写 - options.method = options.method ? options.method.toUpperCase() : "GET"; + options.method = options.method ? options.method.toUpperCase() : 'GET'; // 请求数据 let response = window.fetch(url, options); @@ -32,13 +32,13 @@ fetch.interceptors = { request: { use: handler => { requestInterceptors.push(handler); - } + }, }, response: { use: handler => { responseInterceptors.push(handler); - } - } + }, + }, }; export default fetch; diff --git a/src/request.js b/src/request.js index 09d1016..26c9631 100644 --- a/src/request.js +++ b/src/request.js @@ -1,7 +1,7 @@ -import fetch from "./lib/fetch"; -import { MapCache } from "./utils"; -import WrappedFetch from "./wrapped-fetch"; -import WrappedRpc from "./wrapped-rpc"; +import fetch from './lib/fetch'; +import { MapCache } from './utils'; +import WrappedFetch from './wrapped-fetch'; +import WrappedRpc from './wrapped-rpc'; /** * 获取request实例 调用参数可以覆盖初始化的参数. 用于一些情况的特殊处理. @@ -13,9 +13,9 @@ const request = (initOptions = {}) => { options.headers = { ...initOptions.headers, ...options.headers }; options.params = { ...initOptions.params, ...options.params }; options = { ...initOptions, ...options }; - const method = options.method || "get"; + const method = options.method || 'get'; options.method = method.toLowerCase(); - if (method === "rpc") { + if (method === 'rpc') { // call rpc return new WrappedRpc(input, options, mapCache); } else { @@ -24,10 +24,9 @@ const request = (initOptions = {}) => { }; // 增加语法糖如: request.get request.post - const methods = ["get", "post", "delete", "put", "rpc", "patch"]; + const methods = ['get', 'post', 'delete', 'put', 'rpc', 'patch']; methods.forEach(method => { - instance[method] = (input, options) => - instance(input, { ...options, method }); + instance[method] = (input, options) => instance(input, { ...options, method }); }); // 给request 也增加一个interceptors引用; diff --git a/src/utils.js b/src/utils.js index 903c89d..f8306bf 100644 --- a/src/utils.js +++ b/src/utils.js @@ -51,7 +51,7 @@ export class MapCache { export class RequestError extends Error { constructor(text) { super(text); - this.name = "RequestError"; + this.name = 'RequestError'; } } @@ -61,7 +61,7 @@ export class RequestError extends Error { export class ResponseError extends Error { constructor(response, text, data) { super(text || response.statusText); - this.name = "ResponseError"; + this.name = 'ResponseError'; this.data = data; this.response = response; } @@ -78,7 +78,7 @@ export function readerGBK(file) { resolve(reader.result); }; reader.onerror = reject; - reader.readAsText(file, "GBK"); // setup GBK decoding + reader.readAsText(file, 'GBK'); // setup GBK decoding }); } diff --git a/src/wrapped-fetch.js b/src/wrapped-fetch.js index 0e4a352..1292429 100644 --- a/src/wrapped-fetch.js +++ b/src/wrapped-fetch.js @@ -1,5 +1,5 @@ -import fetch, { responseInterceptors } from "./lib/fetch"; -import { RequestError, ResponseError, readerGBK, safeJsonParse } from "./utils"; +import fetch, { responseInterceptors } from './lib/fetch'; +import { RequestError, ResponseError, readerGBK, safeJsonParse } from './utils'; export default class WrappedFetch { constructor(url, options, cache) { @@ -24,11 +24,11 @@ export default class WrappedFetch { } _doFetch() { - const useCache = this.options.method === "get" && this.options.useCache; + const useCache = this.options.method === 'get' && this.options.useCache; if (useCache) { let response = this.cache.get({ url: this.url, - params: this.options.params + params: this.options.params, }); if (response) { response = response.clone(); @@ -63,12 +63,9 @@ export default class WrappedFetch { if (timeout > 0) { return Promise.race([ new Promise((_, reject) => - setTimeout( - () => reject(new RequestError(`timeout of ${timeout}ms exceeded`)), - timeout - ) + setTimeout(() => reject(new RequestError(`timeout of ${timeout}ms exceeded`)), timeout) ), - instance + instance, ]); } else { return instance; @@ -103,18 +100,14 @@ export default class WrappedFetch { * @param {boolean} useCache 返回类型, 默认json */ _parseResponse(instance, useCache = false) { - const { - responseType = "json", - charset = "utf8", - getResponse = false - } = this.options; + const { responseType = 'json', charset = 'utf8', getResponse = false } = this.options; return new Promise((resolve, reject) => { let copy; instance .then(response => { copy = response.clone(); copy.useCache = useCache; - if (charset === "gbk") { + if (charset === 'gbk') { try { return response .blob() @@ -123,14 +116,14 @@ export default class WrappedFetch { } catch (e) { throw new ResponseError(copy, e.message); } - } else if (responseType === "json" || responseType === "text") { + } else if (responseType === 'json' || responseType === 'text') { return response.text().then(safeJsonParse); } else { try { // 其他如blob, arrayBuffer, formData return response[responseType](); } catch (e) { - throw new ResponseError(copy, "responseType not support"); + throw new ResponseError(copy, 'responseType not support'); } } }) @@ -140,13 +133,13 @@ export default class WrappedFetch { if (getResponse) { resolve({ data, - response: copy + response: copy, }); } else { resolve(data); } } else { - throw new ResponseError(copy, "http error", data); + throw new ResponseError(copy, 'http error', data); } }) .catch(this._handleError.bind(this, { reject, resolve })); diff --git a/test/index.test.js b/test/index.test.js index 9b50e82..53396dd 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -2,6 +2,7 @@ import createTestServer from 'create-test-server'; import iconv from 'iconv-lite'; import request, { extend } from '../src/index'; import { MapCache } from '../src/utils'; + const debug = require('debug')('afx-request:test'); const writeData = (data, res) => { @@ -109,7 +110,7 @@ describe('test fetch:', () => { writeData(req.query, res); }); - let response = await request(prefix('/test/queryParams'), { + const response = await request(prefix('/test/queryParams'), { params: { hello: 'world3', wang: 'hou', @@ -205,7 +206,7 @@ describe('test fetch:', () => { }); // 测试访问一个不存在的网址 try { - let response = await request(prefix('/test/exception'), { + const response = await request(prefix('/test/exception'), { params: { hello: 'world3', wang: 'hou', @@ -225,7 +226,7 @@ describe('test fetch:', () => { writeData(iconv.encode('我是乱码?', 'gbk'), res); }); - let response = await request(prefix('/test/charset'), { charset: 'gbk' }); + const response = await request(prefix('/test/charset'), { charset: 'gbk' }); expect(response).toBe('我是乱码?'); }, 6000); @@ -258,16 +259,14 @@ describe('test fetch:', () => { }); try { - let response = await extendRequest.get('/test/errorHandler'); + const response = await extendRequest.get('/test/errorHandler'); } catch (error) { expect(error).toBe('发生错误啦'); } try { let response = await extendRequest.get('/test/errorHandler', { - errorHandler: error => { - return '返回数据'; - }, + errorHandler: error => '返回数据', }); expect(response).toBe('返回数据'); response = await extendRequest.get('/test/errorHandler', { @@ -307,7 +306,7 @@ describe('test fetch:', () => { writeData({ data: req.body }, res); }); - //server.delete throw error: Cross origin http://localhost forbidden + // server.delete throw error: Cross origin http://localhost forbidden server.all('/api/array/json/delete', (req, res) => { writeData({ data: req.body }, res); }); @@ -395,13 +394,9 @@ describe('test fetch lib:', () => { }); // 测试啥也不返回 - request.interceptors.request.use(() => { - return {}; - }); + request.interceptors.request.use(() => ({})); - request.interceptors.response.use(res => { - return res; - }); + request.interceptors.response.use(res => res); // request拦截器, 加个参数 request.interceptors.request.use((url, options) => { @@ -418,7 +413,7 @@ describe('test fetch lib:', () => { return res; }); - let response = await request(prefix('/test/interceptors'), { + const response = await request(prefix('/test/interceptors'), { timeout: 1200, getResponse: true, }); @@ -447,7 +442,7 @@ describe('test fetch lib:', () => { return { url, options }; }); - let data = await request(prefix('/test/post/interceptors'), { + const data = await request(prefix('/test/post/interceptors'), { method: 'post', data: { bar: 'bar' }, }); From 0cc6cafdced2938d5ae87d4bdd0576e513e18a3a Mon Sep 17 00:00:00 2001 From: clock157 Date: Wed, 24 Apr 2019 19:53:28 +0800 Subject: [PATCH 14/94] publish: 1.0.7 --- CHANGELOG.MD | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 7a818a4..52fb6ae 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,3 +1,7 @@ +## 1.0.7 +* feat: Send data using delete +* feat: use umi-lint + ## 1.0.6 * fix: index.d.ts 缺少 patch 方法定义 @handycode diff --git a/package.json b/package.json index 32dcef2..9499b06 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.0.6", + "version": "1.0.7", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From 2c6e690d46c85fb451b115445512741c6e2a0103 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=99=88=E5=B8=85?= Date: Tue, 7 May 2019 17:15:49 +0800 Subject: [PATCH 15/94] type support async --- types/index.d.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index 86fe3b6..56b89ea 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -54,7 +54,9 @@ export type RequestInterceptor = ( options?: RequestOptionsInit; }; -export type ResponseInterceptor = (response: Response, options: RequestOptionsInit) => Response; +// use async ()=> Response equal ()=> Response + +export type ResponseInterceptor = (response: Response, options: RequestOptionsInit) => Response | Promise; export interface RequestMethod { (url: string, options: RequestOptionsWithResponse): Promise>; From 7f725808029b423afd824a3ffb443d11634ac5b8 Mon Sep 17 00:00:00 2001 From: sorrycc Date: Wed, 8 May 2019 15:38:40 +0800 Subject: [PATCH 16/94] chore: update README.md --- README.md | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index 19e5c77..edf0913 100644 --- a/README.md +++ b/README.md @@ -4,13 +4,9 @@ English | [简体中文](./README_zh-CN.md) 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][npm-image]][npm-url] -[![build status][travis-image]][travis-url] - -[npm-image]: https://img.shields.io/npm/v/umi-request.svg?style=flat-square -[npm-url]: https://npmjs.org/package/umi-request -[travis-image]: https://img.shields.io/travis/umijs/umi-request.svg?style=flat-square -[travis-url]: https://travis-ci.org/umijs/umi-request.svg?branch=master +[![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) -------------------- @@ -261,8 +257,16 @@ request.interceptors.response.use(async (response) => { - 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 From a4b5caf4920ebd06a505723b8c0b438154c85a4b Mon Sep 17 00:00:00 2001 From: Pu Wei Date: Wed, 15 May 2019 09:15:35 +0800 Subject: [PATCH 17/94] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index edf0913..c506a9b 100644 --- a/README.md +++ b/README.md @@ -39,7 +39,7 @@ The network request library, based on fetch encapsulation, combines the features | processing gbk | ✅ | ❎ | ❎ | | quick Support | ✅ | ❓ | ❓ | -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-request/issues) +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 From 9382bed37facfe55d3a645d32956373ebfd70031 Mon Sep 17 00:00:00 2001 From: Pu Wei Date: Wed, 15 May 2019 09:16:02 +0800 Subject: [PATCH 18/94] Update README_zh-CN.md --- README_zh-CN.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README_zh-CN.md b/README_zh-CN.md index 1b8cc4b..92758ef 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -43,7 +43,7 @@ | 处理 gbk | ✅ | ❎ | ❎ | | 快速支持 | ✅ | ❓ | ❓ | -更多讨论参考[传统 Ajax 已死,Fetch 永生](https://github.com/camsong/blog/issues/2), 如果你有好的建议和需求, 请提 [issue](https://github.com/umijs/umi-request/issues) +更多讨论参考[传统 Ajax 已死,Fetch 永生](https://github.com/camsong/blog/issues/2), 如果你有好的建议和需求, 请提 [issue](https://github.com/umijs/umi/issues) ## TODO 欢迎pr From b0cceb61df6883e626808c7bd1e2204b809dcae7 Mon Sep 17 00:00:00 2001 From: sorrycc Date: Wed, 12 Jun 2019 11:29:44 +0800 Subject: [PATCH 19/94] chore: use np for publish --- .gitignore | 2 ++ package.json | 7 +++---- 2 files changed, 5 insertions(+), 4 deletions(-) diff --git a/.gitignore b/.gitignore index fd40d94..b54d010 100644 --- a/.gitignore +++ b/.gitignore @@ -4,3 +4,5 @@ coverage .git .vscode .DS_Store +/yarn.lock + diff --git a/package.json b/package.json index 9499b06..6be8285 100644 --- a/package.json +++ b/package.json @@ -18,9 +18,7 @@ "lint": "umi-lint src", "ci": "npm run lint && npm run test:cover", "coveralls": "cat ./coverage/lcov.info | coveralls", - "prepublishOnly": "rm -rf ./dist && npm run lint && npm run test && npm run build", - "pub": "npm publish", - "pub-beta": "npm publish --tag beta", + "prepublishOnly": "rm -rf ./dist && npm run lint && npm run test && npm run build && np --no-cleanup --yolo --no-publish", "precommit": "umi-lint --staged --prettier --fix" }, "author": { @@ -40,7 +38,8 @@ "umi": "^2.5.0", "umi-lint": "^1.0.0-alpha.1", "umi-plugin-library": "^1.1.6", - "umi-test": "^1.4.0" + "umi-test": "^1.4.0", + "np": "5.0.2" }, "dependencies": { "query-string": "^6.0.0", From 75ffc1449f25888eebd4d1522047bf1591451fb5 Mon Sep 17 00:00:00 2001 From: sorrycc Date: Wed, 12 Jun 2019 11:42:03 +0800 Subject: [PATCH 20/94] v1.0.8 --- package.json | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/package.json b/package.json index 6be8285..9a05edd 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.0.7", + "version": "1.0.8", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", @@ -34,12 +34,12 @@ "eslint-plugin-import": "^2.14.0", "iconv-lite": "^0.4.24", "jest": "^23.5.0", + "np": "5.0.2", "typescript": "^3.0.3", "umi": "^2.5.0", "umi-lint": "^1.0.0-alpha.1", "umi-plugin-library": "^1.1.6", - "umi-test": "^1.4.0", - "np": "5.0.2" + "umi-test": "^1.4.0" }, "dependencies": { "query-string": "^6.0.0", From 6709011b1f357628a840ab409d9050260fecd906 Mon Sep 17 00:00:00 2001 From: Pu Wei Date: Wed, 3 Jul 2019 11:02:19 +0800 Subject: [PATCH 21/94] doc: upload file example code (#54) --- README.md | 8 ++++++-- README_zh-CN.md | 8 ++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index c506a9b..8408472 100644 --- a/README.md +++ b/README.md @@ -37,7 +37,6 @@ The network request library, based on fetch encapsulation, combines the features | prefix | ✅ | ❎ | ❎ | | suffix | ✅ | ❎ | ❎ | | processing gbk | ✅ | ❎ | ❎ | -| quick Support | ✅ | ❓ | ❓ | 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) @@ -134,9 +133,14 @@ request('/api/v1/some/api', { method:'post', requestType: 'form', data: {foo: 'b // reponseType: 'blob', how to handle the returned data, by default text and json are not added. Such as blob or formData need to add request('/api/v1/some/api', { reponseType: 'blob' }); -// Submit other data, such as text, upload files, etc., requestType is not filled, manually add the corresponding header. +// Submit other data, such as text, requestType is not filled, manually add the corresponding header. request('/api/v1/some/api', { method:'post', data: 'some data', headers: { 'Content-Type': 'multipart/form-data'} }); +// upload file +const formData = new FormData(); +formData.append('file', file); +request('/api/v1/some/api', { method:'post', data: formData }); + // The default is to return the data body, if you need the source response to expand, you can use the getResponse parameter. The result will be a set of layers request('/api/v1/some/api', { getResponse: true }).then({data, response} => {   console.log(data, response); diff --git a/README_zh-CN.md b/README_zh-CN.md index 92758ef..900096d 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -41,7 +41,6 @@ | 前缀 | ✅ | ❎ | ❎ | | 后缀 | ✅ | ❎ | ❎ | | 处理 gbk | ✅ | ❎ | ❎ | -| 快速支持 | ✅ | ❓ | ❓ | 更多讨论参考[传统 Ajax 已死,Fetch 永生](https://github.com/camsong/blog/issues/2), 如果你有好的建议和需求, 请提 [issue](https://github.com/umijs/umi/issues) @@ -138,9 +137,14 @@ request('/api/v1/some/api', { method:'post', requestType: 'form', data: {foo: 'b // reponseType: 'blob', 如何处理返回的数据, 默认情况下 text 和 json 都不用加. 如blob 或 formData 之类需要加 request('/api/v1/some/api', { reponseType: 'blob' }); -// 提交其他数据, 如文本, 上传文件等, requestType不填, 手动添加对应header. +// 提交其他数据, requestType不填, 手动添加对应header. request('/api/v1/some/api', { method:'post', data: 'some data', headers: { 'Content-Type': 'multipart/form-data'} }); +// 文件上传, 不要自己设置 Content-Type ! +const formData = new FormData(); +formData.append('file', file); +request('/api/v1/some/api', { method:'post', data: formData }); + // 默认返回的就是数据体, 如果需要源response来扩展, 可用getResponse参数. 返回结果会多套一层 request('/api/v1/some/api', { getResponse: true }).then({data, response} => { console.log(data, response); From 194e5fa118ce6f542f0bec333498bd3da0746a15 Mon Sep 17 00:00:00 2001 From: atzcl Date: Sun, 7 Jul 2019 21:54:36 +0800 Subject: [PATCH 22/94] refactor: revise response data processing (#55) * refactor: revise response data processing * test: more response types --- src/wrapped-fetch.js | 19 +++++++++++-------- test/index.test.js | 21 +++++++++++++++++++++ 2 files changed, 32 insertions(+), 8 deletions(-) diff --git a/src/wrapped-fetch.js b/src/wrapped-fetch.js index 1292429..28f758b 100644 --- a/src/wrapped-fetch.js +++ b/src/wrapped-fetch.js @@ -107,6 +107,7 @@ export default class WrappedFetch { .then(response => { copy = response.clone(); copy.useCache = useCache; + if (charset === 'gbk') { try { return response @@ -116,15 +117,17 @@ export default class WrappedFetch { } catch (e) { throw new ResponseError(copy, e.message); } - } else if (responseType === 'json' || responseType === 'text') { + } + + if (responseType === 'json') { return response.text().then(safeJsonParse); - } else { - try { - // 其他如blob, arrayBuffer, formData - return response[responseType](); - } catch (e) { - throw new ResponseError(copy, 'responseType not support'); - } + } + + try { + // 其他如 text, blob, arrayBuffer, formData + return response[responseType](); + } catch (e) { + throw new ResponseError(copy, 'responseType not support'); } }) .then(data => { diff --git a/test/index.test.js b/test/index.test.js index 53396dd..617bc8c 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -94,6 +94,27 @@ describe('test fetch:', () => { }); expect(response.a).toBe(11); + response = await request(prefix('/test/responseType'), { + method: 'post', + responseType: 'text', + data: { a: 12 }, + }); + expect(typeof response === 'string').toBe(true); + + response = await request(prefix('/test/responseType'), { + method: 'post', + responseType: 'formData', + data: { a: 13 }, + }); + expect(response instanceof FormData).toBe(true); + + response = await request(prefix('/test/responseType'), { + method: 'post', + responseType: 'arrayBuffer', + data: { a: 14 }, + }); + expect(response instanceof ArrayBuffer).toBe(true); + try { response = await request(prefix('/test/responseType'), { responseType: 'other', From aaf0bde5a254fb1097f292064f7df323592b6805 Mon Sep 17 00:00:00 2001 From: Pu Wei Date: Wed, 10 Jul 2019 14:15:41 +0800 Subject: [PATCH 23/94] feat: support promised request interceptor (#57) --- src/lib/fetch.js | 50 ++++++++++++++++++++++++++++++---------------- test/index.test.js | 27 +++++++++++++++++++++++++ 2 files changed, 60 insertions(+), 17 deletions(-) diff --git a/src/lib/fetch.js b/src/lib/fetch.js index a885249..c45a9fb 100644 --- a/src/lib/fetch.js +++ b/src/lib/fetch.js @@ -7,24 +7,40 @@ export const responseInterceptors = []; function fetch(url, options = {}) { if (typeof url !== 'string') throw new Error('url MUST be a string'); - // 执行 request 的拦截器 - requestInterceptors.concat([defaultInterceptor]).forEach(handler => { - const ret = handler(url, options); - url = ret.url || url; - options = ret.options || options; + const combinedRequestInterceptors = requestInterceptors.concat([defaultInterceptor]); + + return new Promise(resolve => { + // 执行 request 的拦截器, 处理完以后再去请求 + // 使用 async/await 可以使代码更简洁, 但会 引入 regenerator-runtime 导致体积增加一倍, 所以用 promise + combinedRequestInterceptors + .reduce( + (promise, handler) => + promise.then(ret => { + if (ret) { + url = ret.url || url; + options = ret.options || options; + } + return handler(url, options); + }), + Promise.resolve() + ) + .then(ret => { + url = ret.url || url; + options = ret.options || options; + + // 将 method 改为大写 + options.method = options.method ? options.method.toUpperCase() : 'GET'; + + // 请求数据 + let response = window.fetch(url, options); + // 执行 response 的拦截器 + responseInterceptors.forEach(handler => { + response = response.then(res => handler(res, options)); + }); + + resolve(response); + }); }); - - // 将 method 改为大写 - options.method = options.method ? options.method.toUpperCase() : 'GET'; - - // 请求数据 - let response = window.fetch(url, options); - // 执行 response 的拦截器 - responseInterceptors.forEach(handler => { - response = response.then(res => handler(res, options)); - }); - - return response; } // 支持拦截器,参考 axios 库的写法: https://github.com/axios/axios#interceptors diff --git a/test/index.test.js b/test/index.test.js index 617bc8c..3d166dc 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -470,6 +470,33 @@ describe('test fetch lib:', () => { expect(data.foo).toBe('foo'); }); + // 使用上边修改数据的用例, 测试 promise 化的 interceptors + it('test promise interceptors', async () => { + server.post('/test/promiseInterceptors', (req, res) => { + writeData(req.body, res); + }); + + request.interceptors.request.use((url, options) => { + return new Promise(resolve => { + setTimeout(() => { + if (options.method.toLowerCase() === 'post') { + options.data = { + ...options.data, + foo: 'foo', + }; + } + resolve({ url, options }); + }, 1000); + }); + }); + + const data = await request(prefix('/test/promiseInterceptors'), { + method: 'post', + data: { bar: 'bar' }, + }); + expect(data.foo).toBe('foo'); + }); + afterAll(() => { server.close(); }); From 832118fbbc20d86a4c4f6871bd10f85d8e5f4f49 Mon Sep 17 00:00:00 2001 From: clock157 Date: Wed, 10 Jul 2019 14:36:19 +0800 Subject: [PATCH 24/94] publish: 1.1.0 --- CHANGELOG.MD | 4 ++++ package.json | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.MD b/CHANGELOG.MD index 52fb6ae..252b43b 100644 --- a/CHANGELOG.MD +++ b/CHANGELOG.MD @@ -1,3 +1,7 @@ +## 1.1.0 +* refactor: #55 revise response data processing @atzcl +* feat: #57 support promised request interceptor @clock157 + ## 1.0.7 * feat: Send data using delete * feat: use umi-lint diff --git a/package.json b/package.json index 9a05edd..8a0ba4c 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.0.8", + "version": "1.1.0", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From 1f77c349963f82d61150e57dbfc0bfec2482afcd Mon Sep 17 00:00:00 2001 From: chenjsh Date: Tue, 16 Jul 2019 16:37:13 +0800 Subject: [PATCH 25/94] feat: support middleware --- README.md | 53 ++++++++++ README_zh-CN.md | 56 +++++++++++ src/core.js | 83 ++++++++++++++++ src/defaultInterceptor.js | 60 ----------- src/index.js | 4 +- src/lib/fetch.js | 60 ----------- src/middleware/addfix.js | 15 +++ src/middleware/fetch.js | 58 +++++++++++ src/middleware/parseResponse.js | 46 +++++++++ src/middleware/simpleGet.js | 26 +++++ src/middleware/simplePost.js | 47 +++++++++ src/onion/compose.js | 29 ++++++ src/onion/onion.js | 22 +++++ src/request.js | 73 +++++++------- src/utils.js | 10 +- src/wrapped-fetch.js | 170 -------------------------------- src/wrapped-rpc.js | 8 -- test/index.test.js | 53 +++++++++- types/index.d.ts | 11 +++ 19 files changed, 540 insertions(+), 344 deletions(-) create mode 100644 src/core.js delete mode 100644 src/defaultInterceptor.js delete mode 100644 src/lib/fetch.js create mode 100644 src/middleware/addfix.js create mode 100644 src/middleware/fetch.js create mode 100644 src/middleware/parseResponse.js create mode 100644 src/middleware/simpleGet.js create mode 100644 src/middleware/simplePost.js create mode 100644 src/onion/compose.js create mode 100644 src/onion/onion.js delete mode 100644 src/wrapped-fetch.js delete mode 100644 src/wrapped-rpc.js diff --git a/README.md b/README.md index 8408472..6e76929 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,7 @@ The network request library, based on fetch encapsulation, combines the features - support for processing gbk - request and response interceptor support for class axios - unified error handling +- middleware support ## umi-request vs fetch vs axios @@ -37,6 +38,7 @@ The network request library, based on fetch encapsulation, combines the features | prefix | ✅ | ❎ | ❎ | | suffix | ✅ | ❎ | ❎ | | processing gbk | ✅ | ❎ | ❎ | +| middleware | ✅ | ❎ | ❎ | 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) @@ -170,6 +172,27 @@ request.interceptors.response.use((response, options) => {   response.headers.append('interceptors', 'yes yo');   return response; }); + + +// use middleware, handling request and response +request.use(async (ctx, next) => { + const { req } = ctx; + const { url, options } = req; + // add prefix + ctx.req.url = `/api/v1/${url}`; + ctx.req.options = { + ...options, + foo: 'foo' + }; + await next(); + + const { res } = ctx; + const { success = false } = res; + if (!success) { + // Handle fail request here + } +}) + ``` ## Error handling @@ -253,6 +276,36 @@ request.interceptors.response.use(async (response) => { }) ``` + +## Middleware +request.use(fn) + +### params +* ctx(Object):context, content request and response +* next(Function):function to call the next middleware + +### example +``` 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: +``` +a1 -> b1 -> response -> b2 -> a2 +``` + ## Development and debugging - npm install diff --git a/README_zh-CN.md b/README_zh-CN.md index 900096d..e776477 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -24,6 +24,7 @@ - 支持处理 gbk - 类 axios 的 request 和 response 拦截器(interceptors)支持 - 统一的错误处理方式 +- 类 koa 洋葱机制的 use 中间件机制支持 ## 与 fetch, axios 异同 @@ -41,6 +42,7 @@ | 前缀 | ✅ | ❎ | ❎ | | 后缀 | ✅ | ❎ | ❎ | | 处理 gbk | ✅ | ❎ | ❎ | +| 中间件 | ✅ | ❎ | ❎ | 更多讨论参考[传统 Ajax 已死,Fetch 永生](https://github.com/camsong/blog/issues/2), 如果你有好的建议和需求, 请提 [issue](https://github.com/umijs/umi/issues) @@ -90,6 +92,8 @@ fetch原其他参数有效, 详见[fetch文档](https://github.github.io/fetch/) | params | 默认带上的query参数 | object | {} | | ... | + + ## 使用 > request 可以进行一层简单封装后再使用, 可参考 [antd-pro](https://github.com/umijs/ant-design-pro/blob/master/src/utils/request.js) @@ -174,8 +178,28 @@ request.interceptors.response.use((response, options) => { response.headers.append('interceptors', 'yes yo'); return response; }); + +// 中间件,对请求前、响应后做处理 +request.use(async (ctx, next) => { + const { req } = ctx; + const { url, options } = req; + // 添加前缀、后缀 + ctx.req.url = `/api/v1/${url}`; + ctx.req.options = { + ...options, + foo: 'foo' + }; + await next(); + + const { res } = ctx; + const { success = false } = res; + if (!success) { + // Handle fail request here + } +}) ``` + ## 错误处理 ```javascript @@ -255,7 +279,39 @@ request.interceptors.response.use(async (response) => { } return response; }) + +``` + + +## 中间件 +request.use(fn) + +### 参数 +* ctx(Object):上下文对象,包括req和res对象 +* next(Function):调用下一个中间件的函数 + +### 例子 +``` 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'); +``` + +执行顺序如下: ``` +a1 -> b1 -> response -> b2 -> a2 +``` + ## 开发和调试 diff --git a/src/core.js b/src/core.js new file mode 100644 index 0000000..43b8f6f --- /dev/null +++ b/src/core.js @@ -0,0 +1,83 @@ +import Onion from './onion/onion'; +import { MapCache } from './utils'; +import fetchMiddleware from './middleware/fetch'; +import addfixMiddleware from './middleware/addfix'; +import parseResponseMiddleware from './middleware/parseResponse'; +import simplePost from './middleware/simplePost'; +import simpleGet from './middleware/simpleGet'; + +const defaultMiddlewares = [addfixMiddleware, simplePost, simpleGet, fetchMiddleware, parseResponseMiddleware]; + +class Core { + constructor(initOptions) { + this.onion = new Onion(defaultMiddlewares); + this.mapCache = new MapCache(initOptions); + this.requestInterceptors = []; + this.responseInterceptors = []; + } + + use(newMiddleware) { + this.onion.use(newMiddleware); + return this; + } + + requestUse(handler) { + if (typeof handler !== 'function') throw new TypeError('Middleware must be an array!'); + this.requestInterceptors.push(handler); + } + + responseUse(handler) { + if (typeof handler !== 'function') throw new TypeError('Middleware must be an array!'); + this.responseInterceptors.push(handler); + } + + beforeRequest(ctx) { + const reducer = (p1, p2) => + p1.then((ret = {}) => { + ctx.req.url = ret.url || ctx.req.url; + ctx.req.options = ret.options || ctx.req.options; + return p2(ctx.req.url, ctx.req.options); + }); + return this.requestInterceptors.reduce(reducer, Promise.resolve()).then((ret = {}) => { + ctx.req.url = ret.url || ctx.req.url; + ctx.req.options = ret.options || ctx.req.options; + return Promise.resolve(); + }); + } + + request(url, options) { + const { onion, responseInterceptors } = this; + const obj = { + req: { url, options }, + res: null, + cache: this.mapCache, + responseInterceptors, + }; + if (typeof url !== 'string') { + throw new Error('url MUST be a string'); + } + + return new Promise((resolve, reject) => { + this.beforeRequest(obj) + .then(() => onion.execute(obj)) + .then(() => { + resolve(obj.res); + }) + .catch(error => { + const { errorHandler } = obj.req.options; + if (errorHandler) { + try { + const data = errorHandler(error); + resolve(data); + } catch (e) { + reject(e); + } + } else { + reject(error); + } + }); + }); + } +} + +export default Core; diff --git a/src/defaultInterceptor.js b/src/defaultInterceptor.js deleted file mode 100644 index 8865c16..0000000 --- a/src/defaultInterceptor.js +++ /dev/null @@ -1,60 +0,0 @@ -import { stringify } from 'query-string'; -/** - * 注册request拦截器 - * get 和 post 参数简化 - * post: - * @param {json|form} requestType 数据的传输方式, 对应Content-Type, 覆盖常见的两种场景, 自动带上header和格式化数据. - * @param {object} data 数据字段 - * - * get: - * @param {object} params query参数 - */ -export default (url, originOptions = {}) => { - const options = { ...originOptions }; - // 默认get, 兼容method大小写 - let method = options.method || 'get'; - method = method.toLowerCase(); - if (method === 'post' || method === 'put' || method === 'patch' || method === 'delete') { - // requestType 简写默认值为 json - const { requestType = 'json', data } = options; - // 数据使用类axios的新字段data, 避免引用后影响旧代码, 如将body stringify多次 - if (data) { - const dataType = Object.prototype.toString.call(data); - if (dataType === '[object Object]' || dataType === '[object Array]') { - if (requestType === 'json') { - options.headers = { - Accept: 'application/json', - 'Content-Type': 'application/json;charset=UTF-8', - ...options.headers, - }; - options.body = JSON.stringify(data); - } else if (requestType === 'form') { - options.headers = { - Accept: 'application/json', - 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', - ...options.headers, - }; - options.body = stringify(data); - } - } else { - // 其他 requestType 自定义header - options.headers = { - Accept: 'application/json', - ...options.headers, - }; - options.body = data; - } - } - } - - // 支持类似axios 参数自动拼装, 其他method也可用, 不冲突. - if (options.params && Object.keys(options.params).length > 0) { - const str = url.indexOf('?') !== -1 ? '&' : '?'; - url = `${url}${str}${stringify(options.params)}`; - } - - return { - url, - options, - }; -}; diff --git a/src/index.js b/src/index.js index ad1f675..502ec31 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,5 @@ -import fetch from './lib/fetch'; import request, { extend } from './request'; import { RequestError, ResponseError } from './utils'; -export { fetch, extend, RequestError, ResponseError }; - +export { extend, RequestError, ResponseError }; export default request; diff --git a/src/lib/fetch.js b/src/lib/fetch.js deleted file mode 100644 index c45a9fb..0000000 --- a/src/lib/fetch.js +++ /dev/null @@ -1,60 +0,0 @@ -import 'whatwg-fetch'; -import defaultInterceptor from '../defaultInterceptor'; - -const requestInterceptors = []; -export const responseInterceptors = []; - -function fetch(url, options = {}) { - if (typeof url !== 'string') throw new Error('url MUST be a string'); - - const combinedRequestInterceptors = requestInterceptors.concat([defaultInterceptor]); - - return new Promise(resolve => { - // 执行 request 的拦截器, 处理完以后再去请求 - // 使用 async/await 可以使代码更简洁, 但会 引入 regenerator-runtime 导致体积增加一倍, 所以用 promise - combinedRequestInterceptors - .reduce( - (promise, handler) => - promise.then(ret => { - if (ret) { - url = ret.url || url; - options = ret.options || options; - } - return handler(url, options); - }), - Promise.resolve() - ) - .then(ret => { - url = ret.url || url; - options = ret.options || options; - - // 将 method 改为大写 - options.method = options.method ? options.method.toUpperCase() : 'GET'; - - // 请求数据 - let response = window.fetch(url, options); - // 执行 response 的拦截器 - responseInterceptors.forEach(handler => { - response = response.then(res => handler(res, options)); - }); - - resolve(response); - }); - }); -} - -// 支持拦截器,参考 axios 库的写法: https://github.com/axios/axios#interceptors -fetch.interceptors = { - request: { - use: handler => { - requestInterceptors.push(handler); - }, - }, - response: { - use: handler => { - responseInterceptors.push(handler); - }, - }, -}; - -export default fetch; diff --git a/src/middleware/addfix.js b/src/middleware/addfix.js new file mode 100644 index 0000000..a7aede9 --- /dev/null +++ b/src/middleware/addfix.js @@ -0,0 +1,15 @@ +export default function addfixMiddleware(ctx, next) { + const { + req: { options = {}, url = '' }, + } = ctx; + const { prefix, suffix } = options; + if (typeof url !== 'string') throw new Error('url MUST be a string'); + + if (prefix) { + ctx.req.url = `${prefix}${url}`; + } + if (suffix) { + ctx.req.url = `${url}${suffix}`; + } + return next().then(Promise.resolve()); +} diff --git a/src/middleware/fetch.js b/src/middleware/fetch.js new file mode 100644 index 0000000..e12e37a --- /dev/null +++ b/src/middleware/fetch.js @@ -0,0 +1,58 @@ +import 'whatwg-fetch'; +import { timeout2Throw } from '../utils'; + +export default function fetchMiddleware(ctx, next) { + const { + req: { options = {}, url = '' }, + cache, + responseInterceptors, + } = ctx; + const { timeout = 0, type = 'normal', useCache = false, method = 'get', params, ttl } = options; + + if (type !== 'normal') { + return next(); + } + if (!window || !window.fetch) { + throw new Error('window or window.fetch not exist!'); + } + + // 从缓存池检查是否有缓存数据 + const needCache = method.toLowerCase() === 'get' && useCache; + if (needCache) { + let responseCache = cache.get({ + url, + params, + }); + if (responseCache) { + responseCache = responseCache.clone(); + responseCache.useCache = true; + ctx.res = responseCache; + return next(); + } + } + let response; + if (timeout > 0) { + response = Promise.race([window.fetch(url, options), timeout2Throw(timeout)]); + } else { + response = window.fetch(url, options); + } + + // 执行 response 的拦截器 + responseInterceptors.forEach(handler => { + response = response.then(res => handler(res, options)); + }); + + return response.then(res => { + // 是否存入缓存池 + if (needCache) { + if (res.status === 200) { + const copy = res.clone(); + copy.useCache = true; + cache.set({ url, params }, copy, ttl); + } + } + + ctx.res = res; + return next(); + }); +} diff --git a/src/middleware/parseResponse.js b/src/middleware/parseResponse.js new file mode 100644 index 0000000..4c95bdf --- /dev/null +++ b/src/middleware/parseResponse.js @@ -0,0 +1,46 @@ +import { safeJsonParse, readerGBK, ResponseError } from '../utils'; + +export default function parseResponseMiddleware(ctx, next) { + const { res, req } = ctx; + const { + options: { responseType = 'json', charset = 'utf8', getResponse = false }, + } = req || {}; + + const copy = res.clone(); + copy.useCache = res.useCache || false; + + return next() + .then(() => { + // 解析数据 + if (charset === 'gbk') { + try { + return res + .blob() + .then(readerGBK) + .then(safeJsonParse); + } catch (e) { + throw new ResponseError(copy, e.message); + } + } else if (responseType === 'json') { + return res.text().then(safeJsonParse); + } + try { + // 其他如text, blob, arrayBuffer, formData + return res[responseType](); + } catch (e) { + throw new ResponseError(copy, 'responseType not support'); + } + }) + .then(body => { + if (copy.status >= 200 && copy.status < 300) { + // 提供源response, 以便自定义处理 + if (getResponse) { + ctx.res = { data: body, response: copy }; + return; + } + ctx.res = body; + return; + } + throw new ResponseError(copy, 'http error', body); + }); +} diff --git a/src/middleware/simpleGet.js b/src/middleware/simpleGet.js new file mode 100644 index 0000000..c08a356 --- /dev/null +++ b/src/middleware/simpleGet.js @@ -0,0 +1,26 @@ +import { stringify } from 'query-string'; + +// 对请求参数做处理,实现 query 简化、 post 简化 +export default function simpleGetMiddleware(ctx, next) { + const { + req: { options = {} }, + } = ctx; + let { + req: { url = '' }, + } = ctx; + + // 将 method 改为大写 + options.method = options.method ? options.method.toUpperCase() : 'GET'; + + // 支持类似axios 参数自动拼装, 其他method也可用, 不冲突. + if (options.params && Object.keys(options.params).length > 0) { + const str = url.indexOf('?') !== -1 ? '&' : '?'; + ctx.req.originUrl = url; + url = `${url}${str}${stringify(options.params)}`; + ctx.req.url = url; + } + + ctx.req.options = options; + + return next(); +} diff --git a/src/middleware/simplePost.js b/src/middleware/simplePost.js new file mode 100644 index 0000000..d87590e --- /dev/null +++ b/src/middleware/simplePost.js @@ -0,0 +1,47 @@ +import { stringify } from 'query-string'; + +// 对请求参数做处理,实现 query 简化、 post 简化 +export default function simplePostMiddleware(ctx, next) { + const { + req: { options = {} }, + } = ctx; + const { method = 'get' } = options; + + if (['post', 'put', 'patch', 'delete'].indexOf(method.toLowerCase()) === -1) { + return next(); + } + + const { requestType = 'json', data } = options; + // 数据使用类axios的新字段data, 避免引用后影响旧代码, 如将body stringify多次 + if (data) { + const dataType = Object.prototype.toString.call(data); + if (dataType === '[object Object]' || dataType === '[object Array]') { + if (requestType === 'json') { + options.headers = { + Accept: 'application/json', + 'Content-Type': 'application/json;charset=UTF-8', + ...options.headers, + }; + options.body = JSON.stringify(data); + } else if (requestType === 'form') { + options.headers = { + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + ...options.headers, + }; + options.body = stringify(data); + } + } else { + // 其他 requestType 自定义header + options.headers = { + Accept: 'application/json', + ...options.headers, + }; + options.body = data; + } + } + + ctx.req.options = options; + + return next(); +} diff --git a/src/onion/compose.js b/src/onion/compose.js new file mode 100644 index 0000000..532e5e4 --- /dev/null +++ b/src/onion/compose.js @@ -0,0 +1,29 @@ +// 返回一个组合了所有插件的“插件” + +export default function compose(middlewares) { + if (!Array.isArray(middlewares)) throw new TypeError('Middlewares must be an array!'); + + const middlewaresLen = middlewares.length; + for (let i = 0; i < middlewaresLen; i++) { + if (typeof middlewares[i] !== 'function') throw new TypeError('Middleware must be componsed of function'); + } + + return function wrapMiddlewares(params, next) { + let index = -1; + function dispatch(i) { + if (i <= index) { + return Promise.reject(new Error('next() should not be called multiple times in one middleware!')); + } + index = i; + const fn = middlewares[i] || next; + if (!fn) return Promise.resolve(); + try { + return Promise.resolve(fn(params, () => dispatch(i + 1))); + } catch (err) { + return Promise.reject(err); + } + } + + return dispatch(0); + }; +} diff --git a/src/onion/onion.js b/src/onion/onion.js new file mode 100644 index 0000000..c64b263 --- /dev/null +++ b/src/onion/onion.js @@ -0,0 +1,22 @@ +// 参考自 puck-core 请求库的插件机制 +import compose from './compose'; + +class Onion { + constructor(defaultMiddlewares) { + if (!Array.isArray(defaultMiddlewares)) throw new TypeError('Default middlewares must be an array!'); + + this.middlewares = [...defaultMiddlewares]; + this.defaultMiddlewaresLen = defaultMiddlewares.length; + } + + use(newMiddleware) { + this.middlewares.splice(this.middlewares.length - this.defaultMiddlewaresLen, 0, newMiddleware); + } + + execute(params = null) { + const fn = compose(this.middlewares); + return fn(params); + } +} + +export default Onion; diff --git a/src/request.js b/src/request.js index 26c9631..9f81427 100644 --- a/src/request.js +++ b/src/request.js @@ -1,47 +1,46 @@ -import fetch from './lib/fetch'; -import { MapCache } from './utils'; -import WrappedFetch from './wrapped-fetch'; -import WrappedRpc from './wrapped-rpc'; +import Core from './core'; +// 通过 request 函数,在 core 之上再封装一层,提供原 umi/request 一致的 api,无缝升级 -/** - * 获取request实例 调用参数可以覆盖初始化的参数. 用于一些情况的特殊处理. - * @param {*} initOptions 初始化参数 - */ const request = (initOptions = {}) => { - const mapCache = new MapCache(initOptions); - const instance = (input, options = {}) => { - options.headers = { ...initOptions.headers, ...options.headers }; - options.params = { ...initOptions.params, ...options.params }; - options = { ...initOptions, ...options }; - const method = options.method || 'get'; - options.method = method.toLowerCase(); - if (method === 'rpc') { - // call rpc - return new WrappedRpc(input, options, mapCache); - } else { - return new WrappedFetch(input, options, mapCache); - } + const coreInstance = new Core(initOptions); + const umiInstance = (url, options = {}) => { + const mergeOptions = { + ...initOptions, + ...options, + headers: { + ...initOptions.headers, + ...options.headers, + }, + params: { + ...initOptions.headers, + ...options.params, + }, + method: (options.method || 'get').toLowerCase(), + }; + return coreInstance.request(url, mergeOptions); }; - // 增加语法糖如: request.get request.post - const methods = ['get', 'post', 'delete', 'put', 'rpc', 'patch']; - methods.forEach(method => { - instance[method] = (input, options) => instance(input, { ...options, method }); - }); + // 中间件 + umiInstance.use = coreInstance.use.bind(coreInstance); + + // 拦截器 + umiInstance.interceptors = { + request: { + use: coreInstance.requestUse.bind(coreInstance), + }, + response: { + use: coreInstance.responseUse.bind(coreInstance), + }, + }; - // 给request 也增加一个interceptors引用; - instance.interceptors = fetch.interceptors; + // 请求语法糖: reguest.get request.post …… + const METHODS = ['get', 'post', 'put', 'rpc', 'patch']; + METHODS.forEach(method => { + umiInstance[method] = (url, options) => umiInstance(url, { ...options, method }); + }); - return instance; + return umiInstance; }; -/** - * extend 方法参考了ky, 让用户可以定制配置. - * initOpions 初始化参数 - * @param {number} maxCache 最大缓存数 - * @param {string} prefix url前缀 - * @param {function} errorHandler 统一错误处理方法 - * @param {object} headers 统一的headers - */ export const extend = initOptions => request(initOptions); export default request(); diff --git a/src/utils.js b/src/utils.js index f8306bf..8b41dad 100644 --- a/src/utils.js +++ b/src/utils.js @@ -88,6 +88,14 @@ export function readerGBK(file) { export function safeJsonParse(data) { try { return JSON.parse(data); - } catch (e) {} // eslint-disable-line + } catch (e) {} // eslint-disable-line no-empty return data; } + +export function timeout2Throw(msec) { + return new Promise((_, reject) => { + setTimeout(() => { + reject(new RequestError(`timeout of ${msec}ms exceeded`)); + }, msec); + }); +} diff --git a/src/wrapped-fetch.js b/src/wrapped-fetch.js deleted file mode 100644 index 28f758b..0000000 --- a/src/wrapped-fetch.js +++ /dev/null @@ -1,170 +0,0 @@ -import fetch, { responseInterceptors } from './lib/fetch'; -import { RequestError, ResponseError, readerGBK, safeJsonParse } from './utils'; - -export default class WrappedFetch { - constructor(url, options, cache) { - this.cache = cache; - this.url = url; - this.options = options; - this._addfix(); - - return this._doFetch(); - } - - _addfix() { - const { prefix, suffix } = this.options; - // 前缀 - if (prefix) { - this.url = `${prefix}${this.url}`; - } - // 后缀 - if (suffix) { - this.url = `${this.url}${suffix}`; - } - } - - _doFetch() { - const useCache = this.options.method === 'get' && this.options.useCache; - if (useCache) { - let response = this.cache.get({ - url: this.url, - params: this.options.params, - }); - if (response) { - response = response.clone(); - let instance = Promise.resolve(response); - // cache也应用response拦截器, 感觉可以不要, 因为只缓存状态200状态的数据? - responseInterceptors.forEach(handler => { - instance = instance.then(res => handler(res, this.options)); - }); - return this._parseResponse(instance, true); - } - } - - let instance = fetch(this.url, this.options); - - // 处理超时 - instance = this._wrappedTimeout(instance); - - // 处理缓存 1.只有get 2.同时参数cache为true 才缓存 - instance = this._wrappedCache(instance, useCache); - - // 返回解析好的数据 - return this._parseResponse(instance); - } - - /** - * 处理超时参数 #TODO 超时后连接还在继续 - * Promise.race方式ref: @期贤 http://gitlab.alipay-inc.com/bigfish/bigfish/raw/a2595e1bc52ba624fefe2c98ac54500b8b735835/packages/umi-plugin-bigfish/src/plugins/bigfishSdk/request.js - * @param {*} instance fetch实例 - */ - _wrappedTimeout(instance) { - const { timeout } = this.options; - if (timeout > 0) { - return Promise.race([ - new Promise((_, reject) => - setTimeout(() => reject(new RequestError(`timeout of ${timeout}ms exceeded`)), timeout) - ), - instance, - ]); - } else { - return instance; - } - } - - /** - * 处理缓存 - * @param {*} instance fetch实例 - * @param {boolean} useCache 是否缓存 - */ - _wrappedCache(instance, useCache) { - if (useCache) { - const { params, ttl } = this.options; - return instance.then(response => { - // 只缓存状态码为 200 - if (response.status === 200) { - const copy = response.clone(); - copy.useCache = true; - this.cache.set({ url: this.url, params }, copy, ttl); - } - return response; - }); - } else { - return instance; - } - } - - /** - * 处理返回类型, 并解析数据 - * @param {*} instance fetch实例 - * @param {boolean} useCache 返回类型, 默认json - */ - _parseResponse(instance, useCache = false) { - const { responseType = 'json', charset = 'utf8', getResponse = false } = this.options; - return new Promise((resolve, reject) => { - let copy; - instance - .then(response => { - copy = response.clone(); - copy.useCache = useCache; - - if (charset === 'gbk') { - try { - return response - .blob() - .then(blob => readerGBK(blob)) - .then(safeJsonParse); - } catch (e) { - throw new ResponseError(copy, e.message); - } - } - - if (responseType === 'json') { - return response.text().then(safeJsonParse); - } - - try { - // 其他如 text, blob, arrayBuffer, formData - return response[responseType](); - } catch (e) { - throw new ResponseError(copy, 'responseType not support'); - } - }) - .then(data => { - if (copy.status >= 200 && copy.status < 300) { - // 提供源response, 以便自定义处理 - if (getResponse) { - resolve({ - data, - response: copy, - }); - } else { - resolve(data); - } - } else { - throw new ResponseError(copy, 'http error', data); - } - }) - .catch(this._handleError.bind(this, { reject, resolve })); - }); - } - - /** - * 处理错误 - * @param {*} param0 - * @param {*} error - */ - _handleError({ reject, resolve }, error) { - const { errorHandler } = this.options; - if (errorHandler) { - try { - const data = errorHandler(error); - resolve(data); - } catch (e) { - reject(e); - } - } else { - reject(error); - } - } -} diff --git a/src/wrapped-rpc.js b/src/wrapped-rpc.js deleted file mode 100644 index 12e7ae0..0000000 --- a/src/wrapped-rpc.js +++ /dev/null @@ -1,8 +0,0 @@ -/** - * rpc 相关, 待实现 - */ -export default class WrappedRpc { - constructor(input) { - return { hello: input }; - } -} diff --git a/test/index.test.js b/test/index.test.js index 3d166dc..110337d 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -354,11 +354,11 @@ describe('test fetch:', () => { }); // 测试rpc #TODO -describe('test rpc:', () => { - it('test hello', () => { - expect(request.rpc('wang').hello).toBe('wang'); - }); -}); +// describe('test rpc:', () => { +// it('test hello', () => { +// expect(request.rpc('wang').hello).toBe('wang'); +// }); +// }); // 测试工具函数 describe('test utils:', () => { @@ -501,3 +501,46 @@ describe('test fetch lib:', () => { server.close(); }); }); + +// 测试中间件机制 +describe('test fetch lib:', () => { + let server; + + beforeAll(async () => { + server = await createTestServer(); + }); + + const prefix = api => `${server.url}${api}`; + + // 使用上边修改数据的用例, 测试 promise 化的 interceptors + it('test middlewares', async () => { + server.post('/test/promiseInterceptors/a/b', (req, res) => { + writeData(req.body, res); + }); + request.use(async (ctx, next) => { + ctx.req.options = { + ...ctx.req.options, + data: { + ...ctx.req.options.data, + foo: 'foo', + }, + }; + await next(); + }); + request.use(async (ctx, next) => { + await next(); + ctx.res.hello = 'hello'; + }); + const data = await request(prefix('/test/promiseInterceptors/a/b'), { + method: 'post', + data: { bar: 'bar' }, + }); + expect(data.bar).toBe('bar'); + expect(data.foo).toBe('foo'); + expect(data.hello).toBe('hello'); + }); + + afterAll(() => { + server.close(); + }); +}); diff --git a/types/index.d.ts b/types/index.d.ts index 56b89ea..83ee667 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -54,10 +54,20 @@ export type RequestInterceptor = ( options?: RequestOptionsInit; }; +export interface Context { + req: { + url: string; + options: RequestOptionsInit; + }; + res: any; +} + // use async ()=> Response equal ()=> Response export type ResponseInterceptor = (response: Response, options: RequestOptionsInit) => Response | Promise; +export type OnionMiddleware = (ctx: Context, next: NextCallback) => void; + export interface RequestMethod { (url: string, options: RequestOptionsWithResponse): Promise>; (url: string, options: RequestOptionsWithoutResponse): Promise; @@ -76,6 +86,7 @@ export interface RequestMethod { use: (handler: ResponseInterceptor) => void; }; }; + use: (handler: OnionMiddleware) => void; } export interface ExtendOnlyOptions { From 74fb55b822d52283c6e0aa0de23e9419baea7e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A0=97=E5=A4=B4?= Date: Fri, 19 Jul 2019 14:19:29 +0800 Subject: [PATCH 26/94] test: add test case to code coverage rate --- src/core.js | 4 +- src/index.js | 3 +- src/middleware/parseResponse.js | 3 + test/index.test.js | 130 ++++++++++++++++++++++++++++++-- 4 files changed, 130 insertions(+), 10 deletions(-) diff --git a/src/core.js b/src/core.js index 43b8f6f..d307140 100644 --- a/src/core.js +++ b/src/core.js @@ -22,12 +22,12 @@ class Core { } requestUse(handler) { - if (typeof handler !== 'function') throw new TypeError('Middleware must be an array!'); + if (typeof handler !== 'function') throw new TypeError('Interceptor must be function!'); this.requestInterceptors.push(handler); } responseUse(handler) { - if (typeof handler !== 'function') throw new TypeError('Middleware must be an array!'); + if (typeof handler !== 'function') throw new TypeError('Interceptor must be function!'); this.responseInterceptors.push(handler); } diff --git a/src/index.js b/src/index.js index 502ec31..3a273f5 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,6 @@ import request, { extend } from './request'; +import Onion from './onion/onion'; import { RequestError, ResponseError } from './utils'; -export { extend, RequestError, ResponseError }; +export { extend, RequestError, ResponseError, Onion }; export default request; diff --git a/src/middleware/parseResponse.js b/src/middleware/parseResponse.js index 4c95bdf..3da2622 100644 --- a/src/middleware/parseResponse.js +++ b/src/middleware/parseResponse.js @@ -6,6 +6,9 @@ export default function parseResponseMiddleware(ctx, next) { options: { responseType = 'json', charset = 'utf8', getResponse = false }, } = req || {}; + if (!res || !res.clone) { + return next(); + } const copy = res.clone(); copy.useCache = res.useCache || false; diff --git a/test/index.test.js b/test/index.test.js index 110337d..28c19bd 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,6 +1,6 @@ import createTestServer from 'create-test-server'; import iconv from 'iconv-lite'; -import request, { extend } from '../src/index'; +import request, { extend, Onion } from '../src/index'; import { MapCache } from '../src/utils'; const debug = require('debug')('afx-request:test'); @@ -42,6 +42,32 @@ describe('test fetch:', () => { } }, 5000); + // 测试请求方法 + it('test methodType', async () => { + server.get('/test/requestType', (req, res) => { + writeData(req.query, res); + }); + + let response = await request(prefix('/test/requestType'), { + method: 'get', + params: { + foo: 'foo', + }, + }); + expect(response.foo).toBe('foo'); + + response = await request(prefix('/test/requestType'), { + method: null, + params: { + foo: 'foo', + }, + }); + expect(response.foo).toBe('foo'); + + response = await request(prefix('/test/requestType')); + expect(response).toStrictEqual({}); + }, 5000); + // 测试请求类型 it('test requestType', async () => { server.post('/test/requestType', (req, res) => { @@ -49,6 +75,12 @@ describe('test fetch:', () => { }); let response = await request(prefix('/test/requestType'), { + method: 'post', + requestType: 'json', + }); + expect(response).toStrictEqual({}); + + response = await request(prefix('/test/requestType'), { method: 'post', requestType: 'form', data: { @@ -71,6 +103,31 @@ describe('test fetch:', () => { data: 'hehe', }); expect(response).toBe('hehe'); + + response = await request(prefix('/test/requestType'), { + method: 'post', + data: 'hehe', + type: 'mini', + }); + expect(response).toBe(null); + }, 5000); + + // 测试非 web 环境无 fetch 情况 + it('test requestType', async () => { + server.post('/test/requestType', (req, res) => { + writeData(req.body, res); + }); + const oldFetch = window.fetch; + window.fetch = null; + try { + let response = await request(prefix('/test/requestType'), { + method: 'post', + requestType: 'json', + }); + } catch (error) { + expect(error.message).toBe('window or window.fetch not exist!'); + } + window.fetch = oldFetch; }, 5000); // 测试返回类型 #TODO 更多类型 @@ -353,12 +410,12 @@ describe('test fetch:', () => { }); }); -// 测试rpc #TODO -// describe('test rpc:', () => { -// it('test hello', () => { -// expect(request.rpc('wang').hello).toBe('wang'); -// }); -// }); +// 测试rpc +xdescribe('test rpc:', () => { + it('test hello', () => { + expect(request.rpc('wang').hello).toBe('wang'); + }); +}); // 测试工具函数 describe('test utils:', () => { @@ -449,6 +506,19 @@ describe('test fetch lib:', () => { } }); + it('test invalid interceptors', async () => { + try { + request.interceptors.request.use('invalid interceptor'); + } catch (error) { + expect(error.message).toBe('Interceptor must be function!'); + } + try { + request.interceptors.response.use('invalid interceptor'); + } catch (error) { + expect(error.message).toBe('Interceptor must be function!'); + } + }); + it('modify request data', async () => { server.post('/test/post/interceptors', (req, res) => { writeData(req.body, res); @@ -544,3 +614,49 @@ describe('test fetch lib:', () => { server.close(); }); }); + +describe('test onion', () => { + it('test constructor', async () => { + try { + const onion = new Onion(); + onion.use(async () => {}); + } catch (error) { + expect(error.message).toBe('Default middlewares must be an array!'); + } + }); + it('test not function middleware', async () => { + try { + const onion = new Onion([]); + onion.use('not a function'); + onion.execute(); + } catch (error) { + expect(error.message).toBe('Middleware must be componsed of function'); + } + }); + it('test multiple use next', async () => { + try { + const onion = new Onion([]); + onion.use(async (ctx, next) => { + await next(); + await next(); + }); + await onion.execute(); + } catch (error) { + expect(error.message).toBe('next() should not be called multiple times in one middleware!'); + } + }); + it('test middleware throw error', async () => { + try { + const onion = new Onion([]); + onion.use(async (ctx, next) => { + await next(); + }); + onion.use(async () => { + throw new Error('error in middleware'); + }); + await onion.execute(); + } catch (error) { + expect(error.message).toBe('error in middleware'); + } + }); +}); From a841cabfc00f097920307c73a3575df71dee55e5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E4=BF=A1=E9=91=AB-King?= <45808948@qq.com> Date: Fri, 19 Jul 2019 14:30:42 +0800 Subject: [PATCH 27/94] Update README.md (#58) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 6e76929..905aff8 100644 --- a/README.md +++ b/README.md @@ -141,7 +141,7 @@ request('/api/v1/some/api', { method:'post', data: 'some data', headers: { 'Cont // upload file const formData = new FormData(); formData.append('file', file); -request('/api/v1/some/api', { method:'post', data: formData }); +request('/api/v1/some/api', { method:'post', body: formData, requestType: 'form' }); // The default is to return the data body, if you need the source response to expand, you can use the getResponse parameter. The result will be a set of layers request('/api/v1/some/api', { getResponse: true }).then({data, response} => { From b0dadc2e9a8412c2cbfa35e58b04983c533dc063 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=A0=97=E5=A4=B4?= Date: Fri, 19 Jul 2019 17:47:52 +0800 Subject: [PATCH 28/94] 1.1.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8a0ba4c..389c215 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.1.0", + "version": "1.1.1", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From 789e8a7db8f0224b3ef2001dc896000958348d7c Mon Sep 17 00:00:00 2001 From: Lu Xiuming Date: Mon, 22 Jul 2019 10:24:37 +0800 Subject: [PATCH 29/94] Add missing properties `credentials` in interface `RequestOptionsInit` (#60) --- types/index.d.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/types/index.d.ts b/types/index.d.ts index 83ee667..017f640 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -31,6 +31,7 @@ export interface RequestOptionsInit extends RequestInit { errorHandler?: (error: ResponseError) => void; prefix?: string; suffix?: string; + credentials?: string; } export interface RequestOptionsWithoutResponse extends RequestOptionsInit { From ff9f83ed8f6e3ba5d4319eb6dbaf4a7b3bb3394e Mon Sep 17 00:00:00 2001 From: chenjsh Date: Wed, 31 Jul 2019 14:09:08 +0800 Subject: [PATCH 30/94] fix: support fetch (#61) * fix: fetch api hidden bug, add test cases for fetch * fix: update fetch's interceptors logic, multiple instance request share the same interceptors Signed-off-by: chenjsh * fix: 'params' option in extend function do not work * fix: addfix should execute before request interceptors * chore: update umi version to 2.8.15 --- package.json | 2 +- src/core.js | 31 +++++++------- src/index.js | 4 +- src/interceptor/addfix.js | 14 +++++++ src/request.js | 38 ++++++++++++----- test/index.test.js | 85 +++++++++++++++++++++++++++++++++++++-- 6 files changed, 141 insertions(+), 33 deletions(-) create mode 100644 src/interceptor/addfix.js diff --git a/package.json b/package.json index 389c215..5c57673 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,7 @@ "jest": "^23.5.0", "np": "5.0.2", "typescript": "^3.0.3", - "umi": "^2.5.0", + "umi": "^2.8.15", "umi-lint": "^1.0.0-alpha.1", "umi-plugin-library": "^1.1.6", "umi-test": "^1.4.0" diff --git a/src/core.js b/src/core.js index d307140..7834ae1 100644 --- a/src/core.js +++ b/src/core.js @@ -1,19 +1,15 @@ import Onion from './onion/onion'; import { MapCache } from './utils'; -import fetchMiddleware from './middleware/fetch'; -import addfixMiddleware from './middleware/addfix'; -import parseResponseMiddleware from './middleware/parseResponse'; -import simplePost from './middleware/simplePost'; -import simpleGet from './middleware/simpleGet'; +import addfixInterceptor from './interceptor/addfix'; -const defaultMiddlewares = [addfixMiddleware, simplePost, simpleGet, fetchMiddleware, parseResponseMiddleware]; +// 旧版拦截器为共享 +const requestInterceptors = [addfixInterceptor]; +const responseInterceptors = []; class Core { - constructor(initOptions) { + constructor(initOptions, defaultMiddlewares) { this.onion = new Onion(defaultMiddlewares); this.mapCache = new MapCache(initOptions); - this.requestInterceptors = []; - this.responseInterceptors = []; } use(newMiddleware) { @@ -21,24 +17,25 @@ class Core { return this; } - requestUse(handler) { + static requestUse(handler) { if (typeof handler !== 'function') throw new TypeError('Interceptor must be function!'); - this.requestInterceptors.push(handler); + requestInterceptors.push(handler); } - responseUse(handler) { + static responseUse(handler) { if (typeof handler !== 'function') throw new TypeError('Interceptor must be function!'); - this.responseInterceptors.push(handler); + responseInterceptors.push(handler); } - beforeRequest(ctx) { + // 执行请求前拦截器 + static dealRequestInterceptors(ctx) { const reducer = (p1, p2) => p1.then((ret = {}) => { ctx.req.url = ret.url || ctx.req.url; ctx.req.options = ret.options || ctx.req.options; return p2(ctx.req.url, ctx.req.options); }); - return this.requestInterceptors.reduce(reducer, Promise.resolve()).then((ret = {}) => { + return requestInterceptors.reduce(reducer, Promise.resolve()).then((ret = {}) => { ctx.req.url = ret.url || ctx.req.url; ctx.req.options = ret.options || ctx.req.options; return Promise.resolve(); @@ -46,7 +43,7 @@ class Core { } request(url, options) { - const { onion, responseInterceptors } = this; + const { onion } = this; const obj = { req: { url, options }, res: null, @@ -58,7 +55,7 @@ class Core { } return new Promise((resolve, reject) => { - this.beforeRequest(obj) + Core.dealRequestInterceptors(obj) .then(() => onion.execute(obj)) .then(() => { resolve(obj.res); diff --git a/src/index.js b/src/index.js index 3a273f5..7b3a6b6 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,6 @@ -import request, { extend } from './request'; +import request, { extend, fetch } from './request'; import Onion from './onion/onion'; import { RequestError, ResponseError } from './utils'; -export { extend, RequestError, ResponseError, Onion }; +export { extend, RequestError, ResponseError, Onion, fetch }; export default request; diff --git a/src/interceptor/addfix.js b/src/interceptor/addfix.js new file mode 100644 index 0000000..aa8c851 --- /dev/null +++ b/src/interceptor/addfix.js @@ -0,0 +1,14 @@ +// 前后缀拦截器 +export default (url, options = {}) => { + const { prefix, suffix } = options; + if (prefix) { + url = `${prefix}${url}`; + } + if (suffix) { + url = `${url}${suffix}`; + } + return { + url, + options, + }; +}; diff --git a/src/request.js b/src/request.js index 9f81427..6892001 100644 --- a/src/request.js +++ b/src/request.js @@ -1,8 +1,12 @@ import Core from './core'; -// 通过 request 函数,在 core 之上再封装一层,提供原 umi/request 一致的 api,无缝升级 +import fetchMiddleware from './middleware/fetch'; +import parseResponseMiddleware from './middleware/parseResponse'; +import simplePost from './middleware/simplePost'; +import simpleGet from './middleware/simpleGet'; -const request = (initOptions = {}) => { - const coreInstance = new Core(initOptions); +// 通过 request 函数,在 core 之上再封装一层,提供原 umi/request 一致的 api,无缝升级 +const request = (initOptions = {}, middleware = []) => { + const coreInstance = new Core(initOptions, middleware); const umiInstance = (url, options = {}) => { const mergeOptions = { ...initOptions, @@ -12,7 +16,7 @@ const request = (initOptions = {}) => { ...options.headers, }, params: { - ...initOptions.headers, + ...initOptions.params, ...options.params, }, method: (options.method || 'get').toLowerCase(), @@ -26,15 +30,15 @@ const request = (initOptions = {}) => { // 拦截器 umiInstance.interceptors = { request: { - use: coreInstance.requestUse.bind(coreInstance), + use: Core.requestUse, }, response: { - use: coreInstance.responseUse.bind(coreInstance), + use: Core.responseUse, }, }; // 请求语法糖: reguest.get request.post …… - const METHODS = ['get', 'post', 'put', 'rpc', 'patch']; + const METHODS = ['get', 'post', 'delete', 'put', 'rpc', 'patch']; METHODS.forEach(method => { umiInstance[method] = (url, options) => umiInstance(url, { ...options, method }); }); @@ -42,5 +46,21 @@ const request = (initOptions = {}) => { return umiInstance; }; -export const extend = initOptions => request(initOptions); -export default request(); +/** + * extend 方法参考了ky, 让用户可以定制配置. + * initOpions 初始化参数 + * @param {number} maxCache 最大缓存数 + * @param {string} prefix url前缀 + * @param {function} errorHandler 统一错误处理方法 + * @param {object} headers 统一的headers + */ +const _extendMiddlewares = [simplePost, simpleGet, fetchMiddleware, parseResponseMiddleware]; +export const extend = initOptions => request(initOptions, _extendMiddlewares); + +/** + * 暴露 fetch 中间件,去除响应处理的中间件和前后缀处理的中间件,保障依旧可以使用 + */ +const _fetchMiddlewares = [simplePost, simpleGet, fetchMiddleware]; +export const fetch = request({}, _fetchMiddlewares); + +export default request({}, _extendMiddlewares); diff --git a/test/index.test.js b/test/index.test.js index 28c19bd..f265c9c 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,6 +1,6 @@ import createTestServer from 'create-test-server'; import iconv from 'iconv-lite'; -import request, { extend, Onion } from '../src/index'; +import request, { extend, Onion, fetch } from '../src/index'; import { MapCache } from '../src/utils'; const debug = require('debug')('afx-request:test'); @@ -213,6 +213,7 @@ describe('test fetch:', () => { maxCache: 2, prefix: server.url, headers: { Connection: 'keep-alive' }, + params: { defaultParams: true }, }); // 第一次写入缓存 @@ -273,6 +274,7 @@ describe('test fetch:', () => { }); expect(response.response.useCache).toBe(false); + expect(response.data.defaultParams).toBe('true'); }, 10000); // 测试异常捕获 @@ -504,7 +506,7 @@ describe('test fetch lib:', () => { } catch (error) { expect(error.message).toBe('url MUST be a string'); } - }); + }, 3000); it('test invalid interceptors', async () => { try { @@ -538,7 +540,7 @@ describe('test fetch lib:', () => { data: { bar: 'bar' }, }); expect(data.foo).toBe('foo'); - }); + }, 3000); // 使用上边修改数据的用例, 测试 promise 化的 interceptors it('test promise interceptors', async () => { @@ -565,7 +567,7 @@ describe('test fetch lib:', () => { data: { bar: 'bar' }, }); expect(data.foo).toBe('foo'); - }); + }, 3000); afterAll(() => { server.close(); @@ -660,3 +662,78 @@ describe('test onion', () => { } }); }); + +describe('test fetch middleware:', () => { + let server; + + beforeAll(async () => { + server = await createTestServer(); + }); + + const prefix = api => `${server.url}${api}`; + + // 测试请求 + it('test normal and unnormal fetch', async () => { + server.get('/test/fetch', (req, res) => { + setTimeout(() => { + writeData('ok', res); + }, 1000); + }); + + // 正常请求 + let response = await fetch(prefix('/test/fetch')); + expect(response.ok).toBe(true); + + // 非法 url + try { + response = await fetch({ hello: 'hello' }); + } catch (error) { + expect(error.message).toBe('url MUST be a string'); + } + }, 5000); + + it('test interceptors', async () => { + server.get('/test/interceptors', (req, res) => { + writeData(req.query, res); + }); + // 测试啥也不返回 + fetch.interceptors.request.use(() => ({})); + + fetch.interceptors.response.use(res => res); + + // request拦截器, 加个参数 + fetch.interceptors.request.use((url, options) => { + return { + url: `${url}&fetch=fetch`, + options: { ...options, interceptors: true }, + }; + }); + + // response拦截器, 修改一个header + fetch.interceptors.response.use((res, options) => { + res.headers.append('interceptors', 'yes yo'); + return res; + }); + + let response = await fetch(prefix('/test/interceptors')); + expect(response.headers.get('interceptors')).toBe('yes yo,yes yo'); + const resText = await response.text(); + expect(JSON.parse(resText).fetch).toBe('fetch'); + + request.interceptors = fetch.interceptors; + request.interceptors.request.use((url, options) => { + return { + url: `${url}&foo=foo`, + options, + }; + }); + + response = await request(prefix('/test/interceptors')); + expect(response.fetch).toBe('fetch'); + expect(response.foo).toBe('foo'); + }, 3000); + + afterAll(() => { + server.close(); + }); +}); From 5aeef72a815dc7c7b649946af67b533a2ce85c39 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Wed, 31 Jul 2019 15:19:06 +0800 Subject: [PATCH 31/94] 1.2.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5c57673..cee800a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.1.1", + "version": "1.2.0", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From db38fdb4551b3c0b4b08cc94b45b6b208ee64248 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Wed, 31 Jul 2019 18:01:21 +0800 Subject: [PATCH 32/94] fix: remove properties in interface (#62) --- types/index.d.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index 017f640..83ee667 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -31,7 +31,6 @@ export interface RequestOptionsInit extends RequestInit { errorHandler?: (error: ResponseError) => void; prefix?: string; suffix?: string; - credentials?: string; } export interface RequestOptionsWithoutResponse extends RequestOptionsInit { From 39d05578c8e028c8c0fe97dc12dc21ff5ff8263a Mon Sep 17 00:00:00 2001 From: chenjsh Date: Wed, 31 Jul 2019 18:03:43 +0800 Subject: [PATCH 33/94] 1.2.1 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index cee800a..256d12d 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.0", + "version": "1.2.1", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From d1cb5c0ff64db9720941533be2531a559f8d7a5e Mon Sep 17 00:00:00 2001 From: chenjsh Date: Sat, 3 Aug 2019 18:15:33 +0800 Subject: [PATCH 34/94] fix: update properties 'next' in interface 'OnionMiddleware' (#63) --- types/index.d.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/types/index.d.ts b/types/index.d.ts index 83ee667..52d20b4 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -66,7 +66,7 @@ export interface Context { export type ResponseInterceptor = (response: Response, options: RequestOptionsInit) => Response | Promise; -export type OnionMiddleware = (ctx: Context, next: NextCallback) => void; +export type OnionMiddleware = (ctx: Context, next: () => void) => void; export interface RequestMethod { (url: string, options: RequestOptionsWithResponse): Promise>; From 010a56a5a7ff391351d16ff94dff505713843303 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Mon, 5 Aug 2019 10:30:43 +0800 Subject: [PATCH 35/94] 1.2.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 256d12d..c67a672 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.1", + "version": "1.2.2", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From aec901b8f65592cb7857b187539aef7ccf8b9c77 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Thu, 22 Aug 2019 10:54:42 +0800 Subject: [PATCH 36/94] Feat/support cancel request (#64) * feat: support cancel request * feat: add parseResponse,throwErrIfParseFail options * fix: method option do not work in extend method --- README.md | 55 ++++- README_zh-CN.md | 59 ++++- src/cancel/cancel.js | 18 ++ src/cancel/cancelToken.js | 55 +++++ src/cancel/isCancel.js | 5 + src/core.js | 12 +- src/interceptor/addfix.js | 6 +- src/middleware/addfix.js | 15 -- src/middleware/fetch.js | 12 +- src/middleware/parseResponse.js | 14 +- src/onion/onion.js | 4 +- src/request.js | 28 +-- src/utils.js | 19 +- test/cancel.test.js | 87 +++++++ test/cancel/cancel.test.js | 15 ++ test/cancel/cancelToken.test.js | 87 +++++++ test/cancel/isCancel.test.js | 12 + test/fetch.test.js | 90 ++++++++ test/index.test.js | 386 ++++---------------------------- test/interceptor.test.js | 132 +++++++++++ test/middleware.test.js | 127 +++++++++++ test/onion/onion.test.js | 53 +++++ test/timeout.test.js | 56 +++++ test/util.test.js | 52 +++++ types/index.d.ts | 39 +++- 25 files changed, 1041 insertions(+), 397 deletions(-) create mode 100644 src/cancel/cancel.js create mode 100644 src/cancel/cancelToken.js create mode 100644 src/cancel/isCancel.js delete mode 100644 src/middleware/addfix.js create mode 100644 test/cancel.test.js create mode 100644 test/cancel/cancel.test.js create mode 100644 test/cancel/cancelToken.test.js create mode 100644 test/cancel/isCancel.test.js create mode 100644 test/fetch.test.js create mode 100644 test/interceptor.test.js create mode 100644 test/middleware.test.js create mode 100644 test/onion/onion.test.js create mode 100644 test/timeout.test.js create mode 100644 test/util.test.js diff --git a/README.md b/README.md index 905aff8..4c478e5 100644 --- a/README.md +++ b/README.md @@ -18,9 +18,10 @@ The network request library, based on fetch encapsulation, combines the features - api timeout support - api request cache support - support for processing gbk -- request and response interceptor support for class axios +- request and response interceptor support like axios - unified error handling - middleware support +- cancel request support like axios ## umi-request vs fetch vs axios @@ -39,6 +40,7 @@ The network request library, based on fetch encapsulation, combines the features | 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) @@ -74,6 +76,11 @@ For more discussion, refer to [Traditional Ajax is dead, Fetch eternal life](htt | errorHandler | exception handling, or override unified exception handling | function(error) | -- | | headers | fetch original parameters | object | -- | {} | | credentials | fetch request with cookies | string | -- | credentials: 'include' | +| parseResponse | response processing simplification | boolean | -- | true | +| throwErrIfParseFail | throw error when JSON parse fail and responseType is 'json' | boolean | -- | false | +| cancelToken | Token to cancel request | CancelToken.token | -- | -- | +| type | request type,type 'normal' would use fetch | string | -- | normal | + The other parameters of fetch are valid. See [fetch documentation](https://github.github.io/fetch/) @@ -306,6 +313,52 @@ order of middlewares be called: a1 -> b1 -> response -> b2 -> a2 ``` +## 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(); +``` + ## Development and debugging - npm install diff --git a/README_zh-CN.md b/README_zh-CN.md index e776477..4e3310e 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -25,6 +25,7 @@ - 类 axios 的 request 和 response 拦截器(interceptors)支持 - 统一的错误处理方式 - 类 koa 洋葱机制的 use 中间件机制支持 +- 类 axios 的取消请求 ## 与 fetch, axios 异同 @@ -43,6 +44,7 @@ | 后缀 | ✅ | ❎ | ❎ | | 处理 gbk | ✅ | ❎ | ❎ | | 中间件 | ✅ | ❎ | ❎ | +| 取消请求 | ✅ | ❎ | ✅ | 更多讨论参考[传统 Ajax 已死,Fetch 永生](https://github.com/camsong/blog/issues/2), 如果你有好的建议和需求, 请提 [issue](https://github.com/umijs/umi/issues) @@ -78,6 +80,11 @@ | errorHandler | 异常处理, 或者覆盖统一的异常处理 | function(error) | -- | | headers | fetch 原有参数 | object | -- | {} | | credentials | fetch 请求包含 cookies 信息 | object | -- | credentials: 'include' | +| parseResponse | 是否对 response 做处理简化 | boolean | -- | true | +| throwErrIfParseFail | 当 responseType 为 'json', 对请求结果做 JSON.parse 出错时是否抛出异常 | boolean | -- | false | +| cancelToken | 取消请求的 Token | CancelToken.token | -- | -- | +| type | 请求类型,normal 为 fetch | string | -- | normal | + fetch原其他参数有效, 详见[fetch文档](https://github.github.io/fetch/) @@ -192,14 +199,13 @@ request.use(async (ctx, next) => { await next(); const { res } = ctx; - const { success = false } = res; + const { success = false } = res; // 假设返回结果为 : { success: false, errorCode: 'B001' } if (!success) { - // Handle fail request here + // 对异常情况做对应处理 } }) ``` - ## 错误处理 ```javascript @@ -312,6 +318,53 @@ const data = await request('/api/v1/a'); a1 -> b1 -> response -> b2 -> a2 ``` +## 取消请求 +你可以通过 **cancel token** 来取消一个请求 +> cancel token API 是基于已被撤销的 [cancelable-promises 方案](https://github.com/tc39/proposal-cancelable-promises) + +1. 你可以通过 **CancelToken.source** 来创建一个 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 { + // 处理异常 + } +}); + +Request.post('/api/cancel', { + name: 'hello world' +}, { + cancelToken: token +}) + +// 取消请求(参数为非必填) +cancel('Operation canceled by the user.'); + +``` + +2. 你也可以通过实例化 CancelToken 来创建一个 token,同时通过传入函数来获取取消方法: +```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(); +``` ## 开发和调试 diff --git a/src/cancel/cancel.js b/src/cancel/cancel.js new file mode 100644 index 0000000..9d5ec1c --- /dev/null +++ b/src/cancel/cancel.js @@ -0,0 +1,18 @@ +'use strict'; + +/** + * 当执行 “取消请求” 操作时会抛出 Cancel 对象作为一场 + * @class + * @param {string=} message The message. + */ +function Cancel(message) { + this.message = message; +} + +Cancel.prototype.toString = function toString() { + return this.message ? `Cancel: ${this.message}` : 'Cancel'; +}; + +Cancel.prototype.__CANCEL__ = true; + +export default Cancel; diff --git a/src/cancel/cancelToken.js b/src/cancel/cancelToken.js new file mode 100644 index 0000000..6ca6702 --- /dev/null +++ b/src/cancel/cancelToken.js @@ -0,0 +1,55 @@ +'use strict'; +import Cancel from './cancel'; + +/** + * 通过 CancelToken 来取消请求操作 + * + * @class + * @param {Function} executor The executor function. + */ +function CancelToken(executor) { + if (typeof executor !== 'function') { + throw new TypeError('executor must be a function.'); + } + + var resolvePromise; + this.promise = new Promise(function promiseExecutor(resolve) { + resolvePromise = resolve; + }); + + var token = this; + executor(function cancel(message) { + if (token.reason) { + // 取消操作已被调用过 + return; + } + + token.reason = new Cancel(message); + resolvePromise(token.reason); + }); +} + +/** + * 如果请求已经取消,抛出 Cancel 异常 + */ +CancelToken.prototype.throwIfRequested = function throwIfRequested() { + if (this.reason) { + throw this.reason; + } +}; + +/** + * 通过 source 来返回 CancelToken 实例和取消 CancelToken 的函数 + */ +CancelToken.source = function source() { + var cancel; + var token = new CancelToken(function executor(c) { + cancel = c; + }); + return { + token: token, + cancel: cancel, + }; +}; + +export default CancelToken; diff --git a/src/cancel/isCancel.js b/src/cancel/isCancel.js new file mode 100644 index 0000000..a444a12 --- /dev/null +++ b/src/cancel/isCancel.js @@ -0,0 +1,5 @@ +'use strict'; + +export default function isCancel(value) { + return !!(value && value.__CANCEL__); +} diff --git a/src/core.js b/src/core.js index 7834ae1..b2488cc 100644 --- a/src/core.js +++ b/src/core.js @@ -1,19 +1,25 @@ import Onion from './onion/onion'; import { MapCache } from './utils'; import addfixInterceptor from './interceptor/addfix'; +import fetchMiddleware from './middleware/fetch'; +import parseResponseMiddleware from './middleware/parseResponse'; +import simplePost from './middleware/simplePost'; +import simpleGet from './middleware/simpleGet'; // 旧版拦截器为共享 const requestInterceptors = [addfixInterceptor]; const responseInterceptors = []; +const defaultMiddlewares = [simplePost, simpleGet, fetchMiddleware, parseResponseMiddleware]; class Core { - constructor(initOptions, defaultMiddlewares) { + constructor(initOptions) { this.onion = new Onion(defaultMiddlewares); + this.fetchIndex = 2; // 请求中间件位置 this.mapCache = new MapCache(initOptions); } - use(newMiddleware) { - this.onion.use(newMiddleware); + use(newMiddleware, index = 0) { + this.onion.use(newMiddleware, index); return this; } diff --git a/src/interceptor/addfix.js b/src/interceptor/addfix.js index aa8c851..40a4d35 100644 --- a/src/interceptor/addfix.js +++ b/src/interceptor/addfix.js @@ -1,5 +1,5 @@ -// 前后缀拦截器 -export default (url, options = {}) => { +// 前后缀拦截 +const addfix = (url, options = {}) => { const { prefix, suffix } = options; if (prefix) { url = `${prefix}${url}`; @@ -12,3 +12,5 @@ export default (url, options = {}) => { options, }; }; + +export default addfix; diff --git a/src/middleware/addfix.js b/src/middleware/addfix.js deleted file mode 100644 index a7aede9..0000000 --- a/src/middleware/addfix.js +++ /dev/null @@ -1,15 +0,0 @@ -export default function addfixMiddleware(ctx, next) { - const { - req: { options = {}, url = '' }, - } = ctx; - const { prefix, suffix } = options; - if (typeof url !== 'string') throw new Error('url MUST be a string'); - - if (prefix) { - ctx.req.url = `${prefix}${url}`; - } - if (suffix) { - ctx.req.url = `${url}${suffix}`; - } - return next().then(Promise.resolve()); -} diff --git a/src/middleware/fetch.js b/src/middleware/fetch.js index e12e37a..af9551c 100644 --- a/src/middleware/fetch.js +++ b/src/middleware/fetch.js @@ -1,5 +1,5 @@ import 'whatwg-fetch'; -import { timeout2Throw } from '../utils'; +import { timeout2Throw, cancel2Throw } from '../utils'; export default function fetchMiddleware(ctx, next) { const { @@ -7,7 +7,7 @@ export default function fetchMiddleware(ctx, next) { cache, responseInterceptors, } = ctx; - const { timeout = 0, type = 'normal', useCache = false, method = 'get', params, ttl } = options; + const { timeout = 0, type = 'normal', useCache = false, method = 'get', params, ttl, cancelToken } = options; if (type !== 'normal') { return next(); @@ -30,14 +30,16 @@ export default function fetchMiddleware(ctx, next) { return next(); } } + let response; + // 超时处理、取消请求处理 if (timeout > 0) { - response = Promise.race([window.fetch(url, options), timeout2Throw(timeout)]); + response = Promise.race([cancel2Throw(options, ctx), window.fetch(url, options), timeout2Throw(timeout)]); } else { - response = window.fetch(url, options); + response = Promise.race([cancel2Throw(options, ctx), window.fetch(url, options)]); } - // 执行 response 的拦截器 + // 兼容老版本 response.interceptor responseInterceptors.forEach(handler => { response = response.then(res => handler(res, options)); }); diff --git a/src/middleware/parseResponse.js b/src/middleware/parseResponse.js index 3da2622..9aef604 100644 --- a/src/middleware/parseResponse.js +++ b/src/middleware/parseResponse.js @@ -3,9 +3,19 @@ import { safeJsonParse, readerGBK, ResponseError } from '../utils'; export default function parseResponseMiddleware(ctx, next) { const { res, req } = ctx; const { - options: { responseType = 'json', charset = 'utf8', getResponse = false }, + options: { + responseType = 'json', + charset = 'utf8', + getResponse = false, + throwErrIfParseFail = false, + parseResponse = true, + }, } = req || {}; + if (!parseResponse) { + return next(); + } + if (!res || !res.clone) { return next(); } @@ -25,7 +35,7 @@ export default function parseResponseMiddleware(ctx, next) { throw new ResponseError(copy, e.message); } } else if (responseType === 'json') { - return res.text().then(safeJsonParse); + return res.text().then(d => safeJsonParse(d, throwErrIfParseFail, copy)); } try { // 其他如text, blob, arrayBuffer, formData diff --git a/src/onion/onion.js b/src/onion/onion.js index c64b263..1797fe0 100644 --- a/src/onion/onion.js +++ b/src/onion/onion.js @@ -9,8 +9,8 @@ class Onion { this.defaultMiddlewaresLen = defaultMiddlewares.length; } - use(newMiddleware) { - this.middlewares.splice(this.middlewares.length - this.defaultMiddlewaresLen, 0, newMiddleware); + use(newMiddleware, index = 0) { + this.middlewares.splice(this.middlewares.length - this.defaultMiddlewaresLen + index, 0, newMiddleware); } execute(params = null) { diff --git a/src/request.js b/src/request.js index 6892001..4a339be 100644 --- a/src/request.js +++ b/src/request.js @@ -1,12 +1,11 @@ import Core from './core'; -import fetchMiddleware from './middleware/fetch'; -import parseResponseMiddleware from './middleware/parseResponse'; -import simplePost from './middleware/simplePost'; -import simpleGet from './middleware/simpleGet'; +import Cancel from './cancel/cancel'; +import CancelToken from './cancel/cancelToken'; +import isCancel from './cancel/isCancel'; // 通过 request 函数,在 core 之上再封装一层,提供原 umi/request 一致的 api,无缝升级 -const request = (initOptions = {}, middleware = []) => { - const coreInstance = new Core(initOptions, middleware); +const request = (initOptions = {}) => { + const coreInstance = new Core(initOptions); const umiInstance = (url, options = {}) => { const mergeOptions = { ...initOptions, @@ -19,13 +18,14 @@ const request = (initOptions = {}, middleware = []) => { ...initOptions.params, ...options.params, }, - method: (options.method || 'get').toLowerCase(), + method: (options.method || initOptions.method || 'get').toLowerCase(), }; return coreInstance.request(url, mergeOptions); }; // 中间件 umiInstance.use = coreInstance.use.bind(coreInstance); + umiInstance.fetchIndex = coreInstance.fetchIndex; // 拦截器 umiInstance.interceptors = { @@ -43,6 +43,10 @@ const request = (initOptions = {}, middleware = []) => { umiInstance[method] = (url, options) => umiInstance(url, { ...options, method }); }); + umiInstance.Cancel = Cancel; + umiInstance.CancelToken = CancelToken; + umiInstance.isCancel = isCancel; + return umiInstance; }; @@ -54,13 +58,11 @@ const request = (initOptions = {}, middleware = []) => { * @param {function} errorHandler 统一错误处理方法 * @param {object} headers 统一的headers */ -const _extendMiddlewares = [simplePost, simpleGet, fetchMiddleware, parseResponseMiddleware]; -export const extend = initOptions => request(initOptions, _extendMiddlewares); +export const extend = initOptions => request(initOptions); /** - * 暴露 fetch 中间件,去除响应处理的中间件和前后缀处理的中间件,保障依旧可以使用 + * 暴露 fetch 中间件,保障依旧可以使用 */ -const _fetchMiddlewares = [simplePost, simpleGet, fetchMiddleware]; -export const fetch = request({}, _fetchMiddlewares); +export const fetch = request({ parseResponse: false }); -export default request({}, _extendMiddlewares); +export default request({}); diff --git a/src/utils.js b/src/utils.js index 8b41dad..bb255fa 100644 --- a/src/utils.js +++ b/src/utils.js @@ -85,10 +85,14 @@ export function readerGBK(file) { /** * 安全的JSON.parse */ -export function safeJsonParse(data) { +export function safeJsonParse(data, throwErrIfParseFail = false, response = null) { try { return JSON.parse(data); - } catch (e) {} // eslint-disable-line no-empty + } catch (e) { + if (throwErrIfParseFail) { + throw new ResponseError(response, 'JSON.parse fail', data); + } + } // eslint-disable-line no-empty return data; } @@ -99,3 +103,14 @@ export function timeout2Throw(msec) { }, msec); }); } + +// If request options contain 'cancelToken', reject request when token has been canceled +export function cancel2Throw(opt) { + return new Promise((_, reject) => { + if (opt.cancelToken) { + opt.cancelToken.promise.then(cancel => { + reject(cancel); + }); + } + }); +} diff --git a/test/cancel.test.js b/test/cancel.test.js new file mode 100644 index 0000000..2f9c372 --- /dev/null +++ b/test/cancel.test.js @@ -0,0 +1,87 @@ +import createTestServer from 'create-test-server'; +import request, { extend } from '../src/index'; + +var Cancel = request.Cancel; +var CancelToken = request.CancelToken; + +const writeData = (data, res) => { + res.setHeader('access-control-allow-origin', '*'); + res.send(data); +}; + +describe('cancel', () => { + let server; + beforeAll(async () => { + server = await createTestServer(); + }); + afterAll(() => { + server.close(); + }); + + describe('when called before sending request', () => { + it('rejects Promise with a Cancel object', done => { + expect.assertions(2); + server.get('/a', (req, res) => { + setTimeout(() => { + writeData('ok', res); + }, 1000); + }); + var source = CancelToken.source(); + source.cancel('Operation has been canceled.'); + request + .get(`${server.url}/a`, { + cancelToken: source.token, + }) + .catch(function(thrown) { + expect(thrown).toEqual(jasmine.any(Cancel)); + expect(thrown.message).toBe('Operation has been canceled.'); + done(); + }); + }); + }); + + describe('when called after request has been sent', () => { + it('rejects Promise with a Cancel object', done => { + expect.assertions(1); + server.get('/cancel/a', (req, res) => { + setTimeout(() => { + writeData('ok', res); + }, 3000); + }); + var source = CancelToken.source(); + request + .get(`${server.url}/cancel/a`, { + cancelToken: source.token, + }) + .catch(err => { + expect(err).toBeInstanceOf(Cancel); + done(); + }); + setTimeout(() => { + source.cancel(); + }, 1000); + }); + }); + + describe('when called after response has been received', () => { + it('does not cause unhandled rejection', done => { + server.get('/cancel/b', (req, res) => { + setTimeout(() => { + writeData('ok', res); + }, 1000); + }); + var source = CancelToken.source(); + request + .get(`${server.url}/cancel/b`, { + cancelToken: source.token, + }) + .then(function() { + window.addEventListener('unhandledrejection', () => { + done.fail('Unhandled rejection.'); + }); + source.cancel(); + setTimeout(done, 100); + }); + }); + }); +}); diff --git a/test/cancel/cancel.test.js b/test/cancel/cancel.test.js new file mode 100644 index 0000000..09bb472 --- /dev/null +++ b/test/cancel/cancel.test.js @@ -0,0 +1,15 @@ +import Cancel from '../../src/cancel/cancel'; + +describe('Cancel', () => { + describe('toString', () => { + it('returns correct result when message is not specified', () => { + const cancel = new Cancel(); + expect(cancel.toString()).toBe('Cancel'); + }); + + it('returns correct result when message is specified', () => { + const cancel = new Cancel('Operation has been canceled.'); + expect(cancel.toString()).toBe('Cancel: Operation has been canceled.'); + }); + }); +}); diff --git a/test/cancel/cancelToken.test.js b/test/cancel/cancelToken.test.js new file mode 100644 index 0000000..8a5f48c --- /dev/null +++ b/test/cancel/cancelToken.test.js @@ -0,0 +1,87 @@ +import CancelToken from '../../src/cancel/cancelToken'; +import Cancel from '../../src/cancel/cancel'; + +describe('CancelToken', () => { + describe('constructor', () => { + it('throws when executor is not specified', () => { + expect(() => { + new CancelToken(); + }).toThrowError(TypeError, 'executor must be a function.'); + }); + + it('throws when executor is not a function', () => { + expect(() => { + new CancelToken(123); + }).toThrowError(TypeError, 'executor must be a function.'); + }); + }); + + describe('reason', () => { + it('returns a Cancel if cancellation has been requested', () => { + let cancel; + const token = new CancelToken(function(c) { + cancel = c; + }); + cancel('Operation has been canceled.'); + expect(token.reason).toEqual(jasmine.any(Cancel)); + expect(token.reason.message).toBe('Operation has been canceled.'); + }); + + it('returns undefined if cancellation has not been requested', () => { + const token = new CancelToken(() => {}); + expect(token.reason).toBeUndefined(); + }); + }); + + describe('promise', () => { + it('returns a Promise that resolves when cancellation is requested', function(done) { + let cancel; + const token = new CancelToken(function(c) { + cancel = c; + }); + token.promise.then(function onFulfilled(value) { + expect(value).toEqual(jasmine.any(Cancel)); + expect(value.message).toBe('Operation has been canceled.'); + done(); + }); + cancel('Operation has been canceled.'); + }); + }); + + describe('throwIfRequested', () => { + it('throws if cancellation has been requested', () => { + // Note: we cannot use expect.toThrowError here as Cancel does not inherit from Error + let cancel; + const token = new CancelToken(function(c) { + cancel = c; + }); + cancel('Operation has been canceled.'); + try { + token.throwIfRequested(); + fail('Expected throwIfRequested to throw.'); + } catch (thrown) { + if (!(thrown instanceof Cancel)) { + fail('Expected throwIfRequested to throw a Cancel, but it threw ' + thrown + '.'); + } + expect(thrown.message).toBe('Operation has been canceled.'); + } + }); + + it('does not throw if cancellation has not been requested', () => { + const token = new CancelToken(() => {}); + token.throwIfRequested(); + }); + }); + + describe('source', () => { + it('returns an object containing token and cancel function', () => { + const source = CancelToken.source(); + expect(source.token).toEqual(jasmine.any(CancelToken)); + expect(source.cancel).toEqual(jasmine.any(Function)); + expect(source.token.reason).toBeUndefined(); + source.cancel('Operation has been canceled.'); + expect(source.token.reason).toEqual(jasmine.any(Cancel)); + expect(source.token.reason.message).toBe('Operation has been canceled.'); + }); + }); +}); diff --git a/test/cancel/isCancel.test.js b/test/cancel/isCancel.test.js new file mode 100644 index 0000000..5fcf40a --- /dev/null +++ b/test/cancel/isCancel.test.js @@ -0,0 +1,12 @@ +import isCancel from '../../src/cancel/isCancel'; +import Cancel from '../../src/cancel/cancel'; + +describe('isCancel', () => { + it('returns true if value is a Cancel', () => { + expect(isCancel(new Cancel())).toBe(true); + }); + + it('returns false if value is not a Cancel', () => { + expect(isCancel({ hello: 'world' })).toBe(false); + }); +}); diff --git a/test/fetch.test.js b/test/fetch.test.js new file mode 100644 index 0000000..3de1d57 --- /dev/null +++ b/test/fetch.test.js @@ -0,0 +1,90 @@ +import createTestServer from 'create-test-server'; +import request, { extend, Onion, fetch } from '../src/index'; + +var Cancel = request.Cancel; +var CancelToken = request.CancelToken; + +const writeData = (data, res) => { + res.setHeader('access-control-allow-origin', '*'); + res.send(data); +}; + +describe('timeout', () => { + let server; + beforeAll(async () => { + server = await createTestServer(); + }); + afterAll(() => { + server.close(); + }); + + const prefix = api => `${server.url}${api}`; + + // 测试请求 + describe('test valid fetch', () => { + it('response should be true', async done => { + server.get('/test/fetch', (req, res) => { + setTimeout(() => { + writeData('ok', res); + }, 1000); + }); + + let response = await fetch(prefix('/test/fetch')); + expect(response.ok).toBe(true); + done(); + }); + + it('test fetch interceptors', async done => { + server.get('/test/interceptors', (req, res) => { + writeData(req.query, res); + }); + fetch.interceptors.request.use(() => ({})); + fetch.interceptors.response.use(res => res); + fetch.interceptors.request.use((url, options) => { + return { + url: `${url}?fetch=fetch`, + options: { ...options, interceptors: true }, + }; + }); + fetch.interceptors.response.use((res, options) => { + res.headers.append('interceptors', 'yes yo'); + return res; + }); + + let response = await fetch(prefix('/test/interceptors')); + expect(response.headers.get('interceptors')).toBe('yes yo'); + const resText = await response.text(); + expect(JSON.parse(resText).fetch).toBe('fetch'); + + request.interceptors = fetch.interceptors; + request.interceptors.request.use((url, options) => { + return { + url: `${url}&foo=foo`, + options, + }; + }); + + response = await request(prefix('/test/interceptors')); + expect(response.fetch).toBe('fetch'); + expect(response.foo).toBe('foo'); + done(); + }); + }); + + describe('test invalid fetch', () => { + it('test normal and unnormal fetch', async done => { + expect.assertions(1); + server.get('/test/fetch', (req, res) => { + setTimeout(() => { + writeData('ok', res); + }, 1000); + }); + try { + response = await fetch({ hello: 'hello' }); + } catch (error) { + expect(error.message).toBe('url MUST be a string'); + done(); + } + }); + }); +}); diff --git a/test/index.test.js b/test/index.test.js index f265c9c..468983c 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -17,30 +17,11 @@ describe('test fetch:', () => { server = await createTestServer(); }); - const prefix = api => `${server.url}${api}`; - - // 测试超时 - it('test timeout', async () => { - server.get('/test/timeout', (req, res) => { - setTimeout(() => { - writeData('ok', res); - }, 1000); - }); - - // 第一次超时前返回 - let response = await request(prefix('/test/timeout'), { - timeout: 1200, - getResponse: true, - }); - expect(response.response.ok).toBe(true); + afterAll(() => { + server.close(); + }); - // 第二次超时异常 - try { - response = await request(prefix('/test/timeout'), { timeout: 800 }); - } catch (error) { - expect(error.name).toBe('RequestError'); - } - }, 5000); + const prefix = api => `${server.url}${api}`; // 测试请求方法 it('test methodType', async () => { @@ -131,6 +112,24 @@ describe('test fetch:', () => { }, 5000); // 测试返回类型 #TODO 更多类型 + it('test invalid responseType', async () => { + expect.assertions(2); + server.post('/test/invalid/response', (req, res) => { + writeData('hello world', res); + }); + try { + let response = await request(prefix('/test/invalid/response'), { + method: 'post', + responseType: 'json', + data: { a: 1 }, + throwErrIfParseFail: true, + }); + console.log('response:'); + } catch (error) { + expect(error.message).toBe('JSON.parse fail'); + expect(error.data).toBe('hello world'); + } + }); it('test responseType', async () => { server.post('/test/responseType', (req, res) => { writeData(req.body, res); @@ -277,6 +276,24 @@ describe('test fetch:', () => { expect(response.data.defaultParams).toBe('true'); }, 10000); + it('test extends', async () => { + server.get('/test/method', (req, res) => { + writeData({ method: req.method }, res); + }); + + server.post('/test/method', (req, res) => { + writeData({ method: req.method }, res); + }); + + const extendRequest = extend({ method: 'POST' }); + + let response = await extendRequest(prefix('/test/method')); + expect(response.method).toBe('POST'); + + const extendRequest2 = extend(); + let response2 = await extendRequest2(prefix('/test/method')); + expect(response2.method).toBe('GET'); + }); // 测试异常捕获 it('test exception', async () => { server.get('/test/exception', (req, res) => { @@ -406,10 +423,6 @@ describe('test fetch:', () => { expect(response.data[0]).toBe('1'); expect(response.data[1]).toBe('2'); }); - - afterAll(() => { - server.close(); - }); }); // 测试rpc @@ -418,322 +431,3 @@ xdescribe('test rpc:', () => { expect(request.rpc('wang').hello).toBe('wang'); }); }); - -// 测试工具函数 -describe('test utils:', () => { - it('test cache:', done => { - const mapCache = new MapCache({ maxCache: 3 }); - - // 设置读取 - const key = { some: 'one' }; - mapCache.set(key, { hello: 'world1' }, 1000); - expect(mapCache.get(key).hello).toBe('world1'); - setTimeout(() => { - expect(mapCache.get(key)).toBe(undefined); - done(); - }, 1001); - - // 删除 - const key2 = { other: 'two' }; - mapCache.set(key2, { hello: 'world1' }, 10000); - mapCache.delete(key2); - expect(mapCache.get(key2)).toBe(undefined); - - // 清除 - const key3 = { other: 'three' }; - mapCache.set(key3, { hello: 'world1' }, 10000); - mapCache.clear(); - expect(mapCache.get(key3)).toBe(undefined); - - // 测试超过最大数 - mapCache.set('max1', { what: 'ok' }, 10000); - mapCache.set('max1', { what: 'ok1' }, 10000); - mapCache.set('max2', { what: 'ok2' }, 10000); - mapCache.set('max3', { what: 'ok3' }, 10000); - expect(mapCache.get('max1').what).toBe('ok1'); - mapCache.set('max4', { what: 'ok4' }, 10000); - expect(mapCache.get('max1')).toBe(undefined); - mapCache.set('max5', { what: 'ok5' }); - mapCache.set('max6', { what: 'ok6' }, 0); - }, 3000); -}); - -// 测试fetch lib -describe('test fetch lib:', () => { - let server; - - beforeAll(async () => { - server = await createTestServer(); - }); - - const prefix = api => `${server.url}${api}`; - - it('test interceptors', async () => { - server.get('/test/interceptors', (req, res) => { - writeData(req.query, res); - }); - - // 测试啥也不返回 - request.interceptors.request.use(() => ({})); - - request.interceptors.response.use(res => res); - - // request拦截器, 加个参数 - request.interceptors.request.use((url, options) => { - debug(url, options); - return { - url: `${url}?interceptors=yes`, - options: { ...options, interceptors: true }, - }; - }); - - // response拦截器, 修改一个header - request.interceptors.response.use((res, options) => { - res.headers.append('interceptors', 'yes yo'); - return res; - }); - - const response = await request(prefix('/test/interceptors'), { - timeout: 1200, - getResponse: true, - }); - expect(response.data.interceptors).toBe('yes'); - expect(response.response.headers.get('interceptors')).toBe('yes yo'); - - // 测试乱写 - try { - request({ hello: 1 }); - } catch (error) { - expect(error.message).toBe('url MUST be a string'); - } - }, 3000); - - it('test invalid interceptors', async () => { - try { - request.interceptors.request.use('invalid interceptor'); - } catch (error) { - expect(error.message).toBe('Interceptor must be function!'); - } - try { - request.interceptors.response.use('invalid interceptor'); - } catch (error) { - expect(error.message).toBe('Interceptor must be function!'); - } - }); - - it('modify request data', async () => { - server.post('/test/post/interceptors', (req, res) => { - writeData(req.body, res); - }); - request.interceptors.request.use((url, options) => { - if (options.method.toLowerCase() === 'post') { - options.data = { - ...options.data, - foo: 'foo', - }; - } - return { url, options }; - }); - - const data = await request(prefix('/test/post/interceptors'), { - method: 'post', - data: { bar: 'bar' }, - }); - expect(data.foo).toBe('foo'); - }, 3000); - - // 使用上边修改数据的用例, 测试 promise 化的 interceptors - it('test promise interceptors', async () => { - server.post('/test/promiseInterceptors', (req, res) => { - writeData(req.body, res); - }); - - request.interceptors.request.use((url, options) => { - return new Promise(resolve => { - setTimeout(() => { - if (options.method.toLowerCase() === 'post') { - options.data = { - ...options.data, - foo: 'foo', - }; - } - resolve({ url, options }); - }, 1000); - }); - }); - - const data = await request(prefix('/test/promiseInterceptors'), { - method: 'post', - data: { bar: 'bar' }, - }); - expect(data.foo).toBe('foo'); - }, 3000); - - afterAll(() => { - server.close(); - }); -}); - -// 测试中间件机制 -describe('test fetch lib:', () => { - let server; - - beforeAll(async () => { - server = await createTestServer(); - }); - - const prefix = api => `${server.url}${api}`; - - // 使用上边修改数据的用例, 测试 promise 化的 interceptors - it('test middlewares', async () => { - server.post('/test/promiseInterceptors/a/b', (req, res) => { - writeData(req.body, res); - }); - request.use(async (ctx, next) => { - ctx.req.options = { - ...ctx.req.options, - data: { - ...ctx.req.options.data, - foo: 'foo', - }, - }; - await next(); - }); - request.use(async (ctx, next) => { - await next(); - ctx.res.hello = 'hello'; - }); - const data = await request(prefix('/test/promiseInterceptors/a/b'), { - method: 'post', - data: { bar: 'bar' }, - }); - expect(data.bar).toBe('bar'); - expect(data.foo).toBe('foo'); - expect(data.hello).toBe('hello'); - }); - - afterAll(() => { - server.close(); - }); -}); - -describe('test onion', () => { - it('test constructor', async () => { - try { - const onion = new Onion(); - onion.use(async () => {}); - } catch (error) { - expect(error.message).toBe('Default middlewares must be an array!'); - } - }); - it('test not function middleware', async () => { - try { - const onion = new Onion([]); - onion.use('not a function'); - onion.execute(); - } catch (error) { - expect(error.message).toBe('Middleware must be componsed of function'); - } - }); - it('test multiple use next', async () => { - try { - const onion = new Onion([]); - onion.use(async (ctx, next) => { - await next(); - await next(); - }); - await onion.execute(); - } catch (error) { - expect(error.message).toBe('next() should not be called multiple times in one middleware!'); - } - }); - it('test middleware throw error', async () => { - try { - const onion = new Onion([]); - onion.use(async (ctx, next) => { - await next(); - }); - onion.use(async () => { - throw new Error('error in middleware'); - }); - await onion.execute(); - } catch (error) { - expect(error.message).toBe('error in middleware'); - } - }); -}); - -describe('test fetch middleware:', () => { - let server; - - beforeAll(async () => { - server = await createTestServer(); - }); - - const prefix = api => `${server.url}${api}`; - - // 测试请求 - it('test normal and unnormal fetch', async () => { - server.get('/test/fetch', (req, res) => { - setTimeout(() => { - writeData('ok', res); - }, 1000); - }); - - // 正常请求 - let response = await fetch(prefix('/test/fetch')); - expect(response.ok).toBe(true); - - // 非法 url - try { - response = await fetch({ hello: 'hello' }); - } catch (error) { - expect(error.message).toBe('url MUST be a string'); - } - }, 5000); - - it('test interceptors', async () => { - server.get('/test/interceptors', (req, res) => { - writeData(req.query, res); - }); - // 测试啥也不返回 - fetch.interceptors.request.use(() => ({})); - - fetch.interceptors.response.use(res => res); - - // request拦截器, 加个参数 - fetch.interceptors.request.use((url, options) => { - return { - url: `${url}&fetch=fetch`, - options: { ...options, interceptors: true }, - }; - }); - - // response拦截器, 修改一个header - fetch.interceptors.response.use((res, options) => { - res.headers.append('interceptors', 'yes yo'); - return res; - }); - - let response = await fetch(prefix('/test/interceptors')); - expect(response.headers.get('interceptors')).toBe('yes yo,yes yo'); - const resText = await response.text(); - expect(JSON.parse(resText).fetch).toBe('fetch'); - - request.interceptors = fetch.interceptors; - request.interceptors.request.use((url, options) => { - return { - url: `${url}&foo=foo`, - options, - }; - }); - - response = await request(prefix('/test/interceptors')); - expect(response.fetch).toBe('fetch'); - expect(response.foo).toBe('foo'); - }, 3000); - - afterAll(() => { - server.close(); - }); -}); diff --git a/test/interceptor.test.js b/test/interceptor.test.js new file mode 100644 index 0000000..8c54a75 --- /dev/null +++ b/test/interceptor.test.js @@ -0,0 +1,132 @@ +import createTestServer from 'create-test-server'; +import request, { extend, Onion, fetch } from '../src/index'; + +const debug = require('debug')('afx-request:test'); + +var Cancel = request.Cancel; +var CancelToken = request.CancelToken; + +const writeData = (data, res) => { + res.setHeader('access-control-allow-origin', '*'); + res.send(data); +}; + +describe('interceptor', () => { + let server; + beforeAll(async () => { + server = await createTestServer(); + }); + afterAll(() => { + server.close(); + }); + + const prefix = api => `${server.url}${api}`; + + it('valid interceptor', async done => { + expect.assertions(3); + server.get('/test/interceptors', (req, res) => { + writeData(req.query, res); + }); + + // return nothing test + request.interceptors.request.use(() => ({})); + + // return same thing + request.interceptors.response.use(res => res); + + // request interceptor of add param to options + request.interceptors.request.use((url, options) => { + return { + url: `${url}?interceptors=yes`, + options: { ...options, interceptors: true }, + }; + }); + + // response interceptor, change response's header + request.interceptors.response.use((res, options) => { + res.headers.append('interceptors', 'yes yo'); + return res; + }); + + const response = await request(prefix('/test/interceptors'), { + timeout: 1200, + getResponse: true, + }); + + expect(response.data.interceptors).toBe('yes'); + expect(response.response.headers.get('interceptors')).toBe('yes yo'); + + // invalid url + try { + request({ hello: 1 }); + } catch (error) { + expect(error.message).toBe('url MUST be a string'); + done(); + } + }); + + it('invalid interceptor constructor', async done => { + expect.assertions(2); + try { + request.interceptors.request.use('invalid interceptor'); + } catch (error) { + expect(error.message).toBe('Interceptor must be function!'); + } + try { + request.interceptors.response.use('invalid interceptor'); + } catch (error) { + expect(error.message).toBe('Interceptor must be function!'); + } + done(); + }); + + it('use interceptor to modify request data', async done => { + server.post('/test/post/interceptors', (req, res) => { + writeData(req.body, res); + }); + request.interceptors.request.use((url, options) => { + if (options.method.toLowerCase() === 'post') { + options.data = { + ...options.data, + foo: 'foo', + }; + } + return { url, options }; + }); + + const data = await request(prefix('/test/post/interceptors'), { + method: 'post', + data: { bar: 'bar' }, + }); + expect(data.foo).toBe('foo'); + done(); + }); + + // use promise to test + it('use promise interceptor to modify request data', async done => { + server.post('/test/promiseInterceptors', (req, res) => { + writeData(req.body, res); + }); + + request.interceptors.request.use((url, options) => { + return new Promise(resolve => { + setTimeout(() => { + if (options.method.toLowerCase() === 'post') { + options.data = { + ...options.data, + promiseFoo: 'promiseFoo', + }; + } + resolve({ url, options }); + }, 1000); + }); + }); + + const data = await request(prefix('/test/promiseInterceptors'), { + method: 'post', + data: { bar: 'bar' }, + }); + expect(data.promiseFoo).toBe('promiseFoo'); + done(); + }); +}); diff --git a/test/middleware.test.js b/test/middleware.test.js new file mode 100644 index 0000000..893828d --- /dev/null +++ b/test/middleware.test.js @@ -0,0 +1,127 @@ +import createTestServer from 'create-test-server'; +import request, { extend, Onion, fetch } from '../src/index'; + +const debug = require('debug')('afx-request:test'); + +var Cancel = request.Cancel; +var CancelToken = request.CancelToken; + +const writeData = (data, res) => { + res.setHeader('access-control-allow-origin', '*'); + res.send(data); +}; + +describe('middleware', () => { + let server; + beforeAll(async () => { + server = await createTestServer(); + }); + afterAll(() => { + server.close(); + }); + + const prefix = api => `${server.url}${api}`; + + describe('use middleware to modify request data and response data', () => { + it('response should be { hello: "hello", foo: "foo" }', async done => { + server.post('/test/promiseInterceptors/a/b', (req, res) => { + writeData(req.body, res); + }); + request.use(async (ctx, next) => { + ctx.req.options = { + ...ctx.req.options, + data: { + ...ctx.req.options.data, + foo: 'foo', + }, + }; + await next(); + }); + request.use(async (ctx, next) => { + await next(); + ctx.res.hello = 'hello'; + }); + const data = await request(prefix('/test/promiseInterceptors/a/b'), { + method: 'post', + data: {}, + }); + expect(data).toEqual({ + foo: 'foo', + hello: 'hello', + }); + done(); + }); + + it('response should be { hello: "hello", foo: "foo" }', async done => { + server.post('/test/promiseInterceptors/a/b', (req, res) => { + writeData(req.body, res); + }); + const data = await request(prefix('/test/promiseInterceptors/a/b'), { + method: 'post', + data: {}, + }); + expect(data).toEqual({ + foo: 'foo', + hello: 'hello', + }); + done(); + }); + }); + describe('add request middleware', () => { + it('it should support rpc request', async done => { + server.post('/test/rpc', (req, res) => { + writeData(req.body, res); + }); + request.use((ctx, next) => { + const { req } = ctx; + const { url, options } = req; + const { method } = options; + if (method.toLowerCase() !== 'rpc') { + return next(); + } + ctx.res = { + success: true, + data: 'rpc response', + }; + return next(); + }, request.fetchIndex); + + const data = await request('/test/rpc', { + type: 'rpc', + method: 'rpc', + parseResponse: false, + }); + expect(data.data).toEqual('rpc response'); + done(); + }); + }); + + describe('request several at same time', () => { + it('it should response all ok', done => { + expect.assertions(2); + + server.post('/test/serveral', (req, res) => { + writeData(req.body, res); + }); + + const r1 = request(prefix('/test/serveral'), { + method: 'post', + data: { + r1: 'r1', + }, + }); + + const r2 = request('/test/rpc', { + type: 'rpc', + method: 'rpc', + parseResponse: false, + }); + + return Promise.all([r1, r2]).then(([ret1, ret2]) => { + expect(ret1.r1).toBe('r1'); + expect(ret2.data).toBe('rpc response'); + done(); + }); + }); + }); +}); diff --git a/test/onion/onion.test.js b/test/onion/onion.test.js new file mode 100644 index 0000000..749bfcc --- /dev/null +++ b/test/onion/onion.test.js @@ -0,0 +1,53 @@ +import createTestServer from 'create-test-server'; +import { Onion } from '../../src/index'; + +describe('Onion', () => { + it('test constructor', async () => { + expect.assertions(1); + try { + const onion = new Onion(); + onion.use(async () => {}); + } catch (error) { + expect(error.message).toBe('Default middlewares must be an array!'); + } + }); + it('middleware should be function', async () => { + expect.assertions(1); + try { + const onion = new Onion([]); + onion.use('not a function'); + onion.execute(); + } catch (error) { + expect(error.message).toBe('Middleware must be componsed of function'); + } + }); + + it('multiple next should not be call in a middleware', async () => { + expect.assertions(1); + try { + const onion = new Onion([]); + onion.use(async (ctx, next) => { + await next(); + await next(); + }); + await onion.execute(); + } catch (error) { + expect(error.message).toBe('next() should not be called multiple times in one middleware!'); + } + }); + it('test middleware of throw error', async () => { + expect.assertions(1); + try { + const onion = new Onion([]); + onion.use(async (ctx, next) => { + await next(); + }); + onion.use(async () => { + throw new Error('error in middleware'); + }); + await onion.execute(); + } catch (error) { + expect(error.message).toBe('error in middleware'); + } + }); +}); diff --git a/test/timeout.test.js b/test/timeout.test.js new file mode 100644 index 0000000..9e3df5e --- /dev/null +++ b/test/timeout.test.js @@ -0,0 +1,56 @@ +import createTestServer from 'create-test-server'; +import request, { extend, Onion, fetch } from '../src/index'; + +var Cancel = request.Cancel; +var CancelToken = request.CancelToken; + +const writeData = (data, res) => { + res.setHeader('access-control-allow-origin', '*'); + res.send(data); +}; + +describe('timeout', () => { + let server; + beforeAll(async () => { + server = await createTestServer(); + }); + afterAll(() => { + server.close(); + }); + + const prefix = api => `${server.url}${api}`; + + it('should be ok when response in time ', async done => { + expect.assertions(1); + server.get('/test/timeout', (req, res) => { + setTimeout(() => { + writeData('ok', res); + }, 1000); + }); + + // receive response before timeout + let response = await request(prefix('/test/timeout'), { + timeout: 1200, + getResponse: true, + }); + expect(response.response.ok).toBe(true); + done(); + }); + + it('should throw Request Error when timeout', async done => { + expect.assertions(1); + server.get('/test/timeout', (req, res) => { + setTimeout(() => { + writeData('ok', res); + }, 1000); + }); + + // receive after timeout + try { + response = await request(prefix('/test/timeout'), { timeout: 800 }); + } catch (error) { + expect(error.name).toBe('RequestError'); + done(); + } + }); +}); diff --git a/test/util.test.js b/test/util.test.js new file mode 100644 index 0000000..177627e --- /dev/null +++ b/test/util.test.js @@ -0,0 +1,52 @@ +import { MapCache } from '../src/utils'; + +// 测试工具函数 +describe('test utils:', () => { + describe('test cache', () => { + it('test timeout', async done => { + const mapCache = new MapCache({ maxCache: 3 }); + + // 设置读取 + const key = { some: 'one' }; + mapCache.set(key, { hello: 'world1' }, 1000); + expect(mapCache.get(key).hello).toBe('world1'); + setTimeout(() => { + expect(mapCache.get(key)).toBe(undefined); + done(); + }, 1001); + }); + + it('test delete', () => { + const mapCache = new MapCache({ maxCache: 3 }); + // 删除 + const key2 = { other: 'two' }; + mapCache.set(key2, { hello: 'world1' }, 10000); + mapCache.delete(key2); + expect(mapCache.get(key2)).toBe(undefined); + }); + + it('test clear', () => { + const mapCache = new MapCache({ maxCache: 3 }); + // 清除 + const key3 = { other: 'three' }; + mapCache.set(key3, { hello: 'world1' }, 10000); + mapCache.clear(); + expect(mapCache.get(key3)).toBe(undefined); + }); + + it('test max cache', () => { + const mapCache = new MapCache({ maxCache: 3 }); + + // 测试超过最大数 + mapCache.set('max1', { what: 'ok' }, 10000); + mapCache.set('max1', { what: 'ok1' }, 10000); + mapCache.set('max2', { what: 'ok2' }, 10000); + mapCache.set('max3', { what: 'ok3' }, 10000); + expect(mapCache.get('max1').what).toBe('ok1'); + mapCache.set('max4', { what: 'ok4' }, 10000); + expect(mapCache.get('max1')).toBe(undefined); + mapCache.set('max5', { what: 'ok5' }); + mapCache.set('max6', { what: 'ok6' }, 0); + }); + }); +}); diff --git a/types/index.d.ts b/types/index.d.ts index 52d20b4..fa4664e 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -31,6 +31,9 @@ export interface RequestOptionsInit extends RequestInit { errorHandler?: (error: ResponseError) => void; prefix?: string; suffix?: string; + throwErrIfParseFail?: boolean; + parseResponse?: boolean; + cancelToken?: CancelToken; } export interface RequestOptionsWithoutResponse extends RequestOptionsInit { @@ -62,8 +65,6 @@ export interface Context { res: any; } -// use async ()=> Response equal ()=> Response - export type ResponseInterceptor = (response: Response, options: RequestOptionsInit) => Response | Promise; export type OnionMiddleware = (ctx: Context, next: () => void) => void; @@ -86,7 +87,11 @@ export interface RequestMethod { use: (handler: ResponseInterceptor) => void; }; }; - use: (handler: OnionMiddleware) => void; + use: (handler: OnionMiddleware, index?: number) => void; + fetchIndex: number; + Cancel: CancelStatic; + CancelToken: CancelTokenStatic; + isCancel(value: any): boolean; } export interface ExtendOnlyOptions { @@ -107,6 +112,34 @@ export interface Extend { export declare var extend: Extend; +export interface CancelStatic { + new (message?: string): Cancel; +} + +export interface Cancel { + message: string; +} + +export interface Canceler { + (message?: string): void; +} + +export interface CancelTokenStatic { + new (executor: (cancel: Canceler) => void): CancelToken; + source(): CancelTokenSource; +} + +export interface CancelToken { + promise: Promise; + reason?: Cancel; + throwIfRequested(): void; +} + +export interface CancelTokenSource { + token: CancelToken; + cancel: Canceler; +} + declare var request: RequestMethod; export default request; From 9ac67504c31d706acacb8562ea66489efde7fc0d Mon Sep 17 00:00:00 2001 From: chenjsh Date: Thu, 22 Aug 2019 11:03:54 +0800 Subject: [PATCH 37/94] 1.2.3 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c67a672..95d1358 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.2", + "version": "1.2.3", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From 4d12e4749bfc2f284e9224b078a7e79e67b5f62c Mon Sep 17 00:00:00 2001 From: chenjsh Date: Thu, 29 Aug 2019 14:38:57 +0800 Subject: [PATCH 38/94] Feat/support node http (#65) * feat: support node fetch * feat: add node request test cases and update doc --- README.md | 21 ++++++++-- README_zh-CN.md | 19 ++++++++- package.json | 4 +- src/middleware/fetch.js | 25 +++++++----- src/middleware/parseResponse.js | 6 ++- src/middleware/simplePost.js | 1 - src/utils.js | 14 +++++++ test/index.test.js | 69 +++++++++++++++++++++++---------- test/middleware.test.js | 4 +- test/onion/onion.test.js | 1 - test/util.test.js | 9 ++++- 11 files changed, 129 insertions(+), 44 deletions(-) diff --git a/README.md b/README.md index 4c478e5..178c0f6 100644 --- a/README.md +++ b/README.md @@ -22,6 +22,7 @@ The network request library, based on fetch encapsulation, combines the features - unified error handling - middleware support - cancel request support like axios +- make http request from node.js ## umi-request vs fetch vs axios @@ -69,7 +70,7 @@ For more discussion, refer to [Traditional Ajax is dead, Fetch eternal life](htt | responseType | How to parse the returned data | string | json , text , blob , formData ... | json , text | | getResponse | Whether to get the source response, the result will wrap a layer | boolean | -- | fasle | | timeout | timeout, default millisecond, write with caution | number | -- | -- | -| useCache | Whether to use caching | boolean | -- | false | +| useCache | Whether to use caching (only support browser environment) | boolean | -- | false | | ttl | Cache duration, 0 is not expired | number | -- | 60000 | | prefix | prefix, generally used to override the uniform settings prefix | string | -- | -- | | suffix | suffix, such as some scenes api need to be unified .json | string | -- | @@ -79,8 +80,6 @@ For more discussion, refer to [Traditional Ajax is dead, Fetch eternal life](htt | parseResponse | response processing simplification | boolean | -- | true | | throwErrIfParseFail | throw error when JSON parse fail and responseType is 'json' | boolean | -- | false | | cancelToken | Token to cancel request | CancelToken.token | -- | -- | -| type | request type,type 'normal' would use fetch | string | -- | normal | - The other parameters of fetch are valid. See [fetch documentation](https://github.github.io/fetch/) @@ -202,6 +201,22 @@ request.use(async (ctx, next) => { ``` +## node request +```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); + }); +``` + + + ## Error handling ```javascript diff --git a/README_zh-CN.md b/README_zh-CN.md index 4e3310e..762e097 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -26,6 +26,7 @@ - 统一的错误处理方式 - 类 koa 洋葱机制的 use 中间件机制支持 - 类 axios 的取消请求 +- 支持在 node 环境发送 http 请求 ## 与 fetch, axios 异同 @@ -73,7 +74,7 @@ | responseType | 如何解析返回的数据 | string | json , text , blob , formData ... | json , text | | getResponse | 是否获取源response, 返回结果将包裹一层 | boolean | -- | fasle | | timeout | 超时时长, 默认毫秒, 写操作慎用 | number | -- | -- | -| useCache | 是否使用缓存 | boolean | -- | false | +| useCache | 是否使用缓存(仅支持浏览器客户端) | boolean | -- | false | | ttl | 缓存时长, 0 为不过期 | number | -- | 60000 | | prefix | 前缀, 一般用于覆盖统一设置的prefix | string | -- | -- | | suffix | 后缀, 比如某些场景 api 需要统一加 .json | string | -- | -- | @@ -83,7 +84,7 @@ | parseResponse | 是否对 response 做处理简化 | boolean | -- | true | | throwErrIfParseFail | 当 responseType 为 'json', 对请求结果做 JSON.parse 出错时是否抛出异常 | boolean | -- | false | | cancelToken | 取消请求的 Token | CancelToken.token | -- | -- | -| type | 请求类型,normal 为 fetch | string | -- | normal | +| type | 请求类型,normal 为 fetch | string | -- | normal | fetch原其他参数有效, 详见[fetch文档](https://github.github.io/fetch/) @@ -206,6 +207,20 @@ request.use(async (ctx, next) => { }) ``` +## node 环境 +```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); + }); +``` + ## 错误处理 ```javascript diff --git a/package.json b/package.json index 95d1358..f7b8cb9 100644 --- a/package.json +++ b/package.json @@ -42,8 +42,8 @@ "umi-test": "^1.4.0" }, "dependencies": { - "query-string": "^6.0.0", - "whatwg-fetch": "^2.0.0" + "isomorphic-fetch": "^2.2.1", + "query-string": "^6.0.0" }, "files": [ "dist/", diff --git a/src/middleware/fetch.js b/src/middleware/fetch.js index af9551c..25cb5cf 100644 --- a/src/middleware/fetch.js +++ b/src/middleware/fetch.js @@ -1,5 +1,5 @@ -import 'whatwg-fetch'; -import { timeout2Throw, cancel2Throw } from '../utils'; +import 'isomorphic-fetch'; +import { timeout2Throw, cancel2Throw, getEnv } from '../utils'; export default function fetchMiddleware(ctx, next) { const { @@ -7,17 +7,24 @@ export default function fetchMiddleware(ctx, next) { cache, responseInterceptors, } = ctx; - const { timeout = 0, type = 'normal', useCache = false, method = 'get', params, ttl, cancelToken } = options; + const { timeout = 0, __umiRequestCoreType__ = 'normal', useCache = false, method = 'get', params, ttl } = options; - if (type !== 'normal') { + if (__umiRequestCoreType__ !== 'normal') { + console.warn( + '__umiRequestCoreType__ is a internal params that use in umi-request, change its value would affect the behavior of request! It only use when you want to extend the request core' + ); return next(); } - if (!window || !window.fetch) { - throw new Error('window or window.fetch not exist!'); + + const adapter = fetch; + + if (!adapter) { + throw new Error('Global fetch not exist!'); } // 从缓存池检查是否有缓存数据 - const needCache = method.toLowerCase() === 'get' && useCache; + const isBrowser = getEnv() === 'BROWSER'; + const needCache = method.toLowerCase() === 'get' && useCache && isBrowser; if (needCache) { let responseCache = cache.get({ url, @@ -34,9 +41,9 @@ export default function fetchMiddleware(ctx, next) { let response; // 超时处理、取消请求处理 if (timeout > 0) { - response = Promise.race([cancel2Throw(options, ctx), window.fetch(url, options), timeout2Throw(timeout)]); + response = Promise.race([cancel2Throw(options, ctx), adapter(url, options), timeout2Throw(timeout)]); } else { - response = Promise.race([cancel2Throw(options, ctx), window.fetch(url, options)]); + response = Promise.race([cancel2Throw(options, ctx), adapter(url, options)]); } // 兼容老版本 response.interceptor diff --git a/src/middleware/parseResponse.js b/src/middleware/parseResponse.js index 9aef604..3d32da5 100644 --- a/src/middleware/parseResponse.js +++ b/src/middleware/parseResponse.js @@ -1,4 +1,4 @@ -import { safeJsonParse, readerGBK, ResponseError } from '../utils'; +import { safeJsonParse, readerGBK, ResponseError, getEnv } from '../utils'; export default function parseResponseMiddleware(ctx, next) { const { res, req } = ctx; @@ -19,7 +19,9 @@ export default function parseResponseMiddleware(ctx, next) { if (!res || !res.clone) { return next(); } - const copy = res.clone(); + + // 只在浏览器环境对 response 做克隆, node 环境如果对 response 克隆会有问题:https://github.com/bitinn/node-fetch/issues/553 + const copy = getEnv() === 'BROWSER' ? res.clone() : res; copy.useCache = res.useCache || false; return next() diff --git a/src/middleware/simplePost.js b/src/middleware/simplePost.js index d87590e..d495c5c 100644 --- a/src/middleware/simplePost.js +++ b/src/middleware/simplePost.js @@ -40,7 +40,6 @@ export default function simplePostMiddleware(ctx, next) { options.body = data; } } - ctx.req.options = options; return next(); diff --git a/src/utils.js b/src/utils.js index bb255fa..7fd4096 100644 --- a/src/utils.js +++ b/src/utils.js @@ -114,3 +114,17 @@ export function cancel2Throw(opt) { } }); } + +// Check env is browser or node +export function getEnv() { + let env; + // Only Node.JS has a process variable that is of [[Class]] process + if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { + // For node use HTTP adapter + env = 'NODE'; + } + if (typeof XMLHttpRequest !== 'undefined') { + env = 'BROWSER'; + } + return env; +} diff --git a/test/index.test.js b/test/index.test.js index 468983c..fa0db89 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -1,15 +1,26 @@ import createTestServer from 'create-test-server'; import iconv from 'iconv-lite'; +import { fetch as whatwgFetch } from 'whatwg-fetch'; import request, { extend, Onion, fetch } from '../src/index'; import { MapCache } from '../src/utils'; const debug = require('debug')('afx-request:test'); - const writeData = (data, res) => { res.setHeader('access-control-allow-origin', '*'); res.send(data); }; +// 拓展浏览器请求内核 +async function browserFetchMiddleware(ctx, next) { + const { + req: { options = {}, url = '' }, + } = ctx; + const { timeout = 0, __umiRequestCoreType__ = 'browser' } = options; + const res = await whatwgFetch(url, options); + ctx.res = res; + return next(); +} + describe('test fetch:', () => { let server; @@ -50,7 +61,7 @@ describe('test fetch:', () => { }, 5000); // 测试请求类型 - it('test requestType', async () => { + it('test requestType', async done => { server.post('/test/requestType', (req, res) => { writeData(req.body, res); }); @@ -81,20 +92,23 @@ describe('test fetch:', () => { response = await request(prefix('/test/requestType'), { method: 'post', - data: 'hehe', + data: {}, }); - expect(response).toBe('hehe'); + expect(response).toEqual({}); response = await request(prefix('/test/requestType'), { method: 'post', data: 'hehe', - type: 'mini', + __umiRequestCoreType__: 'mini', }); expect(response).toBe(null); - }, 5000); + done(); + }); // 测试非 web 环境无 fetch 情况 - it('test requestType', async () => { + + it('test fetch not exist', async done => { + expect.assertions(1); server.post('/test/requestType', (req, res) => { writeData(req.body, res); }); @@ -106,10 +120,11 @@ describe('test fetch:', () => { requestType: 'json', }); } catch (error) { - expect(error.message).toBe('window or window.fetch not exist!'); + expect(error.message).toBe('Global fetch not exist!'); + window.fetch = oldFetch; + done(); } - window.fetch = oldFetch; - }, 5000); + }); // 测试返回类型 #TODO 更多类型 it('test invalid responseType', async () => { @@ -124,13 +139,13 @@ describe('test fetch:', () => { data: { a: 1 }, throwErrIfParseFail: true, }); - console.log('response:'); } catch (error) { expect(error.message).toBe('JSON.parse fail'); expect(error.data).toBe('hello world'); } }); - it('test responseType', async () => { + it('test responseType', async done => { + expect.assertions(5); server.post('/test/responseType', (req, res) => { writeData(req.body, res); }); @@ -143,21 +158,25 @@ describe('test fetch:', () => { } }); - let response = await request(prefix('/test/responseType'), { + const extendRequest = extend({}); + extendRequest.use(browserFetchMiddleware, request.fetchIndex); + + let response = await extendRequest(prefix('/test/responseType'), { method: 'post', responseType: 'json', data: { a: 11 }, }); expect(response.a).toBe(11); - response = await request(prefix('/test/responseType'), { + response = await extendRequest(prefix('/test/responseType'), { method: 'post', responseType: 'text', data: { a: 12 }, }); expect(typeof response === 'string').toBe(true); - response = await request(prefix('/test/responseType'), { + // fetch 从 whatwg-fetch 更换成 isomorphic-fetch,默认导入的是 node-fetch,responseType 不支持 formData、arrayBuffer、blob 等方法 + response = await extendRequest(prefix('/test/responseType'), { method: 'post', responseType: 'formData', data: { a: 13 }, @@ -178,8 +197,9 @@ describe('test fetch:', () => { }); } catch (error) { expect(error.message).toBe('responseType not support'); + done(); } - }, 5000); + }); // 测试拼接参数 it('test queryParams', async () => { @@ -316,19 +336,25 @@ describe('test fetch:', () => { }, 6000); // 测试字符集 gbk支持 https://yuque.antfin-inc.com/zhizheng.ck/me_and_world/rfaldm - it('test charset', async () => { + it('test charset', async done => { + expect.assertions(1); server.get('/test/charset', (req, res) => { res.setHeader('access-control-allow-origin', '*'); res.setHeader('Content-Type', 'text/html; charset=gbk'); writeData(iconv.encode('我是乱码?', 'gbk'), res); }); + // fetch 请求库更换成 isomorphic-fetch 后,默认导入为 node-fetch,response 不支持 blob,通过中间件拓展请求内核来覆盖 + const extendRequest = extend({ __umiRequestCoreType__: 'browser' }); + extendRequest.use(browserFetchMiddleware, request.fetchIndex); - const response = await request(prefix('/test/charset'), { charset: 'gbk' }); + const response = await extendRequest(prefix('/test/charset'), { charset: 'gbk' }); expect(response).toBe('我是乱码?'); - }, 6000); + done(); + }); // 测试错误处理方法 - it('test errorHandler', async () => { + it('test errorHandler', async done => { + expect.assertions(3); server.get('/test/errorHandler', (req, res) => { res.setHeader('access-control-allow-origin', '*'); res.status(401); @@ -374,8 +400,9 @@ describe('test fetch:', () => { // throw response; } catch (error) { expect(error).toBe('统一错误处理被覆盖啦'); + done(); } - }, 6000); + }); it('test prefix and suffix', async () => { server.get('/prefix/api/hello', (req, res) => { diff --git a/test/middleware.test.js b/test/middleware.test.js index 893828d..40bb4e6 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -87,7 +87,7 @@ describe('middleware', () => { }, request.fetchIndex); const data = await request('/test/rpc', { - type: 'rpc', + __umiRequestCoreType__: 'rpc', method: 'rpc', parseResponse: false, }); @@ -112,7 +112,7 @@ describe('middleware', () => { }); const r2 = request('/test/rpc', { - type: 'rpc', + __umiRequestCoreType__: 'rpc', method: 'rpc', parseResponse: false, }); diff --git a/test/onion/onion.test.js b/test/onion/onion.test.js index 749bfcc..7dd31e0 100644 --- a/test/onion/onion.test.js +++ b/test/onion/onion.test.js @@ -1,4 +1,3 @@ -import createTestServer from 'create-test-server'; import { Onion } from '../../src/index'; describe('Onion', () => { diff --git a/test/util.test.js b/test/util.test.js index 177627e..fbd584a 100644 --- a/test/util.test.js +++ b/test/util.test.js @@ -1,4 +1,4 @@ -import { MapCache } from '../src/utils'; +import { MapCache, getEnv } from '../src/utils'; // 测试工具函数 describe('test utils:', () => { @@ -49,4 +49,11 @@ describe('test utils:', () => { mapCache.set('max6', { what: 'ok6' }, 0); }); }); + describe('test getEnv', () => { + it('should return BROWSER', done => { + const env = getEnv(); + expect(env).toBe('BROWSER'); + done(); + }); + }); }); From 827b7d73e71f48af61663a96fe18d3b137e071a8 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Thu, 29 Aug 2019 14:51:31 +0800 Subject: [PATCH 39/94] 1.2.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f7b8cb9..8987aae 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.3", + "version": "1.2.4", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From 38115301cd31ee364c951dcbf42fe8d9002e794a Mon Sep 17 00:00:00 2001 From: chenjsh Date: Mon, 9 Sep 2019 15:55:21 +0800 Subject: [PATCH 40/94] feat: middleware support global type and instance type (#66) * feat: middleware support global type and instance type; update RequestOptionsInit property getResponse * fix: add devDependence 'whatwg-fetch' --- package.json | 3 +- src/core.js | 20 ++++++++---- src/index.js | 2 +- src/middleware/fetch.js | 7 ++-- src/middleware/parseResponse.js | 52 ++++++++++++++++++------------ src/middleware/simpleGet.js | 10 ++---- src/middleware/simplePost.js | 5 ++- src/onion/compose.js | 4 ++- src/onion/index.js | 57 +++++++++++++++++++++++++++++++++ src/onion/onion.js | 22 ------------- test/index.test.js | 7 ++-- test/middleware.test.js | 8 +++-- types/index.d.ts | 4 ++- 13 files changed, 127 insertions(+), 74 deletions(-) create mode 100644 src/onion/index.js delete mode 100644 src/onion/onion.js diff --git a/package.json b/package.json index 8987aae..72c18df 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,8 @@ "umi": "^2.8.15", "umi-lint": "^1.0.0-alpha.1", "umi-plugin-library": "^1.1.6", - "umi-test": "^1.4.0" + "umi-test": "^1.4.0", + "whatwg-fetch": "^3.0.0" }, "dependencies": { "isomorphic-fetch": "^2.2.1", diff --git a/src/core.js b/src/core.js index b2488cc..b4d2650 100644 --- a/src/core.js +++ b/src/core.js @@ -1,4 +1,4 @@ -import Onion from './onion/onion'; +import Onion from './onion'; import { MapCache } from './utils'; import addfixInterceptor from './interceptor/addfix'; import fetchMiddleware from './middleware/fetch'; @@ -9,17 +9,25 @@ import simpleGet from './middleware/simpleGet'; // 旧版拦截器为共享 const requestInterceptors = [addfixInterceptor]; const responseInterceptors = []; -const defaultMiddlewares = [simplePost, simpleGet, fetchMiddleware, parseResponseMiddleware]; + +// 初始化全局和内核中间件 +const globalMiddlewares = [simplePost, simpleGet, parseResponseMiddleware]; +const coreMiddlewares = [fetchMiddleware]; + +Onion.globalMiddlewares = globalMiddlewares; +Onion.defaultGlobalMiddlewaresLength = globalMiddlewares.length; +Onion.coreMiddlewares = coreMiddlewares; +Onion.defaultCoreMiddlewaresLength = coreMiddlewares.length; class Core { constructor(initOptions) { - this.onion = new Onion(defaultMiddlewares); - this.fetchIndex = 2; // 请求中间件位置 + this.onion = new Onion([]); + this.fetchIndex = 0; // 【即将废弃】请求中间件位置 this.mapCache = new MapCache(initOptions); } - use(newMiddleware, index = 0) { - this.onion.use(newMiddleware, index); + use(newMiddleware, opt = { global: false, core: false }) { + this.onion.use(newMiddleware, opt); return this; } diff --git a/src/index.js b/src/index.js index 7b3a6b6..91661cc 100644 --- a/src/index.js +++ b/src/index.js @@ -1,5 +1,5 @@ import request, { extend, fetch } from './request'; -import Onion from './onion/onion'; +import Onion from './onion'; import { RequestError, ResponseError } from './utils'; export { extend, RequestError, ResponseError, Onion, fetch }; diff --git a/src/middleware/fetch.js b/src/middleware/fetch.js index 25cb5cf..bb9c456 100644 --- a/src/middleware/fetch.js +++ b/src/middleware/fetch.js @@ -2,11 +2,8 @@ import 'isomorphic-fetch'; import { timeout2Throw, cancel2Throw, getEnv } from '../utils'; export default function fetchMiddleware(ctx, next) { - const { - req: { options = {}, url = '' }, - cache, - responseInterceptors, - } = ctx; + if (!ctx) return next(); + const { req: { options = {}, url = '' } = {}, cache, responseInterceptors } = ctx; const { timeout = 0, __umiRequestCoreType__ = 'normal', useCache = false, method = 'get', params, ttl } = options; if (__umiRequestCoreType__ !== 'normal') { diff --git a/src/middleware/parseResponse.js b/src/middleware/parseResponse.js index 3d32da5..8e74d68 100644 --- a/src/middleware/parseResponse.js +++ b/src/middleware/parseResponse.js @@ -1,31 +1,34 @@ import { safeJsonParse, readerGBK, ResponseError, getEnv } from '../utils'; export default function parseResponseMiddleware(ctx, next) { - const { res, req } = ctx; - const { - options: { - responseType = 'json', - charset = 'utf8', - getResponse = false, - throwErrIfParseFail = false, - parseResponse = true, - }, - } = req || {}; + let copy; - if (!parseResponse) { - return next(); - } + return next() + .then(() => { + if (!ctx) return; + const { res = {}, req = {} } = ctx; + const { + options: { + responseType = 'json', + charset = 'utf8', + getResponse = false, + throwErrIfParseFail = false, + parseResponse = true, + } = {}, + } = req || {}; + + if (!parseResponse) { + return; + } - if (!res || !res.clone) { - return next(); - } + if (!res || !res.clone) { + return; + } - // 只在浏览器环境对 response 做克隆, node 环境如果对 response 克隆会有问题:https://github.com/bitinn/node-fetch/issues/553 - const copy = getEnv() === 'BROWSER' ? res.clone() : res; - copy.useCache = res.useCache || false; + // 只在浏览器环境对 response 做克隆, node 环境如果对 response 克隆会有问题:https://github.com/bitinn/node-fetch/issues/553 + copy = getEnv() === 'BROWSER' ? res.clone() : res; + copy.useCache = res.useCache || false; - return next() - .then(() => { // 解析数据 if (charset === 'gbk') { try { @@ -47,6 +50,13 @@ export default function parseResponseMiddleware(ctx, next) { } }) .then(body => { + if (!ctx) return; + const { res = {}, req = {} } = ctx; + const { options: { getResponse = false } = {} } = req || {}; + + if (!copy) { + return; + } if (copy.status >= 200 && copy.status < 300) { // 提供源response, 以便自定义处理 if (getResponse) { diff --git a/src/middleware/simpleGet.js b/src/middleware/simpleGet.js index c08a356..bc2fa07 100644 --- a/src/middleware/simpleGet.js +++ b/src/middleware/simpleGet.js @@ -2,13 +2,9 @@ import { stringify } from 'query-string'; // 对请求参数做处理,实现 query 简化、 post 简化 export default function simpleGetMiddleware(ctx, next) { - const { - req: { options = {} }, - } = ctx; - let { - req: { url = '' }, - } = ctx; - + if (!ctx) return next(); + const { req: { options = {} } = {} } = ctx; + let { req: { url = '' } = {} } = ctx; // 将 method 改为大写 options.method = options.method ? options.method.toUpperCase() : 'GET'; diff --git a/src/middleware/simplePost.js b/src/middleware/simplePost.js index d495c5c..fdb691b 100644 --- a/src/middleware/simplePost.js +++ b/src/middleware/simplePost.js @@ -2,9 +2,8 @@ import { stringify } from 'query-string'; // 对请求参数做处理,实现 query 简化、 post 简化 export default function simplePostMiddleware(ctx, next) { - const { - req: { options = {} }, - } = ctx; + if (!ctx) return next(); + const { req: { options = {} } = {} } = ctx; const { method = 'get' } = options; if (['post', 'put', 'patch', 'delete'].indexOf(method.toLowerCase()) === -1) { diff --git a/src/onion/compose.js b/src/onion/compose.js index 532e5e4..654b251 100644 --- a/src/onion/compose.js +++ b/src/onion/compose.js @@ -5,7 +5,9 @@ export default function compose(middlewares) { const middlewaresLen = middlewares.length; for (let i = 0; i < middlewaresLen; i++) { - if (typeof middlewares[i] !== 'function') throw new TypeError('Middleware must be componsed of function'); + if (typeof middlewares[i] !== 'function') { + throw new TypeError('Middleware must be componsed of function'); + } } return function wrapMiddlewares(params, next) { diff --git a/src/onion/index.js b/src/onion/index.js new file mode 100644 index 0000000..e5e59a9 --- /dev/null +++ b/src/onion/index.js @@ -0,0 +1,57 @@ +// 参考自 puck-core 请求库的插件机制 +import compose from './compose'; + +class Onion { + constructor(defaultMiddlewares) { + if (!Array.isArray(defaultMiddlewares)) throw new TypeError('Default middlewares must be an array!'); + + this.middlewares = [...defaultMiddlewares]; + } + + static globalMiddlewares = []; // 全局中间件 + static defaultGlobalMiddlewaresLength = 0; // 内置全局中间件长度 + static coreMiddlewares = []; // 内核中间件 + static defaultCoreMiddlewaresLength = 0; // 内置内核中间件长度 + + use(newMiddleware, opts = { global: false, core: false }) { + let core = false; + let global = false; + if (typeof opts === 'number') { + console.warn( + 'use() options should be object, number property would be deprecated in future,please update use() options to "{ core: true }".' + ); + core = true; + global = false; + } else if (typeof opts === 'object' && opts) { + // index = opts.index || 0; + global = opts.global || false; + core = opts.core || false; + } + + // 全局中间件 + if (global) { + // Onion.globalMiddlewares.push(newMiddleware); + Onion.globalMiddlewares.splice( + Onion.globalMiddlewares.length - Onion.defaultGlobalMiddlewaresLength, + 0, + newMiddleware + ); + return; + } + // 内核中间件 + if (core) { + Onion.coreMiddlewares.splice(Onion.coreMiddlewares.length - Onion.defaultCoreMiddlewaresLength, 0, newMiddleware); + return; + } + + // 实例中间件 + this.middlewares.push(newMiddleware); + } + + execute(params = null) { + const fn = compose([...this.middlewares, ...Onion.globalMiddlewares, ...Onion.coreMiddlewares]); + return fn(params); + } +} + +export default Onion; diff --git a/src/onion/onion.js b/src/onion/onion.js deleted file mode 100644 index 1797fe0..0000000 --- a/src/onion/onion.js +++ /dev/null @@ -1,22 +0,0 @@ -// 参考自 puck-core 请求库的插件机制 -import compose from './compose'; - -class Onion { - constructor(defaultMiddlewares) { - if (!Array.isArray(defaultMiddlewares)) throw new TypeError('Default middlewares must be an array!'); - - this.middlewares = [...defaultMiddlewares]; - this.defaultMiddlewaresLen = defaultMiddlewares.length; - } - - use(newMiddleware, index = 0) { - this.middlewares.splice(this.middlewares.length - this.defaultMiddlewaresLen + index, 0, newMiddleware); - } - - execute(params = null) { - const fn = compose(this.middlewares); - return fn(params); - } -} - -export default Onion; diff --git a/test/index.test.js b/test/index.test.js index fa0db89..dc43e15 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -15,7 +15,8 @@ async function browserFetchMiddleware(ctx, next) { const { req: { options = {}, url = '' }, } = ctx; - const { timeout = 0, __umiRequestCoreType__ = 'browser' } = options; + const { timeout = 0, __umiRequestCoreType__ = 'normal' } = options; + if (__umiRequestCoreType__ !== 'browser') return next(); const res = await whatwgFetch(url, options); ctx.res = res; return next(); @@ -159,7 +160,7 @@ describe('test fetch:', () => { }); const extendRequest = extend({}); - extendRequest.use(browserFetchMiddleware, request.fetchIndex); + extendRequest.use(browserFetchMiddleware, { core: true }); let response = await extendRequest(prefix('/test/responseType'), { method: 'post', @@ -345,7 +346,7 @@ describe('test fetch:', () => { }); // fetch 请求库更换成 isomorphic-fetch 后,默认导入为 node-fetch,response 不支持 blob,通过中间件拓展请求内核来覆盖 const extendRequest = extend({ __umiRequestCoreType__: 'browser' }); - extendRequest.use(browserFetchMiddleware, request.fetchIndex); + // extendRequest.use(browserFetchMiddleware, request.fetchIndex); const response = await extendRequest(prefix('/test/charset'), { charset: 'gbk' }); expect(response).toBe('我是乱码?'); diff --git a/test/middleware.test.js b/test/middleware.test.js index 40bb4e6..5b21f4c 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -72,18 +72,20 @@ describe('middleware', () => { server.post('/test/rpc', (req, res) => { writeData(req.body, res); }); - request.use((ctx, next) => { + request.use(async (ctx, next) => { const { req } = ctx; const { url, options } = req; const { method } = options; if (method.toLowerCase() !== 'rpc') { - return next(); + await next(); + return; } ctx.res = { success: true, data: 'rpc response', }; - return next(); + await next(); + return; }, request.fetchIndex); const data = await request('/test/rpc', { diff --git a/types/index.d.ts b/types/index.d.ts index fa4664e..be3616a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -34,6 +34,7 @@ export interface RequestOptionsInit extends RequestInit { throwErrIfParseFail?: boolean; parseResponse?: boolean; cancelToken?: CancelToken; + getResponse?: boolean; } export interface RequestOptionsWithoutResponse extends RequestOptionsInit { @@ -68,6 +69,7 @@ export interface Context { export type ResponseInterceptor = (response: Response, options: RequestOptionsInit) => Response | Promise; export type OnionMiddleware = (ctx: Context, next: () => void) => void; +export type OnionOptions = { global?: boolean; core?: boolean }; export interface RequestMethod { (url: string, options: RequestOptionsWithResponse): Promise>; @@ -87,7 +89,7 @@ export interface RequestMethod { use: (handler: ResponseInterceptor) => void; }; }; - use: (handler: OnionMiddleware, index?: number) => void; + use: (handler: OnionMiddleware, options?: OnionOptions) => void; fetchIndex: number; Cancel: CancelStatic; CancelToken: CancelTokenStatic; From d01dada313d9cd9f9a37024d2e9df3a04276fdc9 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Mon, 9 Sep 2019 16:16:23 +0800 Subject: [PATCH 41/94] 1.2.5 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 72c18df..34432d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.4", + "version": "1.2.5", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From a95c802fb037e7b82e65730ea9652c2727a67afe Mon Sep 17 00:00:00 2001 From: chenjsh Date: Mon, 23 Sep 2019 15:02:20 +0800 Subject: [PATCH 42/94] feat: add request and response in error object; update readme doc (#67) --- README.md | 710 +++++++++++++++++++++-------- README_zh-CN.md | 690 ++++++++++++++++++++-------- src/middleware/fetch.js | 10 +- src/middleware/parseResponse.js | 10 +- src/onion/index.js | 10 +- src/request.js | 2 +- src/utils.js | 14 +- test/fetch.test.js | 15 + test/index.test.js | 31 ++ test/middleware/simpleGet.test.js | 38 ++ test/middleware/simplePost.test.js | 30 ++ test/onion/onion.test.js | 43 ++ test/timeout.test.js | 4 +- types/index.d.ts | 6 + 14 files changed, 1216 insertions(+), 397 deletions(-) create mode 100644 test/middleware/simpleGet.test.js create mode 100644 test/middleware/simplePost.test.js diff --git a/README.md b/README.md index 178c0f6..99c985a 100644 --- a/README.md +++ b/README.md @@ -47,7 +47,6 @@ For more discussion, refer to [Traditional Ajax is dead, Fetch eternal life](htt ## TODO Welcome pr -- [ ] rpc support - [x] Test case coverage 85%+ - [x] write a document - [x] CI integration @@ -56,152 +55,131 @@ For more discussion, refer to [Traditional Ajax is dead, Fetch eternal life](htt ## Installation -`npm install umi-request --save` - -## request options +``` +npm install --save umi-request +``` -| Parameter | Description | Type | Optional Value | Default | -| :--- | :--- | :--- | :--- | :--- | -| method | request method | string | get , post , put ... | get | -| params | url request parameters | object | -- | -- | -| charset | character set | string | utf8 , gbk | utf8 | -| requestType | post request data type | string | json , form | json | -| data | Submitted data | any | -- | -- | -| responseType | How to parse the returned data | string | json , text , blob , formData ... | json , text | -| getResponse | Whether to get the source response, the result will wrap a layer | boolean | -- | fasle | -| timeout | timeout, default millisecond, write with caution | number | -- | -- | -| useCache | Whether to use caching (only support browser environment) | boolean | -- | false | -| ttl | Cache duration, 0 is not expired | number | -- | 60000 | -| prefix | prefix, generally used to override the uniform settings prefix | string | -- | -- | -| suffix | suffix, such as some scenes api need to be unified .json | string | -- | -| errorHandler | exception handling, or override unified exception handling | function(error) | -- | -| headers | fetch original parameters | object | -- | {} | -| credentials | fetch request with cookies | string | -- | credentials: 'include' | -| parseResponse | response processing simplification | boolean | -- | true | -| throwErrIfParseFail | throw error when JSON parse fail and responseType is 'json' | boolean | -- | false | -| cancelToken | Token to cancel request | CancelToken.token | -- | -- | +## Example +Performing a ```GET``` request -The other parameters of fetch are valid. See [fetch documentation](https://github.github.io/fetch/) +``` javascript +import request from 'umi-request'; -## extend options Initialize default parameters, support all of the above +request.get('/api/v1/xxx?id=1') + .then(function (response) { + console.log(response); + }) + .catch(function (error) { + console.log(error); + }); -| Parameter | Description | Type | Default | -| :--- | :--- | :--- | :--- | -| maxCache | Maximum number of caches | number | Any | -| prefix | default url prefix | string | -- | -| errorHandler | default exception handling | function(error) | -- | -| headers | default headers | object | {} | -| params | default with the query parameter | object | {} | -| ... | +// use options.params +request.get('/api/v1/xxx', { + params: { + id: 1 + } + }) + .then(function (response) { + console.log(response); + }) + .catch(function (error) { + console.log(error); + }); +``` -## Use +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); + }); +``` -> request can be used in a simple package and can be used with reference to [antd-pro](https://github.com/umijs/ant-design-pro/blob/master/src/utils/request.js) +## umi-request API +Requests can be made by passing relevant options to ```umi-request``` +**umi-request(url[, options])** ```javascript -// request is the default instance that can be used directly, extend is a configurable method, passing a series of default parameters, returning a new request instance, usage is consistent with the request. -import { extend } from 'umi-request'; +import request from 'umi-request'; -const request = extend({ -  maxCache: 10, // The maximum number of caches. When it is exceeded, it will automatically clear the first one according to the time. -  prefix: '/api/v1', // prefix -  suffix: '.json', // suffix -  errorHandler: (error) => { -    // Centralized processing error -  }, -  headers: { -    Some: 'header' // unified headers -  }, -  params: { -    Hello: 'world' // the query parameter to be included with each request -  } -}); -request('/some/api'); - -// Support syntax sugar such as: request.get request.post ... -request.post('/api/v1/some/api', { data: {foo: 'bar'}}); - -// request an api, no method parameter defaults to get -request('/api/v1/some/api').then(res => { -  console.log(res); -}).catch(err => { -  console.log(err); -}); +request('/api/v1/xxx', { + method: 'get', + params: { id: 1 } + }) + .then(function (response) { + console.log(response); + }) + .catch(function (error) { + console.log(error); + }); -// url parameter serialization -request('/api/v1/some/api', { params: {foo: 'bar'} }); +request('/api/v1/user', { + method: 'post', + data: { + name: 'Mike' + } + }) + .then(function (response) { + console.log(response); + }) + .catch(function (error) { + console.log(error); + }); -// post data submission simplification -// When data is object, the default requestType: 'json' can not write, header will automatically bring application / json -request('/api/v1/some/api', { method:'post', data: {foo: 'bar'} }); +``` -// requestType: 'form', header will automatically bring application/x-www-form-urlencoded -request('/api/v1/some/api', { method:'post', requestType: 'form', data: {foo: 'bar'} }); +## Request method aliases +For convenience umi-request have been provided for all supported methods. -// reponseType: 'blob', how to handle the returned data, by default text and json are not added. Such as blob or formData need to add -request('/api/v1/some/api', { reponseType: 'blob' }); +**request.get(url[, options])** -// Submit other data, such as text, requestType is not filled, manually add the corresponding header. -request('/api/v1/some/api', { method:'post', data: 'some data', headers: { 'Content-Type': 'multipart/form-data'} }); +**request.post(url[, options])** -// upload file -const formData = new FormData(); -formData.append('file', file); -request('/api/v1/some/api', { method:'post', body: formData, requestType: 'form' }); +**request.delete(url[, options])** -// The default is to return the data body, if you need the source response to expand, you can use the getResponse parameter. The result will be a set of layers -request('/api/v1/some/api', { getResponse: true }).then({data, response} => { -  console.log(data, response); -}); +**request.put(url[, options])** -// Timeout in milliseconds, but after the timeout, although the client returns a timeout, but the api request will not be disconnected, the write operation is used with caution. -request('/api/v1/some/api', { timeout: 3000 }); +**request.patch(url[, options])** -// Use the cache, only valid when get. Units of milliseconds, do not add ttl default 60s, ttl = 0 does not expire. cache key for url + params combination -request('/api/v1/some/api', { params: { hello: 'world' }, useCache: true, ttl: 10000 }); +**request.head(url[, options])** -// This parameter can be used when the server returns gbk to avoid garbled characters. -request('/api/v1/some/api', { charset: 'gbk' }); +**request.options(url[, options])** -// request interceptor, change url or options. -request.interceptors.request.use((url, options) => { -  return ( -    { -      url: `${url}&interceptors=yes`, -      options: { ...options, interceptors: true }, -    } -  ); -}); -// response interceptor, handling response -request.interceptors.response.use((response, options) => { -  response.headers.append('interceptors', 'yes yo'); -  return response; -}); +## Creating an instance +You can use ```extend({[options]})``` to create a new instance of umi-request. +**extend([options])** -// use middleware, handling request and response -request.use(async (ctx, next) => { - const { req } = ctx; - const { url, options } = req; - // add prefix - ctx.req.url = `/api/v1/${url}`; - ctx.req.options = { - ...options, - foo: 'foo' - }; - await next(); +``` javascript +import { extend } from 'umi-request'; - const { res } = ctx; - const { success = false } = res; - if (!success) { - // Handle fail request here +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); + }); ``` -## node request +Create an instance of umi-request in NodeJS enviroment + ```javascript const umi = require('umi-request'); const extendRequest = umi.extend({ timeout: 10000 }) @@ -215,98 +193,283 @@ extendRequest('/api/user') }); ``` +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 | -- | -- | +| 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: 'include' | +| 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 + params: { id: 1 }, + + // '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: 'include', + + // ’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' 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 'umi-request'; -/** - * Here are four ways to deal with - */ -/** - * 1. Unified processing - * Commonly used in projects where the error code is more standardized, and the error is handled centrally. - */ - -const codeMap = { -  '021': 'An error has occurred', -  '022': 'It’s a big mistake,' -  ... -}; - -const errorHandler = (error) => { -  const { response, data } = error; -  message.error(codeMap[data.errorCode]); - -  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. -} -const extendRequest = extend({ -  prefix: server.url, -  errorHandler -}); +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); + } -const response = await extendRequest('/some/api'); // will automatically handle the error, no catch. If throw needs to catch. -if (response) { -  // do something + 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'}; } -/** -* 2. Separate special treatment -* If unified processing is configured, but an api needs special handling. When requested, the errorHandler is passed as a parameter. -*/ -const response = await extendRequest('/some/api', { -  method: 'get', -  errorHandler: (error) => { -    // do something -  } -}); +// 1. Unified processing +const extendRequest = extend({ errorHandler }); -/** - * 3. not configure errorHandler, the response will be directly treated as promise, and it will be caught. - */ -try { -  const response = await request('/some/api'); -} catch (error) { -  // do something -} +// 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 }); -/** -* 4. Based on response interceptors -*/ -request.interceptors.response.use((response) => { -  const codeMaps = { -    502: 'Gateway error. ', -    503: 'The service is unavailable, the server is temporarily overloaded or maintained. ', -    504: 'The gateway timed out. ', -  }; -  message.error(codeMaps[response.status]); -  return response; -}); -/** -* 5. For the status code is actually 200 errors -*/ -request.interceptors.response.use(async (response) => { -  const data = await response.clone().json(); -  if(data && data.NOT_LOGIN) { -    location.href = 'login url'; -  } -  return response; +// 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 -request.use(fn) +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) => { @@ -328,6 +491,141 @@ 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 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 @@ -374,6 +672,44 @@ Request.get('/api/cancel', { 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 diff --git a/README_zh-CN.md b/README_zh-CN.md index 762e097..18515f2 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -26,7 +26,7 @@ - 统一的错误处理方式 - 类 koa 洋葱机制的 use 中间件机制支持 - 类 axios 的取消请求 -- 支持在 node 环境发送 http 请求 +- 支持 node 环境发送 http 请求 ## 与 fetch, axios 异同 @@ -51,7 +51,6 @@ ## TODO 欢迎pr -- [ ] rpc支持 - [x] 测试用例覆盖85%+ - [x] 写文档 - [x] CI集成 @@ -59,144 +58,481 @@ - [x] typescript ## 安装 +``` +npm install --save umi-request +``` + +## 快速上手 +执行 **GET** 请求 + +``` 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); + }); + +// 也可将 URL 的参数放到 options.params 里 +request.get('/api/v1/xxx', { + params: { + id: 1 + } + }) + .then(function (response) { + console.log(response); + }) + .catch(function (error) { + console.log(error); + }); +``` + +执行 **POST** 请求 +``` javascript +request.post('/api/v1/user', { + data: { + name: 'Mike' + } + }) + .then(function (response) { + console.log(response); + }) + .catch(function (error) { + console.log(error); + }); +``` + +## umi-request API + +可以通过向 **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); + }); + +``` + +## 请求方法的别名 +为了方便起见,为所有支持的请求方法提供了别名, ```method``` 属性不必在配置中指定 + +**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])** + + +## 创建实例 +有些通用的配置我们不想每个请求里都去添加,那么可以通过 ```extend``` 新建一个 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); + }); +``` + +NodeJS 环境创建实例 + +```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); + }); +``` + + +以下是可用的实例方法,指定的配置将与实例的配置合并。 -`npm install umi-request --save` +**request.get(url[, options])** -## request 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])** + + +umi-request 可以进行一层简单封装后再使用, 可参考 [antd-pro](https://github.com/umijs/ant-design-pro/blob/master/src/utils/request.js) + + +## 请求配置 + +### request options 参数 | 参数 | 说明 | 类型 | 可选值 | 默认值 | | :--- | :--- | :--- | :--- | :--- | | method | 请求方式 | string | get , post , put ... | get | | params | url请求参数 | object | -- | -- | -| charset | 字符集 | string | utf8 , gbk | utf8 | -| requestType | post请求时数据类型 | string | json , form | json | | data | 提交的数据 | any | -- | -- | -| responseType | 如何解析返回的数据 | string | json , text , blob , formData ... | json , text | -| getResponse | 是否获取源response, 返回结果将包裹一层 | boolean | -- | fasle | +| headers | fetch 原有参数 | object | -- | {} | | timeout | 超时时长, 默认毫秒, 写操作慎用 | number | -- | -- | -| useCache | 是否使用缓存(仅支持浏览器客户端) | boolean | -- | false | -| ttl | 缓存时长, 0 为不过期 | number | -- | 60000 | | prefix | 前缀, 一般用于覆盖统一设置的prefix | string | -- | -- | | suffix | 后缀, 比如某些场景 api 需要统一加 .json | string | -- | -- | -| errorHandler | 异常处理, 或者覆盖统一的异常处理 | function(error) | -- | -| headers | fetch 原有参数 | object | -- | {} | | credentials | fetch 请求包含 cookies 信息 | object | -- | credentials: 'include' | +| useCache | 是否使用缓存(仅支持浏览器客户端) | boolean | -- | false | +| ttl | 缓存时长, 0 为不过期 | number | -- | 60000 | +| maxCache | 最大缓存数 | number | -- | 无限 | +| requestType | post请求时数据类型 | string | json , form | json | | parseResponse | 是否对 response 做处理简化 | boolean | -- | true | -| throwErrIfParseFail | 当 responseType 为 'json', 对请求结果做 JSON.parse 出错时是否抛出异常 | boolean | -- | false | +| charset | 字符集 | string | utf8 , gbk | utf8 | +| responseType | 如何解析返回的数据 | string | json , text , blob , formData ... | json , text | +| throwErrIfParseFail | 当 responseType 为 'json', 对请求结果做 JSON.parse 出错时是否抛出异常 | boolean | -- |false | +| getResponse | 是否获取源response, 返回结果将包裹一层 | boolean | -- | fasle | +| errorHandler | 异常处理, 或者覆盖统一的异常处理 | function(error) | -- | | cancelToken | 取消请求的 Token | CancelToken.token | -- | -- | -| type | 请求类型,normal 为 fetch | string | -- | normal | fetch原其他参数有效, 详见[fetch文档](https://github.github.io/fetch/) -## extend options 初始化默认参数, 支持以上所有 +### extend options 初始化默认参数, 支持以上所有 -| 参数 | 说明 | 类型 | 默认值 | -| :--- | :--- | :--- | :--- | -| maxCache | 最大缓存数 | number | 不限 | -| prefix | 默认url前缀 | string | -- | -| errorHandler | 默认异常处理 | function(error) | -- | -| headers | 默认headers | object | {} | -| params | 默认带上的query参数 | object | {} | +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +| :--- | :--- | :--- | :--- | :--- | +| method | 请求方式 | string | get , post , put ... | get | +| params | url请求参数 | object | -- | -- | +| data | 提交的数据 | any | -- | -- | | ... | +``` javascript +{ + // 'method' 是创建请求时使用的方法 + method: 'get', // default + + // 'params' 是即将于请求一起发送的 URL 参数,参数会自动 encode 后添加到 URL 中 + params: { id: 1 }, + + // 'data' 作为请求主体被发送的数据 + // 适用于这些请求方法 'PUT', 'POST', 和 'PATCH' + // 必须是以下类型之一: + // - string, plain object, ArrayBuffer, ArrayBufferView, URLSearchParams + // - 浏览器专属:FormData, File, Blob + // - Node 专属: Stream + data: { name: 'Mike' }, + + // 'headers' 请求头 + headers: { 'Content-Type': 'multipart/form-data' }, + + // 'timeout' 指定请求超时的毫秒数(0 表示无超时时间) + // 如果请求超过了 'timeout' 时间,请求将被中断并抛出请求异常 + timeout: 1000, + + // ’prefix‘ 前缀,统一设置 url 前缀 + // ( e.g. request('/user/save', { prefix: '/api/v1' }) => request('/api/v1/user/save') ) + prefix: '', + + // ’suffix‘ 后缀,统一设置 url 后缀 + // ( e.g. request('/api/v1/user/save', { suffix: '.json'}) => request('/api/v1/user/save.json') ) + suffix: '', + + // 'credentials' 发送带凭据的请求 + // 为了让浏览器发送包含凭据的请求(即使是跨域源),需要设置 credentials: 'include' + // 如果只想在请求URL与调用脚本位于同一起源处时发送凭据,请添加credentials: 'same-origin' + // 要改为确保浏览器不在请求中包含凭据,请使用credentials: 'omit' + credentials: 'include', + + // ’useCache‘ 是否使用缓存,当值为 true 时,GET 请求在 ttl 毫秒内将被缓存,缓存策略唯一 key 为 url + params 组合 + useCache: false, // default + + // ’ttl‘ 缓存时长(毫秒), 0 为不过期 + ttl: 60000, + + // 'maxCache' 最大缓存数, 0 为无限制 + maxCache: 0, + + // 'requestType' 当 data 为对象或者数组时, umi-request 会根据 requestType 动态添加 headers 和设置 body(可传入 headers 覆盖 Accept 和 Content-Type 头部属性): + // 1. requestType === 'json' 时, (默认为 json ) + // 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. 其他 requestType + // options.headers = { + // Accept: 'application/json', + // ...options.headers, + // }; + // options.body = data; + requestType: 'json', // default + + // ’parseResponse‘ 是否对请求返回的 Response 对象做格式、状态码解析 + parseResponse: true, // default + + // ’charset‘ 当服务端返回的数据编码类型为 gbk 时可使用该参数,umi-request 会按 gbk 编码做解析,避免得到乱码, 默认为 utf8 + // 当 parseResponse 值为 false 时该参数无效 + charset: 'gbk', + + // 'responseType': 如何解析返回的数据,当 parseResponse 值为 false 时该参数无效 + // 默认为 'json', 对返回结果进行 Response.text().then( d => JSON.parse(d) ) 解析 + // 其他(text, blob, arrayBuffer, formData), 做 Response[responseType]() 解析 + responseType: 'json', // default + + // 'throwErrIfParseFail': 当 responseType 为 json 但 JSON.parse(data) fail 时,是否抛出异常。默认不抛出异常而返回 Response.text() 后的结果,如需要抛出异常,可设置 throwErrIfParseFail 为 true + throwErrIfParseFail: false, // default + + // 'getResponse': 是否获取源 Response, 返回结果将包含一层: { data, response } + getResponse: false,// default + + // 'errorHandler' 统一的异常处理,供开发者对请求发生的异常做统一处理,详细使用请参考下方的错误处理文档 + errorHandler: function(error) { /* 异常处理 */ }, + + // 'cancelToken' 取消请求的 Token,详细使用请参考下方取消请求文档 + cancelToken: null, +} +``` -## 使用 +## 响应结构 -> request 可以进行一层简单封装后再使用, 可参考 [antd-pro](https://github.com/umijs/ant-design-pro/blob/master/src/utils/request.js) +某个请求的响应返回的响应对象 Response 如下: +``` javascript +{ + // `data` 由服务器提供的响应, 需要进行解析才能获取 + data: {}, -```javascript -// request 是默认实例可直接使用, extend为可配置方法, 传入一系列默认参数, 返回一个新的request实例, 用法与request一致. -import { extend } from 'umi-request'; + // `status` 来自服务器响应的 HTTP 状态码 + status: 200, -const request = extend({ - maxCache: 10, // 最大缓存个数, 超出后会自动清掉按时间最开始的一个. - prefix: '/api/v1', // prefix - suffix: '.json', // suffix - errorHandler: (error) => { - // 集中处理错误 - }, - headers: { - some: 'header' // 统一的headers - }, - params: { - hello: 'world' // 每个请求都要带上的query参数 + // `statusText` 来自服务器响应的 HTTP 状态信息 + statusText: 'OK', + + // `headers` 服务器响应的头 + headers: {}, +} +``` + +当 options.getResponse === false 时, 响应结构为解析后的 data + +``` javascript +request.get('/api/v1/xxx', { getResponse: false }) + .then(function(data) { + console.log(data); + }) +``` + +当 options.getResponse === true 时,响应结构为包含 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); + }) + +``` + +在使用 catch 或者 errorHandler, 响应对象可以通过 ```error``` 对象获取使用,参考**错误处理**这一节文档。 + + +## 错误处理 + +``` javascript +import request, { extend } from 'umi-request'; + +const errorHandler = function (error) { + const codeMap = { + '021': '发生错误啦', + '022': '发生大大大大错误啦', + // .... + }; + if (error.response) { + // 请求已发送但服务端返回状态码非 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 { + // 请求初始化时出错或者没有响应返回的异常 + console.log(error.message); } -}); -request('/some/api'); -// 支持语法糖 如: request.get request.post ... -request.post('/api/v1/some/api', { data: {foo: 'bar'}}); + throw error; // 如果throw. 错误将继续抛出. + + // 如果return, 则将值作为返回. 'return;' 相当于return undefined, 在处理结果时判断response是否有值即可. + // return {some: 'data'}; +} -// 请求一个api, 没有method参数默认为get -request('/api/v1/some/api').then(res => { - console.log(res); -}).catch(err => { - console.log(err); -}); +// 1. 作为统一错误处理 +const extendRequest = extend({ errorHandler }); -// url参数序列化 -request('/api/v1/some/api', { params: {foo: 'bar'} }); +// 2. 单独特殊处理, 如果配置了统一处理, 但某个api需要特殊处理. 则在请求时, 将errorHandler作为参数传入. +request('/api/v1/xxx', { errorHandler }); -// post 数据提交简化 -// 当data为object时, 默认requestType: 'json'可不写, header会自动带上 application/json -request('/api/v1/some/api', { method:'post', data: {foo: 'bar'} }); -// requestType: 'form', header会自动带上 application/x-www-form-urlencoded -request('/api/v1/some/api', { method:'post', requestType: 'form', data: {foo: 'bar'} }); +// 3. 通过 Promise.catch 做错误处理 +request('/api/v1/xxx') +.then(function (response) { + console.log(response); +}) +.catch(function (error) { + return errorHandler(error); +}) -// reponseType: 'blob', 如何处理返回的数据, 默认情况下 text 和 json 都不用加. 如blob 或 formData 之类需要加 -request('/api/v1/some/api', { reponseType: 'blob' }); +``` -// 提交其他数据, requestType不填, 手动添加对应header. -request('/api/v1/some/api', { method:'post', data: 'some data', headers: { 'Content-Type': 'multipart/form-data'} }); -// 文件上传, 不要自己设置 Content-Type ! -const formData = new FormData(); -formData.append('file', file); -request('/api/v1/some/api', { method:'post', data: formData }); +## 中间件 +类 koa 的洋葱机制,让开发者优雅地做请求前后的增强处理,支持创建实例、全局、内核中间件。 -// 默认返回的就是数据体, 如果需要源response来扩展, 可用getResponse参数. 返回结果会多套一层 -request('/api/v1/some/api', { getResponse: true }).then({data, response} => { - console.log(data, response); -}); +**实例中间件(默认)** :request.use(fn) 不同实例创建的中间件相互独立不影响; -// 超时 单位毫秒, 但是超时后客户端虽然返回超时, 但api请求不会断开, 写操作慎用. -request('/api/v1/some/api', { timeout: 3000 }); +**全局中间件** : request.use(fn, { global: true }) 全局中间件,不同实例共享全局中间件; -// 使用缓存, 只有get时有效. 单位毫秒, 不加ttl默认60s, ttl=0不过期. cache key为url+params组合 -request('/api/v1/some/api', { params: { hello: 'world' }, useCache: true, ttl: 10000 }); +**内核中间件** :request.use(fn, { core: true }) 内核中间件, 方便开发者拓展请求内核; -// 当服务端返回的是gbk时可用这个参数, 避免得到乱码 -request('/api/v1/some/api', { charset: 'gbk' }); -// request拦截器, 改变url 或 options. -request.interceptors.request.use((url, options) => { - return ( - { - url: `${url}&interceptors=yes`, - options: { ...options, interceptors: true }, - } - ); -}); +request.use(fn[, options]) -// response拦截器, 处理response -request.interceptors.response.use((response, options) => { - response.headers.append('interceptors', 'yes yo'); - return response; -}); +### 参数 +fn 入参 +* ctx(Object):上下文对象,包括req和res对象 +* next(Function):调用下一个中间件的函数 + +options 参数 +* global(boolean): 是否为全局中间件,优先级比 core 高 +* core(boolean): 是否为内核中间件 + +### 例子 +1. 同类型中间件执行顺序 +``` 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'); +``` + +执行顺序如下: +``` +a1 -> b1 -> response -> b2 -> a2 +``` -// 中间件,对请求前、响应后做处理 +2. 不同类型中间件执行顺序 +``` 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 }) +``` + +执行顺序如下: +``` +instanceA1 -> instanceB1 -> globalA1 -> coreA1 -> coreA2 -> globalA2 -> instanceB2 -> instanceA2 +``` + +3. 使用中间件对请求前后做处理 +``` javascript request.use(async (ctx, next) => { const { req } = ctx; const { url, options } = req; - // 添加前缀、后缀 - ctx.req.url = `/api/v1/${url}`; + + // 判断是否需要添加前缀,如果是统一添加可通过 prefix、suffix 参数配置 + if ( url.indexOf('/api') !== 0 ) { + ctx.req.url = `/api/v1/${url}`; + } ctx.req.options = { ...options, foo: 'foo' }; + await next(); const { res } = ctx; @@ -205,81 +541,71 @@ request.use(async (ctx, next) => { // 对异常情况做对应处理 } }) + ``` -## node 环境 -```javascript -const umi = require('umi-request'); -const extendRequest = umi.extend({ timeout: 10000 }) +4. 使用内核中间件拓展请求能力 +``` javascript -extendRequest('/api/user') - .then(res => { - console.log(res); - }) - .catch(err => { - console.log(err); - }); -``` +request.use(async (ctx, next) => { + const { req } = ctx; + const { url, options } = req; + const { __umiRequestCoreType__ = 'normal' } = options; + + // __umiRequestCoreType__ 用于区分请求内核类型 + // 值为 'normal' 使用 umi-request 内置的请求内核 + if ( __umiRequestCoreType__ === 'normal') { + await next(); + return; + } -## 错误处理 + // 非 normal 使用自定义请求内核获取响应数据 + const response = getResponseByOtherWay(); -```javascript -import request, { extend } from 'umi-request'; -/** - * 这里介绍四种处理方式 - */ -/** - * 1. 统一处理 - * 常用于错误码较规范的项目中, 集中处理错误. - */ - -const codeMap = { - '021': '发生错误啦', - '022': '发生大大大大错误啦', - ... -}; - -const errorHandler = (error) => { - const { response, data } = error; - message.error(codeMap[data.errorCode]); + // 将响应数据写入 ctx 中 + ctx.res = response; - throw error; // 如果throw. 错误将继续抛出. - // return {some: 'data'}; 如果return, 将值作为返回. 不写则相当于return undefined, 在处理结果时判断response是否有值即可. -} + await next(); + return; +}, { core: true }); -const extendRequest = extend({ - prefix: server.url, - errorHandler -}); -const response = await extendRequest('/some/api'); // 将自动处理错误, 不用catch. 如果throw了需要catch. -if (response) { - // do something -} +// 使用自定义请求内核 +request('/api/v1/rpc', { + __umiRequestCoreType__: 'rpc', + parseResponse: false, +}) + .then(function (response) { + console.log(response); + }) + .catch(function (error) { + console.log(error); + }) -/** -* 2. 单独特殊处理 -* 如果配置了统一处理, 但某个api需要特殊处理. 则在请求时, 将errorHandler作为参数传入. -*/ -const response = await extendRequest('/some/api', { - method: 'get', - errorHandler: (error) => { - // do something - } +``` + + +## 拦截器 +在请求或响应被 ```then``` 或 ```catch``` 处理前拦截它们。 + +``` javascript +// request拦截器, 改变url 或 options. +request.interceptors.request.use((url, options) => { + return ( + { + url: `${url}&interceptors=yes`, + options: { ...options, interceptors: true }, + } + ); }); -/** - * 3. 不配置 errorHandler, 将reponse直接当promise处理, 自己catch. - */ -try { - const response = await request('/some/api'); -} catch (error) { - // do something -} +// response拦截器, 处理response +request.interceptors.response.use((response, options) => { + response.headers.append('interceptors', 'yes yo'); + return response; +}); -/** -* 4. 基于response interceptors -*/ +// 提前对响应做异常处理 request.interceptors.response.use((response) => { const codeMaps = { 502: '网关错误。', @@ -290,9 +616,7 @@ request.interceptors.response.use((response) => { return response; }); -/** -* 5. 对于状态码实际是 200 的错误 -*/ +// 克隆响应对象做解析处理 request.interceptors.response.use(async (response) => { const data = await response.clone().json(); if(data && data.NOT_LOGIN) { @@ -300,37 +624,6 @@ request.interceptors.response.use(async (response) => { } return response; }) - -``` - - -## 中间件 -request.use(fn) - -### 参数 -* ctx(Object):上下文对象,包括req和res对象 -* next(Function):调用下一个中间件的函数 - -### 例子 -``` 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'); -``` - -执行顺序如下: -``` -a1 -> b1 -> response -> b2 -> a2 ``` ## 取消请求 @@ -381,6 +674,27 @@ Request.get('/api/cancel', { cancel(); ``` +## 案例 +### 如何获取响应头信息 + +通过 **Headers.get()** 获取响应头信息。(可参考 [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'); +}) +``` + +### 文件上传 +使用 FormData() 构造函数时,浏览器会自动识别并添加请求头 ```"Content-Type: multipart/form-data"```, 且参数依旧是表单提交时那种键值对,因此不需要开发者手动设置 **Content-Type** +``` javascript +const formData = new FormData(); +formData.append('file', file); +request('/api/v1/some/api', { method:'post', data: formData }); +``` + +如果希望获取自定义头部信息,需要在服务器设置 [Access-Control-Expose-Headers](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Expose-Headers),然后可按照上述方式获取自定义头部信息。 + ## 开发和调试 - npm install diff --git a/src/middleware/fetch.js b/src/middleware/fetch.js index bb9c456..d534b07 100644 --- a/src/middleware/fetch.js +++ b/src/middleware/fetch.js @@ -7,9 +7,11 @@ export default function fetchMiddleware(ctx, next) { const { timeout = 0, __umiRequestCoreType__ = 'normal', useCache = false, method = 'get', params, ttl } = options; if (__umiRequestCoreType__ !== 'normal') { - console.warn( - '__umiRequestCoreType__ is a internal params that use in umi-request, change its value would affect the behavior of request! It only use when you want to extend the request core' - ); + if (process && process.env && process.env.NODE_ENV === 'development') { + console.warn( + '__umiRequestCoreType__ is a internal property that use in umi-request, change its value would affect the behavior of request! It only use when you want to extend the request core.' + ); + } return next(); } @@ -38,7 +40,7 @@ export default function fetchMiddleware(ctx, next) { let response; // 超时处理、取消请求处理 if (timeout > 0) { - response = Promise.race([cancel2Throw(options, ctx), adapter(url, options), timeout2Throw(timeout)]); + response = Promise.race([cancel2Throw(options, ctx), adapter(url, options), timeout2Throw(timeout, ctx.req)]); } else { response = Promise.race([cancel2Throw(options, ctx), adapter(url, options)]); } diff --git a/src/middleware/parseResponse.js b/src/middleware/parseResponse.js index 8e74d68..4556805 100644 --- a/src/middleware/parseResponse.js +++ b/src/middleware/parseResponse.js @@ -35,18 +35,18 @@ export default function parseResponseMiddleware(ctx, next) { return res .blob() .then(readerGBK) - .then(safeJsonParse); + .then(d => safeJsonParse(d, false, copy, req)); } catch (e) { - throw new ResponseError(copy, e.message); + throw new ResponseError(copy, e.message, null, req); } } else if (responseType === 'json') { - return res.text().then(d => safeJsonParse(d, throwErrIfParseFail, copy)); + return res.text().then(d => safeJsonParse(d, throwErrIfParseFail, copy, req)); } try { // 其他如text, blob, arrayBuffer, formData return res[responseType](); } catch (e) { - throw new ResponseError(copy, 'responseType not support'); + throw new ResponseError(copy, 'responseType not support', null, req); } }) .then(body => { @@ -66,6 +66,6 @@ export default function parseResponseMiddleware(ctx, next) { ctx.res = body; return; } - throw new ResponseError(copy, 'http error', body); + throw new ResponseError(copy, 'http error', body, req); }); } diff --git a/src/onion/index.js b/src/onion/index.js index e5e59a9..4a8dd2c 100644 --- a/src/onion/index.js +++ b/src/onion/index.js @@ -17,20 +17,20 @@ class Onion { let core = false; let global = false; if (typeof opts === 'number') { - console.warn( - 'use() options should be object, number property would be deprecated in future,please update use() options to "{ core: true }".' - ); + if (process && process.env && process.env.NODE_ENV === 'development') { + console.warn( + 'use() options should be object, number property would be deprecated in future,please update use() options to "{ core: true }".' + ); + } core = true; global = false; } else if (typeof opts === 'object' && opts) { - // index = opts.index || 0; global = opts.global || false; core = opts.core || false; } // 全局中间件 if (global) { - // Onion.globalMiddlewares.push(newMiddleware); Onion.globalMiddlewares.splice( Onion.globalMiddlewares.length - Onion.defaultGlobalMiddlewaresLength, 0, diff --git a/src/request.js b/src/request.js index 4a339be..2e6736b 100644 --- a/src/request.js +++ b/src/request.js @@ -38,7 +38,7 @@ const request = (initOptions = {}) => { }; // 请求语法糖: reguest.get request.post …… - const METHODS = ['get', 'post', 'delete', 'put', 'rpc', 'patch']; + const METHODS = ['get', 'post', 'delete', 'put', 'patch', 'head', 'options', 'rpc']; METHODS.forEach(method => { umiInstance[method] = (url, options) => umiInstance(url, { ...options, method }); }); diff --git a/src/utils.js b/src/utils.js index 7fd4096..0e35723 100644 --- a/src/utils.js +++ b/src/utils.js @@ -49,9 +49,10 @@ export class MapCache { * 请求异常 */ export class RequestError extends Error { - constructor(text) { + constructor(text, request) { super(text); this.name = 'RequestError'; + this.request = request; } } @@ -59,11 +60,12 @@ export class RequestError extends Error { * 响应异常 */ export class ResponseError extends Error { - constructor(response, text, data) { + constructor(response, text, data, request) { super(text || response.statusText); this.name = 'ResponseError'; this.data = data; this.response = response; + this.request = request; } } @@ -85,21 +87,21 @@ export function readerGBK(file) { /** * 安全的JSON.parse */ -export function safeJsonParse(data, throwErrIfParseFail = false, response = null) { +export function safeJsonParse(data, throwErrIfParseFail = false, response = null, request = null) { try { return JSON.parse(data); } catch (e) { if (throwErrIfParseFail) { - throw new ResponseError(response, 'JSON.parse fail', data); + throw new ResponseError(response, 'JSON.parse fail', data, request); } } // eslint-disable-line no-empty return data; } -export function timeout2Throw(msec) { +export function timeout2Throw(msec, request) { return new Promise((_, reject) => { setTimeout(() => { - reject(new RequestError(`timeout of ${msec}ms exceeded`)); + reject(new RequestError(`timeout of ${msec}ms exceeded`, request)); }, msec); }); } diff --git a/test/fetch.test.js b/test/fetch.test.js index 3de1d57..9309aa5 100644 --- a/test/fetch.test.js +++ b/test/fetch.test.js @@ -87,4 +87,19 @@ describe('timeout', () => { } }); }); + + describe('test __umiRequestCoreType__', () => { + it('should warn when __umiRequestCoreType__ not "normal"', async done => { + jest.spyOn(console, 'warn'); + process.env.NODE_ENV = 'development'; + expect(console.warn.mock.calls.length).toBe(0); + await fetch('/api/test', { __umiRequestCoreType__: 'other' }); + expect(console.warn.mock.calls.length).toBe(1); + expect(console.warn.mock.calls[0][0]).toBe( + '__umiRequestCoreType__ is a internal property that use in umi-request, change its value would affect the behavior of request! It only use when you want to extend the request core.' + ); + process.env.NODE_ENV = 'test'; + done(); + }); + }); }); diff --git a/test/index.test.js b/test/index.test.js index dc43e15..164522d 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -453,6 +453,37 @@ describe('test fetch:', () => { }); }); +describe('test http error', () => { + let server; + + beforeAll(async () => { + server = await createTestServer(); + }); + + afterAll(() => { + server.close(); + }); + + it('error handler should return request in error object', async done => { + expect.assertions(3); + server.get('/api/error', (req, res) => { + res.setHeader('access-control-allow-origin', '*'); + res.status(500); + res.send({ errorMessage: 'server error' }); + }); + + try { + let response = await request(`${server.url}/api/error`, { hello: 'world' }); + } catch (e) { + const { response, message, data, request } = e; + expect(response.status).toBe(500); + expect(request.url).toBe(`${server.url}/api/error`); + expect(request.options.hello).toBe('world'); + done(); + } + }); +}); + // 测试rpc xdescribe('test rpc:', () => { it('test hello', () => { diff --git a/test/middleware/simpleGet.test.js b/test/middleware/simpleGet.test.js new file mode 100644 index 0000000..377734a --- /dev/null +++ b/test/middleware/simpleGet.test.js @@ -0,0 +1,38 @@ +import simpleGet from '../../src/middleware/simpleGet'; + +const next = () => {}; + +describe('test simpleGet middleware', () => { + it('should do nothing when ctx is null', async done => { + const ctx = null; + await simpleGet(ctx, next); + expect(ctx).toBe(null); + done(); + }); + it('ctx.req.options.url should contain "a=1"', async done => { + const ctx = { + req: { + url: '/api/test', + options: { + params: { a: 1 }, + }, + }, + }; + await simpleGet(ctx, next); + expect(ctx.req.url).toBe('/api/test?a=1'); + done(); + }); + it('ctx.req.options.url should contain "b=2"', async done => { + const ctx = { + req: { + url: '/api/test?a=1', + options: { + params: { b: 2 }, + }, + }, + }; + await simpleGet(ctx, next); + expect(ctx.req.url).toBe('/api/test?a=1&b=2'); + done(); + }); +}); diff --git a/test/middleware/simplePost.test.js b/test/middleware/simplePost.test.js new file mode 100644 index 0000000..51c72d8 --- /dev/null +++ b/test/middleware/simplePost.test.js @@ -0,0 +1,30 @@ +import simplePost from '../../src/middleware/simplePost'; + +const next = () => {}; + +describe('test simplePost middleware', () => { + it('should do nothing when ctx is null', async done => { + const ctx = null; + await simplePost(ctx, next); + expect(ctx).toBe(null); + done(); + }); + it('should has form header when requestType is form', async done => { + const ctx = { + req: { + url: '/api/test', + options: { + method: 'post', + requestType: 'form', + data: { a: 1 }, + }, + }, + }; + await simplePost(ctx, next); + expect(ctx.req.options.headers).toEqual({ + Accept: 'application/json', + 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', + }); + done(); + }); +}); diff --git a/test/onion/onion.test.js b/test/onion/onion.test.js index 7dd31e0..365964f 100644 --- a/test/onion/onion.test.js +++ b/test/onion/onion.test.js @@ -1,6 +1,22 @@ import { Onion } from '../../src/index'; +import compose from '../../src/onion/compose'; + +describe('compose', () => { + it('should throw error', async done => { + expect.assertions(1); + try { + compose(''); + } catch (e) { + expect(e.message).toBe('Middlewares must be an array!'); + done(); + } + }); +}); describe('Onion', () => { + beforeAll(() => { + Onion.globalMiddlewares = []; + }); it('test constructor', async () => { expect.assertions(1); try { @@ -49,4 +65,31 @@ describe('Onion', () => { expect(error.message).toBe('error in middleware'); } }); + it('should warning when options is number', async done => { + jest.spyOn(console, 'warn'); + process.env.NODE_ENV = 'development'; + const onion = new Onion([]); + expect(console.warn.mock.calls.length).toBe(0); + onion.use(async (ctx, next) => { + await next(); + }, 1); + expect(console.warn.mock.calls.length).toBe(1); + expect(console.warn.mock.calls[0][0]).toBe( + 'use() options should be object, number property would be deprecated in future,please update use() options to "{ core: true }".' + ); + process.env.NODE_ENV = 'test'; + done(); + }); + + it('should have 1 global middleware', async done => { + const onion = new Onion([]); + onion.use( + async (ctx, next) => { + await next(); + }, + { global: true } + ); + expect(Onion.globalMiddlewares.length).toBe(1); + done(); + }); }); diff --git a/test/timeout.test.js b/test/timeout.test.js index 9e3df5e..bda3ad1 100644 --- a/test/timeout.test.js +++ b/test/timeout.test.js @@ -38,7 +38,7 @@ describe('timeout', () => { }); it('should throw Request Error when timeout', async done => { - expect.assertions(1); + expect.assertions(3); server.get('/test/timeout', (req, res) => { setTimeout(() => { writeData('ok', res); @@ -50,6 +50,8 @@ describe('timeout', () => { response = await request(prefix('/test/timeout'), { timeout: 800 }); } catch (error) { expect(error.name).toBe('RequestError'); + expect(error.message).toBe('timeout of 800ms exceeded'); + expect(error.request.options.timeout).toBe(800); done(); } }); diff --git a/types/index.d.ts b/types/index.d.ts index be3616a..a057b6a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -3,6 +3,10 @@ export interface ResponseError extends Error { name: string; data: D; response: Response; + request: { + url: string; + options: RequestOptionsInit; + }; } /** * 增加的参数 @@ -80,6 +84,8 @@ export interface RequestMethod { delete: RequestMethod; put: RequestMethod; patch: RequestMethod; + head: RequestMethod; + options: RequestMethod; rpc: RequestMethod; interceptors: { request: { From 3ef4c7361b3bb6d17bdb52713175b93f99e02958 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Mon, 23 Sep 2019 16:36:51 +0800 Subject: [PATCH 43/94] 1.2.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 34432d6..8377697 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.5", + "version": "1.2.6", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From e21a3c741f95bf953fa1a16e6d72b3c28d72dcc4 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Thu, 10 Oct 2019 10:42:39 +0800 Subject: [PATCH 44/94] Fix/credentials default value (#68) * fix: update 'credentials' default value to same-origin' * fix: fix params when params is nesting object; support URLSearchParams type params; provide 'paramsSerializer' in charge of serializing 'params' * feat: update property 'params' and 'paramsSerializer' in RequestOptionsInit * fix: update test cases of simpleGet --- README.md | 12 +++++-- README_zh-CN.md | 12 +++++-- src/middleware/simpleGet.js | 58 ++++++++++++++++++++++++++++--- src/request.js | 5 +-- src/utils.js | 47 +++++++++++++++++++++++++ test/index.test.js | 21 +++++++++++ test/middleware/simpleGet.test.js | 49 +++++++++++++++++++++++++- types/index.d.ts | 3 +- 8 files changed, 192 insertions(+), 15 deletions(-) diff --git a/README.md b/README.md index 99c985a..ab12ff1 100644 --- a/README.md +++ b/README.md @@ -217,13 +217,13 @@ More umi-request cases can see [antd-pro](https://github.com/umijs/ant-design-pr | Parameter | Description | Type | Optional Value | Default | | :--- | :--- | :--- | :--- | :--- | | method | request method | string | get , post , put ... | get | -| params | url request parameters | object | -- | -- | +| 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: 'include' | +| 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) | @@ -254,8 +254,14 @@ The other parameters of fetch are valid. See [fetch documentation](https://githu 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' // 必须是以下类型之一: @@ -290,7 +296,7 @@ The other parameters of fetch are valid. See [fetch documentation](https://githu // 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: 'include', + 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'. diff --git a/README_zh-CN.md b/README_zh-CN.md index 18515f2..89108b0 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -225,13 +225,13 @@ umi-request 可以进行一层简单封装后再使用, 可参考 [antd-pro](htt | 参数 | 说明 | 类型 | 可选值 | 默认值 | | :--- | :--- | :--- | :--- | :--- | | method | 请求方式 | string | get , post , put ... | get | -| params | url请求参数 | object | -- | -- | +| params | url请求参数 | object 或 URLSearchParams 对象 | -- | -- | | data | 提交的数据 | any | -- | -- | | headers | fetch 原有参数 | object | -- | {} | | timeout | 超时时长, 默认毫秒, 写操作慎用 | number | -- | -- | | prefix | 前缀, 一般用于覆盖统一设置的prefix | string | -- | -- | | suffix | 后缀, 比如某些场景 api 需要统一加 .json | string | -- | -- | -| credentials | fetch 请求包含 cookies 信息 | object | -- | credentials: 'include' | +| credentials | fetch 请求包含 cookies 信息 | object | -- | credentials: 'same-origin' | | useCache | 是否使用缓存(仅支持浏览器客户端) | boolean | -- | false | | ttl | 缓存时长, 0 为不过期 | number | -- | 60000 | | maxCache | 最大缓存数 | number | -- | 无限 | @@ -262,8 +262,14 @@ fetch原其他参数有效, 详见[fetch文档](https://github.github.io/fetch/) method: 'get', // default // 'params' 是即将于请求一起发送的 URL 参数,参数会自动 encode 后添加到 URL 中 + // 类型需为 Object 对象或者 URLSearchParams 对象 params: { id: 1 }, + // 'paramsSerializer' 开发者可通过该函数对 params 做序列化(注意:此时传入的 params 为合并了 extends 中 params 参数的对象,如果传入的是 URLSearchParams 对象会转化为 Object 对象 + paramsSerializer: function (params) { + return Qs.stringify(params, { arrayFormat: 'brackets' }) + }, + // 'data' 作为请求主体被发送的数据 // 适用于这些请求方法 'PUT', 'POST', 和 'PATCH' // 必须是以下类型之一: @@ -291,7 +297,7 @@ fetch原其他参数有效, 详见[fetch文档](https://github.github.io/fetch/) // 为了让浏览器发送包含凭据的请求(即使是跨域源),需要设置 credentials: 'include' // 如果只想在请求URL与调用脚本位于同一起源处时发送凭据,请添加credentials: 'same-origin' // 要改为确保浏览器不在请求中包含凭据,请使用credentials: 'omit' - credentials: 'include', + credentials: 'same-origin', // default // ’useCache‘ 是否使用缓存,当值为 true 时,GET 请求在 ttl 毫秒内将被缓存,缓存策略唯一 key 为 url + params 组合 useCache: false, // default diff --git a/src/middleware/simpleGet.js b/src/middleware/simpleGet.js index bc2fa07..5f2c0fa 100644 --- a/src/middleware/simpleGet.js +++ b/src/middleware/simpleGet.js @@ -1,19 +1,67 @@ import { stringify } from 'query-string'; +import { isArray, isURLSearchParams, forEach2ObjArr, isObject, isDate } from '../utils'; + +export function paramsSerialize(params, paramsSerializer) { + let serializedParams; + let jsonStringifiedParams; + // 支持参数自动拼装,其他 method 也可用,不冲突 + if (params) { + if (paramsSerializer) { + serializedParams = paramsSerializer(params); + } else if (isURLSearchParams(params)) { + serializedParams = params.toString(); + } else { + if (isArray(params)) { + jsonStringifiedParams = []; + forEach2ObjArr(params, function(item) { + if (item === null || typeof item === 'undefined') { + return; + } + jsonStringifiedParams.push(isObject(item) ? JSON.stringify(item) : item); + }); + serializedParams = stringify(jsonStringifiedParams); + } else { + jsonStringifiedParams = {}; + forEach2ObjArr(params, function(value, key) { + let jsonStringifiedValue = value; + if (value === null || typeof value === 'undefined') return; + if (isDate(value)) { + jsonStringifiedValue = value.toISOString(); + } else if (isArray(value)) { + jsonStringifiedValue = value; + } else if (isObject(value)) { + jsonStringifiedValue = JSON.stringify(value); + } + jsonStringifiedParams[key] = jsonStringifiedValue; + }); + serializedParams = stringify(jsonStringifiedParams); + } + } + } + return serializedParams; +} // 对请求参数做处理,实现 query 简化、 post 简化 export default function simpleGetMiddleware(ctx, next) { if (!ctx) return next(); const { req: { options = {} } = {} } = ctx; + const { paramsSerializer, params } = options; let { req: { url = '' } = {} } = ctx; // 将 method 改为大写 options.method = options.method ? options.method.toUpperCase() : 'GET'; + // 设置 credentials 默认值为 same-origin,确保当开发者没有设置时,各浏览器对请求是否发送 cookies 保持一致的行为 + // - omit: 从不发送cookies. + // - same-origin: 只有当URL与响应脚本同源才发送 cookies、 HTTP Basic authentication 等验证信息.(浏览器默认值,在旧版本浏览器,例如safari 11依旧是omit,safari 12已更改) + // - include: 不论是不是跨域的请求,总是发送请求资源域在本地的 cookies、 HTTP Basic authentication 等验证信息. + options.credentials = options.credentials || 'same-origin'; + // 支持类似axios 参数自动拼装, 其他method也可用, 不冲突. - if (options.params && Object.keys(options.params).length > 0) { - const str = url.indexOf('?') !== -1 ? '&' : '?'; - ctx.req.originUrl = url; - url = `${url}${str}${stringify(options.params)}`; - ctx.req.url = url; + let serializedParams = paramsSerialize(params, paramsSerializer); + ctx.req.originUrl = url; + if (serializedParams) { + const urlSign = url.indexOf('?') !== -1 ? '&' : '?'; + ctx.req.url = `${url}${urlSign}${serializedParams}`; } ctx.req.options = options; diff --git a/src/request.js b/src/request.js index 2e6736b..43b4eb0 100644 --- a/src/request.js +++ b/src/request.js @@ -2,6 +2,7 @@ import Core from './core'; import Cancel from './cancel/cancel'; import CancelToken from './cancel/cancelToken'; import isCancel from './cancel/isCancel'; +import { getParamObject } from './utils'; // 通过 request 函数,在 core 之上再封装一层,提供原 umi/request 一致的 api,无缝升级 const request = (initOptions = {}) => { @@ -15,8 +16,8 @@ const request = (initOptions = {}) => { ...options.headers, }, params: { - ...initOptions.params, - ...options.params, + ...getParamObject(initOptions.params), + ...getParamObject(options.params), }, method: (options.method || initOptions.method || 'get').toLowerCase(), }; diff --git a/src/utils.js b/src/utils.js index 0e35723..fc28fc1 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,6 +2,7 @@ * 实现一个简单的Map cache, 稍后可以挪到 utils中, 提供session local map三种前端cache方式. * 1. 可直接存储对象 2. 内存无5M限制 3.缺点是刷新就没了, 看反馈后期完善. */ +import { parse } from 'query-string'; export class MapCache { constructor(options) { @@ -130,3 +131,49 @@ export function getEnv() { } return env; } + +export function isArray(val) { + return toString.call(val) === '[object Array]'; +} + +export function isURLSearchParams(val) { + return typeof URLSearchParams !== 'undefined' && val instanceof URLSearchParams; +} + +export function isDate(val) { + return toString.call(val) === '[object Date]'; +} + +export function isObject(val) { + return val !== null && typeof val === 'object'; +} + +export function forEach2ObjArr(target, callback) { + if (!target) return; + + if (typeof target !== 'object') { + target = [target]; + } + + if (isArray(target)) { + for (let i = 0; i < target.length; i++) { + callback.call(null, target[i], i, target); + } + } else { + for (let key in target) { + if (Object.prototype.hasOwnProperty.call(target, key)) { + callback.call(null, target[key], key, target); + } + } + } +} + +export function getParamObject(val) { + if (isURLSearchParams(val)) { + return parse(val.toString()); + } + if (typeof val === 'string') { + return [val]; + } + return val; +} diff --git a/test/index.test.js b/test/index.test.js index 164522d..a9bd8c8 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -215,6 +215,27 @@ describe('test fetch:', () => { }, }); expect(response.wang).toBe('hou'); + expect(response.hello).toBe('world3'); + + const reponse1 = await request(prefix('/test/queryParams'), { + params: new URLSearchParams('foo=hello'), + }); + + expect(reponse1.foo).toBe('hello'); + + const response2 = await request(prefix('/test/queryParams'), { + params: { + bar: 'woo', + }, + paramsSerializer: params => `bar=${params.bar}&car=pengpeng`, + }); + expect(response2.bar).toBe('woo'); + expect(response2.car).toBe('pengpeng'); + + const response3 = await request(prefix('/test/queryParams'), { + params: 'stringparams', + }); + expect(response3[0]).toBe('stringparams'); }, 5000); // 测试缓存 diff --git a/test/middleware/simpleGet.test.js b/test/middleware/simpleGet.test.js index 377734a..e0095d9 100644 --- a/test/middleware/simpleGet.test.js +++ b/test/middleware/simpleGet.test.js @@ -1,4 +1,4 @@ -import simpleGet from '../../src/middleware/simpleGet'; +import simpleGet, { paramsSerialize } from '../../src/middleware/simpleGet'; const next = () => {}; @@ -36,3 +36,50 @@ describe('test simpleGet middleware', () => { done(); }); }); + +describe('test paramsSerialize', () => { + it('should support object params', () => { + expect(paramsSerialize({ a: 1 })).toBe('a=1'); + }); + + it('should support array params', () => { + expect(paramsSerialize([1, 2, 3])).toBe('0=1&1=2&2=3'); + }); + + it('should support nesting object params', () => { + expect(paramsSerialize({ a: { b: 1 } })).toBe('a=%7B%22b%22%3A1%7D'); + }); + + it('should support nesting Date params', () => { + expect(paramsSerialize({ a: new Date('05 October 2011 14:48 UTC') })).toBe('a=2011-10-05T14%3A48%3A00.000Z'); + }); + + it('should support nesting array params', () => { + expect(paramsSerialize([{ a: 1 }, { b: { c: 2 } }])).toBe( + '0=%7B%22a%22%3A1%7D&1=%7B%22b%22%3A%7B%22c%22%3A2%7D%7D' + ); + + expect(paramsSerialize({ a: [1, 2, 3] })).toBe('a=1&a=2&a=3'); + }); + + it('should be undefined when params is null、undefined、{}、[]', () => { + expect(paramsSerialize(null)).toBe(undefined); + expect(paramsSerialize(undefined)).toBe(undefined); + + expect(paramsSerialize([undefined])).toBe(''); + expect(paramsSerialize([])).toBe(''); + expect(paramsSerialize({})).toBe(''); + }); + + it('should support URLSearchParams params', () => { + const url = new URL('https://a.com?a=1&b=2'); + const params = new URLSearchParams(url.search.slice(1)); + params.append('c', 3); + + expect(paramsSerialize(params)).toBe('a=1&b=2&c=3'); + }); + + it('should support paramsSerializer', () => { + expect(paramsSerialize({ a: 1 }, val => val.a)).toBe(1); + }); +}); diff --git a/types/index.d.ts b/types/index.d.ts index a057b6a..0368dbc 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -27,7 +27,8 @@ export interface RequestOptionsInit extends RequestInit { charset?: 'utf8' | 'gbk'; requestType?: 'json' | 'form'; data?: any; - params?: object; + params?: object | URLSearchParams; + paramsSerializer?: (params: object) => string; responseType?: ResponseType; useCache?: boolean; ttl?: number; From 86c1b7dd0a3ef1521c64439ade569d42d7dce091 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Thu, 10 Oct 2019 10:43:45 +0800 Subject: [PATCH 45/94] 1.2.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8377697..1f4d185 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.6", + "version": "1.2.7", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From 56daaead9b44f778b653e54fb892d414d04d2102 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Thu, 10 Oct 2019 10:50:32 +0800 Subject: [PATCH 46/94] Update README.md --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index ab12ff1..b43c458 100644 --- a/README.md +++ b/README.md @@ -412,7 +412,7 @@ import request, { extend } from 'umi-request'; const errorHandler = function (error) { const codeMap = { - '021': 'An error has occurred'', + '021': 'An error has occurred', '022': 'It’s a big mistake,', // .... }; From 8ebf824d96074373ef64600d69488e22a52faa74 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Sun, 13 Oct 2019 17:49:03 +0800 Subject: [PATCH 47/94] fix: params serialize fail when keyvalue is 'null'. (#70) --- src/middleware/simpleGet.js | 13 ++++++++----- src/utils.js | 4 ++-- test/middleware/simpleGet.test.js | 3 ++- 3 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/middleware/simpleGet.js b/src/middleware/simpleGet.js index 5f2c0fa..2518cbf 100644 --- a/src/middleware/simpleGet.js +++ b/src/middleware/simpleGet.js @@ -15,17 +15,19 @@ export function paramsSerialize(params, paramsSerializer) { jsonStringifiedParams = []; forEach2ObjArr(params, function(item) { if (item === null || typeof item === 'undefined') { - return; + jsonStringifiedParams.push(item); + } else { + jsonStringifiedParams.push(isObject(item) ? JSON.stringify(item) : item); } - jsonStringifiedParams.push(isObject(item) ? JSON.stringify(item) : item); }); serializedParams = stringify(jsonStringifiedParams); } else { jsonStringifiedParams = {}; forEach2ObjArr(params, function(value, key) { let jsonStringifiedValue = value; - if (value === null || typeof value === 'undefined') return; - if (isDate(value)) { + if (value === null || typeof value === 'undefined') { + jsonStringifiedParams[key] = value; + } else if (isDate(value)) { jsonStringifiedValue = value.toISOString(); } else if (isArray(value)) { jsonStringifiedValue = value; @@ -34,7 +36,8 @@ export function paramsSerialize(params, paramsSerializer) { } jsonStringifiedParams[key] = jsonStringifiedValue; }); - serializedParams = stringify(jsonStringifiedParams); + const tmp = stringify(jsonStringifiedParams); + serializedParams = tmp; } } } diff --git a/src/utils.js b/src/utils.js index fc28fc1..0b5d76f 100644 --- a/src/utils.js +++ b/src/utils.js @@ -133,7 +133,7 @@ export function getEnv() { } export function isArray(val) { - return toString.call(val) === '[object Array]'; + return typeof val === 'object' && Object.prototype.toString.call(val) === '[object Array]'; } export function isURLSearchParams(val) { @@ -141,7 +141,7 @@ export function isURLSearchParams(val) { } export function isDate(val) { - return toString.call(val) === '[object Date]'; + return typeof val === 'object' && Object.prototype.toString.call(val) === '[object Date]'; } export function isObject(val) { diff --git a/test/middleware/simpleGet.test.js b/test/middleware/simpleGet.test.js index e0095d9..a938cd1 100644 --- a/test/middleware/simpleGet.test.js +++ b/test/middleware/simpleGet.test.js @@ -39,7 +39,8 @@ describe('test simpleGet middleware', () => { describe('test paramsSerialize', () => { it('should support object params', () => { - expect(paramsSerialize({ a: 1 })).toBe('a=1'); + expect(paramsSerialize({ a: null, b: undefined, c: 1, d: 'asdf' })).toBe('a&c=1&d=asdf'); + expect(paramsSerialize([null, undefined, 1, 2])).toBe('0&2=1&3=2'); }); it('should support array params', () => { From af4c10ce204f3d143cac331538512a13fc8cc22a Mon Sep 17 00:00:00 2001 From: chenjsh Date: Sun, 13 Oct 2019 17:49:59 +0800 Subject: [PATCH 48/94] 1.2.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1f4d185..2edca05 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.7", + "version": "1.2.8", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From 2853dc81c724502fb4c4b81270978c93d0a3140c Mon Sep 17 00:00:00 2001 From: Chao Date: Wed, 16 Oct 2019 10:15:08 +0800 Subject: [PATCH 49/94] Update utils.js (#69) fix: ie11 window.toString --- src/utils.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/utils.js b/src/utils.js index 0b5d76f..7771e54 100644 --- a/src/utils.js +++ b/src/utils.js @@ -118,11 +118,13 @@ export function cancel2Throw(opt) { }); } +const toString = Object.prototype.toString; + // Check env is browser or node export function getEnv() { let env; // Only Node.JS has a process variable that is of [[Class]] process - if (typeof process !== 'undefined' && Object.prototype.toString.call(process) === '[object process]') { + if (typeof process !== 'undefined' && toString.call(process) === '[object process]') { // For node use HTTP adapter env = 'NODE'; } From 84b2955a002430e300b41533c06e34647d20e383 Mon Sep 17 00:00:00 2001 From: zouxiaomingya <41714119+zouxiaomingya@users.noreply.github.com> Date: Sat, 9 Nov 2019 19:20:09 +0800 Subject: [PATCH 50/94] feat: add instance Interceptors (#71) * feat: add instance Interceptors --- .eslintrc | 3 +- README.md | 46 +++++++++++++++++++++++++ README_zh-CN.md | 47 ++++++++++++++++++++++++++ src/core.js | 37 ++++++++++++++------- src/request.js | 4 +-- test/interceptor.test.js | 72 ++++++++++++++++++++++++++++++++++++++++ 6 files changed, 194 insertions(+), 15 deletions(-) diff --git a/.eslintrc b/.eslintrc index a80167f..f9ac451 100644 --- a/.eslintrc +++ b/.eslintrc @@ -5,7 +5,8 @@ "no-param-reassign": 0, "max-len": 0, "no-else-return": 0, - "no-underscore-dangle": 0 + "no-underscore-dangle": 0, + "linebreak-style": [0, "error", "windows"], }, "env": { "browser": true diff --git a/README.md b/README.md index b43c458..210fd2b 100644 --- a/README.md +++ b/README.md @@ -594,6 +594,7 @@ request('/api/v1/rpc', { ## 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) => { @@ -605,6 +606,16 @@ request.interceptors.request.use((url, options) => { ); }); +// 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'); @@ -632,6 +643,41 @@ request.interceptors.response.use(async (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 diff --git a/README_zh-CN.md b/README_zh-CN.md index 89108b0..4289b8b 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -594,6 +594,7 @@ request('/api/v1/rpc', { ## 拦截器 在请求或响应被 ```then``` 或 ```catch``` 处理前拦截它们。 +1. 全局拦截器 ``` javascript // request拦截器, 改变url 或 options. request.interceptors.request.use((url, options) => { @@ -605,6 +606,17 @@ request.interceptors.request.use((url, options) => { ); }); +// 和上一个相同 +request.interceptors.request.use((url, options) => { + return ( + { + url: `${url}&interceptors=yes`, + options: { ...options, interceptors: true }, + } + ); +}, { global: true }); + + // response拦截器, 处理response request.interceptors.response.use((response, options) => { response.headers.append('interceptors', 'yes yo'); @@ -632,6 +644,41 @@ request.interceptors.response.use(async (response) => { }) ``` +2. 实例内部拦截器 +``` javascript +// 全局拦截器直接使用 request 实例中的方法 +request.interceptors.request.use((url, options) => { + return { + url: `${url}&interceptors=yes`, + options: { ...options, interceptors: true }, + }; +}, { global: false }); // 第二个参数不传默认为 { global: true } + +function createClient(baseUrl) { + const request = extend({ + prefix: baseUrl + }); + return request; +} + +const clientA = createClient('/api'); +const clientB = createClient('/api'); +// 局部拦截器使用 +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 token** 来取消一个请求 > cancel token API 是基于已被撤销的 [cancelable-promises 方案](https://github.com/tc39/proposal-cancelable-promises) diff --git a/src/core.js b/src/core.js index b4d2650..8370592 100644 --- a/src/core.js +++ b/src/core.js @@ -6,10 +6,6 @@ import parseResponseMiddleware from './middleware/parseResponse'; import simplePost from './middleware/simplePost'; import simpleGet from './middleware/simpleGet'; -// 旧版拦截器为共享 -const requestInterceptors = [addfixInterceptor]; -const responseInterceptors = []; - // 初始化全局和内核中间件 const globalMiddlewares = [simplePost, simpleGet, parseResponseMiddleware]; const coreMiddlewares = [fetchMiddleware]; @@ -24,32 +20,49 @@ class Core { this.onion = new Onion([]); this.fetchIndex = 0; // 【即将废弃】请求中间件位置 this.mapCache = new MapCache(initOptions); + this.initOptions = initOptions; + this.instanceRequestInterceptors = []; + this.instanceResponseInterceptors = []; } + // 旧版拦截器为共享 + static requestInterceptors = [addfixInterceptor]; + static responseInterceptors = []; use(newMiddleware, opt = { global: false, core: false }) { this.onion.use(newMiddleware, opt); return this; } - static requestUse(handler) { + // 请求拦截器 默认 { global: true } 兼容旧版本拦截器 + static requestUse(handler, opt = { global: true }) { if (typeof handler !== 'function') throw new TypeError('Interceptor must be function!'); - requestInterceptors.push(handler); + if(opt.global){ + Core.requestInterceptors.push(handler); + } else { + this.instanceRequestInterceptors.push(handler); + } } - static responseUse(handler) { + // 响应拦截器 默认 { global: true } 兼容旧版本拦截器 + static responseUse(handler, opt = { global: true }) { if (typeof handler !== 'function') throw new TypeError('Interceptor must be function!'); - responseInterceptors.push(handler); + if(opt.global){ + Core.responseInterceptors.push(handler); + } else { + this.instanceResponseInterceptors.push(handler); + } } // 执行请求前拦截器 - static dealRequestInterceptors(ctx) { + dealRequestInterceptors(ctx) { const reducer = (p1, p2) => p1.then((ret = {}) => { ctx.req.url = ret.url || ctx.req.url; ctx.req.options = ret.options || ctx.req.options; return p2(ctx.req.url, ctx.req.options); }); - return requestInterceptors.reduce(reducer, Promise.resolve()).then((ret = {}) => { + const allInterceptors = [...Core.requestInterceptors, ...this.instanceRequestInterceptors]; + return allInterceptors.reduce(reducer, Promise.resolve()).then((ret = {}) => { ctx.req.url = ret.url || ctx.req.url; ctx.req.options = ret.options || ctx.req.options; return Promise.resolve(); @@ -62,14 +75,14 @@ class Core { req: { url, options }, res: null, cache: this.mapCache, - responseInterceptors, + responseInterceptors: [...Core.responseInterceptors, ...this.instanceResponseInterceptors], }; if (typeof url !== 'string') { throw new Error('url MUST be a string'); } return new Promise((resolve, reject) => { - Core.dealRequestInterceptors(obj) + this.dealRequestInterceptors(obj) .then(() => onion.execute(obj)) .then(() => { resolve(obj.res); diff --git a/src/request.js b/src/request.js index 43b4eb0..486f2aa 100644 --- a/src/request.js +++ b/src/request.js @@ -31,10 +31,10 @@ const request = (initOptions = {}) => { // 拦截器 umiInstance.interceptors = { request: { - use: Core.requestUse, + use: Core.requestUse.bind(coreInstance), }, response: { - use: Core.responseUse, + use: Core.responseUse.bind(coreInstance), }, }; diff --git a/test/interceptor.test.js b/test/interceptor.test.js index 8c54a75..c991150 100644 --- a/test/interceptor.test.js +++ b/test/interceptor.test.js @@ -65,6 +65,78 @@ describe('interceptor', () => { } }); + it('global and instance interceptor', async done => { + expect.assertions(6); + server.get('/test/global/interceptors', (req, res) => { + writeData(req.query, res); + }); + + // request global interceptors change request's url + request.interceptors.request.use((url, options) => { + return { + url: `${url}&isGlobal=yes`, + options: { ...options, interceptors: true }, + }; + }); + + request.interceptors.request.use( + (url, options) => { + return { + url: `${url}&instance=request`, + options: { ...options, interceptors: true }, + }; + }, + { global: false } + ); + + request.interceptors.response.use( + (res, options) => { + res.headers.append('instance', 'yes request'); + return res; + }, + { global: false } + ); + + const clientA = extend(); + + // request instance self interceptors change request's url + clientA.interceptors.request.use( + (url, options) => { + return { + url: `${url}&instance=clientA`, + options, + }; + }, + { global: false } + ); + + // response instance self interceptor, change response's header + clientA.interceptors.response.use( + (res, options) => { + res.headers.append('instance', 'yes clientA'); + return res; + }, + { global: false } + ); + + const responseClientA = await clientA(prefix('/test/global/interceptors'), { + getResponse: true, + }); + + const response = await request(prefix('/test/global/interceptors'), { + getResponse: true, + }); + + expect(response.data.instance).toBe('request'); + expect(response.data.isGlobal).toBe('yes'); + expect(response.response.headers.get('instance')).toBe('yes request'); + + expect(responseClientA.data.instance).toBe('clientA'); + expect(responseClientA.data.isGlobal).toBe('yes'); + expect(responseClientA.response.headers.get('instance')).toBe('yes clientA'); + done(); + }); + it('invalid interceptor constructor', async done => { expect.assertions(2); try { From 051f4d6f82f588a27984fe71e6d3d0b33ba18730 Mon Sep 17 00:00:00 2001 From: xiaoxintang <383453652@qq.com> Date: Sat, 9 Nov 2019 19:20:59 +0800 Subject: [PATCH 51/94] update:replace symbol (#72) --- README.md | 24 ++++++++++++------------ README_zh-CN.md | 24 ++++++++++++------------ 2 files changed, 24 insertions(+), 24 deletions(-) diff --git a/README.md b/README.md index 210fd2b..24adc6a 100644 --- a/README.md +++ b/README.md @@ -30,18 +30,18 @@ The network request library, based on fetch encapsulation, combines the features | :---------- | :-------------- | :-------------- | :-------------- | | 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 | ✅ | ❎ | ✅ | +| 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) diff --git a/README_zh-CN.md b/README_zh-CN.md index 4289b8b..b55dff9 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -34,18 +34,18 @@ | :---------- | :-------------- | :-------------- | :-------------- | | 实现 | 浏览器原生支持 | 浏览器原生支持 | XMLHttpRequest | | 大小 | 9k | 4k (polyfill) | 14k | -| query 简化 | ✅ | ❎ | ✅ | -| post 简化 | ✅ | ❎ | ❎ | -| 超时 | ✅ | ❎ | ✅ | -| 缓存 | ✅ | ❎ | ❎ | -| 错误检查 | ✅ | ❎ | ❎ | -| 错误处理 | ✅ | ❎ | ✅ | -| 拦截器 | ✅ | ❎ | ✅ | -| 前缀 | ✅ | ❎ | ❎ | -| 后缀 | ✅ | ❎ | ❎ | -| 处理 gbk | ✅ | ❎ | ❎ | -| 中间件 | ✅ | ❎ | ❎ | -| 取消请求 | ✅ | ❎ | ✅ | +| query 简化 | ✅ | ❌ | ✅ | +| post 简化 | ✅ | ❌ | ❌ | +| 超时 | ✅ | ❌ | ✅ | +| 缓存 | ✅ | ❌ | ❌ | +| 错误检查 | ✅ | ❌ | ❌ | +| 错误处理 | ✅ | ❌ | ✅ | +| 拦截器 | ✅ | ❌ | ✅ | +| 前缀 | ✅ | ❌ | ❌ | +| 后缀 | ✅ | ❌ | ❌ | +| 处理 gbk | ✅ | ❌ | ❌ | +| 中间件 | ✅ | ❌ | ❌ | +| 取消请求 | ✅ | ❌ | ✅ | 更多讨论参考[传统 Ajax 已死,Fetch 永生](https://github.com/camsong/blog/issues/2), 如果你有好的建议和需求, 请提 [issue](https://github.com/umijs/umi/issues) From 9f5d40e7ba9fe2e2a02e456e1e914a52832ac21d Mon Sep 17 00:00:00 2001 From: chenjsh Date: Sat, 9 Nov 2019 19:23:30 +0800 Subject: [PATCH 52/94] 1.2.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 2edca05..fc6c8e1 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.8", + "version": "1.2.9", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From 2bc31139197cdb64fa066ad24eed3b029900e4b8 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Wed, 20 Nov 2019 21:03:09 +0800 Subject: [PATCH 53/94] Feat/qs cache (#74) * feat: use 'qs' module instead of 'query-string' to dealwith pack problem * feat: add 'validateCache' for cache strategy --- README.md | 8 +++++++- README_zh-CN.md | 11 ++++++++--- package.json | 2 +- src/middleware/fetch.js | 30 +++++++++++++++++++++++++----- src/middleware/simpleGet.js | 7 ++++--- src/middleware/simplePost.js | 2 +- src/utils.js | 4 ++-- test/fetch.test.js | 2 +- test/index.test.js | 31 +++++++++++++++++++++++++++++++ test/interceptor.test.js | 22 ++++++++++++++++++++++ types/index.d.ts | 1 + 11 files changed, 103 insertions(+), 17 deletions(-) diff --git a/README.md b/README.md index 24adc6a..69e1400 100644 --- a/README.md +++ b/README.md @@ -225,6 +225,7 @@ More umi-request cases can see [antd-pro](https://github.com/umijs/ant-design-pr | 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 | @@ -299,7 +300,7 @@ The other parameters of fetch are valid. See [fetch documentation](https://githu 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'. + // The cache key would be 'url + params + method'. useCache: false, // default // 'ttl' cache duration(milliseconds),0 is infinity @@ -308,6 +309,11 @@ The other parameters of fetch are valid. See [fetch documentation](https://githu // '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 = { diff --git a/README_zh-CN.md b/README_zh-CN.md index b55dff9..85ddb34 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -231,8 +231,9 @@ umi-request 可以进行一层简单封装后再使用, 可参考 [antd-pro](htt | timeout | 超时时长, 默认毫秒, 写操作慎用 | number | -- | -- | | prefix | 前缀, 一般用于覆盖统一设置的prefix | string | -- | -- | | suffix | 后缀, 比如某些场景 api 需要统一加 .json | string | -- | -- | -| credentials | fetch 请求包含 cookies 信息 | object | -- | credentials: 'same-origin' | +| credentials | fetch 请求包含 cookies 信息 | string | -- | credentials: 'same-origin' | | useCache | 是否使用缓存(仅支持浏览器客户端) | boolean | -- | false | +| validateCache | 缓存策略函数 | (url, options) => boolean | -- | 默认 get 请求做缓存 | | ttl | 缓存时长, 0 为不过期 | number | -- | 60000 | | maxCache | 最大缓存数 | number | -- | 无限 | | requestType | post请求时数据类型 | string | json , form | json | @@ -299,7 +300,7 @@ fetch原其他参数有效, 详见[fetch文档](https://github.github.io/fetch/) // 要改为确保浏览器不在请求中包含凭据,请使用credentials: 'omit' credentials: 'same-origin', // default - // ’useCache‘ 是否使用缓存,当值为 true 时,GET 请求在 ttl 毫秒内将被缓存,缓存策略唯一 key 为 url + params 组合 + // ’useCache‘ 是否使用缓存,当值为 true 时,GET 请求在 ttl 毫秒内将被缓存,缓存策略唯一 key 为 url + params + method 组合 useCache: false, // default // ’ttl‘ 缓存时长(毫秒), 0 为不过期 @@ -308,6 +309,9 @@ fetch原其他参数有效, 详见[fetch文档](https://github.github.io/fetch/) // 'maxCache' 最大缓存数, 0 为无限制 maxCache: 0, + // 根据协议规范, GET 请求用于获取、查询服务端数据,在数据更新频率不频繁的情况下做必要的缓存能减少服务端的压力,因为缓存策略是默认对 GET 请求做缓存,但对于一些特殊场景需要缓存其他类型请求的响应数据时,我们提供 validateCache 供用户自定义何时需要进行缓存, key 依旧为 url + params + method + validateCache: (url, options) => { return options.method.toLowerCase() === 'get' }, + // 'requestType' 当 data 为对象或者数组时, umi-request 会根据 requestType 动态添加 headers 和设置 body(可传入 headers 覆盖 Accept 和 Content-Type 头部属性): // 1. requestType === 'json' 时, (默认为 json ) // options.headers = { @@ -452,7 +456,8 @@ request('/api/v1/xxx') ## 中间件 -类 koa 的洋葱机制,让开发者优雅地做请求前后的增强处理,支持创建实例、全局、内核中间件。 + +类 koa 的洋葱机制,让开发者优雅地做请求前后的增强处理,支持创建实例、全局、内核中间件。 **实例中间件(默认)** :request.use(fn) 不同实例创建的中间件相互独立不影响; diff --git a/package.json b/package.json index fc6c8e1..90eda18 100644 --- a/package.json +++ b/package.json @@ -44,7 +44,7 @@ }, "dependencies": { "isomorphic-fetch": "^2.2.1", - "query-string": "^6.0.0" + "qs": "^6.9.1" }, "files": [ "dist/", diff --git a/src/middleware/fetch.js b/src/middleware/fetch.js index d534b07..7d02a80 100644 --- a/src/middleware/fetch.js +++ b/src/middleware/fetch.js @@ -1,15 +1,33 @@ import 'isomorphic-fetch'; import { timeout2Throw, cancel2Throw, getEnv } from '../utils'; +// 是否已经警告过 +let warnedCoreType = false; + +// 默认缓存判断,开放缓存判断给非 get 请求使用 +function __defaultValidateCache(url, options) { + const { method = 'get' } = options; + return method.toLowerCase() === 'get'; +} + export default function fetchMiddleware(ctx, next) { if (!ctx) return next(); const { req: { options = {}, url = '' } = {}, cache, responseInterceptors } = ctx; - const { timeout = 0, __umiRequestCoreType__ = 'normal', useCache = false, method = 'get', params, ttl } = options; + const { + timeout = 0, + __umiRequestCoreType__ = 'normal', + useCache = false, + method = 'get', + params, + ttl, + validateCache = __defaultValidateCache, + } = options; if (__umiRequestCoreType__ !== 'normal') { - if (process && process.env && process.env.NODE_ENV === 'development') { + if (process && process.env && process.env.NODE_ENV === 'development' && warnedCoreType === false) { + warnedCoreType = true; console.warn( - '__umiRequestCoreType__ is a internal property that use in umi-request, change its value would affect the behavior of request! It only use when you want to extend the request core.' + '__umiRequestCoreType__ is a internal property that use in umi-request, change its value would affect the behavior of request! It only use when you want to extend or use request core.' ); } return next(); @@ -23,11 +41,13 @@ export default function fetchMiddleware(ctx, next) { // 从缓存池检查是否有缓存数据 const isBrowser = getEnv() === 'BROWSER'; - const needCache = method.toLowerCase() === 'get' && useCache && isBrowser; + const needCache = validateCache(url, options) && useCache && isBrowser; + console.log('needCache:', needCache, url); if (needCache) { let responseCache = cache.get({ url, params, + method, }); if (responseCache) { responseCache = responseCache.clone(); @@ -56,7 +76,7 @@ export default function fetchMiddleware(ctx, next) { if (res.status === 200) { const copy = res.clone(); copy.useCache = true; - cache.set({ url, params }, copy, ttl); + cache.set({ url, params, method }, copy, ttl); } } diff --git a/src/middleware/simpleGet.js b/src/middleware/simpleGet.js index 2518cbf..ded579b 100644 --- a/src/middleware/simpleGet.js +++ b/src/middleware/simpleGet.js @@ -1,4 +1,4 @@ -import { stringify } from 'query-string'; +import { stringify } from 'qs'; import { isArray, isURLSearchParams, forEach2ObjArr, isObject, isDate } from '../utils'; export function paramsSerialize(params, paramsSerializer) { @@ -20,7 +20,8 @@ export function paramsSerialize(params, paramsSerializer) { jsonStringifiedParams.push(isObject(item) ? JSON.stringify(item) : item); } }); - serializedParams = stringify(jsonStringifiedParams); + // a: [1,2,3] => a=1&a=2&a=3 + serializedParams = stringify(jsonStringifiedParams, { arrayFormat: 'repeat', strictNullHandling: true }); } else { jsonStringifiedParams = {}; forEach2ObjArr(params, function(value, key) { @@ -36,7 +37,7 @@ export function paramsSerialize(params, paramsSerializer) { } jsonStringifiedParams[key] = jsonStringifiedValue; }); - const tmp = stringify(jsonStringifiedParams); + const tmp = stringify(jsonStringifiedParams, { arrayFormat: 'repeat', strictNullHandling: true }); serializedParams = tmp; } } diff --git a/src/middleware/simplePost.js b/src/middleware/simplePost.js index fdb691b..c5ad6bc 100644 --- a/src/middleware/simplePost.js +++ b/src/middleware/simplePost.js @@ -1,4 +1,4 @@ -import { stringify } from 'query-string'; +import { stringify } from 'qs'; // 对请求参数做处理,实现 query 简化、 post 简化 export default function simplePostMiddleware(ctx, next) { diff --git a/src/utils.js b/src/utils.js index 7771e54..2838450 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,7 +2,7 @@ * 实现一个简单的Map cache, 稍后可以挪到 utils中, 提供session local map三种前端cache方式. * 1. 可直接存储对象 2. 内存无5M限制 3.缺点是刷新就没了, 看反馈后期完善. */ -import { parse } from 'query-string'; +import { parse } from 'qs'; export class MapCache { constructor(options) { @@ -172,7 +172,7 @@ export function forEach2ObjArr(target, callback) { export function getParamObject(val) { if (isURLSearchParams(val)) { - return parse(val.toString()); + return parse(val.toString(), { strictNullHandling: true }); } if (typeof val === 'string') { return [val]; diff --git a/test/fetch.test.js b/test/fetch.test.js index 9309aa5..a6e513b 100644 --- a/test/fetch.test.js +++ b/test/fetch.test.js @@ -96,7 +96,7 @@ describe('timeout', () => { await fetch('/api/test', { __umiRequestCoreType__: 'other' }); expect(console.warn.mock.calls.length).toBe(1); expect(console.warn.mock.calls[0][0]).toBe( - '__umiRequestCoreType__ is a internal property that use in umi-request, change its value would affect the behavior of request! It only use when you want to extend the request core.' + '__umiRequestCoreType__ is a internal property that use in umi-request, change its value would affect the behavior of request! It only use when you want to extend or use request core.' ); process.env.NODE_ENV = 'test'; done(); diff --git a/test/index.test.js b/test/index.test.js index a9bd8c8..6506c87 100644 --- a/test/index.test.js +++ b/test/index.test.js @@ -318,6 +318,37 @@ describe('test fetch:', () => { expect(response.data.defaultParams).toBe('true'); }, 10000); + it('test validate cache', async () => { + server.post('/test/validate/cache', (req, res) => { + writeData(req.query, res); + }); + server.get('/test/validate/cache', (req, res) => { + writeData(req.query, res); + }); + const extendRequest = extend({ + maxCache: 2, + prefix: server.url, + headers: { Connection: 'keep-alive' }, + params: { defaultParams: true }, + validateCache: (url, options) => { + const { method = 'get' } = options; + if (method.toLowerCase() === 'post') { + return true; + } + return false; + }, + getResponse: true, + useCache: true, + }); + let response = await extendRequest('/test/validate/cache', { method: 'post' }); + response = await extendRequest('/test/validate/cache', { method: 'post' }); + + expect(response.response.useCache).toBe(true); + + response = await extendRequest('/test/validate/cache', { method: 'get' }); + expect(response.response.useCache).toBe(false); + }); + it('test extends', async () => { server.get('/test/method', (req, res) => { writeData({ method: req.method }, res); diff --git a/test/interceptor.test.js b/test/interceptor.test.js index c991150..e658518 100644 --- a/test/interceptor.test.js +++ b/test/interceptor.test.js @@ -201,4 +201,26 @@ describe('interceptor', () => { expect(data.promiseFoo).toBe('promiseFoo'); done(); }); + + // reject in interceptor + it('throw error in response interceptor', async done => { + server.post('/test/reject/interceptor', (req, res) => { + writeData(req.body, res); + }); + + request.interceptors.response.use((response, options) => { + const { status, url } = response; + console.log('status', status, url); + if (status === 200 && url.indexOf('/test/reject/interceptor')) { + throw Error('reject when response is 200 status'); + } + }); + + try { + const data = await request(prefix('/test/reject/interceptor'), { method: 'post' }); + } catch (e) { + expect(e.message).toBe('reject when response is 200 status'); + done(); + } + }); }); diff --git a/types/index.d.ts b/types/index.d.ts index 0368dbc..8c80f30 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -40,6 +40,7 @@ export interface RequestOptionsInit extends RequestInit { parseResponse?: boolean; cancelToken?: CancelToken; getResponse?: boolean; + validateCache?: (url: string, options: RequestOptionsInit) => boolean; } export interface RequestOptionsWithoutResponse extends RequestOptionsInit { From 3a1e787f693a519902d543000cf5c035883e307f Mon Sep 17 00:00:00 2001 From: chenjsh Date: Wed, 20 Nov 2019 21:04:48 +0800 Subject: [PATCH 54/94] 1.2.10 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 90eda18..c436163 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.9", + "version": "1.2.10", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From e0bb0077be40962bf9ac6497660ebd518f09b1ec Mon Sep 17 00:00:00 2001 From: chenjsh Date: Thu, 21 Nov 2019 22:47:05 +0800 Subject: [PATCH 55/94] fix: qs stringify problem (#75) --- src/middleware/fetch.js | 1 - src/middleware/simpleGet.js | 7 +++---- src/middleware/simplePost.js | 4 ++-- src/utils.js | 6 +++++- test/interceptor.test.js | 1 - test/middleware/simplePost.test.js | 6 ++++-- 6 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/middleware/fetch.js b/src/middleware/fetch.js index 7d02a80..85bbc42 100644 --- a/src/middleware/fetch.js +++ b/src/middleware/fetch.js @@ -42,7 +42,6 @@ export default function fetchMiddleware(ctx, next) { // 从缓存池检查是否有缓存数据 const isBrowser = getEnv() === 'BROWSER'; const needCache = validateCache(url, options) && useCache && isBrowser; - console.log('needCache:', needCache, url); if (needCache) { let responseCache = cache.get({ url, diff --git a/src/middleware/simpleGet.js b/src/middleware/simpleGet.js index ded579b..c67860e 100644 --- a/src/middleware/simpleGet.js +++ b/src/middleware/simpleGet.js @@ -1,5 +1,4 @@ -import { stringify } from 'qs'; -import { isArray, isURLSearchParams, forEach2ObjArr, isObject, isDate } from '../utils'; +import { isArray, isURLSearchParams, forEach2ObjArr, isObject, isDate, reqStringify } from '../utils'; export function paramsSerialize(params, paramsSerializer) { let serializedParams; @@ -21,7 +20,7 @@ export function paramsSerialize(params, paramsSerializer) { } }); // a: [1,2,3] => a=1&a=2&a=3 - serializedParams = stringify(jsonStringifiedParams, { arrayFormat: 'repeat', strictNullHandling: true }); + serializedParams = reqStringify(jsonStringifiedParams, { arrayFormat: 'repeat', strictNullHandling: true }); } else { jsonStringifiedParams = {}; forEach2ObjArr(params, function(value, key) { @@ -37,7 +36,7 @@ export function paramsSerialize(params, paramsSerializer) { } jsonStringifiedParams[key] = jsonStringifiedValue; }); - const tmp = stringify(jsonStringifiedParams, { arrayFormat: 'repeat', strictNullHandling: true }); + const tmp = reqStringify(jsonStringifiedParams, { arrayFormat: 'repeat', strictNullHandling: true }); serializedParams = tmp; } } diff --git a/src/middleware/simplePost.js b/src/middleware/simplePost.js index c5ad6bc..4ce5549 100644 --- a/src/middleware/simplePost.js +++ b/src/middleware/simplePost.js @@ -1,4 +1,4 @@ -import { stringify } from 'qs'; +import { reqStringify } from '../utils'; // 对请求参数做处理,实现 query 简化、 post 简化 export default function simplePostMiddleware(ctx, next) { @@ -28,7 +28,7 @@ export default function simplePostMiddleware(ctx, next) { 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', ...options.headers, }; - options.body = stringify(data); + options.body = reqStringify(data, { arrayFormat: 'repeat', strictNullHandling: true }); } } else { // 其他 requestType 自定义header diff --git a/src/utils.js b/src/utils.js index 2838450..eb3a981 100644 --- a/src/utils.js +++ b/src/utils.js @@ -2,7 +2,7 @@ * 实现一个简单的Map cache, 稍后可以挪到 utils中, 提供session local map三种前端cache方式. * 1. 可直接存储对象 2. 内存无5M限制 3.缺点是刷新就没了, 看反馈后期完善. */ -import { parse } from 'qs'; +import { parse, stringify } from 'qs'; export class MapCache { constructor(options) { @@ -179,3 +179,7 @@ export function getParamObject(val) { } return val; } + +export function reqStringify(val) { + return stringify(val, { arrayFormat: 'repeat', strictNullHandling: true }); +} diff --git a/test/interceptor.test.js b/test/interceptor.test.js index e658518..7552bc4 100644 --- a/test/interceptor.test.js +++ b/test/interceptor.test.js @@ -210,7 +210,6 @@ describe('interceptor', () => { request.interceptors.response.use((response, options) => { const { status, url } = response; - console.log('status', status, url); if (status === 200 && url.indexOf('/test/reject/interceptor')) { throw Error('reject when response is 200 status'); } diff --git a/test/middleware/simplePost.test.js b/test/middleware/simplePost.test.js index 51c72d8..c0f8ae0 100644 --- a/test/middleware/simplePost.test.js +++ b/test/middleware/simplePost.test.js @@ -1,5 +1,5 @@ import simplePost from '../../src/middleware/simplePost'; - +import querystring from 'query-string'; const next = () => {}; describe('test simplePost middleware', () => { @@ -16,7 +16,7 @@ describe('test simplePost middleware', () => { options: { method: 'post', requestType: 'form', - data: { a: 1 }, + data: { a: 1, b: [1, 2, 3, 4] }, }, }, }; @@ -25,6 +25,8 @@ describe('test simplePost middleware', () => { Accept: 'application/json', 'Content-Type': 'application/x-www-form-urlencoded;charset=UTF-8', }); + expect(ctx.req.options.body).toBe(querystring.stringify(ctx.req.options.data)); + expect(querystring.stringify(ctx.req.options.data)).toBe('a=1&b=1&b=2&b=3&b=4'); done(); }); }); From eb7ed77ad69012bf21c1ecd756f0ecdca9e6c521 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Thu, 21 Nov 2019 22:47:50 +0800 Subject: [PATCH 56/94] 1.2.11 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c436163..ca9f8a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.10", + "version": "1.2.11", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From b9b76a2ab4eb00b3b2b751bd2a9a01a24a999702 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Thu, 5 Dec 2019 11:53:53 +0800 Subject: [PATCH 57/94] feat: provide 'extendOptions()' (#80) * feat: provide 'extendOptions()' --- README.md | 76 ++++++++++++++++++-------------- README_zh-CN.md | 52 +++++++++++++++++----- package.json | 1 + src/core.js | 21 +++++---- src/request.js | 18 ++------ src/utils.js | 20 +++++++++ test/extend/extendOption.test.js | 52 ++++++++++++++++++++++ types/index.d.ts | 3 ++ 8 files changed, 177 insertions(+), 66 deletions(-) create mode 100644 test/extend/extendOption.test.js diff --git a/README.md b/README.md index 69e1400..76dc206 100644 --- a/README.md +++ b/README.md @@ -88,6 +88,7 @@ request.get('/api/v1/xxx', { ``` Performing a ```POST``` request + ``` javascript request.post('/api/v1/user', { data: { @@ -103,9 +104,11 @@ request.post('/api/v1/user', { ``` ## umi-request API + Requests can be made by passing relevant options to ```umi-request``` **umi-request(url[, options])** + ```javascript import request from 'umi-request'; @@ -136,6 +139,7 @@ request('/api/v1/user', { ``` ## Request method aliases + For convenience umi-request have been provided for all supported methods. **request.get(url[, options])** @@ -152,8 +156,8 @@ For convenience umi-request have been provided for all supported methods. **request.options(url[, options])** - ## Creating an instance + You can use ```extend({[options]})``` to create a new instance of umi-request. **extend([options])** @@ -211,7 +215,6 @@ The available instance methods are list below. The specified options will be mer 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 | @@ -248,7 +251,6 @@ The other parameters of fetch are valid. See [fetch documentation](https://githu | data | Submitted data | any | -- | -- | | ... | - ``` javascript { // 'method' is the request method to be used when making the request @@ -364,11 +366,21 @@ The other parameters of fetch are valid. See [fetch documentation](https://githu } ``` +### 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 @@ -410,7 +422,6 @@ request.get('/api/v1/xxx', { getResponse: true }) You can get Response from ```error``` object in errorHandler or request.catch. - ## Error handling ```javascript @@ -430,7 +441,6 @@ const errorHandler = function (error) { 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); @@ -460,8 +470,8 @@ request('/api/v1/xxx') }) ``` - ## 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. @@ -471,17 +481,21 @@ Expressive HTTP middleware framework for node.js. For development to enhance bef 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) => { @@ -499,12 +513,13 @@ 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'); @@ -529,11 +544,13 @@ request.use( async (ctx, next) => { ``` 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; @@ -559,6 +576,7 @@ request.use(async (ctx, next) => { ``` 4. Use core middleware to expand request core. + ``` javascript request.use(async (ctx, next) => { @@ -596,11 +614,12 @@ request('/api/v1/rpc', { ``` - ## 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) => { @@ -650,6 +669,7 @@ request.interceptors.response.use(async (response) => { ``` 1. instance Interceptor + ``` javascript // Global interceptors are used `request` instance method directly request.interceptors.request.use((url, options) => { @@ -685,13 +705,15 @@ clientB.interceptors.request.use((url, options) => { ``` ## 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) { @@ -707,48 +729,34 @@ Request.post('/api/cancel', { }, { 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(); ``` -## 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}) => { @@ -756,18 +764,20 @@ request('/api/v1/some/api', { getResponse: true }) }) ``` +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 diff --git a/README_zh-CN.md b/README_zh-CN.md index 85ddb34..ea06954 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -140,6 +140,7 @@ request('/api/v1/user', { ``` ## 请求方法的别名 + 为了方便起见,为所有支持的请求方法提供了别名, ```method``` 属性不必在配置中指定 **request.get(url[, options])** @@ -156,8 +157,8 @@ request('/api/v1/user', { **request.options(url[, options])** - ## 创建实例 + 有些通用的配置我们不想每个请求里都去添加,那么可以通过 ```extend``` 新建一个 umi-request 实例 **extend([options])** @@ -245,7 +246,6 @@ umi-request 可以进行一层简单封装后再使用, 可参考 [antd-pro](htt | errorHandler | 异常处理, 或者覆盖统一的异常处理 | function(error) | -- | | cancelToken | 取消请求的 Token | CancelToken.token | -- | -- | - fetch原其他参数有效, 详见[fetch文档](https://github.github.io/fetch/) ### extend options 初始化默认参数, 支持以上所有 @@ -361,10 +361,23 @@ fetch原其他参数有效, 详见[fetch文档](https://github.github.io/fetch/) } ``` +### 更新拓展实例默认参数 + +实例化一个请求实例后,有时还需要动态更新默认参数,umi-request 提供 **extendOptions** 方法供用户进行更新: + +```javascript +const request = extend({ timeout: 1000, params: { a: '1' }}) +// 默认参数是 { timeout: 1000, params: { a: '1' }} + +request.extendOptions({ timeout: 3000, params: { b: '2' }}) +// 此时默认参数是 { timeout: 3000, params: { a: '1', b: '2' }} + +``` ## 响应结构 某个请求的响应返回的响应对象 Response 如下: + ``` javascript { // `data` 由服务器提供的响应, 需要进行解析才能获取 @@ -405,7 +418,6 @@ request.get('/api/v1/xxx', { getResponse: true }) 在使用 catch 或者 errorHandler, 响应对象可以通过 ```error``` 对象获取使用,参考**错误处理**这一节文档。 - ## 错误处理 ``` javascript @@ -465,20 +477,24 @@ request('/api/v1/xxx') **内核中间件** :request.use(fn, { core: true }) 内核中间件, 方便开发者拓展请求内核; - request.use(fn[, options]) ### 参数 + fn 入参 + * ctx(Object):上下文对象,包括req和res对象 * next(Function):调用下一个中间件的函数 options 参数 + * global(boolean): 是否为全局中间件,优先级比 core 高 * core(boolean): 是否为内核中间件 ### 例子 + 1. 同类型中间件执行顺序 + ``` javascript import request, { extend } from 'umi-request'; request.use(async (ctx, next) => { @@ -496,11 +512,13 @@ const data = await request('/api/v1/a'); ``` 执行顺序如下: -``` + +``` shell a1 -> b1 -> response -> b2 -> a2 ``` 2. 不同类型中间件执行顺序 + ``` javascript request.use( async (ctx, next) => { console.log('instanceA1'); @@ -525,11 +543,13 @@ request.use( async (ctx, next) => { ``` 执行顺序如下: -``` + +``` shell instanceA1 -> instanceB1 -> globalA1 -> coreA1 -> coreA2 -> globalA2 -> instanceB2 -> instanceA2 ``` 3. 使用中间件对请求前后做处理 + ``` javascript request.use(async (ctx, next) => { const { req } = ctx; @@ -556,6 +576,7 @@ request.use(async (ctx, next) => { ``` 4. 使用内核中间件拓展请求能力 + ``` javascript request.use(async (ctx, next) => { @@ -595,11 +616,12 @@ request('/api/v1/rpc', { ``` - ## 拦截器 + 在请求或响应被 ```then``` 或 ```catch``` 处理前拦截它们。 1. 全局拦截器 + ``` javascript // request拦截器, 改变url 或 options. request.interceptors.request.use((url, options) => { @@ -624,7 +646,7 @@ request.interceptors.request.use((url, options) => { // response拦截器, 处理response request.interceptors.response.use((response, options) => { - response.headers.append('interceptors', 'yes yo'); + const contentType = response.headers.get('Content-Type'); return response; }); @@ -650,6 +672,7 @@ request.interceptors.response.use(async (response) => { ``` 2. 实例内部拦截器 + ``` javascript // 全局拦截器直接使用 request 实例中的方法 request.interceptors.request.use((url, options) => { @@ -685,16 +708,18 @@ clientB.interceptors.request.use((url, options) => { ``` ## 取消请求 + 你可以通过 **cancel token** 来取消一个请求 > cancel token API 是基于已被撤销的 [cancelable-promises 方案](https://github.com/tc39/proposal-cancelable-promises) 1. 你可以通过 **CancelToken.source** 来创建一个 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) { @@ -710,19 +735,20 @@ Request.post('/api/cancel', { }, { cancelToken: token }) - + // 取消请求(参数为非必填) cancel('Operation canceled by the user.'); ``` 2. 你也可以通过实例化 CancelToken 来创建一个 token,同时通过传入函数来获取取消方法: + ```javascript import Request from 'umi-request'; const CancelToken = Request.CancelToken; let cancel; - + Request.get('/api/cancel', { cancelToken: new CancelToken(function executor(c) { cancel = c; @@ -733,9 +759,11 @@ cancel(); ``` ## 案例 + ### 如何获取响应头信息 通过 **Headers.get()** 获取响应头信息。(可参考 [MDN 文档](https://developer.mozilla.org/zh-CN/docs/Web/API/Headers/get)) + ``` javascript request('/api/v1/some/api', { getResponse: true }) .then(({ data, response}) => { @@ -744,7 +772,9 @@ request('/api/v1/some/api', { getResponse: true }) ``` ### 文件上传 + 使用 FormData() 构造函数时,浏览器会自动识别并添加请求头 ```"Content-Type: multipart/form-data"```, 且参数依旧是表单提交时那种键值对,因此不需要开发者手动设置 **Content-Type** + ``` javascript const formData = new FormData(); formData.append('file', file); diff --git a/package.json b/package.json index ca9f8a6..356db24 100644 --- a/package.json +++ b/package.json @@ -35,6 +35,7 @@ "iconv-lite": "^0.4.24", "jest": "^23.5.0", "np": "5.0.2", + "query-string": "^6.9.0", "typescript": "^3.0.3", "umi": "^2.8.15", "umi-lint": "^1.0.0-alpha.1", diff --git a/src/core.js b/src/core.js index 8370592..ff3f41d 100644 --- a/src/core.js +++ b/src/core.js @@ -1,5 +1,5 @@ import Onion from './onion'; -import { MapCache } from './utils'; +import { MapCache, mergeRequestOptions } from './utils'; import addfixInterceptor from './interceptor/addfix'; import fetchMiddleware from './middleware/fetch'; import parseResponseMiddleware from './middleware/parseResponse'; @@ -28,15 +28,10 @@ class Core { static requestInterceptors = [addfixInterceptor]; static responseInterceptors = []; - use(newMiddleware, opt = { global: false, core: false }) { - this.onion.use(newMiddleware, opt); - return this; - } - // 请求拦截器 默认 { global: true } 兼容旧版本拦截器 static requestUse(handler, opt = { global: true }) { if (typeof handler !== 'function') throw new TypeError('Interceptor must be function!'); - if(opt.global){ + if (opt.global) { Core.requestInterceptors.push(handler); } else { this.instanceRequestInterceptors.push(handler); @@ -46,13 +41,23 @@ class Core { // 响应拦截器 默认 { global: true } 兼容旧版本拦截器 static responseUse(handler, opt = { global: true }) { if (typeof handler !== 'function') throw new TypeError('Interceptor must be function!'); - if(opt.global){ + if (opt.global) { Core.responseInterceptors.push(handler); } else { this.instanceResponseInterceptors.push(handler); } } + use(newMiddleware, opt = { global: false, core: false }) { + this.onion.use(newMiddleware, opt); + return this; + } + + extendOptions(options) { + this.initOptions = mergeRequestOptions(this.initOptions, options); + this.mapCache.extendOptions(options); + } + // 执行请求前拦截器 dealRequestInterceptors(ctx) { const reducer = (p1, p2) => diff --git a/src/request.js b/src/request.js index 486f2aa..7f395b3 100644 --- a/src/request.js +++ b/src/request.js @@ -2,25 +2,13 @@ import Core from './core'; import Cancel from './cancel/cancel'; import CancelToken from './cancel/cancelToken'; import isCancel from './cancel/isCancel'; -import { getParamObject } from './utils'; +import { getParamObject, mergeRequestOptions } from './utils'; // 通过 request 函数,在 core 之上再封装一层,提供原 umi/request 一致的 api,无缝升级 const request = (initOptions = {}) => { const coreInstance = new Core(initOptions); const umiInstance = (url, options = {}) => { - const mergeOptions = { - ...initOptions, - ...options, - headers: { - ...initOptions.headers, - ...options.headers, - }, - params: { - ...getParamObject(initOptions.params), - ...getParamObject(options.params), - }, - method: (options.method || initOptions.method || 'get').toLowerCase(), - }; + const mergeOptions = mergeRequestOptions(coreInstance.initOptions, options); return coreInstance.request(url, mergeOptions); }; @@ -48,6 +36,8 @@ const request = (initOptions = {}) => { umiInstance.CancelToken = CancelToken; umiInstance.isCancel = isCancel; + umiInstance.extendOptions = coreInstance.extendOptions.bind(coreInstance); + return umiInstance; }; diff --git a/src/utils.js b/src/utils.js index eb3a981..7063827 100644 --- a/src/utils.js +++ b/src/utils.js @@ -8,6 +8,10 @@ export class MapCache { constructor(options) { this.cache = new Map(); this.timer = {}; + this.extendOptions(options); + } + + extendOptions(options) { this.maxCache = options.maxCache || 0; } @@ -183,3 +187,19 @@ export function getParamObject(val) { export function reqStringify(val) { return stringify(val, { arrayFormat: 'repeat', strictNullHandling: true }); } + +export function mergeRequestOptions(options, options2Merge) { + return { + ...options, + ...options2Merge, + headers: { + ...options.headers, + ...options2Merge.headers, + }, + params: { + ...getParamObject(options.params), + ...getParamObject(options2Merge.params), + }, + method: (options.method || options2Merge.method || 'get').toLowerCase(), + }; +} diff --git a/test/extend/extendOption.test.js b/test/extend/extendOption.test.js new file mode 100644 index 0000000..8ce34e6 --- /dev/null +++ b/test/extend/extendOption.test.js @@ -0,0 +1,52 @@ +import createTestServer from 'create-test-server'; +import request, { extend, Onion, fetch } from '../../src/index'; + +const debug = require('debug')('afx-request:test'); + +const writeData = (data, res) => { + res.setHeader('access-control-allow-origin', '*'); + res.send(data); +}; + +describe('extendOption', () => { + let server; + beforeAll(async () => { + server = await createTestServer(); + }); + afterAll(() => { + server.close(); + }); + + const prefix = api => `${server.url}${api}`; + + it('should update option', async () => { + server.get('/test/extendoptions', (req, res) => { + writeData({ ...req.query, ...req.headers }, res); + }); + const req = extend({ + timeout: 1000, + headers: { + traceId: '10000', + }, + params: { + parama: 'a', + }, + }); + req.extendOptions({ + timeout: 2000, + headers: { + traceId: '20000', + hahah: '2222', + }, + params: { + paramb: 'b', + }, + }); + + const res = await req(prefix('/test/extendoptions')); + expect(res.parama).toBe('a'); + expect(res.paramb).toBe('b'); + expect(res.traceid).toBe('20000'); + expect(res.hahah).toBe('2222'); + }); +}); diff --git a/types/index.d.ts b/types/index.d.ts index 8c80f30..bd16f10 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -41,6 +41,7 @@ export interface RequestOptionsInit extends RequestInit { cancelToken?: CancelToken; getResponse?: boolean; validateCache?: (url: string, options: RequestOptionsInit) => boolean; + __umiRequestCoreType__?: string; } export interface RequestOptionsWithoutResponse extends RequestOptionsInit { @@ -102,6 +103,7 @@ export interface RequestMethod { Cancel: CancelStatic; CancelToken: CancelTokenStatic; isCancel(value: any): boolean; + extendOptions: (options: RequestOptionsInit) => void; } export interface ExtendOnlyOptions { @@ -151,5 +153,6 @@ export interface CancelTokenSource { } declare var request: RequestMethod; +declare var fetch: RequestMethod; export default request; From 8131592495ece140709db1dc2a7ba38498f12cec Mon Sep 17 00:00:00 2001 From: chenjsh Date: Thu, 5 Dec 2019 14:16:21 +0800 Subject: [PATCH 58/94] 1.2.12 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 356db24..c4fc206 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.11", + "version": "1.2.12", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From 8b38e1c32c0c336ef5a0efc6c1234153dca58b9e Mon Sep 17 00:00:00 2001 From: chenjsh Date: Mon, 9 Dec 2019 16:49:44 +0800 Subject: [PATCH 59/94] 1.2.13 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c4fc206..d22fec0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.12", + "version": "1.2.13", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From b519983eedd12ee3a9fc54e10bb367a907a4da62 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Mon, 9 Dec 2019 18:04:00 +0800 Subject: [PATCH 60/94] fix: 'Cannot clone a disturbed Response' bug in ios 10 (#82) --- src/middleware/fetch.js | 3 +++ test/interceptor.test.js | 44 +++++++++++++++++++++++++++++++++------- 2 files changed, 40 insertions(+), 7 deletions(-) diff --git a/src/middleware/fetch.js b/src/middleware/fetch.js index 85bbc42..890d1d6 100644 --- a/src/middleware/fetch.js +++ b/src/middleware/fetch.js @@ -64,6 +64,9 @@ export default function fetchMiddleware(ctx, next) { response = Promise.race([cancel2Throw(options, ctx), adapter(url, options)]); } + // Fix multiple clones not working, issue: https://github.com/github/fetch/issues/504 + response = response.clone ? response.clone() : response; + // 兼容老版本 response.interceptor responseInterceptors.forEach(handler => { response = response.then(res => handler(res, options)); diff --git a/test/interceptor.test.js b/test/interceptor.test.js index 7552bc4..37b7b4c 100644 --- a/test/interceptor.test.js +++ b/test/interceptor.test.js @@ -208,18 +208,48 @@ describe('interceptor', () => { writeData(req.body, res); }); - request.interceptors.response.use((response, options) => { - const { status, url } = response; - if (status === 200 && url.indexOf('/test/reject/interceptor')) { - throw Error('reject when response is 200 status'); - } - }); + const req = extend({}); + + req.interceptors.response.use( + (response, options) => { + const { status, url } = response; + if (status === 200 && url.indexOf('/test/reject/interceptor')) { + throw Error('reject when response is 200 status'); + } + }, + { global: false } + ); try { - const data = await request(prefix('/test/reject/interceptor'), { method: 'post' }); + const data = await req(prefix('/test/reject/interceptor'), { method: 'post' }); } catch (e) { expect(e.message).toBe('reject when response is 200 status'); done(); } }); + + // clone response + it('should throw error when reponse.clone().json() result is fail', async done => { + server.post('/test/multiple/clone/response', (req, res) => { + writeData({ result: { success: false } }, res); + }); + const req = extend({}); + req.interceptors.response.use( + async (response, options) => { + const { result } = await response.clone().json(); + if (result && result.success === false) { + throw Error('reject when response is fail'); + } + return response; + }, + { global: false } + ); + + try { + const data = await req(prefix('/test/multiple/clone/response'), { method: 'post' }); + } catch (e) { + expect(e.message).toBe('reject when response is fail'); + done(); + } + }); }); From d6a064e59735bf6c96bd9adb0b6d5fc736d9404f Mon Sep 17 00:00:00 2001 From: chenjsh Date: Mon, 9 Dec 2019 19:19:05 +0800 Subject: [PATCH 61/94] fix: multiple clone response on ios.10 will throw error (#83) --- src/middleware/fetch.js | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/middleware/fetch.js b/src/middleware/fetch.js index 890d1d6..8d577f7 100644 --- a/src/middleware/fetch.js +++ b/src/middleware/fetch.js @@ -64,12 +64,13 @@ export default function fetchMiddleware(ctx, next) { response = Promise.race([cancel2Throw(options, ctx), adapter(url, options)]); } - // Fix multiple clones not working, issue: https://github.com/github/fetch/issues/504 - response = response.clone ? response.clone() : response; - // 兼容老版本 response.interceptor responseInterceptors.forEach(handler => { - response = response.then(res => handler(res, options)); + response = response.then(res => { + // Fix multiple clones not working, issue: https://github.com/github/fetch/issues/504 + let clonedRes = typeof res.clone === 'function' ? res.clone() : res; + return handler(clonedRes, options); + }); }); return response.then(res => { From 6374e47fd307db09c0071553cfeab72c1c2b9646 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Mon, 9 Dec 2019 19:20:14 +0800 Subject: [PATCH 62/94] 1.2.14 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index d22fec0..5da9d28 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.13", + "version": "1.2.14", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From 93377bc6848f27775a016b19936e2e761ff48a93 Mon Sep 17 00:00:00 2001 From: DiamondYuan Date: Thu, 12 Dec 2019 18:49:52 +0800 Subject: [PATCH 63/94] fix: update type of interceptors (#89) --- types/index.d.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/types/index.d.ts b/types/index.d.ts index bd16f10..bcd07da 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -92,10 +92,10 @@ export interface RequestMethod { rpc: RequestMethod; interceptors: { request: { - use: (handler: RequestInterceptor) => void; + use: (handler: RequestInterceptor, options?: OnionOptions) => void; }; response: { - use: (handler: ResponseInterceptor) => void; + use: (handler: ResponseInterceptor, options?: OnionOptions) => void; }; }; use: (handler: OnionMiddleware, options?: OnionOptions) => void; From b52f2eab91a04f6363da4255b9dc8bc69bdfae3c Mon Sep 17 00:00:00 2001 From: chenjsh Date: Thu, 12 Dec 2019 19:21:52 +0800 Subject: [PATCH 64/94] 1.2.15 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 5da9d28..24eaa2f 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.14", + "version": "1.2.15", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From fb0137dbec254e48b7d35e2e0844df066b960387 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Tue, 24 Dec 2019 16:58:59 +0800 Subject: [PATCH 65/94] Fix/catch reject from fetch (#95) * feat: catch reject from fetch * feat: add property to ResponseError --- src/middleware/parseResponse.js | 20 ++++++++++++---- src/utils.js | 10 ++++---- test/errorHandler.test.js | 41 +++++++++++++++++++++++++++++++++ types/index.d.ts | 4 +++- 4 files changed, 66 insertions(+), 9 deletions(-) create mode 100644 test/errorHandler.test.js diff --git a/src/middleware/parseResponse.js b/src/middleware/parseResponse.js index 4556805..1b4eaad 100644 --- a/src/middleware/parseResponse.js +++ b/src/middleware/parseResponse.js @@ -1,4 +1,4 @@ -import { safeJsonParse, readerGBK, ResponseError, getEnv } from '../utils'; +import { safeJsonParse, readerGBK, ResponseError, getEnv, RequestError } from '../utils'; export default function parseResponseMiddleware(ctx, next) { let copy; @@ -37,7 +37,7 @@ export default function parseResponseMiddleware(ctx, next) { .then(readerGBK) .then(d => safeJsonParse(d, false, copy, req)); } catch (e) { - throw new ResponseError(copy, e.message, null, req); + throw new ResponseError(copy, e.message, null, req, 'ParseError'); } } else if (responseType === 'json') { return res.text().then(d => safeJsonParse(d, throwErrIfParseFail, copy, req)); @@ -46,7 +46,7 @@ export default function parseResponseMiddleware(ctx, next) { // 其他如text, blob, arrayBuffer, formData return res[responseType](); } catch (e) { - throw new ResponseError(copy, 'responseType not support', null, req); + throw new ResponseError(copy, 'responseType not support', null, req, 'ParseError'); } }) .then(body => { @@ -66,6 +66,18 @@ export default function parseResponseMiddleware(ctx, next) { ctx.res = body; return; } - throw new ResponseError(copy, 'http error', body, req); + throw new ResponseError(copy, 'http error', body, req, 'HttpError'); + }) + .catch(e => { + if (e instanceof RequestError || e instanceof ResponseError) { + throw e; + } + // 对未知错误进行处理 + const { req, res } = ctx; + e.request = req; + e.response = res; + e.type = e.name; + e.data = undefined; + throw e; }); } diff --git a/src/utils.js b/src/utils.js index 7063827..32de3a4 100644 --- a/src/utils.js +++ b/src/utils.js @@ -54,10 +54,11 @@ export class MapCache { * 请求异常 */ export class RequestError extends Error { - constructor(text, request) { + constructor(text, request, type = 'RequestError') { super(text); this.name = 'RequestError'; this.request = request; + this.type = type; } } @@ -65,12 +66,13 @@ export class RequestError extends Error { * 响应异常 */ export class ResponseError extends Error { - constructor(response, text, data, request) { + constructor(response, text, data, request, type = 'ResponseError') { super(text || response.statusText); this.name = 'ResponseError'; this.data = data; this.response = response; this.request = request; + this.type = type; } } @@ -97,7 +99,7 @@ export function safeJsonParse(data, throwErrIfParseFail = false, response = null return JSON.parse(data); } catch (e) { if (throwErrIfParseFail) { - throw new ResponseError(response, 'JSON.parse fail', data, request); + throw new ResponseError(response, 'JSON.parse fail', data, request, 'ParseError'); } } // eslint-disable-line no-empty return data; @@ -106,7 +108,7 @@ export function safeJsonParse(data, throwErrIfParseFail = false, response = null export function timeout2Throw(msec, request) { return new Promise((_, reject) => { setTimeout(() => { - reject(new RequestError(`timeout of ${msec}ms exceeded`, request)); + reject(new RequestError(`timeout of ${msec}ms exceeded`, request, 'Timeout')); }, msec); }); } diff --git a/test/errorHandler.test.js b/test/errorHandler.test.js new file mode 100644 index 0000000..d452067 --- /dev/null +++ b/test/errorHandler.test.js @@ -0,0 +1,41 @@ +import createTestServer from 'create-test-server'; +import request, { extend, Onion, fetch } from '../src/index'; + +const debug = require('debug')('afx-request:test'); + +const writeData = (data, res) => { + res.setHeader('access-control-allow-origin', '*'); + res.send(data); +}; + +describe('error handle', () => { + let server; + beforeAll(async () => { + server = await createTestServer(); + }); + afterAll(() => { + server.close(); + }); + + const prefix = api => `${server.url}${api}`; + + it('should catch fetch error and get reponse', async done => { + server.get('/test/reject/302', (req, res) => { + res.setHeader('access-control-allow-origin', '*'); + res.status(302); + res.setHeader({ location: 'https://www.baidu.com' }); + res.send({ errorMsg: 'error response', errorCode: 'B000' }); + }); + + const req = extend({}); + + try { + const res = await req(prefix('/test/reject/302')); + } catch (e) { + expect(e.message).toBe('http error'); + expect(e.type).toBe('HttpError'); + expect(e.response.headers.get('Content-Type')).toBe('text/html; charset=utf-8'); + done(); + } + }); +}); diff --git a/types/index.d.ts b/types/index.d.ts index bcd07da..524e48a 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -7,6 +7,7 @@ export interface ResponseError extends Error { url: string; options: RequestOptionsInit; }; + type: string; } /** * 增加的参数 @@ -153,6 +154,7 @@ export interface CancelTokenSource { } declare var request: RequestMethod; -declare var fetch: RequestMethod; + +export declare var fetch: RequestMethod; export default request; From 5db96ac95cbaf159b863130c237ba3e0e982993f Mon Sep 17 00:00:00 2001 From: chenjsh Date: Tue, 24 Dec 2019 17:01:10 +0800 Subject: [PATCH 66/94] 1.2.16 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 24eaa2f..ad5b807 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.15", + "version": "1.2.16", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From ed430f87e01d8eaa67bf0ef9eb9b52cb3c3ed6f8 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Thu, 26 Dec 2019 16:34:35 +0800 Subject: [PATCH 67/94] fix: extendOptions fail on method (#97) --- src/utils.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/utils.js b/src/utils.js index 32de3a4..09e152a 100644 --- a/src/utils.js +++ b/src/utils.js @@ -202,6 +202,6 @@ export function mergeRequestOptions(options, options2Merge) { ...getParamObject(options.params), ...getParamObject(options2Merge.params), }, - method: (options.method || options2Merge.method || 'get').toLowerCase(), + method: (options2Merge.method || options.method || 'get').toLowerCase(), }; } From 2fe1f9baa1f0ca8085765d2bcc0524a1e9072821 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Thu, 26 Dec 2019 16:37:06 +0800 Subject: [PATCH 68/94] 1.2.17 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index ad5b807..0854163 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.16", + "version": "1.2.17", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From e8253139ef70c51618bad8b4a9503cf8491bebbb Mon Sep 17 00:00:00 2001 From: chenjsh Date: Mon, 20 Jan 2020 15:05:26 +0800 Subject: [PATCH 69/94] feat: add defaultInstance option for middlewares (#102) --- src/onion/index.js | 22 ++++++++++++++++++---- src/request.js | 9 +++++++++ test/middleware.test.js | 40 ++++++++++++++++++++++++++++++++++++++++ types/index.d.ts | 8 +++++++- 4 files changed, 74 insertions(+), 5 deletions(-) diff --git a/src/onion/index.js b/src/onion/index.js index 4a8dd2c..57e505c 100644 --- a/src/onion/index.js +++ b/src/onion/index.js @@ -4,8 +4,8 @@ import compose from './compose'; class Onion { constructor(defaultMiddlewares) { if (!Array.isArray(defaultMiddlewares)) throw new TypeError('Default middlewares must be an array!'); - - this.middlewares = [...defaultMiddlewares]; + this.defaultMiddlewares = [...defaultMiddlewares]; + this.middlewares = []; } static globalMiddlewares = []; // 全局中间件 @@ -13,9 +13,11 @@ class Onion { static coreMiddlewares = []; // 内核中间件 static defaultCoreMiddlewaresLength = 0; // 内置内核中间件长度 - use(newMiddleware, opts = { global: false, core: false }) { + use(newMiddleware, opts = { global: false, core: false, defaultInstance: false }) { let core = false; let global = false; + let defaultInstance = false; + if (typeof opts === 'number') { if (process && process.env && process.env.NODE_ENV === 'development') { console.warn( @@ -27,6 +29,7 @@ class Onion { } else if (typeof opts === 'object' && opts) { global = opts.global || false; core = opts.core || false; + defaultInstance = opts.defaultInstance || false; } // 全局中间件 @@ -44,12 +47,23 @@ class Onion { return; } + // 默认实例中间件,供开发者使用 + if (defaultInstance) { + this.defaultMiddlewares.push(newMiddleware); + return; + } + // 实例中间件 this.middlewares.push(newMiddleware); } execute(params = null) { - const fn = compose([...this.middlewares, ...Onion.globalMiddlewares, ...Onion.coreMiddlewares]); + const fn = compose([ + ...this.middlewares, + ...this.defaultMiddlewares, + ...Onion.globalMiddlewares, + ...Onion.coreMiddlewares, + ]); return fn(params); } } diff --git a/src/request.js b/src/request.js index 7f395b3..fedbcd3 100644 --- a/src/request.js +++ b/src/request.js @@ -2,6 +2,7 @@ import Core from './core'; import Cancel from './cancel/cancel'; import CancelToken from './cancel/cancelToken'; import isCancel from './cancel/isCancel'; +import Oinon from './onion'; import { getParamObject, mergeRequestOptions } from './utils'; // 通过 request 函数,在 core 之上再封装一层,提供原 umi/request 一致的 api,无缝升级 @@ -38,6 +39,14 @@ const request = (initOptions = {}) => { umiInstance.extendOptions = coreInstance.extendOptions.bind(coreInstance); + // 暴露各个实例的中间件,供开发者自由组合 + umiInstance.middlewares = { + instance: coreInstance.onion.middlewares, + defaultInstance: coreInstance.onion.defaultMiddlewares, + global: Oinon.globalMiddlewares, + core: Oinon.coreMiddlewares, + }; + return umiInstance; }; diff --git a/test/middleware.test.js b/test/middleware.test.js index 5b21f4c..d4135be 100644 --- a/test/middleware.test.js +++ b/test/middleware.test.js @@ -22,6 +22,46 @@ describe('middleware', () => { const prefix = api => `${server.url}${api}`; + describe('use defaultInstance middlewares', () => { + it('default middleware should excute after instance middleware', async done => { + jest.spyOn(console, 'log'); + process.env.NODE_ENV = 'development'; + const req = extend({}); + server.get('/test/defaultMiddleware', (req, res) => { + writeData(req.body, res); + }); + + req.use( + async (ctx, next) => { + console.log('default a'); + await next(); + console.log('default b'); + }, + { defaultInstance: true } + ); + + req.use(async (ctx, next) => { + console.log('instance a'); + await next(); + console.log('instance b'); + }); + expect(console.log.mock.calls.length).toBe(0); + const data = await req( + prefix('/test/defaultMiddleware', (req, res) => { + writeData(req.body, res); + }) + ); + expect(console.log.mock.calls.length).toBe(4); + expect(console.log.mock.calls[0][0]).toBe('instance a'); + expect(console.log.mock.calls[1][0]).toBe('default a'); + expect(console.log.mock.calls[2][0]).toBe('default b'); + expect(console.log.mock.calls[3][0]).toBe('instance b'); + expect(req.middlewares.defaultInstance.length).toBe(1); + expect(req.middlewares.instance.length).toBe(1); + done(); + }); + }); + describe('use middleware to modify request data and response data', () => { it('response should be { hello: "hello", foo: "foo" }', async done => { server.post('/test/promiseInterceptors/a/b', (req, res) => { diff --git a/types/index.d.ts b/types/index.d.ts index 524e48a..4fd08c5 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -77,7 +77,7 @@ export interface Context { export type ResponseInterceptor = (response: Response, options: RequestOptionsInit) => Response | Promise; export type OnionMiddleware = (ctx: Context, next: () => void) => void; -export type OnionOptions = { global?: boolean; core?: boolean }; +export type OnionOptions = { global?: boolean; core?: boolean; defaultInstance?: boolean }; export interface RequestMethod { (url: string, options: RequestOptionsWithResponse): Promise>; @@ -105,6 +105,12 @@ export interface RequestMethod { CancelToken: CancelTokenStatic; isCancel(value: any): boolean; extendOptions: (options: RequestOptionsInit) => void; + middlewares: { + instance: OnionMiddleware[]; + defaultInstance: OnionMiddleware[]; + global: OnionMiddleware[]; + core: OnionMiddleware[]; + }; } export interface ExtendOnlyOptions { From 192b5a54ef47edfbb1246ec15582ac23d3d951a4 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Mon, 20 Jan 2020 15:06:23 +0800 Subject: [PATCH 70/94] 1.2.18 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 0854163..8d037a6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.17", + "version": "1.2.18", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From 75bed1beda83b0dc4216da4c952f9c9b9ae0e8da Mon Sep 17 00:00:00 2001 From: chenjsh Date: Tue, 18 Feb 2020 16:37:14 +0800 Subject: [PATCH 71/94] fix: overwrite the error object (#108) --- src/middleware/parseResponse.js | 8 +++---- test/interceptor.test.js | 38 +++++++++++++++++++++++++++++++++ 2 files changed, 42 insertions(+), 4 deletions(-) diff --git a/src/middleware/parseResponse.js b/src/middleware/parseResponse.js index 1b4eaad..97772c9 100644 --- a/src/middleware/parseResponse.js +++ b/src/middleware/parseResponse.js @@ -74,10 +74,10 @@ export default function parseResponseMiddleware(ctx, next) { } // 对未知错误进行处理 const { req, res } = ctx; - e.request = req; - e.response = res; - e.type = e.name; - e.data = undefined; + e.request = e.request || req; + e.response = e.response || res; + e.type = e.type || e.name; + e.data = e.data || undefined; throw e; }); } diff --git a/test/interceptor.test.js b/test/interceptor.test.js index 37b7b4c..491500b 100644 --- a/test/interceptor.test.js +++ b/test/interceptor.test.js @@ -228,6 +228,44 @@ describe('interceptor', () => { } }); + it('throw error in response interceptor', async done => { + server.post('/test/reject/responseerror', (req, res) => { + writeData(req.body, res); + }); + + class ResponseError extends Error { + constructor({ response, data }) { + super('x-error'); + + this.name = 'x-error'; + this.type = 'x-type'; + this.response = response; + this.data = data; + } + } + + const req = extend({}); + + req.interceptors.response.use( + (response, options) => { + const { status, url } = response; + if (status === 200 && url.indexOf('/test/reject/responseerror')) { + throw new ResponseError({ response: response, data: { hello: 'world' } }); + } + }, + { global: false } + ); + + try { + const data = await req(prefix('/test/reject/responseerror'), { method: 'post' }); + } catch (e) { + expect(e.name).toBe('x-error'); + expect(e instanceof ResponseError).toBe(true); + expect(e.data.hello).toEqual('world'); + done(); + } + }); + // clone response it('should throw error when reponse.clone().json() result is fail', async done => { server.post('/test/multiple/clone/response', (req, res) => { From d9eb1299163a2c80d8bb448a72809e66c4fd4348 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Tue, 18 Feb 2020 16:38:34 +0800 Subject: [PATCH 72/94] 1.2.19 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8d037a6..f78a685 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.18", + "version": "1.2.19", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From 4522b07f3cade11f548e7d3c90bdb9a75f41d021 Mon Sep 17 00:00:00 2001 From: bxer Date: Thu, 21 May 2020 20:03:20 +0800 Subject: [PATCH 73/94] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0abort=20control?= =?UTF-8?q?ler=20=E5=8F=96=E6=B6=88=E8=AF=B7=E6=B1=82=20(#90)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 温鑫 --- README.md | 483 +++++++++++++++------------ README_zh-CN.md | 500 +++++++++++++++------------- package.json | 1 + src/cancel/abortControllerCancel.js | 3 + src/index.js | 3 +- 5 files changed, 533 insertions(+), 457 deletions(-) create mode 100644 src/cancel/abortControllerCancel.js diff --git a/README.md b/README.md index 76dc206..970f431 100644 --- a/README.md +++ b/README.md @@ -8,7 +8,7 @@ The network request library, based on fetch encapsulation, combines the features [![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 @@ -26,22 +26,22 @@ The network request library, based on fetch encapsulation, combines the features ## 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 | ✅ | ❌ | ✅ | +| 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) @@ -60,52 +60,56 @@ npm install --save umi-request ``` ## Example -Performing a ```GET``` request -``` javascript +Performing a `GET` request + +```javascript import request from 'umi-request'; -request.get('/api/v1/xxx?id=1') - .then(function (response) { +request + .get('/api/v1/xxx?id=1') + .then(function(response) { console.log(response); }) - .catch(function (error) { + .catch(function(error) { console.log(error); }); // use options.params -request.get('/api/v1/xxx', { +request + .get('/api/v1/xxx', { params: { - id: 1 - } + id: 1, + }, }) - .then(function (response) { + .then(function(response) { console.log(response); }) - .catch(function (error) { + .catch(function(error) { console.log(error); }); ``` -Performing a ```POST``` request +Performing a `POST` request -``` javascript -request.post('/api/v1/user', { +```javascript +request + .post('/api/v1/user', { data: { - name: 'Mike' - } + name: 'Mike', + }, }) - .then(function (response) { + .then(function(response) { console.log(response); }) - .catch(function (error) { + .catch(function(error) { console.log(error); }); ``` ## umi-request API -Requests can be made by passing relevant options to ```umi-request``` +Requests can be made by passing relevant options to `umi-request` **umi-request(url[, options])** @@ -113,29 +117,28 @@ Requests can be made by passing relevant options to ```umi-request``` import request from 'umi-request'; request('/api/v1/xxx', { - method: 'get', - params: { id: 1 } - }) - .then(function (response) { + method: 'get', + params: { id: 1 }, +}) + .then(function(response) { console.log(response); }) - .catch(function (error) { + .catch(function(error) { console.log(error); }); request('/api/v1/user', { - method: 'post', - data: { - name: 'Mike' - } - }) - .then(function (response) { + method: 'post', + data: { + name: 'Mike', + }, +}) + .then(function(response) { console.log(response); }) - .catch(function (error) { + .catch(function(error) { console.log(error); }); - ``` ## Request method aliases @@ -158,26 +161,27 @@ For convenience umi-request have been provided for all supported methods. ## Creating an instance -You can use ```extend({[options]})``` to create a new instance of umi-request. +You can use `extend({[options]})` to create a new instance of umi-request. **extend([options])** -``` javascript +```javascript import { extend } from 'umi-request'; const request = extend({ prefix: '/api/v1', timeout: 1000, headers: { - 'Content-Type': 'multipart/form-data' - } + 'Content-Type': 'multipart/form-data', + }, }); -request.get('/user') - .then(function (response) { +request + .get('/user') + .then(function(response) { console.log(response); }) - .catch(function (error) { + .catch(function(error) { console.log(error); }); ``` @@ -186,7 +190,7 @@ Create an instance of umi-request in NodeJS enviroment ```javascript const umi = require('umi-request'); -const extendRequest = umi.extend({ timeout: 10000 }) +const extendRequest = umi.extend({ timeout: 10000 }); extendRequest('/api/user') .then(res => { @@ -217,41 +221,41 @@ More umi-request cases can see [antd-pro](https://github.com/umijs/ant-design-pr ## 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 | -- | -- | +| 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 | -- | -- | -| ... | +| Parameter | Description | Type | Optional Value | Default | +| :-------- | :--------------------- | :----- | :------------------- | :------ | +| method | request method | string | get , post , put ... | get | +| params | url request parameters | object | -- | -- | +| data | Submitted data | any | -- | -- | +| ... | -``` javascript +```javascript { // 'method' is the request method to be used when making the request method: 'get', // default @@ -324,7 +328,7 @@ The other parameters of fetch are valid. See [fetch documentation](https://githu // ...options.headers, // } // options.body = JSON.stringify(data) - // + // // 2. requestType === 'form': // options.headers = { // Accept: 'application/json', @@ -332,7 +336,7 @@ The other parameters of fetch are valid. See [fetch documentation](https://githu // ...options.headers, // }; // options.body = query-string.stringify(data); - // + // // 3. other requestType // options.headers = { // Accept: 'application/json', @@ -341,7 +345,7 @@ The other parameters of fetch are valid. See [fetch documentation](https://githu // options.body = data; requestType: 'json', // default - // 'parseResponse' whether processing response + // '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) @@ -367,13 +371,14 @@ The other parameters of fetch are valid. See [fetch documentation](https://githu ``` ### 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' }}) +const request = extend({ timeout: 1000, params: { a: '1' } }); // default options is: { timeout: 1000, params: { a: '1' }} -request.extendOptions({ timeout: 3000, params: { b: '2' }}) +request.extendOptions({ timeout: 3000, params: { b: '2' } }); // after extendOptions: { timeout: 3000, params: { a: '1', b: '2' }} ``` @@ -381,7 +386,7 @@ request.extendOptions({ timeout: 3000, params: { b: '2' }}) The response for a request contains the following information. -``` javascript +```javascript { // 'data' is the response that was provided by the server data: {}, @@ -400,34 +405,31 @@ The response for a request contains the following information. When options.getResponse === false, the response schema would be 'data' -``` javascript -request.get('/api/v1/xxx', { getResponse: false }) - .then(function(data) { - console.log(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); - }) - +```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. +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 errorHandler = function(error) { const codeMap = { '021': 'An error has occurred', '022': 'It’s a big mistake,', @@ -440,17 +442,17 @@ const errorHandler = function (error) { console.log(error.response.headers); console.log(error.data); console.log(error.request); - console.log(codeMap[error.data.status]) + 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'}; -} + // return {some: 'data'}; +}; // 1. Unified processing const extendRequest = extend({ errorHandler }); @@ -459,15 +461,14 @@ const extendRequest = extend({ errorHandler }); // 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); -}) + .then(function(response) { + console.log(response); + }) + .catch(function(error) { + return errorHandler(error); + }); ``` ## Middleware @@ -484,30 +485,30 @@ request.use(fn[, options]) fn params -* ctx(Object):context, content request and response -* next(Function):function to call the next middleware +- 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 +- global(boolean): whether global, higher priority than core +- core(boolean): whether core ### example 1. same type of middlewares -``` javascript +```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'); ``` @@ -520,27 +521,33 @@ a1 -> b1 -> response -> b2 -> a2 2. Defferent type of middlewares -``` javascript -request.use( async (ctx, next) => { +```javascript +request.use(async (ctx, next) => { console.log('instanceA1'); await next(); console.log('instanceA2'); -}) -request.use( async (ctx, next) => { +}); +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 }) +}); +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: @@ -551,67 +558,66 @@ instanceA1 -> instanceB1 -> globalA1 -> coreA1 -> coreA2 -> globalA2 -> instance 3. Enhance request -``` javascript +```javascript request.use(async (ctx, next) => { const { req } = ctx; const { url, options } = req; - if ( url.indexOf('/api') !== 0 ) { + if (url.indexOf('/api') !== 0) { ctx.req.url = `/api/v1/${url}`; } ctx.req.options = { ...options, - foo: 'foo' + foo: 'foo', }; await next(); const { res } = ctx; - const { success = false } = res; + 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(); +```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; + } - ctx.res = response; + // when value is not normal, use your request func. + const response = getResponseByOtherWay(); - await next(); - return; -}, { core: true }); + ctx.res = response; + await next(); + return; + }, + { core: true } +); request('/api/v1/rpc', { __umiRequestCoreType__: 'rpc', parseResponse: false, }) - .then(function (response) { + .then(function(response) { console.log(response); }) - .catch(function (error) { + .catch(function(error) { console.log(error); - }) - + }); ``` ## Interceptor @@ -620,26 +626,25 @@ You can intercept requests or responses before they are handled by then or catch 1. global Interceptor -``` javascript +```javascript // request interceptor, change url or options. request.interceptors.request.use((url, options) => { - return ( - { - url: `${url}&interceptors=yes`, - options: { ...options, interceptors: true }, - } - ); + return { + url: `${url}&interceptors=yes`, + options: { ...options, interceptors: true }, + }; }); // Same as the last one -request.interceptors.request.use((url, options) => { - return ( - { +request.interceptors.request.use( + (url, options) => { + return { url: `${url}&interceptors=yes`, options: { ...options, interceptors: true }, - } - ); -}, { global: true }); + }; + }, + { global: true } +); // response interceptor, chagne response request.interceptors.response.use((response, options) => { @@ -648,7 +653,7 @@ request.interceptors.response.use((response, options) => { }); // handling error in response interceptor -request.interceptors.response.use((response) => { +request.interceptors.response.use(response => { const codeMaps = { 502: '网关错误。', 503: '服务不可用,服务器暂时过载或维护。', @@ -659,29 +664,32 @@ request.interceptors.response.use((response) => { }); // clone response in response interceptor -request.interceptors.response.use(async (response) => { +request.interceptors.response.use(async response => { const data = await response.clone().json(); - if(data && data.NOT_LOGIN) { + if (data && data.NOT_LOGIN) { location.href = '登录url'; } return response; -}) +}); ``` 1. instance Interceptor -``` javascript +```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 } +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 + prefix: baseUrl, }); return request; } @@ -689,19 +697,25 @@ function createClient(baseUrl) { 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 }); +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 @@ -715,7 +729,7 @@ const CancelToken = Request.CancelToken; const { token, cancel } = CancelToken.source(); Request.get('/api/cancel', { - cancelToken: token + cancelToken: token, }).catch(function(thrown) { if (Request.isCancel(thrown)) { console.log('Request canceled', thrown.message); @@ -724,17 +738,22 @@ Request.get('/api/cancel', { } }); -Request.post('/api/cancel', { - name: 'hello world' -}, { - cancelToken: token -}) +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'; @@ -744,36 +763,56 @@ let cancel; Request.get('/api/cancel', { cancelToken: new CancelToken(function executor(c) { cancel = c; - }) + }), }); // cancel request cancel(); ``` +3. create AbortCOntroller cancel + +```javascript +import Request, { AbortController } from 'umi-request'; + +const controller = new AbortController(); +const { signal } = controller; + +signal.addEventListener('abort', () => { + console.log('aborted!'); +}); + +Request('http://127.0.0.1:3009/', { + signal, +}); +// 取消请求 +setTimeout(() => { + controller.abort(); +}, 1000); +``` + ## 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}) => { +```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** +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 +```javascript const formData = new FormData(); formData.append('file', file); -request('/api/v1/some/api', { method:'post', data: formData }); +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) diff --git a/README_zh-CN.md b/README_zh-CN.md index ea06954..c06575a 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -2,7 +2,7 @@ # umi-request -网络请求库,基于 fetch 封装, 兼具 fetch 与 axios 的特点, 旨在为开发者提供一个统一的api调用方式, 简化使用, 并提供诸如缓存, 超时, 字符编码处理, 错误处理等常用功能. +网络请求库,基于 fetch 封装, 兼具 fetch 与 axios 的特点, 旨在为开发者提供一个统一的 api 调用方式, 简化使用, 并提供诸如缓存, 超时, 字符编码处理, 错误处理等常用功能. [![NPM version][npm-image]][npm-url] [![build status][travis-image]][travis-url] @@ -12,7 +12,7 @@ [travis-image]: https://img.shields.io/travis/umijs/umi-request.svg?style=flat-square [travis-url]: https://travis-ci.org/umijs/umi-request.svg?branch=master --------------------- +--- ## 支持的功能 @@ -31,76 +31,82 @@ ## 与 fetch, axios 异同 | 特性 | umi-request | fetch | axios | -| :---------- | :-------------- | :-------------- | :-------------- | +| :--------- | :------------- | :------------- | :------------- | | 实现 | 浏览器原生支持 | 浏览器原生支持 | XMLHttpRequest | | 大小 | 9k | 4k (polyfill) | 14k | -| query 简化 | ✅ | ❌ | ✅ | -| post 简化 | ✅ | ❌ | ❌ | -| 超时 | ✅ | ❌ | ✅ | -| 缓存 | ✅ | ❌ | ❌ | -| 错误检查 | ✅ | ❌ | ❌ | -| 错误处理 | ✅ | ❌ | ✅ | -| 拦截器 | ✅ | ❌ | ✅ | -| 前缀 | ✅ | ❌ | ❌ | -| 后缀 | ✅ | ❌ | ❌ | -| 处理 gbk | ✅ | ❌ | ❌ | -| 中间件 | ✅ | ❌ | ❌ | -| 取消请求 | ✅ | ❌ | ✅ | +| query 简化 | ✅ | ❌ | ✅ | +| post 简化 | ✅ | ❌ | ❌ | +| 超时 | ✅ | ❌ | ✅ | +| 缓存 | ✅ | ❌ | ❌ | +| 错误检查 | ✅ | ❌ | ❌ | +| 错误处理 | ✅ | ❌ | ✅ | +| 拦截器 | ✅ | ❌ | ✅ | +| 前缀 | ✅ | ❌ | ❌ | +| 后缀 | ✅ | ❌ | ❌ | +| 处理 gbk | ✅ | ❌ | ❌ | +| 中间件 | ✅ | ❌ | ❌ | +| 取消请求 | ✅ | ❌ | ✅ | 更多讨论参考[传统 Ajax 已死,Fetch 永生](https://github.com/camsong/blog/issues/2), 如果你有好的建议和需求, 请提 [issue](https://github.com/umijs/umi/issues) -## TODO 欢迎pr +## TODO 欢迎 pr -- [x] 测试用例覆盖85%+ +- [x] 测试用例覆盖 85%+ - [x] 写文档 -- [x] CI集成 +- [x] CI 集成 - [x] 发布配置 - [x] typescript ## 安装 + ``` npm install --save umi-request ``` ## 快速上手 + 执行 **GET** 请求 -``` javascript +```javascript import request from 'umi-request'; -request.get('/api/v1/xxx?id=1') - .then(function (response) { +request + .get('/api/v1/xxx?id=1') + .then(function(response) { console.log(response); }) - .catch(function (error) { + .catch(function(error) { console.log(error); }); // 也可将 URL 的参数放到 options.params 里 -request.get('/api/v1/xxx', { +request + .get('/api/v1/xxx', { params: { - id: 1 - } + id: 1, + }, }) - .then(function (response) { + .then(function(response) { console.log(response); }) - .catch(function (error) { + .catch(function(error) { console.log(error); }); ``` 执行 **POST** 请求 -``` javascript -request.post('/api/v1/user', { + +```javascript +request + .post('/api/v1/user', { data: { - name: 'Mike' - } + name: 'Mike', + }, }) - .then(function (response) { + .then(function(response) { console.log(response); }) - .catch(function (error) { + .catch(function(error) { console.log(error); }); ``` @@ -110,38 +116,38 @@ request.post('/api/v1/user', { 可以通过向 **umi-request** 传参来发起请求 **umi-request(url[, options])** + ```javascript import request from 'umi-request'; request('/api/v1/xxx', { - method: 'get', - params: { id: 1 } - }) - .then(function (response) { + method: 'get', + params: { id: 1 }, +}) + .then(function(response) { console.log(response); }) - .catch(function (error) { + .catch(function(error) { console.log(error); }); request('/api/v1/user', { - method: 'post', - data: { - name: 'Mike' - } - }) - .then(function (response) { + method: 'post', + data: { + name: 'Mike', + }, +}) + .then(function(response) { console.log(response); }) - .catch(function (error) { + .catch(function(error) { console.log(error); }); - ``` ## 请求方法的别名 -为了方便起见,为所有支持的请求方法提供了别名, ```method``` 属性不必在配置中指定 +为了方便起见,为所有支持的请求方法提供了别名, `method` 属性不必在配置中指定 **request.get(url[, options])** @@ -159,26 +165,27 @@ request('/api/v1/user', { ## 创建实例 -有些通用的配置我们不想每个请求里都去添加,那么可以通过 ```extend``` 新建一个 umi-request 实例 +有些通用的配置我们不想每个请求里都去添加,那么可以通过 `extend` 新建一个 umi-request 实例 **extend([options])** -``` javascript +```javascript import { extend } from 'umi-request'; const request = extend({ prefix: '/api/v1', timeout: 1000, headers: { - 'Content-Type': 'multipart/form-data' - } + 'Content-Type': 'multipart/form-data', + }, }); -request.get('/user') - .then(function (response) { +request + .get('/user') + .then(function(response) { console.log(response); }) - .catch(function (error) { + .catch(function(error) { console.log(error); }); ``` @@ -187,7 +194,7 @@ NodeJS 环境创建实例 ```javascript const umi = require('umi-request'); -const extendRequest = umi.extend({ timeout: 10000 }) +const extendRequest = umi.extend({ timeout: 10000 }); extendRequest('/api/user') .then(res => { @@ -198,7 +205,6 @@ extendRequest('/api/user') }); ``` - 以下是可用的实例方法,指定的配置将与实例的配置合并。 **request.get(url[, options])** @@ -215,49 +221,47 @@ extendRequest('/api/user') **request.options(url[, options])** - umi-request 可以进行一层简单封装后再使用, 可参考 [antd-pro](https://github.com/umijs/ant-design-pro/blob/master/src/utils/request.js) - ## 请求配置 ### request options 参数 -| 参数 | 说明 | 类型 | 可选值 | 默认值 | -| :--- | :--- | :--- | :--- | :--- | -| method | 请求方式 | string | get , post , put ... | get | -| params | url请求参数 | object 或 URLSearchParams 对象 | -- | -- | -| data | 提交的数据 | any | -- | -- | -| headers | fetch 原有参数 | object | -- | {} | -| timeout | 超时时长, 默认毫秒, 写操作慎用 | number | -- | -- | -| prefix | 前缀, 一般用于覆盖统一设置的prefix | string | -- | -- | -| suffix | 后缀, 比如某些场景 api 需要统一加 .json | string | -- | -- | -| credentials | fetch 请求包含 cookies 信息 | string | -- | credentials: 'same-origin' | -| useCache | 是否使用缓存(仅支持浏览器客户端) | boolean | -- | false | -| validateCache | 缓存策略函数 | (url, options) => boolean | -- | 默认 get 请求做缓存 | -| ttl | 缓存时长, 0 为不过期 | number | -- | 60000 | -| maxCache | 最大缓存数 | number | -- | 无限 | -| requestType | post请求时数据类型 | string | json , form | json | -| parseResponse | 是否对 response 做处理简化 | boolean | -- | true | -| charset | 字符集 | string | utf8 , gbk | utf8 | -| responseType | 如何解析返回的数据 | string | json , text , blob , formData ... | json , text | -| throwErrIfParseFail | 当 responseType 为 'json', 对请求结果做 JSON.parse 出错时是否抛出异常 | boolean | -- |false | -| getResponse | 是否获取源response, 返回结果将包裹一层 | boolean | -- | fasle | -| errorHandler | 异常处理, 或者覆盖统一的异常处理 | function(error) | -- | -| cancelToken | 取消请求的 Token | CancelToken.token | -- | -- | - -fetch原其他参数有效, 详见[fetch文档](https://github.github.io/fetch/) +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +| :------------------ | :-------------------------------------------------------------------- | :----------------------------- | :-------------------------------- | :------------------------- | +| method | 请求方式 | string | get , post , put ... | get | +| params | url 请求参数 | object 或 URLSearchParams 对象 | -- | -- | +| data | 提交的数据 | any | -- | -- | +| headers | fetch 原有参数 | object | -- | {} | +| timeout | 超时时长, 默认毫秒, 写操作慎用 | number | -- | -- | +| prefix | 前缀, 一般用于覆盖统一设置的 prefix | string | -- | -- | +| suffix | 后缀, 比如某些场景 api 需要统一加 .json | string | -- | -- | +| credentials | fetch 请求包含 cookies 信息 | string | -- | credentials: 'same-origin' | +| useCache | 是否使用缓存(仅支持浏览器客户端) | boolean | -- | false | +| validateCache | 缓存策略函数 | (url, options) => boolean | -- | 默认 get 请求做缓存 | +| ttl | 缓存时长, 0 为不过期 | number | -- | 60000 | +| maxCache | 最大缓存数 | number | -- | 无限 | +| requestType | post 请求时数据类型 | string | json , form | json | +| parseResponse | 是否对 response 做处理简化 | boolean | -- | true | +| charset | 字符集 | string | utf8 , gbk | utf8 | +| responseType | 如何解析返回的数据 | string | json , text , blob , formData ... | json , text | +| throwErrIfParseFail | 当 responseType 为 'json', 对请求结果做 JSON.parse 出错时是否抛出异常 | boolean | -- | false | +| getResponse | 是否获取源 response, 返回结果将包裹一层 | boolean | -- | fasle | +| errorHandler | 异常处理, 或者覆盖统一的异常处理 | function(error) | -- | +| cancelToken | 取消请求的 Token | CancelToken.token | -- | -- | + +fetch 原其他参数有效, 详见[fetch 文档](https://github.github.io/fetch/) ### extend options 初始化默认参数, 支持以上所有 -| 参数 | 说明 | 类型 | 可选值 | 默认值 | -| :--- | :--- | :--- | :--- | :--- | -| method | 请求方式 | string | get , post , put ... | get | -| params | url请求参数 | object | -- | -- | -| data | 提交的数据 | any | -- | -- | -| ... | +| 参数 | 说明 | 类型 | 可选值 | 默认值 | +| :----- | :----------- | :----- | :------------------- | :----- | +| method | 请求方式 | string | get , post , put ... | get | +| params | url 请求参数 | object | -- | -- | +| data | 提交的数据 | any | -- | -- | +| ... | -``` javascript +```javascript { // 'method' 是创建请求时使用的方法 method: 'get', // default @@ -366,19 +370,18 @@ fetch原其他参数有效, 详见[fetch文档](https://github.github.io/fetch/) 实例化一个请求实例后,有时还需要动态更新默认参数,umi-request 提供 **extendOptions** 方法供用户进行更新: ```javascript -const request = extend({ timeout: 1000, params: { a: '1' }}) +const request = extend({ timeout: 1000, params: { a: '1' } }); // 默认参数是 { timeout: 1000, params: { a: '1' }} -request.extendOptions({ timeout: 3000, params: { b: '2' }}) +request.extendOptions({ timeout: 3000, params: { b: '2' } }); // 此时默认参数是 { timeout: 3000, params: { a: '1', b: '2' }} - ``` ## 响应结构 某个请求的响应返回的响应对象 Response 如下: -``` javascript +```javascript { // `data` 由服务器提供的响应, 需要进行解析才能获取 data: {}, @@ -396,34 +399,31 @@ request.extendOptions({ timeout: 3000, params: { b: '2' }}) 当 options.getResponse === false 时, 响应结构为解析后的 data -``` javascript -request.get('/api/v1/xxx', { getResponse: false }) - .then(function(data) { - console.log(data); - }) +```javascript +request.get('/api/v1/xxx', { getResponse: false }).then(function(data) { + console.log(data); +}); ``` 当 options.getResponse === true 时,响应结构为包含 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); - }) - +```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); +}); ``` -在使用 catch 或者 errorHandler, 响应对象可以通过 ```error``` 对象获取使用,参考**错误处理**这一节文档。 +在使用 catch 或者 errorHandler, 响应对象可以通过 `error` 对象获取使用,参考**错误处理**这一节文档。 ## 错误处理 -``` javascript +```javascript import request, { extend } from 'umi-request'; -const errorHandler = function (error) { +const errorHandler = function(error) { const codeMap = { '021': '发生错误啦', '022': '发生大大大大错误啦', @@ -435,18 +435,17 @@ const errorHandler = function (error) { console.log(error.response.headers); console.log(error.data); console.log(error.request); - console.log(codeMap[error.data.status]) - + console.log(codeMap[error.data.status]); } else { // 请求初始化时出错或者没有响应返回的异常 console.log(error.message); } - throw error; // 如果throw. 错误将继续抛出. - + throw error; // 如果throw. 错误将继续抛出. + // 如果return, 则将值作为返回. 'return;' 相当于return undefined, 在处理结果时判断response是否有值即可. - // return {some: 'data'}; -} + // return {some: 'data'}; +}; // 1. 作为统一错误处理 const extendRequest = extend({ errorHandler }); @@ -454,19 +453,16 @@ const extendRequest = extend({ errorHandler }); // 2. 单独特殊处理, 如果配置了统一处理, 但某个api需要特殊处理. 则在请求时, 将errorHandler作为参数传入. request('/api/v1/xxx', { errorHandler }); - // 3. 通过 Promise.catch 做错误处理 request('/api/v1/xxx') -.then(function (response) { - console.log(response); -}) -.catch(function (error) { - return errorHandler(error); -}) - + .then(function(response) { + console.log(response); + }) + .catch(function(error) { + return errorHandler(error); + }); ``` - ## 中间件 类 koa 的洋葱机制,让开发者优雅地做请求前后的增强处理,支持创建实例、全局、内核中间件。 @@ -483,85 +479,91 @@ request.use(fn[, options]) fn 入参 -* ctx(Object):上下文对象,包括req和res对象 -* next(Function):调用下一个中间件的函数 +- ctx(Object):上下文对象,包括 req 和 res 对象 +- next(Function):调用下一个中间件的函数 options 参数 -* global(boolean): 是否为全局中间件,优先级比 core 高 -* core(boolean): 是否为内核中间件 +- global(boolean): 是否为全局中间件,优先级比 core 高 +- core(boolean): 是否为内核中间件 ### 例子 1. 同类型中间件执行顺序 -``` javascript +```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'); ``` 执行顺序如下: -``` shell +```shell a1 -> b1 -> response -> b2 -> a2 ``` 2. 不同类型中间件执行顺序 -``` javascript -request.use( async (ctx, next) => { +```javascript +request.use(async (ctx, next) => { console.log('instanceA1'); await next(); console.log('instanceA2'); -}) -request.use( async (ctx, next) => { +}); +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 }) +}); +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 } +); ``` 执行顺序如下: -``` shell +```shell instanceA1 -> instanceB1 -> globalA1 -> coreA1 -> coreA2 -> globalA2 -> instanceB2 -> instanceA2 ``` 3. 使用中间件对请求前后做处理 -``` javascript +```javascript request.use(async (ctx, next) => { const { req } = ctx; const { url, options } = req; // 判断是否需要添加前缀,如果是统一添加可通过 prefix、suffix 参数配置 - if ( url.indexOf('/api') !== 0 ) { + if (url.indexOf('/api') !== 0) { ctx.req.url = `/api/v1/${url}`; } ctx.req.options = { ...options, - foo: 'foo' + foo: 'foo', }; await next(); @@ -571,78 +573,75 @@ request.use(async (ctx, next) => { if (!success) { // 对异常情况做对应处理 } -}) - +}); ``` 4. 使用内核中间件拓展请求能力 -``` javascript - -request.use(async (ctx, next) => { - const { req } = ctx; - const { url, options } = req; - const { __umiRequestCoreType__ = 'normal' } = options; - - // __umiRequestCoreType__ 用于区分请求内核类型 - // 值为 'normal' 使用 umi-request 内置的请求内核 - if ( __umiRequestCoreType__ === 'normal') { - await next(); - return; - } +```javascript +request.use( + async (ctx, next) => { + const { req } = ctx; + const { url, options } = req; + const { __umiRequestCoreType__ = 'normal' } = options; + + // __umiRequestCoreType__ 用于区分请求内核类型 + // 值为 'normal' 使用 umi-request 内置的请求内核 + if (__umiRequestCoreType__ === 'normal') { + await next(); + return; + } - // 非 normal 使用自定义请求内核获取响应数据 - const response = getResponseByOtherWay(); + // 非 normal 使用自定义请求内核获取响应数据 + const response = getResponseByOtherWay(); - // 将响应数据写入 ctx 中 - ctx.res = response; - - await next(); - return; -}, { core: true }); + // 将响应数据写入 ctx 中 + ctx.res = response; + await next(); + return; + }, + { core: true } +); // 使用自定义请求内核 request('/api/v1/rpc', { __umiRequestCoreType__: 'rpc', parseResponse: false, }) - .then(function (response) { + .then(function(response) { console.log(response); }) - .catch(function (error) { + .catch(function(error) { console.log(error); - }) - + }); ``` ## 拦截器 -在请求或响应被 ```then``` 或 ```catch``` 处理前拦截它们。 +在请求或响应被 `then` 或 `catch` 处理前拦截它们。 1. 全局拦截器 -``` javascript +```javascript // request拦截器, 改变url 或 options. request.interceptors.request.use((url, options) => { - return ( - { - url: `${url}&interceptors=yes`, - options: { ...options, interceptors: true }, - } - ); + return { + url: `${url}&interceptors=yes`, + options: { ...options, interceptors: true }, + }; }); // 和上一个相同 -request.interceptors.request.use((url, options) => { - return ( - { +request.interceptors.request.use( + (url, options) => { + return { url: `${url}&interceptors=yes`, options: { ...options, interceptors: true }, - } - ); -}, { global: true }); - + }; + }, + { global: true } +); // response拦截器, 处理response request.interceptors.response.use((response, options) => { @@ -651,7 +650,7 @@ request.interceptors.response.use((response, options) => { }); // 提前对响应做异常处理 -request.interceptors.response.use((response) => { +request.interceptors.response.use(response => { const codeMaps = { 502: '网关错误。', 503: '服务不可用,服务器暂时过载或维护。', @@ -662,29 +661,32 @@ request.interceptors.response.use((response) => { }); // 克隆响应对象做解析处理 -request.interceptors.response.use(async (response) => { +request.interceptors.response.use(async response => { const data = await response.clone().json(); - if(data && data.NOT_LOGIN) { + if (data && data.NOT_LOGIN) { location.href = '登录url'; } return response; -}) +}); ``` 2. 实例内部拦截器 -``` javascript +```javascript // 全局拦截器直接使用 request 实例中的方法 -request.interceptors.request.use((url, options) => { - return { - url: `${url}&interceptors=yes`, - options: { ...options, interceptors: true }, - }; -}, { global: false }); // 第二个参数不传默认为 { global: true } +request.interceptors.request.use( + (url, options) => { + return { + url: `${url}&interceptors=yes`, + options: { ...options, interceptors: true }, + }; + }, + { global: false } +); // 第二个参数不传默认为 { global: true } function createClient(baseUrl) { const request = extend({ - prefix: baseUrl + prefix: baseUrl, }); return request; } @@ -692,24 +694,31 @@ function createClient(baseUrl) { const clientA = createClient('/api'); const clientB = createClient('/api'); // 局部拦截器使用 -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 }); +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 token** 来取消一个请求 + > cancel token API 是基于已被撤销的 [cancelable-promises 方案](https://github.com/tc39/proposal-cancelable-promises) 1. 你可以通过 **CancelToken.source** 来创建一个 cancel token,如下所示: @@ -721,7 +730,7 @@ const CancelToken = Request.CancelToken; const { token, cancel } = CancelToken.source(); Request.get('/api/cancel', { - cancelToken: token + cancelToken: token, }).catch(function(thrown) { if (Request.isCancel(thrown)) { console.log('Request canceled', thrown.message); @@ -730,15 +739,18 @@ Request.get('/api/cancel', { } }); -Request.post('/api/cancel', { - name: 'hello world' -}, { - cancelToken: token -}) +Request.post( + '/api/cancel', + { + name: 'hello world', + }, + { + cancelToken: token, + } +); // 取消请求(参数为非必填) cancel('Operation canceled by the user.'); - ``` 2. 你也可以通过实例化 CancelToken 来创建一个 token,同时通过传入函数来获取取消方法: @@ -752,33 +764,53 @@ let cancel; Request.get('/api/cancel', { cancelToken: new CancelToken(function executor(c) { cancel = c; - }) + }), }); // 取消请求 cancel(); ``` +3. 通过 AbortCOntroller 取消 + +```javascript +import Request, { AbortController } from 'umi-request'; + +const controller = new AbortController(); +const { signal } = controller; + +signal.addEventListener('abort', () => { + console.log('aborted!'); +}); + +Request('http://127.0.0.1:3009/', { + signal, +}); +// 取消请求 +setTimeout(() => { + controller.abort(); +}, 1000); +``` + ## 案例 ### 如何获取响应头信息 通过 **Headers.get()** 获取响应头信息。(可参考 [MDN 文档](https://developer.mozilla.org/zh-CN/docs/Web/API/Headers/get)) -``` javascript -request('/api/v1/some/api', { getResponse: true }) -.then(({ data, response}) => { +```javascript +request('/api/v1/some/api', { getResponse: true }).then(({ data, response }) => { response.headers.get('Content-Type'); -}) +}); ``` ### 文件上传 -使用 FormData() 构造函数时,浏览器会自动识别并添加请求头 ```"Content-Type: multipart/form-data"```, 且参数依旧是表单提交时那种键值对,因此不需要开发者手动设置 **Content-Type** +使用 FormData() 构造函数时,浏览器会自动识别并添加请求头 `"Content-Type: multipart/form-data"`, 且参数依旧是表单提交时那种键值对,因此不需要开发者手动设置 **Content-Type** -``` javascript +```javascript const formData = new FormData(); formData.append('file', file); -request('/api/v1/some/api', { method:'post', data: formData }); +request('/api/v1/some/api', { method: 'post', data: formData }); ``` 如果希望获取自定义头部信息,需要在服务器设置 [Access-Control-Expose-Headers](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Expose-Headers),然后可按照上述方式获取自定义头部信息。 diff --git a/package.json b/package.json index f78a685..c5f7ba8 100644 --- a/package.json +++ b/package.json @@ -44,6 +44,7 @@ "whatwg-fetch": "^3.0.0" }, "dependencies": { + "abort-controller": "^3.0.0", "isomorphic-fetch": "^2.2.1", "qs": "^6.9.1" }, diff --git a/src/cancel/abortControllerCancel.js b/src/cancel/abortControllerCancel.js new file mode 100644 index 0000000..20ef316 --- /dev/null +++ b/src/cancel/abortControllerCancel.js @@ -0,0 +1,3 @@ +import AbortController from 'abort-controller'; + +export default AbortController; diff --git a/src/index.js b/src/index.js index 91661cc..a834cf5 100644 --- a/src/index.js +++ b/src/index.js @@ -1,6 +1,7 @@ import request, { extend, fetch } from './request'; import Onion from './onion'; import { RequestError, ResponseError } from './utils'; +import AbortController from './cancel/abortControllerCancel'; -export { extend, RequestError, ResponseError, Onion, fetch }; +export { extend, RequestError, ResponseError, Onion, fetch, AbortController }; export default request; From 6a424cfed66871883bac6808f3be0990ccf68582 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Fri, 22 May 2020 17:21:26 +0800 Subject: [PATCH 74/94] Feat: add abort request polyfill and update types (#148) * feat: add AbortController polyfill * doc: update cases; update types --- README.md | 49 ++++++++++++--------- README_zh-CN.md | 53 ++++++++++++++--------- src/cancel/abortControllerCancel.js | 21 ++++++++- src/index.js | 5 ++- test/cancel/abortControllerCancel.test.js | 41 ++++++++++++++++++ types/index.d.ts | 4 ++ 6 files changed, 128 insertions(+), 45 deletions(-) create mode 100644 test/cancel/abortControllerCancel.test.js diff --git a/README.md b/README.md index 970f431..00bc687 100644 --- a/README.md +++ b/README.md @@ -720,6 +720,34 @@ clientB.interceptors.request.use( ## Cancel request +### Use AbortController + +Base on [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) that allows you to abort one or more Web requests as and when desired. + +```javascript +import Request, { AbortController } from 'umi-request'; + +const controller = new AbortController(); // create a controller +const { signal } = controller; // grab a reference to its associated AbortSignal object using the AbortController.signal property + +signal.addEventListener('abort', () => { + console.log('aborted!'); +}); + +Request('/api/response_after_1_sec', { + signal, // pass in the AbortSignal as an option inside the request's options object (see {signal}, below). This associates the signal and controller with the fetch request and allows us to abort it by calling AbortController.abort(), +}); + +// 取消请求 +setTimeout(() => { + controller.abort(); // Aborts a DOM request before it has completed. This is able to abort fetch requests, consumption of any response Body, and streams. +}, 100); +``` + +### Use Cancel Token + +> Cancel Token still work, but we don’t recommend using them in the new code. + 1. You can cancel a request using a cancel token. ```javascript @@ -770,27 +798,6 @@ Request.get('/api/cancel', { cancel(); ``` -3. create AbortCOntroller cancel - -```javascript -import Request, { AbortController } from 'umi-request'; - -const controller = new AbortController(); -const { signal } = controller; - -signal.addEventListener('abort', () => { - console.log('aborted!'); -}); - -Request('http://127.0.0.1:3009/', { - signal, -}); -// 取消请求 -setTimeout(() => { - controller.abort(); -}, 1000); -``` - ## Cases ### How to get Response Headers diff --git a/README_zh-CN.md b/README_zh-CN.md index c06575a..91d6807 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -715,11 +715,41 @@ clientB.interceptors.request.use( ); ``` -## 取消请求 +## 中止请求 + +### 通过 AbortController 来中止请求 + +基于 [AbortController](https://developer.mozilla.org/zh-CN/docs/Web/API/FetchController) 方案来中止一个或多个DOM请求 + +```javascript +import Request, { AbortController } from 'umi-request'; + +const controller = new AbortController(); // 创建一个控制器 +const { signal } = controller; // 返回一个 AbortSignal 对象实例,它可以用来 with/abort 一个 DOM 请求。 + +signal.addEventListener('abort', () => { + console.log('aborted!'); +}); + +Request('/api/response_after_1_sec', { + signal, // 这将信号和控制器与获取请求相关联然后允许我们通过调用 AbortController.abort() 中止请求 +}); + +// 取消请求 +setTimeout(() => { + controller.abort(); // 中止一个尚未完成的DOM请求。这能够中止 fetch 请求,任何响应Body的消费者和流。 +}, 100); +``` + +### 使用cancel token 方案来中止请求 + +> Cancel Token 将逐步退出历史舞台,推荐使用 AbortController 来实现请求中止。 + 你可以通过 **cancel token** 来取消一个请求 -> cancel token API 是基于已被撤销的 [cancelable-promises 方案](https://github.com/tc39/proposal-cancelable-promises) +> cancel token API 是基于已被撤销的 [cancelable-promises 方案](https://github.com/tc39/proposal-cancelable-promises); + 1. 你可以通过 **CancelToken.source** 来创建一个 cancel token,如下所示: @@ -770,26 +800,7 @@ Request.get('/api/cancel', { cancel(); ``` -3. 通过 AbortCOntroller 取消 - -```javascript -import Request, { AbortController } from 'umi-request'; - -const controller = new AbortController(); -const { signal } = controller; -signal.addEventListener('abort', () => { - console.log('aborted!'); -}); - -Request('http://127.0.0.1:3009/', { - signal, -}); -// 取消请求 -setTimeout(() => { - controller.abort(); -}, 1000); -``` ## 案例 diff --git a/src/cancel/abortControllerCancel.js b/src/cancel/abortControllerCancel.js index 20ef316..3faf9f5 100644 --- a/src/cancel/abortControllerCancel.js +++ b/src/cancel/abortControllerCancel.js @@ -1,3 +1,22 @@ -import AbortController from 'abort-controller'; +import { AbortController as AcAbortController, AbortSignal as AcAbortSignal } from 'abort-controller'; + +let AbortController = undefined; +let AbortSignal = undefined; + +const g = + typeof self !== 'undefined' + ? self + : typeof window !== 'undefined' + ? window + : typeof global !== 'undefined' + ? global + : /* otherwise */ undefined; + +if (g) { + AbortController = typeof g.AbortController !== 'undefined' ? g.AbortController : AcAbortController; + AbortSignal = typeof g.AbortSignal !== 'undefined' ? g.AbortSignal : AcAbortSignal; +} export default AbortController; + +export { AbortController, AbortSignal }; diff --git a/src/index.js b/src/index.js index a834cf5..79e9c1e 100644 --- a/src/index.js +++ b/src/index.js @@ -1,7 +1,8 @@ import request, { extend, fetch } from './request'; import Onion from './onion'; import { RequestError, ResponseError } from './utils'; -import AbortController from './cancel/abortControllerCancel'; +import { AbortController, AbortSignal } from './cancel/abortControllerCancel'; + +export { extend, RequestError, ResponseError, Onion, fetch, AbortController, AbortSignal }; -export { extend, RequestError, ResponseError, Onion, fetch, AbortController }; export default request; diff --git a/test/cancel/abortControllerCancel.test.js b/test/cancel/abortControllerCancel.test.js new file mode 100644 index 0000000..5d6b97c --- /dev/null +++ b/test/cancel/abortControllerCancel.test.js @@ -0,0 +1,41 @@ +import createTestServer from 'create-test-server'; +import request, { AbortController } from '../../src/index'; + +const writeData = (data, res) => { + res.setHeader('access-control-allow-origin', '*'); + res.send(data); +}; + +describe('test abortController', () => { + let server; + + beforeAll(async () => { + server = await createTestServer(); + }); + + afterAll(() => { + server.close(); + }); + + const prefix = api => `${server.url}${api}`; + + jest.useFakeTimers(); + + it('test request abort', () => { + expect.assertions(2); + server.get('/test/abort1', (req, res) => { + setTimeout(() => { + writeData(req.query, res); + }, 2000); + }); + + const controller = new AbortController(); + const { signal } = controller; + setTimeout(() => { + controller.abort(); + }, 500); + expect(signal.aborted).toBe(false); + jest.runAllTimers(); + expect(signal.aborted).toBe(true); + }); +}); diff --git a/types/index.d.ts b/types/index.d.ts index 4fd08c5..8947aa6 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -43,6 +43,7 @@ export interface RequestOptionsInit extends RequestInit { getResponse?: boolean; validateCache?: (url: string, options: RequestOptionsInit) => boolean; __umiRequestCoreType__?: string; + [key: string]: any; } export interface RequestOptionsWithoutResponse extends RequestOptionsInit { @@ -163,4 +164,7 @@ declare var request: RequestMethod; export declare var fetch: RequestMethod; +export declare var AbortController: { prototype: AbortController; new (): AbortController }; +export declare var AbortSignal: { prototype: AbortSignal; new (): AbortSignal }; + export default request; From a7db8c062d92a346f987788b08f313eb82ff3f8a Mon Sep 17 00:00:00 2001 From: chenjsh Date: Fri, 22 May 2020 17:23:54 +0800 Subject: [PATCH 75/94] 1.3.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index c5f7ba8..926b9d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.2.19", + "version": "1.3.0", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From 60917c1024cff2d2d926f019b573273e47b54160 Mon Sep 17 00:00:00 2001 From: chenjsh Date: Mon, 25 May 2020 14:46:18 +0800 Subject: [PATCH 76/94] fix: add AbortController polyfill (#150) --- src/cancel/abortControllerCancel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cancel/abortControllerCancel.js b/src/cancel/abortControllerCancel.js index 3faf9f5..792a88c 100644 --- a/src/cancel/abortControllerCancel.js +++ b/src/cancel/abortControllerCancel.js @@ -1,4 +1,4 @@ -import { AbortController as AcAbortController, AbortSignal as AcAbortSignal } from 'abort-controller'; +import 'abort-controller/polyfill'; let AbortController = undefined; let AbortSignal = undefined; From 0232e30484da7d56db1695941764bd2444e1d8ba Mon Sep 17 00:00:00 2001 From: chenjsh Date: Mon, 25 May 2020 15:25:02 +0800 Subject: [PATCH 77/94] 1.3.2 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 926b9d6..7f7f9a0 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.3.0", + "version": "1.3.2", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From eaca25ea07d6a9a13e6a9d310061dd4bdc28c4ec Mon Sep 17 00:00:00 2001 From: chenjsh Date: Wed, 17 Jun 2020 10:47:46 +0800 Subject: [PATCH 78/94] 1.3.4 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 7f7f9a0..8e5ae83 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.3.2", + "version": "1.3.4", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From 5882668fac7cfb95262a4924c04de604205b239b Mon Sep 17 00:00:00 2001 From: Zeb Wu Date: Mon, 14 Sep 2020 13:39:04 +0800 Subject: [PATCH 79/94] [typo] fix typo (#155) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 00bc687..3ebbdfa 100644 --- a/README.md +++ b/README.md @@ -519,7 +519,7 @@ order of middlewares be called: a1 -> b1 -> response -> b2 -> a2 ``` -2. Defferent type of middlewares +2. Different type of middlewares ```javascript request.use(async (ctx, next) => { From 7d2f452fcfe0351bb7d167d52577a1c635712043 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A5=9E=E4=B8=80=E5=8D=8A=E7=9A=84=E7=94=B7=E4=BA=BA?= Date: Mon, 14 Sep 2020 13:39:20 +0800 Subject: [PATCH 80/94] =?UTF-8?q?fix:=20=E4=BF=AE=E5=A4=8D=20Oinon=20?= =?UTF-8?q?=E6=8B=BC=E5=86=99=E9=94=99=E8=AF=AF=20(#153)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: 夏柏阳 --- src/request.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/request.js b/src/request.js index fedbcd3..e303953 100644 --- a/src/request.js +++ b/src/request.js @@ -2,7 +2,7 @@ import Core from './core'; import Cancel from './cancel/cancel'; import CancelToken from './cancel/cancelToken'; import isCancel from './cancel/isCancel'; -import Oinon from './onion'; +import Onion from './onion'; import { getParamObject, mergeRequestOptions } from './utils'; // 通过 request 函数,在 core 之上再封装一层,提供原 umi/request 一致的 api,无缝升级 @@ -43,8 +43,8 @@ const request = (initOptions = {}) => { umiInstance.middlewares = { instance: coreInstance.onion.middlewares, defaultInstance: coreInstance.onion.defaultMiddlewares, - global: Oinon.globalMiddlewares, - core: Oinon.coreMiddlewares, + global: Onion.globalMiddlewares, + core: Onion.coreMiddlewares, }; return umiInstance; From 6773ef1104d46417b14562072167edc0f54822d7 Mon Sep 17 00:00:00 2001 From: Ahmed Abdel-Aziz Date: Mon, 14 Sep 2020 08:40:05 +0300 Subject: [PATCH 81/94] Fix typo (#151) --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 3ebbdfa..082670b 100644 --- a/README.md +++ b/README.md @@ -814,7 +814,7 @@ If want to get a custem header, you need to set [Access-Control-Expose-Headers]( ### 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** +Use FormData() contructor,the browser will add request header `"Content-Type: multipart/form-data"` automatically, developer don't need to add request header **Content-Type** ```javascript const formData = new FormData(); From 318ba2d4c07462e6ad7893106e48ae634ad5e182 Mon Sep 17 00:00:00 2001 From: Troy Li Date: Wed, 19 May 2021 15:15:52 +0800 Subject: [PATCH 82/94] feat: store origin request url in options (#244) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 因为 prefix 配置和 interceptor 内部都可以修改 url,而在后续的 interceptor 中难以将其还原。 在 options 中储存一下原始入参的 url 值,以便后续的 interceptor 读取。 --- src/core.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core.js b/src/core.js index ff3f41d..0111049 100644 --- a/src/core.js +++ b/src/core.js @@ -77,7 +77,7 @@ class Core { request(url, options) { const { onion } = this; const obj = { - req: { url, options }, + req: { url, options: { ...options, url } }, res: null, cache: this.mapCache, responseInterceptors: [...Core.responseInterceptors, ...this.instanceResponseInterceptors], From b8eb5f3e6b31139fda1799765254bd7bce78656b Mon Sep 17 00:00:00 2001 From: "troy.lty" Date: Tue, 25 May 2021 12:12:55 +0800 Subject: [PATCH 83/94] 1.3.6 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 8e5ae83..99269dc 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.3.4", + "version": "1.3.6", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From 0f65468c65eca6f6247e9bd7c1ea777fdd622c2f Mon Sep 17 00:00:00 2001 From: kdot <441002295@qq.com> Date: Tue, 25 May 2021 12:35:10 +0800 Subject: [PATCH 84/94] chore: switch build tool to father (#248) * chore: switch build tool to father * chore: use father-build instead of father Co-authored-by: troy.lty --- .fatherrc.js | 3 +++ .travis.yml | 1 - .umirc.js | 10 ---------- package.json | 5 +++-- 4 files changed, 6 insertions(+), 13 deletions(-) create mode 100644 .fatherrc.js delete mode 100644 .umirc.js diff --git a/.fatherrc.js b/.fatherrc.js new file mode 100644 index 0000000..aaec67d --- /dev/null +++ b/.fatherrc.js @@ -0,0 +1,3 @@ +export default { + esm: 'rollup', +}; diff --git a/.travis.yml b/.travis.yml index 0ddadc5..cfbf9d8 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,7 +1,6 @@ language: node_js node_js: - - "8" - "10" before_script: diff --git a/.umirc.js b/.umirc.js deleted file mode 100644 index 2ffb4ed..0000000 --- a/.umirc.js +++ /dev/null @@ -1,10 +0,0 @@ -export default { - plugins: [ - [ - "umi-plugin-library", - { - umd: false - } - ] - ] -}; diff --git a/package.json b/package.json index 99269dc..f57bea3 100644 --- a/package.json +++ b/package.json @@ -10,8 +10,8 @@ }, "bugs": "https://github.com/umijs/umi-request/issues", "scripts": { - "dev": "umi lib build --w", - "build": "umi lib build", + "dev": "father-build --w", + "build": "father-build", "test": "umi-test", "test:watch": "umi-test --watch", "test:cover": "umi-test --coverage", @@ -32,6 +32,7 @@ "eslint-config-airbnb-base": "^13.1.0", "eslint-config-prettier": "^3.3.0", "eslint-plugin-import": "^2.14.0", + "father-build": "^1.19.5", "iconv-lite": "^0.4.24", "jest": "^23.5.0", "np": "5.0.2", From 6fe176a130f702b920248d496aeb52cb1a072c8f Mon Sep 17 00:00:00 2001 From: "troy.lty" Date: Tue, 25 May 2021 12:36:13 +0800 Subject: [PATCH 85/94] 1.3.7 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index f57bea3..2320a91 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.3.6", + "version": "1.3.7", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.es.js", From ce6c73f24508683f35d7f2f6b83a26b5ef62b3ec Mon Sep 17 00:00:00 2001 From: Troy Li Date: Tue, 25 May 2021 13:52:10 +0800 Subject: [PATCH 86/94] fix: missing package main entry (#250) --- .fatherrc.js | 1 + package.json | 3 ++- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/.fatherrc.js b/.fatherrc.js index aaec67d..cc3a27b 100644 --- a/.fatherrc.js +++ b/.fatherrc.js @@ -1,3 +1,4 @@ export default { esm: 'rollup', + cjs: 'rollup', }; diff --git a/package.json b/package.json index 2320a91..1e915a1 100644 --- a/package.json +++ b/package.json @@ -3,7 +3,7 @@ "version": "1.3.7", "description": "A request tool based on fetch.", "main": "dist/index.js", - "module": "dist/index.es.js", + "module": "dist/index.esm.js", "repository": { "type": "git", "url": "git@github.com:umijs/umi-request.git" @@ -36,6 +36,7 @@ "iconv-lite": "^0.4.24", "jest": "^23.5.0", "np": "5.0.2", + "prettier": "^2.3.0", "query-string": "^6.9.0", "typescript": "^3.0.3", "umi": "^2.8.15", From f86c873008cfec7aec656dd87ad01bc6d749f7d8 Mon Sep 17 00:00:00 2001 From: "troy.lty" Date: Tue, 25 May 2021 13:54:02 +0800 Subject: [PATCH 87/94] 1.3.8 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 1e915a1..a126c19 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.3.7", + "version": "1.3.8", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.esm.js", From 17867b5db4d6692b2dce40544b4a2e1171052e75 Mon Sep 17 00:00:00 2001 From: Troy Li Date: Tue, 25 May 2021 19:21:30 +0800 Subject: [PATCH 88/94] chore: remove abort controller polyfill (#251) * chore: remove abort controller polyfill * test: update test case and remove AbortController export * docs: update doc for abort controller --- README.md | 4 +++- README_zh-CN.md | 4 +++- package.json | 1 - src/cancel/abortControllerCancel.js | 22 ---------------------- src/index.js | 3 +-- test/cancel/abortControllerCancel.test.js | 4 ++-- 6 files changed, 9 insertions(+), 29 deletions(-) delete mode 100644 src/cancel/abortControllerCancel.js diff --git a/README.md b/README.md index 082670b..ced7de1 100644 --- a/README.md +++ b/README.md @@ -725,7 +725,9 @@ clientB.interceptors.request.use( Base on [AbortController](https://developer.mozilla.org/en-US/docs/Web/API/AbortController) that allows you to abort one or more Web requests as and when desired. ```javascript -import Request, { AbortController } from 'umi-request'; +// polyfill abort controller if needed +import 'yet-another-abortcontroller-polyfill' +import Request from 'umi-request'; const controller = new AbortController(); // create a controller const { signal } = controller; // grab a reference to its associated AbortSignal object using the AbortController.signal property diff --git a/README_zh-CN.md b/README_zh-CN.md index 91d6807..a812436 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -722,7 +722,9 @@ clientB.interceptors.request.use( 基于 [AbortController](https://developer.mozilla.org/zh-CN/docs/Web/API/FetchController) 方案来中止一个或多个DOM请求 ```javascript -import Request, { AbortController } from 'umi-request'; +// 按需决定是否使用 polyfill +import 'yet-another-abortcontroller-polyfill' +import Request from 'umi-request'; const controller = new AbortController(); // 创建一个控制器 const { signal } = controller; // 返回一个 AbortSignal 对象实例,它可以用来 with/abort 一个 DOM 请求。 diff --git a/package.json b/package.json index a126c19..b90ca8d 100644 --- a/package.json +++ b/package.json @@ -46,7 +46,6 @@ "whatwg-fetch": "^3.0.0" }, "dependencies": { - "abort-controller": "^3.0.0", "isomorphic-fetch": "^2.2.1", "qs": "^6.9.1" }, diff --git a/src/cancel/abortControllerCancel.js b/src/cancel/abortControllerCancel.js deleted file mode 100644 index 792a88c..0000000 --- a/src/cancel/abortControllerCancel.js +++ /dev/null @@ -1,22 +0,0 @@ -import 'abort-controller/polyfill'; - -let AbortController = undefined; -let AbortSignal = undefined; - -const g = - typeof self !== 'undefined' - ? self - : typeof window !== 'undefined' - ? window - : typeof global !== 'undefined' - ? global - : /* otherwise */ undefined; - -if (g) { - AbortController = typeof g.AbortController !== 'undefined' ? g.AbortController : AcAbortController; - AbortSignal = typeof g.AbortSignal !== 'undefined' ? g.AbortSignal : AcAbortSignal; -} - -export default AbortController; - -export { AbortController, AbortSignal }; diff --git a/src/index.js b/src/index.js index 79e9c1e..a480d4d 100644 --- a/src/index.js +++ b/src/index.js @@ -1,8 +1,7 @@ import request, { extend, fetch } from './request'; import Onion from './onion'; import { RequestError, ResponseError } from './utils'; -import { AbortController, AbortSignal } from './cancel/abortControllerCancel'; -export { extend, RequestError, ResponseError, Onion, fetch, AbortController, AbortSignal }; +export { extend, RequestError, ResponseError, Onion, fetch }; export default request; diff --git a/test/cancel/abortControllerCancel.test.js b/test/cancel/abortControllerCancel.test.js index 5d6b97c..3cec383 100644 --- a/test/cancel/abortControllerCancel.test.js +++ b/test/cancel/abortControllerCancel.test.js @@ -1,5 +1,5 @@ import createTestServer from 'create-test-server'; -import request, { AbortController } from '../../src/index'; +import request from '../../src/index'; const writeData = (data, res) => { res.setHeader('access-control-allow-origin', '*'); @@ -17,7 +17,7 @@ describe('test abortController', () => { server.close(); }); - const prefix = api => `${server.url}${api}`; + const prefix = (api) => `${server.url}${api}`; jest.useFakeTimers(); From 9146ebc6a706b895b65436003c79c0ee27c191bf Mon Sep 17 00:00:00 2001 From: "troy.lty" Date: Wed, 26 May 2021 10:57:46 +0800 Subject: [PATCH 89/94] 1.3.9 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index a126c19..47e1806 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.3.8", + "version": "1.3.9", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.esm.js", From 99c32819ded190b3ab6e45c6a189cae870fc9a7d Mon Sep 17 00:00:00 2001 From: Ding Date: Tue, 14 Sep 2021 19:51:29 +0800 Subject: [PATCH 90/94] =?UTF-8?q?feature:=20=E5=A2=9E=E5=8A=A0=20timeoutMe?= =?UTF-8?q?ssage=20=E9=85=8D=E7=BD=AE=20(#267)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: shiluo.dwt --- README.md | 27 ++++++++++++++------------- README_zh-CN.md | 27 ++++++++++++++------------- src/middleware/fetch.js | 3 ++- src/utils.js | 4 ++-- test/timeout.test.js | 2 +- types/index.d.ts | 1 + 6 files changed, 34 insertions(+), 30 deletions(-) diff --git a/README.md b/README.md index ced7de1..844bc3d 100644 --- a/README.md +++ b/README.md @@ -30,18 +30,18 @@ The network request library, based on fetch encapsulation, combines the features | :------------------- | :--------------------- | :--------------------- | :------------- | | 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 | ✅ | ❌ | ✅ | +| 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) @@ -227,7 +227,8 @@ More umi-request cases can see [antd-pro](https://github.com/umijs/ant-design-pr | params | url request parameters | object or URLSearchParams | -- | -- | | data | Submitted data | any | -- | -- | | headers | fetch original parameters | object | -- | {} | -| timeout | timeout, default millisecond, write with caution | number | -- | -- | +| timeout | timeout, default millisecond, write with caution | number | -- | +| timeoutMessage | customize timeout error message, please config `timeout` first | string | -- | -- | | 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' | diff --git a/README_zh-CN.md b/README_zh-CN.md index a812436..755a07f 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -34,18 +34,18 @@ | :--------- | :------------- | :------------- | :------------- | | 实现 | 浏览器原生支持 | 浏览器原生支持 | XMLHttpRequest | | 大小 | 9k | 4k (polyfill) | 14k | -| query 简化 | ✅ | ❌ | ✅ | -| post 简化 | ✅ | ❌ | ❌ | -| 超时 | ✅ | ❌ | ✅ | -| 缓存 | ✅ | ❌ | ❌ | -| 错误检查 | ✅ | ❌ | ❌ | -| 错误处理 | ✅ | ❌ | ✅ | -| 拦截器 | ✅ | ❌ | ✅ | -| 前缀 | ✅ | ❌ | ❌ | -| 后缀 | ✅ | ❌ | ❌ | -| 处理 gbk | ✅ | ❌ | ❌ | -| 中间件 | ✅ | ❌ | ❌ | -| 取消请求 | ✅ | ❌ | ✅ | +| query 简化 | ✅ | ❌ | ✅ | +| post 简化 | ✅ | ❌ | ❌ | +| 超时 | ✅ | ❌ | ✅ | +| 缓存 | ✅ | ❌ | ❌ | +| 错误检查 | ✅ | ❌ | ❌ | +| 错误处理 | ✅ | ❌ | ✅ | +| 拦截器 | ✅ | ❌ | ✅ | +| 前缀 | ✅ | ❌ | ❌ | +| 后缀 | ✅ | ❌ | ❌ | +| 处理 gbk | ✅ | ❌ | ❌ | +| 中间件 | ✅ | ❌ | ❌ | +| 取消请求 | ✅ | ❌ | ✅ | 更多讨论参考[传统 Ajax 已死,Fetch 永生](https://github.com/camsong/blog/issues/2), 如果你有好的建议和需求, 请提 [issue](https://github.com/umijs/umi/issues) @@ -233,7 +233,8 @@ umi-request 可以进行一层简单封装后再使用, 可参考 [antd-pro](htt | params | url 请求参数 | object 或 URLSearchParams 对象 | -- | -- | | data | 提交的数据 | any | -- | -- | | headers | fetch 原有参数 | object | -- | {} | -| timeout | 超时时长, 默认毫秒, 写操作慎用 | number | -- | -- | +| timeout | 超时时长, 默认毫秒, 写操作慎用 | number | -- | +| timeoutMessage | 超时可自定义提示文案, 需先定义 timeout | string | -- | -- | | prefix | 前缀, 一般用于覆盖统一设置的 prefix | string | -- | -- | | suffix | 后缀, 比如某些场景 api 需要统一加 .json | string | -- | -- | | credentials | fetch 请求包含 cookies 信息 | string | -- | credentials: 'same-origin' | diff --git a/src/middleware/fetch.js b/src/middleware/fetch.js index 8d577f7..6d30176 100644 --- a/src/middleware/fetch.js +++ b/src/middleware/fetch.js @@ -15,6 +15,7 @@ export default function fetchMiddleware(ctx, next) { const { req: { options = {}, url = '' } = {}, cache, responseInterceptors } = ctx; const { timeout = 0, + timeoutMessage, __umiRequestCoreType__ = 'normal', useCache = false, method = 'get', @@ -59,7 +60,7 @@ export default function fetchMiddleware(ctx, next) { let response; // 超时处理、取消请求处理 if (timeout > 0) { - response = Promise.race([cancel2Throw(options, ctx), adapter(url, options), timeout2Throw(timeout, ctx.req)]); + response = Promise.race([cancel2Throw(options, ctx), adapter(url, options), timeout2Throw(timeout, timeoutMessage, ctx.req)]); } else { response = Promise.race([cancel2Throw(options, ctx), adapter(url, options)]); } diff --git a/src/utils.js b/src/utils.js index 09e152a..81a7410 100644 --- a/src/utils.js +++ b/src/utils.js @@ -105,10 +105,10 @@ export function safeJsonParse(data, throwErrIfParseFail = false, response = null return data; } -export function timeout2Throw(msec, request) { +export function timeout2Throw(msec, timeoutMessage, request) { return new Promise((_, reject) => { setTimeout(() => { - reject(new RequestError(`timeout of ${msec}ms exceeded`, request, 'Timeout')); + reject(new RequestError(timeoutMessage || `timeout of ${msec}ms exceeded`, request, 'Timeout')); }, msec); }); } diff --git a/test/timeout.test.js b/test/timeout.test.js index bda3ad1..6a19f08 100644 --- a/test/timeout.test.js +++ b/test/timeout.test.js @@ -50,7 +50,7 @@ describe('timeout', () => { response = await request(prefix('/test/timeout'), { timeout: 800 }); } catch (error) { expect(error.name).toBe('RequestError'); - expect(error.message).toBe('timeout of 800ms exceeded'); + expect(error.message).toBe(error.request.options.timeoutMessage || 'timeout of 800ms exceeded'); expect(error.request.options.timeout).toBe(800); done(); } diff --git a/types/index.d.ts b/types/index.d.ts index 8947aa6..0156976 100644 --- a/types/index.d.ts +++ b/types/index.d.ts @@ -34,6 +34,7 @@ export interface RequestOptionsInit extends RequestInit { useCache?: boolean; ttl?: number; timeout?: number; + timeoutMessage?: string; errorHandler?: (error: ResponseError) => void; prefix?: string; suffix?: string; From 7ac65e00e8c4eba822c217f2501b43924ac6afeb Mon Sep 17 00:00:00 2001 From: chenshuai2144 Date: Tue, 14 Sep 2021 19:54:28 +0800 Subject: [PATCH 91/94] v1.4.0 --- package.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/package.json b/package.json index 47c1ddb..d0176b4 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "umi-request", - "version": "1.3.9", + "version": "1.4.0", "description": "A request tool based on fetch.", "main": "dist/index.js", "module": "dist/index.esm.js", From 7856e981199f443e5c84b144430eef057de937e6 Mon Sep 17 00:00:00 2001 From: xiaocaiji <50451924+2462870727@users.noreply.github.com> Date: Tue, 14 Sep 2021 19:57:31 +0800 Subject: [PATCH 92/94] =?UTF-8?q?=E4=BF=AE=E6=94=B9=E6=96=87=E6=A1=A3?= =?UTF-8?q?=E4=B8=8A=E4=BC=A0=E6=96=87=E4=BB=B6=E7=A4=BA=E4=BE=8B=E9=97=AE?= =?UTF-8?q?=E9=A2=98=20(#265)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 上传文件应当指定指定 requestType: 'form',文档示例缺少改部分代码 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 844bc3d..83871dd 100644 --- a/README.md +++ b/README.md @@ -822,7 +822,7 @@ Use FormData() contructor,the browser will add request header `"Content-Type: ```javascript const formData = new FormData(); formData.append('file', file); -request('/api/v1/some/api', { method: 'post', data: formData }); +request('/api/v1/some/api', { method: 'post', data: formData, requestType: 'form',}); ``` 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) From 48a074f9f165185bd4a599090d662b8e6fb15b80 Mon Sep 17 00:00:00 2001 From: wood3n <31716713+wood3n@users.noreply.github.com> Date: Tue, 14 Sep 2021 19:57:46 +0800 Subject: [PATCH 93/94] Update README_zh-CN.md (#256) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 文件上传补充说明 --- README_zh-CN.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/README_zh-CN.md b/README_zh-CN.md index 755a07f..0b0416c 100644 --- a/README_zh-CN.md +++ b/README_zh-CN.md @@ -819,12 +819,12 @@ request('/api/v1/some/api', { getResponse: true }).then(({ data, response }) => ### 文件上传 -使用 FormData() 构造函数时,浏览器会自动识别并添加请求头 `"Content-Type: multipart/form-data"`, 且参数依旧是表单提交时那种键值对,因此不需要开发者手动设置 **Content-Type** +使用 FormData() 构造函数时,需要制定`requestType: "form"`,然后浏览器会自动识别并添加请求头 `"Content-Type: multipart/form-data"`,且参数依旧是表单提交时那种键值对,因此不需要开发者手动设置请求头 **Content-Type**,否则可能接口会报 500 的错误。 ```javascript const formData = new FormData(); formData.append('file', file); -request('/api/v1/some/api', { method: 'post', data: formData }); +request('/api/v1/some/api', { method: 'post', requestType: "form", data: formData }); ``` 如果希望获取自定义头部信息,需要在服务器设置 [Access-Control-Expose-Headers](https://developer.mozilla.org/zh-CN/docs/Web/HTTP/Headers/Access-Control-Expose-Headers),然后可按照上述方式获取自定义头部信息。 From 50197be3c854f7480713d407f16209adc1134aa9 Mon Sep 17 00:00:00 2001 From: weishaodaren <45391716+weishaodaren@users.noreply.github.com> Date: Tue, 14 Sep 2021 19:58:05 +0800 Subject: [PATCH 94/94] Update cancel.js (#234) --- src/cancel/cancel.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/cancel/cancel.js b/src/cancel/cancel.js index 9d5ec1c..b69bd5c 100644 --- a/src/cancel/cancel.js +++ b/src/cancel/cancel.js @@ -1,7 +1,7 @@ 'use strict'; /** - * 当执行 “取消请求” 操作时会抛出 Cancel 对象作为一场 + * 当执行 “取消请求” 操作时会抛出 Cancel 对象作为异常 * @class * @param {string=} message The message. */