用Proxy的方式统一管理接口请求

420 阅读4分钟

现状描述

我们一般在做vue或者react的单页面项目的时候,对所有接口的请求的管理一般都是创建一个公共的js,里面导出许多返回一个Promise的函数。

例如我在src/api目录下创建了一个common.js,代码如下:(注:request方法默认的Content-Type为application/json)

import request from '@/utils/request';
import qs from 'qs';
/***** 分组 *****/
// 分组列表
export function groupList(params) {
  return request({
    url: '/zm/group',
    method: 'get',
    params,
  });
}
// 分组详情
export function groupDetail(id) {
  return request({
    url: `/zm/group/${id}`,
    method: 'get',
  });
}
// 分组新增
export function groupAdd(data) {
  return request({
    url: '/zm/group',
    method: 'post',
    data,
  });
}
// 分组修改
export function groupUpdate(id, data) {
  return request({
    url: `/zm/group/${id}`,
    method: 'put',
    data,
  });
}
// 分组删除
export function groupDelete(id) {
  return request({
    url: `/zm/group/${id}`,
    method: 'delete',
  });
}
/***** 应用 *****/
// 应用列表
export function applicationList(fromType, unitSetId, params) {
  return request({
    url: `/zm/${fromType}/${unitSetId}/application`,
    method: 'get',
    params,
  });
}
// 应用详情
export function applicationDetail(fromType, unitSetId, id) {
  return request({
    url: `/zm/${fromType}/${unitSetId}/application/${id}`,
    method: 'get',
  });
}
// 应用新增
export function applicationAdd(fromType, unitSetId, data) {
  return request({
    url: `/zm/${fromType}/${unitSetId}/application`,
    method: 'post',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    transformRequest: [(data) => qs.stringify(data)],
    data,
  });
}
// 应用修改
export function applicationUpdate(dOpts, data) {
  const { fromType, unitSetId, id } = dOpts;
  return request({
    url: `/zm/${fromType}/${unitSetId}/application/${id}`,
    method: 'put',
    headers: {
      'Content-Type': 'application/x-www-form-urlencoded',
    },
    transformRequest: [(data) => qs.stringify(data)],
    data,
  });
}
/***** 文件上传 *****/
// 图片上传通用接口
export function sysUpload(formdata) {
  return request({
    url: '/zm/sys/upload',
    method: 'post',
    headers: {
      'Content-Type': 'multipart/form-data',
    },
    data: formdata,
  });
}

这也是我经常在一些项目中看到的一种方式,虽然这样做到对接口的统一管理,但是从我的角度看,还是有一些不足的地方:

  1. 接口数不太多,但是代码量却拉的很长,因为重复的代码太多了,例如 export function、return request等。
  2. Content-Type不同时,需要额外费点精力处理下。
  3. 因为只要是一个返回一个Promise的函数,导致写代码时个人自主性太强,很难形成一个规范,多人进行开发任务的时候,由于没有一个统一规范,不利于统一管理和维护。例如上面应用新增这个接口,某个开发人员把data这个形参放在第一个,后续其他人接手开发的时候,就很可能产生问题。

使用Proxy写接口请求服务

Proxy 对象可以拦截目标对象的任意属性,这所以很适用来写请求接口的服务。直接上代码,在src/service/index.js里的代码如下:

import qs from 'qs';
import request from '@/utils/request';

// 说明: 'post/2', 'put/2', 'patch/2' 表示Content-Type为application/x-www-form-urlencode的请求
export default new Proxy(
  {
    /***** 分组 *****/
    // #region
    // 分组列表
    groupList: 'get /zm/group',
    // 分组详情
    groupDetail: { method: 'get', url: (id) => `/zm/group/${id}` },
    // 分组新增
    groupAdd: 'post /zm/group',
    // 分组修改
    groupUpdate: { method: 'put', url: (id) => `/zm/group/${id}` },
    // 分组删除
    groupDel: { method: 'delete', url: (id) => `/zm/group/${id}` },
    // 所有分组列表
    groupAll: 'get /zm/group/all',
    // #endregion

    /***** 应用 *****/
    // #region
    // 应用列表
    applicationList: {
      method: 'get',
      url: (fromType, unitSetId) => `/zm/${fromType}/${unitSetId}/application`,
    },
    // 应用详情
    applicationDetail: {
      method: 'get',
      url: (fromType, unitSetId, id) => `/zm/${fromType}/${unitSetId}/application/${id}`,
    },
    // 应用新增
    applicationAdd: {
      method: 'post/2',
      url: (fromType, unitSetId) => `/zm/${fromType}/${unitSetId}/application`,
      config: { headers: { 'i-custom-header': 'ftx' } },
    },
    // 应用修改
    applicationUpdate: {
      method: 'put/2',
      url: (fromType, unitSetId, id) => `/zm/${fromType}/${unitSetId}/application/${id}`,
    },
    // #endregion
    /***** 文件上传 *****/
    // #region
    // 图片上传通用接口
    sysUpload: 'fileupload /zm/sys/upload',
    // #endregion
  },
  {
    cache: new Map(),
    handleRequest(url, method, conf) {
      if (['get', 'post', 'put', 'patch', 'delete'].includes(method)) {
        const dkey = ['get', 'delete'].includes(method) ? 'params' : 'data';
        return (params, config) =>
          request({
            url,
            method,
            [dkey]: params,
            ...conf,
            ...config,
          });
      } else if (['post/2', 'put/2', 'patch/2'].includes(method)) {
        const [m] = method.split('/');
        return (params, config) =>
          request({
            url,
            method: m,
            data: params,
            ...conf,
            ...config,
            headers: {
              ...(config?.headers ? config?.headers : conf?.headers),
              'Content-Type': 'application/x-www-form-urlencoded',
            },
            transformRequest: [(data) => qs.stringify(data)],
          });
      } else if (method === 'fileupload') {
        return (params, config) =>
          request({
            url,
            method: 'post',
            data: params,
            ...conf,
            ...config,
            headers: {
              ...(config?.headers ? config?.headers : conf?.headers),
              'Content-Type': 'multipart/form-data',
            },
          });
      }
    },
    get(target, prop, receiver) {
      const currentCache = this.cache;
      const handleRequest = this.handleRequest;
      if (currentCache.has(prop)) return currentCache.get(prop);
      let fun;
      const methodList = [
        'get',
        'post',
        'put',
        'patch',
        'delete',
        'post/2',
        'put/2',
        'patch/2',
        'fileupload',
      ];
      if (typeof target[prop] === 'string') {
        const [method, url] = target[prop].split(' ');
        if (methodList.includes(method)) fun = handleRequest(url, method);
      } else if (Object.prototype.toString.call(target[prop]) === '[object Object]') {
        const { method, url, config: config = {} } = target[prop];
        if (methodList.includes(method)) {
          if (typeof url === 'string') {
            fun = handleRequest(url, method, config);
          } else if (typeof url === 'function') {
            fun = (...args) => {
              if (Array.isArray(args[0])) {
                // args[0]是一个数组,是拼接url需要的参数 args[1]为请求参数 args[2]为config参数
                return handleRequest(url(...args[0]), method, config)(args[1], args[2]);
              } else {
                return handleRequest(url(...args), method, config)();
              }
            };
          }
        }
      }
      if (fun) {
        currentCache.set(prop, fun);
        return fun;
      }
      return Reflect.get(target, prop, receiver);
    },
    set() {
      throw new Error('error');
    },
  },
);

增加了一个缓存,提高性能。

那么我们请求接口的时候我们就可以这样去请求:

import service from '@/service';

export default {
  methods: {
    // 请求分组列表接口
    async groupList() {
      const res = await service.groupList({ name: '', limit: 10, page: 1 });
    },
    // 请求分组详情接口
    async groupDetail() {
      const res = await service.groupDetail(123);
    },
    // 请求分组新增接口
    async groupAdd() {
      const res = await service.groupAdd({ name: 'xxxx', value: 2222 });
    },
    // 请求分组修改接口
    async groupUpdate() {
      const res = await service.groupUpdate([123], { name: 'xxxx', value: 333 });
    },
    // 请求分组删除接口
    async groupDelete() {
      const res = await service.groupDelete(123, { headers: { token: '6utm3UBVGS=' } });
    },

    // 请求应用列表接口
    async applicationList() {
      const res = await service.applicationList(['from', 234], { name: '', limit: 10, page: 1 });
    },
    // 请求应用详情接口
    async applicationDetail() {
      const res = await service.applicationDetail('from', 234, 123);
    },
    // 请求应用新增接口
    async applicationAdd() {
      const res = await service.applicationAdd(['from', 234], { name: 'xxxx', value: 2222 });
    },
    // 请求应用修改接口
    async applicationUpdate() {
      const res = await service.applicationUpdate(['from', 234, 123], { name: 'xxxx', value: 333 });
    },
    // 请求图片上传通用接口
    async sysUpload(File) {
      const formdata = new FormData();
      formdata.append('file', File);
      formdata.append('name', 'e123');
      const res = await service.sysUpload(formdata);
    },
  },
};

如果你的项目中有需要jsonp的请求方式,或者是app内嵌的h5需要调用native的方法,都可以用类似的方法处理,例如这样的去做处理(写的比较简单):

import jsonp from '@/utils/jsonp';
import util from '@/utils/util';

export default new Proxy(
  {
    authStatus: 'jsonp /user/authStatus',
    getClientInfo: 'nativepost /client/getClientInfo',
  },
  {
    get(target, prop, receiver) {
      if (typeof target[prop] === 'string') {
        const [method, url] = target[prop].split(' ');
        if (method === 'jsonp') return (params, conf) => jsonp(url, params, conf);
        if (method === 'nativepost') {
          if (util.isIos()) {
            return (params, conf) => IosNativePost(url, params, conf);
          } else if (util.isAndroid()) {
            return (params, conf) => AndroidNativePost(url, params, conf);
          }
        }
      }
      return Reflect.get(target, prop, receiver);
    },
  },
);

使用Proxy的方式还有许多的好处,例如:1、在请求接口之前做拦截,就拿我举例子,我以前做的一个项目,需要接入单点登录,单点架构部门是通过提供一个jsonp的接口,如果请求需要鉴权的接口,让我们去请求他们的服务,然后会返回登录是否有权限或者超时,如果超时需要我们自己跳转到单点登录页。对于这种需求用Proxy很方便,在需要鉴权的接口之前请求下认证接口,如果认证通过则返回函数,认证不通过跳转登录页。 2、做vue、react ssr的情况下请求接口,比如请求的接口的时候需要携带cookie认证信息,一般我们是使用axios作http库(因为它可以从node.js创建 http 请求),在node环境中需要加入{ headers: { cookie: 'xxx=www' } },但是在浏览器中是不能有这段代码的,通过使用Proxy拦截的方式,很容易就能解决这么个问题。

以上,内容到这里就介绍完了。