最近参照antd-pro脚手架进行开发,因此接触到了umi-request。
umijs/umi-requestgithub.comumi-request对fetch进行了封装,简化了api的使用,结合了fetch和axios的特点,具体可参照umi-request的readme介绍。
文件结构
核心文件夹为src文件夹,内含
- lib文件夹为修改后的fetch.js;
- defaultInterceptor.js用于注册拦截器;
- index.js是外部调用的入口文件;
- request.js提供request实例;
- utils.js定义了处理缓存、请求异常、响应异常、处理gbk、json语法分析的功能类;
- wrapped-fetch.js从文件名可以看出来为封装fetch,实现更新版数据的传递;
- wrapped-rpc.js从文件可以看出封装了rpc通信,待实现;
代码分析
原始代码里含的注释基本上已经很全面了
defaultInterceptor.js
功能有:数据提交方式简化/针对常见两种数据传输格式补全文件头“Accept: 'application/json', 'Content-Type': 'application/json(或者x-www-form-urlencoded);charset=UTF-8'”/url 参数自动序列化
主要是对传入的url和option进行格式化的处理,以便后续的fetch.js进行处理。
关于option的新增参数参见request.js。
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 简写默认值为 jsonconst { 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 自定义headeroptions.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,};
};
fetch.js
主要是定义了request和response拦截器。
拦截器,在AOP(Aspect-Oriented Programming)中用于在某个方法或字段被访问之前,进行拦截,然后在之前或之后加入某些操作。可以对http请求进行批量处理。
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');// 执行 request 的拦截器,使用defaultInterceptor,依据handler方式对url,option进行处理requestInterceptors.concat([defaultInterceptor]).forEach(handler => {const ret = handler(url, options);url = ret.url || url;options = ret.options || options;});// 将 method 改为大写options.method = options.method ? options.method.toUpperCase() : 'GET';// 请求数据let response = window.fetch(url, options);// 执行 response 的拦截器,依据handler方式对url,option进行处理responseInterceptors.forEach(handler => {response = response.then(res => handler(res, options));});return 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;
utils.js
定义了处理缓存(Mapcache)、请求异常(RequestError)、响应异常(ResponseError)、处理gbk(readerGBK)、json语法转换(safeJsonParse)的功能类;
- 处理缓存
options.maxCache是option的extend参数,可以在封装umi-request时指定,
如antd-pro. const request = extend({maxCache: 50,});
实现变量的获取、变量删除、缓存清空、value存入缓存(其中key json化后为关键字)
export class MapCache {constructor(options) {this.cache = new Map();this.timer = {};this.maxCache = options.maxCache || 0;}get(key) {return this.cache.get(JSON.stringify(key));}set(key, value, ttl = 60000) {// 如果超过最大缓存数, 删除头部的第一个缓存.if (this.maxCache > 0 && this.cache.size >= this.maxCache) {const deleteKey = [...this.cache.keys()][0];this.cache.delete(deleteKey);if (this.timer[deleteKey]) {clearTimeout(this.timer[deleteKey]);}}const cacheKey = JSON.stringify(key);this.cache.set(cacheKey, value);if (ttl > 0) {this.timer[cacheKey] = setTimeout(() => {this.cache.delete(cacheKey);delete this.timer[cacheKey];}, ttl);}}delete(key) {const cacheKey = JSON.stringify(key);delete this.timer[cacheKey];return this.cache.delete(cacheKey);}clear() {this.timer = {};return this.cache.clear();}
}
在wrapped-fetch.js中调用
_wrappedCache(instance, useCache) {if (useCache) {const { params, ttl } = this.options;//url请求参数params,和缓存时长ttl为option传入参数return instance.then(response => {// 只缓存状态码为 200的数据if (response.status === 200) {const copy = response.clone();copy.useCache = true;//this.cache为通过request.js调用传入的参数const mapCache = new MapCache(initOptions);this.cache.set({ url: this.url, params }, copy, ttl);}return response;});} else {return instance;}
}
- 请求异常
export class RequestError extends Error {constructor(text) {super(text);this.name = 'RequestError';}
}
在wrapped-fetch.js中如此使用,用于输出超时信息,其中timeout为option传入参数。需要注意的是超时后客户端虽然返回超时, 但api请求不会断开, 写操作慎用。
return Promise.race([new Promise((_, reject) =>setTimeout(() => reject(new RequestError(`timeout of ${timeout}ms exceeded`)), timeout)),instance,
]);
- 响应异常
export class ResponseError extends Error {constructor(response, text, data) {super(text || response.statusText);this.name = 'ResponseError';this.data = data;this.response = response;}
}
在wrapped-fetch.js中调用,示例,当异常时抛出异常
catch (e) {throw new ResponseError(response, e.message);}
- 支持gbk并进行json格式的转换
判断返回的数据时表示gbk编码的,如果是,将数据以gbk格式读入,然后再利用safeJsonParse转化为json格式
export function readerGBK(file) {return new Promise((resolve, reject) => {const reader = new FileReader();reader.onload = () => {resolve(reader.result);};reader.onerror = reject;reader.readAsText(file, 'GBK'); // setup GBK decoding});
}export function safeJsonParse(data) {try {return JSON.parse(data);} catch (e) {} // eslint-disable-linereturn data;
}
wrappedfetch.js
核心部分,基于fetch进行数据交互
一个 Promise 就是一个对象,它代表了一个异步操作的最终完成或者失败,通过 .then() 形式添加的回调函数,连续执行两个或者多个异步操作(链式调用,多个then,一个catch接收异常)。
一个 Promise有以下几种状态:
- pending: 初始状态,既不是成功,也不是失败状态。
- fulfilled: 意味着操作成功完成。
- rejected: 意味着操作失败。
当其中任一种情况出现时,Promise 对象的 then 方法绑定的处理方法(handlers )就会被调用(then方法包含两个参数:onfulfilled 和 onrejected,它们都是 Function 类型。当Promise状态为fulfilled时,调用 then 的 onfulfilled 方法,当Promise状态为rejected时,调用 then 的 onrejected 方法。
Promise.resolve(value)方法返回一个以给定值解析后的Promise 对象。但如果这个值是个thenable(即带有then方法),返回的promise会“跟随”这个thenable的对象,采用它的最终状态(指resolved/rejected/pending/settled);如果传入的value本身就是promise对象,则该对象作为Promise.resolve方法的返回值返回;否则以该值为成功状态返回promise对象。
Promise.reject(reason)返回一个状态为失败的Promise对象,并将给定的失败信息传递给对应的处理方法
export default class WrappedFetch {constructor(url, options, cache) {this.cache = cache;//cache是MapCache的实例this.url = url;this.options = options;this._addfix();return this._doFetch();}_doFetch() {//如果使用cache,只有在option的method是get且useCache为true时使用cacheif (useCache) {let response = this.cache.get({url: this.url,params: this.options.params,});if (response) {response = response.clone();let instance = Promise.resolve(response);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);}//对url进行自动序列化,添加前缀和后缀_addfix() {} //调用RequestError,在超时后输出信息_wrappedTimeout(instance) {}//调用MapCache,在用户使用usecache时将response复制到擦车中,并设置缓存时间_wrappedCache(instance, useCache) {}//处理返回类型, 并解析数据,如果编码方式时gbk则利用readerGBK和safeJsonParse进行转化,在读取response时,加入responseError处理_parseResponse(instance, useCache = false) {}//处理错误_handleError({ reject, resolve }, error) {}
}
request.js
option里支持的参数有:(较fetch增加的)
* @param {string} requestType post类型, 用来简化写content-Type, 默认json
* @param {*} data post数据
* @param {object} params query参数
* @param {string} responseType 服务端返回的数据类型, 用来解析数据, 默认json
* @param {boolean} useCache 是否使用缓存,只有get时有效, 默认关闭, 启用后如果命中缓存, response中有useCache=true. 另: 内存缓存, 刷新就没。一个简单的Map cache, 提供session local map三种前端cache方式.
* @param {number} ttl 缓存生命周期, 默认60秒, 单位毫秒
* @param {number} timeout 超时时长, 默认未设, 单位毫秒
* @param {boolean} getResponse 是否获取response源
* @param {function} errorHandler 错误处理
* @param {string} prefix 前缀
* @param {string} suffix 后缀
* @param {string} charset 字符集, 默认utf8
extend里支持的参数有:
* @param {number} maxCache 最大缓存数
* @param {string} prefix url前缀
* @param {function} errorHandler 统一错误处理方法
* @param {object} headers 统一的headers
method包括:'get', 'post', 'delete', 'put', 'patch','rpc'。其中,'get', 'post', 'delete', 'put', 'patch'属于REST风格,是http协议的一种直接应用,默认基于json作为传输格式,使用简单,学习成本低效率高。RPC是指远程过程调用直观说法就是A通过网络调用B的过程方法,是分布式系统中常见的方法。
- GET操作是安全且等幂的。所谓安全是指不管进行多少次操作,资源的状态都不会改变。
- PUT,DELETE操作是幂等的。所谓幂等是指不管进行多少次操作,结果都一样。比如我用PUT修改一篇文章,然后在做同样的操作,每次操作后的结果并没有不同,DELETE也是一样。
- POST操作既不是安全的,也不是幂等的,比如常见的POST重复加载问题:当我们多次发出同样的POST请求后,其结果是创建出了若干的资源。
安全和幂等的意义在于:当操作没有达到预期的目标时,我们可以不停的重试,而不会对资源产生副作用。从这个意义上说,POST操作需要谨慎。还有一点需要注意的就是,创建操作可以使用POST,也可以使用PUT,区别在于POST 是作用在一个集合资源之上的(/uri),而PUT操作是作用在一个具体资源之上的(/uri/xxx),再通俗点说,如果URL可以在客户端确定,那么就使用PUT,如果是在服务端确定,那么就使用POST,比如说很多资源使用数据库自增主键作为标识信息,而创建的资源的标识信息到底是什么只能由服务端提供,这个时候就必须使用POST。
- PATCH新引入的,是对PUT方法的补充,用来对已知资源进行局部更新。在没有patch之前,我们都是用put进行更新操作,这时候我们的接口中通常会有一个逻辑规则,如:如果对象的的一个字符属性为NULL,那么就是不更新该属性(字段)值,如果对象的字符属性是“”,那么就更新该属性(字段)的值,通过这种方式来避免全部覆盖的操作。现在有了patch就解决了这种判断,在put接口中不管属性是不是null,都进行更新,在patch接口中就对非null的进行更新
import fetch from './lib/fetch';
import { MapCache } from './utils';
import WrappedFetch from './wrapped-fetch';
import WrappedRpc from './wrapped-rpc';/*** 获取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 rpcreturn new WrappedRpc(input, options, mapCache);} else {return new WrappedFetch(input, options, mapCache);}};// 增加语法糖如: request.get request.post// 对应着对资源的查看,创建,删除,创建或更新,部分更新,rpcconst methods = ['get', 'post', 'delete', 'put', 'patch','rpc',];methods.forEach(method => {instance[method] = (input, options) => instance(input, { ...options, method });});// 给request 也增加一个interceptors引用;instance.interceptors = fetch.interceptors;return instance;
};/*** 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();
使用示例
通过extend进行简单封装,参照antd-pro,dva数据流处理方式。
import { extend } from 'umi-request';
......
const request = extend({errorHandler, // 默认错误处理函数credentials: 'include', // 默认请求是否带上cookie// useCache: false,// maxCache: 50,
});
export default request;
后续在api.js中的使用
import request from '@/utils/request';
//get请求
export async function function1({ classId }) {return request(`/api/classes/${classId}/teachers/`);}
//post请求
export async function function2({ classId }) {return request.post('/api/classes/${classId}/teachers/', { data: {foo: 'bar'}});}
// 使用缓存
export async function function3({ classId }) {return request(`/api/classes/${classId}/teachers/`, { useCache: true, ttl: 10000 });}
每一个页面对应的model中,可以通过下面的方式进行使用
* fetchBasic({ classId }, { call, put }) {//利用call方式调用api.js中声明的function1 const response = yield call(fuction1, classId);//对接收到的response进行处理yield put({type: 'saveResponse',//reducers中对应的处理方法payload: response});
},
特点
- url 参数自动序列化
- post 数据提交方式简化
- response 返回处理简化
- api 超时支持
- api 请求缓存支持
- 支持处理 gbk
- 类 axios 的 request 和 response 拦截器(interceptors)支持
更多内容查看umi-request项目、dva文档、antd-pro项目umi-request项目github.comDva 概念 | DvaJSdvajs.comantd-pro项目github.com