Skip to content

Latest commit

 

History

History
844 lines (656 loc) · 26.9 KB

README_zh-CN.md

File metadata and controls

844 lines (656 loc) · 26.9 KB

English | 简体中文

umi-request

网络请求库,基于 fetch 封装, 兼具 fetch 与 axios 的特点, 旨在为开发者提供一个统一的 api 调用方式, 简化使用, 并提供诸如缓存, 超时, 字符编码处理, 错误处理等常用功能.

NPM version build status


支持的功能

  • url 参数自动序列化
  • post 数据提交方式简化
  • response 返回处理简化
  • api 超时支持
  • api 请求缓存支持
  • 支持处理 gbk
  • 类 axios 的 request 和 response 拦截器(interceptors)支持
  • 统一的错误处理方式
  • 类 koa 洋葱机制的 use 中间件机制支持
  • 类 axios 的取消请求
  • 支持 node 环境发送 http 请求

与 fetch, axios 异同

特性 umi-request fetch axios
实现 浏览器原生支持 浏览器原生支持 XMLHttpRequest
大小 9k 4k (polyfill) 14k
query 简化
post 简化
超时
缓存
错误检查
错误处理
拦截器
前缀
后缀
处理 gbk
中间件
取消请求

更多讨论参考传统 Ajax 已死,Fetch 永生, 如果你有好的建议和需求, 请提 issue

TODO 欢迎 pr

  • 测试用例覆盖 85%+
  • 写文档
  • CI 集成
  • 发布配置
  • typescript

安装

npm install --save umi-request

快速上手

执行 GET 请求

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 请求

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])

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])

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 环境创建实例

const umi = require('umi-request');
const extendRequest = umi.extend({ timeout: 10000 });

extendRequest('/api/user')
  .then(res => {
    console.log(res);
  })
  .catch(err => {
    console.log(err);
  });

以下是可用的实例方法,指定的配置将与实例的配置合并。

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])

umi-request 可以进行一层简单封装后再使用, 可参考 antd-pro

请求配置

request options 参数

参数 说明 类型 可选值 默认值
method 请求方式 string get , post , put ... get
params url 请求参数 object 或 URLSearchParams 对象 -- --
data 提交的数据 any -- --
headers fetch 原有参数 object -- {}
timeout 超时时长, 默认毫秒, 写操作慎用 number --
timeoutMessage 超时可自定义提示文案, 需先定义 timeout string -- --
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 文档

extend options 初始化默认参数, 支持以上所有

参数 说明 类型 可选值 默认值
method 请求方式 string get , post , put ... get
params url 请求参数 object -- --
data 提交的数据 any -- --
...
{
  // 'method' 是创建请求时使用的方法
  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'
  // 必须是以下类型之一:
  // - 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: 'same-origin', // default

  // ’useCache‘ 是否使用缓存,当值为 true 时,GET 请求在 ttl 毫秒内将被缓存,缓存策略唯一 key 为 url + params + method 组合
  useCache: false, // default

  // ’ttl‘ 缓存时长(毫秒), 0 为不过期
  ttl: 60000,

  // '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 = {
  //   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,
}

更新拓展实例默认参数

实例化一个请求实例后,有时还需要动态更新默认参数,umi-request 提供 extendOptions 方法供用户进行更新:

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 如下:

{
  // `data` 由服务器提供的响应, 需要进行解析才能获取
  data: {},

  // `status` 来自服务器响应的 HTTP 状态码
  status: 200,

  // `statusText` 来自服务器响应的 HTTP 状态信息
  statusText: 'OK',

  // `headers` 服务器响应的头
  headers: {},
}

当 options.getResponse === false 时, 响应结构为解析后的 data

request.get('/api/v1/xxx', { getResponse: false }).then(function(data) {
  console.log(data);
});

当 options.getResponse === true 时,响应结构为包含 data 和 Response 的对象

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 对象获取使用,参考错误处理这一节文档。

错误处理

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);
  }

  throw error; // 如果throw. 错误将继续抛出.

  // 如果return, 则将值作为返回. 'return;' 相当于return undefined, 在处理结果时判断response是否有值即可.
  // return {some: 'data'};
};

// 1. 作为统一错误处理
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);
  });

中间件

类 koa 的洋葱机制,让开发者优雅地做请求前后的增强处理,支持创建实例、全局、内核中间件。

实例中间件(默认) :request.use(fn) 不同实例创建的中间件相互独立不影响;

全局中间件 : request.use(fn, { global: true }) 全局中间件,不同实例共享全局中间件;

内核中间件 :request.use(fn, { core: true }) 内核中间件, 方便开发者拓展请求内核;

request.use(fn[, options])

参数

fn 入参

  • ctx(Object):上下文对象,包括 req 和 res 对象
  • next(Function):调用下一个中间件的函数

options 参数

  • global(boolean): 是否为全局中间件,优先级比 core 高
  • core(boolean): 是否为内核中间件

例子

  1. 同类型中间件执行顺序
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
  1. 不同类型中间件执行顺序
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
  1. 使用中间件对请求前后做处理
request.use(async (ctx, next) => {
  const { req } = ctx;
  const { url, options } = req;

  // 判断是否需要添加前缀,如果是统一添加可通过 prefix、suffix 参数配置
  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; // 假设返回结果为 : { success: false, errorCode: 'B001' }
  if (!success) {
    // 对异常情况做对应处理
  }
});
  1. 使用内核中间件拓展请求能力
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();

    // 将响应数据写入 ctx 中
    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);
  });

拦截器

在请求或响应被 thencatch 处理前拦截它们。

  1. 全局拦截器
// request拦截器, 改变url 或 options.
request.interceptors.request.use((url, options) => {
  return {
    url: `${url}&interceptors=yes`,
    options: { ...options, interceptors: true },
  };
});

// 和上一个相同
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) => {
  const contentType = response.headers.get('Content-Type');
  return response;
});

// 提前对响应做异常处理
request.interceptors.response.use(response => {
  const codeMaps = {
    502: '网关错误。',
    503: '服务不可用,服务器暂时过载或维护。',
    504: '网关超时。',
  };
  message.error(codeMaps[response.status]);
  return response;
});

// 克隆响应对象做解析处理
request.interceptors.response.use(async response => {
  const data = await response.clone().json();
  if (data && data.NOT_LOGIN) {
    location.href = '登录url';
  }
  return response;
});
  1. 实例内部拦截器
// 全局拦截器直接使用 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 }
);

中止请求

通过 AbortController 来中止请求

基于 AbortController 方案来中止一个或多个DOM请求

// 按需决定是否使用 polyfill
import 'yet-another-abortcontroller-polyfill'
import Request 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 方案

  1. 你可以通过 CancelToken.source 来创建一个 cancel token,如下所示:
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.');
  1. 你也可以通过实例化 CancelToken 来创建一个 token,同时通过传入函数来获取取消方法:
import Request from 'umi-request';

const CancelToken = Request.CancelToken;
let cancel;

Request.get('/api/cancel', {
  cancelToken: new CancelToken(function executor(c) {
    cancel = c;
  }),
});
// 取消请求
cancel();

案例

如何获取响应头信息

通过 Headers.get() 获取响应头信息。(可参考 MDN 文档)

request('/api/v1/some/api', { getResponse: true }).then(({ data, response }) => {
  response.headers.get('Content-Type');
});

文件上传

使用 FormData() 构造函数时,需要制定requestType: "form",然后浏览器会自动识别并添加请求头 "Content-Type: multipart/form-data",且参数依旧是表单提交时那种键值对,因此不需要开发者手动设置请求头 Content-Type,否则可能接口会报 500 的错误。

const formData = new FormData();
formData.append('file', file);
request('/api/v1/some/api', { method: 'post', requestType: "form", data: formData });

如果希望获取自定义头部信息,需要在服务器设置 Access-Control-Expose-Headers,然后可按照上述方式获取自定义头部信息。

开发和调试

  • npm install
  • npm run dev
  • npm link
  • 然后到你测试的项目中执行 npm link umi-request
  • 引入并使用

代码贡献者

  • @clock157
  • @yesmeck
  • @yutingzhao1991