前端工程丨Vue3丨TS丨封装请求多个不同域的接口

共 15500字,需浏览 31分钟

 ·

2021-03-17 03:23

(关注前端精,让你前端越来越精 ~ )

前面说的话

本文主要讲述在项目中遇到的一些业务场景,并提炼出来的解决方案。供小伙伴们参考~

在一个项目中,我们可能会遇到这样子的场景,项目请求的接口如 https://a.com/xxx,由于业务的交集,可能还需要请求第二个域名的接口,如 https://b.com/xxx

针对这种场景,我们可能会想到几个方案:
(注意:由于浏览器同源策略,一个前端工程在打包发布之后,通常我们会把资源放在与后端接口服务同一个域下。所以当有第二个域接口时,就会出现跨域请求导致请求失败。)

  1. 后端处理请求 “第二个域接口”,相当于代理动作。这样子前端就不会有跨域问题,无需做其他事。

存在问题:如果只是单纯的做代理,个人觉得有一种耦合的感觉,方法较为不优雅。

  1. 在前端请求两个不同域的接口。

存在问题:

  • 由于浏览器同源策略,必须会有一个域的接口跨域,后端需要设置允许跨域白名单。
  • 一般来说我们会对请求框架进行封装,类似 request.get('getUser'),我们还会设置一个 “baseURL” 为默认域名,如 https://a.com。这样子 “request” 默认发起的请求都是 https://a.com 下的相关接口。
    那请求域名 https://b.com 相关接口我们该怎样进行封装呢?

针对以上的两个方案分析,我们得出了一个较优的处理方案,请继续往下看:

先看下处理封装后的最终效果

本文 demo 以请求 掘金,思否,简书 的接口来为例。

// ...
const requestMaster = async () => {
  const { err_no, data, err_msg } = await $request.get('user_api/v1/author/recommend');
};
const requestSifou = async () => {
  const { status, data } = await $request.get.sifou('api/live/recommend');
};
const requestJianshu = async () => {
  const { users } = await $request.get.jianshu('users/recommended');
};
// ...

我们封装 $request 作为主要对象,并扩展 .get 方法,sifoujianshu 为其属性作为两个不同域接口的方法,从而实现了我们在一个前端工程中请求多个不同域接口。接下来让我们看看实现的相关代码吧(当前只展示部分核心代码)~

二次封装 axios 的 request 请求插件

这里我们拿 axios 为例,先对它进行一个封装:

// src/plugins/request
import axios from 'axios';
import apiConfig from '@/api.config';
import _merge from 'lodash/merge';
import validator from './validator';
import { App } from 'vue';
export const _request = (config: IAxiosRequestConfig) => {
  config.branch = config.branch || 'master';
  let baseURL = '';
  // 开发模式开启代理
  if (process.env.NODE_ENV === 'development') {
    config.url = `/${config.branch}/${config.url}`;
  } else {
    baseURL = apiConfig(process.env.MY_ENV, config.branch);
  }
  return axios
    .request(
      _merge(
        {
          timeout20000,
          headers: {
            'Content-Type''application/json',
            token'xxx'
          }
        },
        { baseURL },
        config
      )
    )
    .then(res => {
      const data = res.data;
      if (data && res.status === 200) {
        // 开始验证请求成功的业务错误
        validator.start(config.branch!, data, config);
        return data;
      }
      return Promise.reject(new Error('Response Error'));
    })
    .catch(error => {
      // 网络相关的错误,这里可用弹框进行全局提示
      return Promise.reject(error);
    });
};

/**
 * @desc 请求方法类封装
 */

class Request {
  private extends: any;
  // request 要被作为一个插件,需要有 install 方法
  public install: (app: App, ...options: any[]) => any;
  constructor() {
    this.extends = [];
    this.install = () => {};
  }
  extend(extend: any) {
    this.extends.push(extend);
    return this;
  }
  merge() {
    const obj = this.extends.reduce((prev: any, curr: any) => {
      return _merge(prev, curr);
    }, {});
    Object.keys(obj).forEach(key => {
      Object.assign((this as any)[key], obj[key]);
    });
  }
  get(path: string, data: object = {}, config: IAxiosRequestConfig = {}) {
    return _request({
      ...config,
      method'GET',
      url: path,
      params: data
    });
  }
  post(path: string, data: object = {}, config: IAxiosRequestConfig = {}) {
    return _request({
      ...config,
      method'POST',
      url: path,
      data
    });
  }
}
export default Request;

现在我们来一一解释 “request” 插件

策略模式,不同环境的接口域名配置

import apiConfig from '@/api.config';

// @/api.config
const APIConfig = require('./apiConfig');
const apiConfig = new APIConfig();
apiConfig
  .add('master', {
    test'https://api.juejin.cn',
    prod'https://prod.api.juejin.cn'
  })
  .add('jianshu', {
    test'https://www.jianshu.com',
    prod'https://www.prod.jianshu.com'
  })
  .add('sifou', {
    test'https://segmentfault.com',
    prod'https://prod.segmentfault.com'
  });
module.exports = (myenv, branch) => apiConfig.get(myenv, branch);

使用策略模式添加不同域接口的 测试/正式环境 域名。

策略模式,扩展 $request.get 方法

// src/plugins/request/branchs/jianshu
import { _request } from '../request';
export default {
  get: {
    jianshu(path: string, data: object = {}, config: IAxiosRequestConfig = {}) {
      return _request({
        ...config,
        method'GET',
        url: path,
        data,
        branch'jianshu',
        // 在 headers 加入 token 之类的凭证
        headers: {
          'my-token''jianshu-test'
        }
      });
    }
  },
  post: {
     // ...
  }
};
// src/plugins/request
import { App } from 'vue';
import Request from './request';
import sifou from './branchs/sifou';
import jianshu from './branchs/jianshu';
const request = new Request();
request.extend(sifou).extend(jianshu);
request.merge();
request.install = (app: App, ...options: any[]) => {
  app.config.globalProperties.$request = request;
};
export default request;

通过 Request 类的 extend 方法,我们就可以进行扩展 $request 的 get 方法,实现优雅的调用其他域接口。

策略模式,根据接口返回的 “code” 进行全局弹框错误提示

import validator from './validator';

考虑到不同域接口的出参 “code” 的 key 和 value 都不一致,如掘金的 code 为 err_no,思否的 code 为 status,但是简书却没有设计返回的 code ~

让我们仔细看两段代码(当前只展示部分核心代码):

// src/plugins/request/strategies
import { parseCode, showMsg } from './helper';
import router from '@/router';
import { IStrategieInParams, IStrategieType } from './index.type';
/**
 * @desc 请求成功返回的业务逻辑相关错误处理策略
 */

const strategies: Record<
  IStrategieType,
  (obj: IStrategieInParams) => string | undefined
> = {
  // 业务逻辑异常
  BUSINESS_ERROR({ data, codeKey, codeValue }) {
    const message = '系统异常,请稍后再试';
    data[codeKey] = parseCode(data[codeKey]);
    if (data[codeKey] === codeValue) {
      showMsg(message);
      return message;
    }
  },
  // 没有授权登录
  NOT_AUTH({ data, codeKey, codeValue }) {
    const message = '用户未登录,请先登录';
    data[codeKey] = parseCode(data[codeKey]);
    if (data[codeKey] === codeValue) {
      showMsg(message);
      router.replace({ path'/login' });
      return message;
    }
  }

  /* ...更多策略... */
};
export default strategies;
// src/plugins/request/validator
import Validator from './validator';
const validator = new Validator();
validator
  .add('master', [
    {
      strategy'BUSINESS_ERROR',
      codeKey'err_no',
      /* 
        配置 code 错误时值为1,如果返回 1 就会全局弹框显示。
        想要看到效果的话,可以改为 0,仅测试显示全局错误弹框,
       */

      codeValue1
    },
    {
      strategy'NOT_AUTH',
      codeKey'err_no',
      /* 
        配置 code 错误时值为3000,如果返回 3000 就会自动跳转至登录页。
        想要看到效果的话,可以改为 0,仅测试跳转至登录页
       */

      codeValue3000
    }
  ])
  .add('sifou', [
    {
      strategy'BUSINESS_ERROR',
      codeKey'status',
      // 配置 code 错误时值为1
      codeValue1
    },
    {
      strategy'NOT_AUTH',
      codeKey'status',
      codeValue3000
    }
  ]);
/* ...更多域相关配置... */
// .add();
export default validator;

因为不同域的接口,可能是不同的后端开发人员开发,所以出参风格不一致是一个很常见的问题,这里采用了策略模式来进行一个灵活的配置。在后端返回业务逻辑错误时,就可以进行 全局性的错误提示统一跳转至登录页整个前端工程达成更好的统一化。

Proxy 代理多个域

本地开发 node 配置代理应该是每个小伙伴的基本操作吧。现在我们在本地开发时,不管后端是否开启跨域,都给每个域加上代理,这步也是为了达成一个统一。目前我们需要代理三个域:

// vue.config.js
// ...
const proxy = {
  '/master': {
    target: apiConfig(MY_ENV, 'master'),
    securetrue,
    changeOrigintrue,
    // 代理的时候路径是有 master 的,因为这样子就可以针对代理,不会代理到其他无用的。但实际请求的接口是不需要 master 的,所以在请求前要把它去掉
    pathRewrite: {
      '^/master'''
    }
  },
  '/jianshu': {
    target: apiConfig(MY_ENV, 'jianshu'),
    // ...
  },
  '/sifou': {
    target: apiConfig(MY_ENV, 'sifou'),
    // ...
  }
};
// ...

TS 环境下 global.d.ts 声明,让调用更方便

// src/global.d.ts
import { ComponentInternalInstance } from 'vue';
import { AxiosRequestConfig } from 'axios';
declare global {
  interface IAxiosRequestConfig extends AxiosRequestConfig {
    // 标记当前请求的接口域名是什么,默认master,不需要手动控制
    branch?: string;
    // 全局显示 loading,默认false
    loading?: boolean;

    /* ...更多配置... */
  }

  type IRequestMethod = (
    path: string,
    data?: object,
    config?: IAxiosRequestConfig
  ) => any;
  type IRequestMember = IRequestMethod & {
    jianshu: IRequestMethod;
  } & {
    sifou: IRequestMethod;
  };
  interface IRequest {
    get: IRequestMember;
    post: IRequestMember;
  }

  interface IGlobalAPI {
    $request: IRequest;

    /* ...更多其他全局方法... */
  }

  // 全局方法钩子声明
  interface ICurrentInstance extends ComponentInternalInstance {
    appContext: {
      config: { globalProperties: IGlobalAPI };
    };
  }
}

/**
 * 如果你在 Vue3 框架中还留恋 Vue2 Options Api 的写法,需要再新增这段声明
 *
 * @example
 * created(){
 *  this.$request.get();
 *  this.$request.get.sifou();
 *  this.$request.get.jianshu();
 * }
 */

declare module '@vue/runtime-core' {
  export interface ComponentCustomProperties {
    $request: IRequest;
  }
}
export {};

注意

项目正式上线时,除了 master 主要接口,其他分支的不同域接口,服务端需要开启跨域白名单。

总结

本文为一个前端项目请求多个不同域的接口,提供了封装的思路,基础框架为 Vue3+TS
不同的项目业务场景复杂程度不一致,可能还需要更多的封装,针对业务的抽象架构才是不耍流氓的架构。
以上只是阐述了一些核心代码,具体还是要看源码才能更加了解。

感谢您的点赞和在看 ❤️  

点击【阅读原文】,查看源码

浏览 43
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报