umi-request使用及原理解析

简介

umi-request 是一个基于 fetch 的轻量级 HTTP 请求库,专为 UmiJS 框架设计,但也适用于其他前端项目。它提供了简洁的 API 和强大的扩展能力,支持中间件、拦截器、全局配置等功能,非常适合现代前端开发。

与axios对比

特性 umi-request axios
实现 Fetch XMLHttpRequest
大小 9k 14k
typescript 内置完整类型 需安装@types/axios
全局配置
超时
取消请求
错误处理
拦截器
query 简化
post 简化
缓存
错误检查
前缀
后缀
处理 gbk
中间件

功能介绍

url 参数自动序列化

库会自动处理请求参数params,将其转换为 URL 查询字符串。如果需要自定义序列化逻辑,可以通过拦截器或中间件实现。

通过全局中间件实现

源码(\src\middleware\simpleGet.js):

ini 复制代码
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也可用, 不冲突.
  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;

  return next();
}

post 数据提交方式简化

当你使用 POST 请求时,库会根据 data 的类型自动选择合适的数据提交方式,并设置相应的 Content-Type

支持的 data 类型

  • 普通对象 :自动序列化为 JSON 格式,并设置 Content-Type: application/json
  • URLSearchParams 对象 :自动序列化为 URL 编码格式,并设置 Content-Type: application/x-www-form-urlencoded
  • FormData 对象 :自动以 multipart/form-data 格式提交,并设置 Content-Type: multipart/form-data

通过全局中间件实现

源码(\src\middleware\simplePost.js):

ini 复制代码
export default function simplePostMiddleware(ctx, next) {
  if (!ctx) return 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 = reqStringify(data, { arrayFormat: 'repeat', strictNullHandling: true });
      }
    } else {
      // 其他 requestType 自定义header
      options.headers = {
        Accept: 'application/json',
        ...options.headers,
      };
      options.body = data;
    }
  }
  ctx.req.options = options;

  return next();
}

response 返回处理简化

库对响应数据进行了统一的封装和处理,使得开发者可以更方便地获取和处理响应内容。umi-request 默认会将响应数据解析为 JavaScript 对象,并提供了一些便捷的方法来处理常见的响应场景,如状态码检查、错误处理等

通过全局中间件实现

源码(\src\middleware\parseResponse.js):

ini 复制代码
export default function parseResponseMiddleware(ctx, next) {
  let copy;

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

      // 只在浏览器环境对 response 做克隆, node 环境如果对 response 克隆会有问题:https://github.com/bitinn/node-fetch/issues/553
      copy = getEnv() === 'BROWSER' ? res.clone() : res;
      copy.useCache = res.useCache || false;

      // 解析数据
      if (charset === 'gbk') {
        try {
          return res
            .blob()
            .then(readerGBK)
            .then(d => safeJsonParse(d, false, copy, req));
        } catch (e) {
          throw new ResponseError(copy, e.message, null, req, 'ParseError');
        }
      } else if (responseType === 'json') {
        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', null, req, 'ParseError');
      }
    })
    .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) {
          ctx.res = { data: body, response: copy };
          return;
        }
        ctx.res = body;
        return;
      }
      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 = e.request || req;
      e.response = e.response || res;
      e.type = e.type || e.name;
      e.data = e.data || undefined;
      throw e;
    });
}

api 超时支持

库提供了配置项来设置请求的超时时间。如果请求在指定时间内未完成,umi-request 会自动中断请求并抛出超时错误。这一功能对于优化用户体验和避免长时间等待非常重要。

go 复制代码
const errorHandler = (error) => {
    if (error && error.message === 'go to login') {
        return Promise.reject(error);
    }
    ElNotification({
        title: 'Tip',
        type: 'error',
        message: error && error.message ? error.message : 'Request Fail',
    });
    return Promise.reject(error);
};
// 在实例上统一做超时处理
const request = extend({
    timeout: 180000,
    errorHandler,
});
json 复制代码
// errorHandler回调时error参数为:
{
    "name": "RequestError",
    "request": {
        "url": "/xxx",
        "options": {
            "timeout": 1,
            "params": {},
            "method": "GET",
            "headers": {
                "traceparent": "xxx"
            },
            "url": "/xxx",
            "credentials": "same-origin"
        },
        "originUrl": "/xxx"
    },
    "type": "Timeout"
}

在内核中间件fetchMiddleware中通过Promise.race实现

源码(\src\middleware\fetch.js):

scss 复制代码
let response;
  // 超时处理、取消请求处理
  if (timeout > 0) {
    response = Promise.race([cancel2Throw(options, ctx), adapter(url, options), timeout2Throw(timeout, timeoutMessage, ctx.req)]);
  } else {
    response = Promise.race([cancel2Throw(options, ctx), adapter(url, options)]);
  }

api 请求缓存支持

库提供了一种机制,可以将请求的响应结果缓存起来,当相同的请求再次发起时,直接从缓存中返回结果,而不是重新发起网络请求。这种机制可以显著提升应用的性能,尤其是在数据变化不频繁的场景下。

在内核中间件fetchMiddleware中通过Map实现

源码(\src\middleware\fetch.js):

ini 复制代码
// 从缓存池检查是否有缓存数据
  const isBrowser = getEnv() === 'BROWSER';
  const needCache = validateCache(url, options) && useCache && isBrowser;
  if (needCache) {
    let responseCache = cache.get({
      url,
      params,
      method,
    });
    if (responseCache) {
      responseCache = responseCache.clone();
      responseCache.useCache = true;
      ctx.res = responseCache;
      return next();
    }
  }
  
...
  
return response.then(res => {
    // 是否存入缓存池
    if (needCache) {
      if (res.status === 200) {
        const copy = res.clone();
        copy.useCache = true;
        cache.set({ url, params, method }, copy, ttl);
      }
    }

    ctx.res = res;
    return next();
  });

cache定义(\src\utils.js):

kotlin 复制代码
export class MapCache {
  constructor(options) {
    this.cache = new Map();
    this.timer = {};
    this.extendOptions(options);
  }

  extendOptions(options) {
    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();
  }
}

支持处理 gbk

库能够处理以 GBK 编码的响应数据。由于现代 Web 开发中普遍使用 UTF-8 编码,但某些旧的接口或服务可能仍然使用 GBK 编码(尤其是中文环境下)

通过全局中间件实现

源码(\src\middleware\parseResponse.js):

javascript 复制代码
if (charset === 'gbk') {
        try {
          return res
            .blob()
            .then(readerGBK)
            .then(d => safeJsonParse(d, false, copy, req));
        } catch (e) {
          throw new ResponseError(copy, e.message, null, req, 'ParseError');
        }
      }

readerGBK定义(\src\utils.js):

ini 复制代码
/**
 * http://gitlab.alipay-inc.com/KBSJ/gxt/blob/release_gxt_S8928905_20180531/src/util/request.js#L63
 * 支持gbk
 */
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
  });
}

类 axios 的 request 和 response 拦截器(interceptors)支持

提供了类似于 axios 的请求和响应拦截器(interceptors)支持,这是一种强大的机制,允许你在请求发送之前和响应返回之后对请求和响应进行全局的处理。

通过global选项可以区分是全局拦截器还是实例拦截器

转成类axios的调用方式(\src\request.js):

css 复制代码
// 拦截器
  umiInstance.interceptors = {
    request: {
      use: Core.requestUse.bind(coreInstance),
    },
    response: {
      use: Core.responseUse.bind(coreInstance),
    },
  };

定义(\src\core.js):

javascript 复制代码
// 请求拦截器 默认 { global: true } 兼容旧版本拦截器
  static requestUse(handler, opt = { global: true }) {
    if (typeof handler !== 'function') throw new TypeError('Interceptor must be function!');
    if (opt.global) {
      Core.requestInterceptors.push(handler);
    } else {
      this.instanceRequestInterceptors.push(handler);
    }
  }

  // 响应拦截器 默认 { global: true } 兼容旧版本拦截器
  static responseUse(handler, opt = { global: true }) {
    if (typeof handler !== 'function') throw new TypeError('Interceptor must be function!');
    if (opt.global) {
      Core.responseInterceptors.push(handler);
    } else {
      this.instanceResponseInterceptors.push(handler);
    }
  }

在请求发起前,收到响应后执行拦截器(\src\core.js):

javascript 复制代码
// 执行请求前拦截器
  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);
      });
    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();
    });
  }

  request(url, options) {
    const { onion } = this;
    const obj = {
      req: { url, options: { ...options, url } },
      res: null,
      cache: this.mapCache,
      responseInterceptors: [...Core.responseInterceptors, ...this.instanceResponseInterceptors],
    };
    if (typeof url !== 'string') {
      throw new Error('url MUST be a string');
    }

    return new Promise((resolve, reject) => {
      this.dealRequestInterceptors(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);
          }
        });
    });
  }
}

统一的错误处理方式

可以让你在一个地方集中处理各种请求错误,避免在每个请求的 catch 块中重复编写错误处理逻辑,提高代码的可维护性和可读性。

  • 可以在响应拦截器中统一处理错误
javascript 复制代码
import request from 'umi-request';

// 注册响应拦截器
request.interceptors.response.use(async (response) => {
    const cloneResponse = response.clone(); // 克隆响应,避免后续无法读取响应体
    try {
        const data = await cloneResponse.json(); // 尝试解析响应体为 JSON
        // 检查业务逻辑错误,假设服务器返回的 JSON 中有一个 code 字段表示业务状态
        if (data.code!== 200) {
            throw new Error(data.message || '业务逻辑错误');
        }
        return data;
    } catch (error) {
        // 处理 HTTP 状态码错误和解析错误
        if (response.status >= 400) {
            const errorMessage = `请求失败,状态码: ${response.status}`;
            console.error(errorMessage);
            throw new Error(errorMessage);
        }
        // 其他解析错误
        console.error('解析响应数据时出错:', error);
        throw error;
    }
}, (error) => {
    // 处理网络错误
    if (error.type === 'Timeout') {
        console.error('请求超时,请检查网络连接');
    } else if (error.type === 'Network Error') {
        console.error('网络连接错误,请检查网络设置');
    } else {
        console.error('未知错误:', error);
    }
    return Promise.reject(error);
});

// 发送请求
request.get('https://api.example.com/data')
    .then((response) => {
        console.log('请求成功:', response);
    })
    .catch((error) => {
        // 错误已经在响应拦截器中处理过
    });
  • 定义通用选项errorHandler
lua 复制代码
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 });

类 koa 洋葱机制的 use 中间件机制支持

类 Koa 洋葱机制的 use 中间件机制,这种机制能够让开发者在请求的发送与响应过程里插入自定义逻辑,就像 Koa 框架里的中间件一样

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

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

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

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

执行顺序如下:

rust 复制代码
a1 -> b1 -> response -> b2 -> a2
  1. 不同类型中间件执行顺序
vbnet 复制代码
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 }
);

执行顺序如下:

rust 复制代码
instanceA1 -> instanceB1 -> globalA1 -> coreA1 -> coreA2 -> globalA2 -> instanceB2 -> instanceA2
  1. 使用中间件对请求前后做处理 4. 使用内核中间件拓展请求能力

源码(\src\onion\index.js):

ini 复制代码
// 参考自 puck-core 请求库的插件机制
import compose from './compose';

class Onion {
  constructor(defaultMiddlewares) {
    if (!Array.isArray(defaultMiddlewares)) throw new TypeError('Default middlewares must be an array!');
    this.defaultMiddlewares = [...defaultMiddlewares];
    this.middlewares = [];
  }

  static globalMiddlewares = []; // 全局中间件
  static defaultGlobalMiddlewaresLength = 0; // 内置全局中间件长度
  static coreMiddlewares = []; // 内核中间件
  static defaultCoreMiddlewaresLength = 0; // 内置内核中间件长度

  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(
          '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) {
      global = opts.global || false;
      core = opts.core || false;
      defaultInstance = opts.defaultInstance || false;
    }

    // 全局中间件
    if (global) {
      Onion.globalMiddlewares.splice(
        Onion.globalMiddlewares.length - Onion.defaultGlobalMiddlewaresLength,
        0,
        newMiddleware
      );
      return;
    }
    // 内核中间件
    if (core) {
      Onion.coreMiddlewares.splice(Onion.coreMiddlewares.length - Onion.defaultCoreMiddlewaresLength, 0, newMiddleware);
      return;
    }

    // 默认实例中间件,供开发者使用
    if (defaultInstance) {
      this.defaultMiddlewares.push(newMiddleware);
      return;
    }

    // 实例中间件
    this.middlewares.push(newMiddleware);
  }

  execute(params = null) {
    const fn = compose([
      ...this.middlewares,
      ...this.defaultMiddlewares,
      ...Onion.globalMiddlewares,
      ...Onion.coreMiddlewares,
    ]);
    return fn(params);
  }
}

export default Onion;

compose定义(\src\onion\compose.js):

javascript 复制代码
// 返回一个组合了所有插件的"插件"

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

类 axios 的取消请求

在网络请求场景中,有时候我们可能需要取消正在进行的请求,比如在组件卸载时取消未完成的请求以避免内存泄漏和不必要的资源浪费。umi-request 提供了类似于 axios 的取消请求功能

通过 AbortController 来中止请求

AbortController 接口表示一个控制器对象,允许你根据需要中止一个或多个 Web 请求。

这是fetch的原生能力

javascript 复制代码
// 按需决定是否使用 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 来实现请求中止。

在内核中间件fetchMiddleware中通过Promise.race实现

源码(\src\middleware\fetch.js):

scss 复制代码
let response;
  // 超时处理、取消请求处理
  if (timeout > 0) {
    response = Promise.race([cancel2Throw(options, ctx), adapter(url, options), timeout2Throw(timeout, timeoutMessage, ctx.req)]);
  } else {
    response = Promise.race([cancel2Throw(options, ctx), adapter(url, options)]);
  }

cancel2Throw定义(\src\utils.js):

javascript 复制代码
export function cancel2Throw(opt) {
  return new Promise((_, reject) => {
    if (opt.cancelToken) {
      opt.cancelToken.promise.then(cancel => {
        reject(cancel);
      });
    }
  });
}

CancelToken定义(\src\cancel\cancelToken.js):

javascript 复制代码
'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;

支持 node 环境发送 http 请求

ini 复制代码
const umi = require('umi-request');
const extendRequest = umi.extend({ timeout: 10000 });

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

在内核中间件fetchMiddleware中通过导入isomorphic-fetch实现

源码(\src\middleware\fetch.js):

arduino 复制代码
import 'isomorphic-fetch';

原理解析

源码目录

github.com/umijs/umi-r...

go 复制代码
umi-request-master
├─ .editorconfig
├─ .eslintignore
├─ .eslintrc
├─ .fatherrc.js
├─ .prettierrc
├─ .travis.yml
├─ CHANGELOG.MD
├─ jest.json
├─ package.json
├─ README.md
├─ README_zh-CN.md
├─ src
│  ├─ cancel // 取消请求
│  │  ├─ cancel.js
│  │  ├─ cancelToken.js
│  │  └─ isCancel.js
│  ├─ core.js // 核心
│  ├─ index.js // 导出
│  ├─ interceptor // 前后缀拦截
│  │  └─ addfix.js
│  ├─ middleware // 中间件
│  │  ├─ fetch.js
│  │  ├─ parseResponse.js
│  │  ├─ simpleGet.js
│  │  └─ simplePost.js
│  ├─ onion // 中间件机制
│  │  ├─ compose.js
│  │  └─ index.js
│  ├─ request.js // 在 core 之上再封装一层,提供原 umi/request 一致的 api,无缝升级
│  └─ utils.js // 工具函数
├─ test // 测试用例
└─ types // 类型定义
   └─ index.d.ts

request实例解析

core 基础上封装一个实例umiInstance

  1. 提供默认配置
arduino 复制代码
export default request({});

这里为空

  1. 请求语法糖: reguest.get request.post ......
ini 复制代码
// 请求语法糖: reguest.get request.post ......
  const METHODS = ['get', 'post', 'delete', 'put', 'patch', 'head', 'options', 'rpc'];
  METHODS.forEach(method => {
    umiInstance[method] = (url, options) => umiInstance(url, { ...options, method });
  });
  1. 暴露各个实例的中间件,供开发者自由组合
yaml 复制代码
// 暴露各个实例的中间件,供开发者自由组合
  umiInstance.middlewares = {
    instance: coreInstance.onion.middlewares,
    defaultInstance: coreInstance.onion.defaultMiddlewares,
    global: Onion.globalMiddlewares,
    core: Onion.coreMiddlewares,
  };

core

初始化全局和内核中间件
ini 复制代码
const globalMiddlewares = [simplePost, simpleGet, parseResponseMiddleware];
const coreMiddlewares = [fetchMiddleware];
全局中间件
  • simplePost

    post 数据提交方式简化

  • simpleGet

    url 参数自动序列化

  • parseResponseMiddleware

    response 返回处理简化

内核中间件
  • fetchMiddleware
    调用fetch执行核心逻辑
ini 复制代码
export default function fetchMiddleware(ctx, next) {
  if (!ctx) return next();
  const { req: { options = {}, url = '' } = {}, cache, responseInterceptors } = ctx;
  const {
    timeout = 0,
    timeoutMessage,
    __umiRequestCoreType__ = 'normal',
    useCache = false,
    method = 'get',
    params,
    ttl,
    validateCache = __defaultValidateCache,
  } = options;

  if (__umiRequestCoreType__ !== 'normal') {
    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 or use request core.'
      );
    }
    return next();
  }

  const adapter = fetch;

  if (!adapter) {
    throw new Error('Global fetch not exist!');
  }

  // 从缓存池检查是否有缓存数据
  const isBrowser = getEnv() === 'BROWSER';
  const needCache = validateCache(url, options) && useCache && isBrowser;
  if (needCache) {
    let responseCache = cache.get({
      url,
      params,
      method,
    });
    if (responseCache) {
      responseCache = responseCache.clone();
      responseCache.useCache = true;
      ctx.res = responseCache;
      return next();
    }
  }

  let response;
  // 超时处理、取消请求处理
  if (timeout > 0) {
    response = Promise.race([cancel2Throw(options, ctx), adapter(url, options), timeout2Throw(timeout, timeoutMessage, ctx.req)]);
  } else {
    response = Promise.race([cancel2Throw(options, ctx), adapter(url, options)]);
  }

  // 兼容老版本 response.interceptor
  responseInterceptors.forEach(handler => {
    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 => {
    // 是否存入缓存池
    if (needCache) {
      if (res.status === 200) {
        const copy = res.clone();
        copy.useCache = true;
        cache.set({ url, params, method }, copy, ttl);
      }
    }

    ctx.res = res;
    return next();
  });
}
将中间件信息绑定到Onin
ini 复制代码
Onion.globalMiddlewares = globalMiddlewares;
Onion.defaultGlobalMiddlewaresLength = globalMiddlewares.length;
Onion.coreMiddlewares = coreMiddlewares;
Onion.defaultCoreMiddlewaresLength = coreMiddlewares.length;
Core 类定义

Onion 类定义

整体逻辑

业务使用

  • 统一管理 API 请求
  • 可以通过拦截器和中间件,插入自定义业务逻辑,如权限检查,请求url及参数的通用处理,返回数据的统一处理,记录日志或上报错误信息
  • 可以利用缓存能力,减少频繁发起重复请求
  • 利用取消请求的能力,减少不必要的请求

通过合理使用 umi-request,可以显著提升项目的开发效率和代码质量。

参考

github.com/umijs/umi-r...

相关推荐
travel_wsy31 分钟前
webrtc 视频直播
前端·vue.js·音视频·webrtc
zybsjn42 分钟前
开发 Chrome 扩展中的侧边栏图标设置实录(Manifest V3)
前端·chrome
weixin_428498491 小时前
在Star-CCM+中实现UDF并引用场数据和网格数据
java·前端
小猫猫改bug2 小时前
threejs 添加css3d标签 vue3
前端·javascript·css3
前端小巷子2 小时前
CSS3 过渡与动画
前端·css·css3
m0_zj2 小时前
58.[前端开发-前端工程化]Day05-webpack-Git安装-配置-Git命令
前端·webpack·node.js
GanGuaGua2 小时前
CSS:字体和文本样式
前端·css
Attacking-Coder2 小时前
前端面试宝典---JavaScript import 与 Node.js require 的区别
前端·javascript·node.js
切糕师学AI2 小时前
vue 中如何使用region?
前端·javascript·vue.js
蓝婷儿2 小时前
前端面试每日三题 - Day 28
前端·面试·职场和发展