前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >Vue3丨TS丨封装接口详解

Vue3丨TS丨封装接口详解

原创
作者头像
花落花相惜
发布2021-12-16 22:41:03
2K0
发布2021-12-16 22:41:03
举报
  1. 后端处理请求 “第二个域接口”,相当于代理动作。这样子前端就不会有跨域问题,无需做其他事。

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

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

存在问题:

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

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

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

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

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

我们封装 $request 作为主要对象,并扩展 .get 方法,sifoujianshu

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

二次封装 axios 的 request 请求插件

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

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

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

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

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

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

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

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

import validator from './validator';

考虑到不同域接口的出参 “code” 的 key 和 value 都不一致,如掘金的 code 为 err_no,思否的 code 为

status,但是简书却没有设计返回的 code ~

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

代码语言:txt
复制
// src/plugins/request/strategies
代码语言:txt
复制
import { parseCode, showMsg } from './helper';
代码语言:txt
复制
import router from '@/router';
代码语言:txt
复制
import { IStrategieInParams, IStrategieType } from './index.type';
代码语言:txt
复制
/**
代码语言:txt
复制
 * @desc 请求成功返回的业务逻辑相关错误处理策略
 */
const strategies: Record<
  IStrategieType,
  (obj: IStrategieInParams) => string | undefined
> = {
代码语言:txt
复制
  // 业务逻辑异常
代码语言:txt
复制
  BUSINESS_ERROR({ data, codeKey, codeValue }) {
代码语言:txt
复制
    const message = '系统异常,请稍后再试';
代码语言:txt
复制
    data[codeKey] = parseCode(data[codeKey]);
代码语言:txt
复制
    if (data[codeKey] === codeValue) {
代码语言:txt
复制
      showMsg(message);
代码语言:txt
复制
      return message;
代码语言:txt
复制
    }
代码语言:txt
复制
  },
代码语言:txt
复制
  // 没有授权登录
代码语言:txt
复制
  NOT_AUTH({ data, codeKey, codeValue }) {
代码语言:txt
复制
    const message = '用户未登录,请先登录';
代码语言:txt
复制
    data[codeKey] = parseCode(data[codeKey]);
代码语言:txt
复制
    if (data[codeKey] === codeValue) {
代码语言:txt
复制
      showMsg(message);
代码语言:txt
复制
      router.replace({ path: '/login' });
代码语言:txt
复制
      return message;
代码语言:txt
复制
    }
代码语言:txt
复制
  }
代码语言:txt
复制
  /* ...更多策略... */
代码语言:txt
复制
};
代码语言:txt
复制
export default strategies;
代码语言:txt
复制
// src/plugins/request/validator
代码语言:txt
复制
import Validator from './validator';
代码语言:txt
复制
const validator = new Validator();
代码语言:txt
复制
validator
代码语言:txt
复制
  .add('master', [
代码语言:txt
复制
    {
代码语言:txt
复制
      strategy: 'BUSINESS_ERROR',
代码语言:txt
复制
      codeKey: 'err_no',
代码语言:txt
复制
      /* 
代码语言:txt
复制
        配置 code 错误时值为1,如果返回 1 就会全局弹框显示。
代码语言:txt
复制
        想要看到效果的话,可以改为 0,仅测试显示全局错误弹框,
代码语言:txt
复制
       */
代码语言:txt
复制
      codeValue: 1
代码语言:txt
复制
    },
代码语言:txt
复制
    {
代码语言:txt
复制
      strategy: 'NOT_AUTH',
代码语言:txt
复制
      codeKey: 'err_no',
代码语言:txt
复制
      /* 
代码语言:txt
复制
        配置 code 错误时值为3000,如果返回 3000 就会自动跳转至登录页。
代码语言:txt
复制
        想要看到效果的话,可以改为 0,仅测试跳转至登录页
代码语言:txt
复制
       */
代码语言:txt
复制
      codeValue: 3000
代码语言:txt
复制
    }
代码语言:txt
复制
  ])
代码语言:txt
复制
  .add('sifou', [
代码语言:txt
复制
    {
代码语言:txt
复制
      strategy: 'BUSINESS_ERROR',
代码语言:txt
复制
      codeKey: 'status',
代码语言:txt
复制
      // 配置 code 错误时值为1
代码语言:txt
复制
      codeValue: 1
代码语言:txt
复制
    },
代码语言:txt
复制
    {
代码语言:txt
复制
      strategy: 'NOT_AUTH',
代码语言:txt
复制
      codeKey: 'status',
代码语言:txt
复制
      codeValue: 3000
代码语言:txt
复制
    }
代码语言:txt
复制
  ]);
代码语言:txt
复制
/* ...更多域相关配置... */
代码语言:txt
复制
// .add();
代码语言:txt
复制
export default validator;

因为不同域的接口,可能是不同的后端开发人员开发,所以出参风格不一致是一个很常见的问题,这里采用了策略模式来进行一个灵活的配置。在后端返回业务逻辑错误时,就可以进行

全局性的错误提示 统一跳转至登录页 。整个前端工程达成更好的统一化。

Proxy 代理多个域

本地开发 node 配置代理应该是每个小伙伴的基本操作吧。现在我们在 本地开发

时,不管后端是否开启跨域,都给每个域加上代理,这步也是为了达成一个统一。目前我们需要代理三个域:

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

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

代码语言:txt
复制
// src/global.d.ts
代码语言:txt
复制
import { ComponentInternalInstance } from 'vue';
代码语言:txt
复制
import { AxiosRequestConfig } from 'axios';
代码语言:txt
复制
declare global {
代码语言:txt
复制
  interface IAxiosRequestConfig extends AxiosRequestConfig {
代码语言:txt
复制
    // 标记当前请求的接口域名是什么,默认master,不需要手动控制
代码语言:txt
复制
    branch?: string;
代码语言:txt
复制
    // 全局显示 loading,默认false
代码语言:txt
复制
    loading?: boolean;
代码语言:txt
复制
    /* ...更多配置... */
代码语言:txt
复制
  }
代码语言:txt
复制
  type IRequestMethod = (
代码语言:txt
复制
    path: string,
代码语言:txt
复制
    data?: object,
代码语言:txt
复制
    config?: IAxiosRequestConfig
代码语言:txt
复制
  ) => any;
代码语言:txt
复制
  type IRequestMember = IRequestMethod & {
代码语言:txt
复制
    jianshu: IRequestMethod;
代码语言:txt
复制
  } & {
代码语言:txt
复制
    sifou: IRequestMethod;
代码语言:txt
复制
  };
代码语言:txt
复制
  interface IRequest {
代码语言:txt
复制
    get: IRequestMember;
代码语言:txt
复制
    post: IRequestMember;
代码语言:txt
复制
  }
代码语言:txt
复制
  interface IGlobalAPI {
代码语言:txt
复制
    $request: IRequest;
代码语言:txt
复制
    /* ...更多其他全局方法... */
代码语言:txt
复制
  }
代码语言:txt
复制
  // 全局方法钩子声明
代码语言:txt
复制
  interface ICurrentInstance extends ComponentInternalInstance {
代码语言:txt
复制
    appContext: {
代码语言:txt
复制
      config: { globalProperties: IGlobalAPI };
代码语言:txt
复制
    };
代码语言:txt
复制
  }
代码语言:txt
复制
}
代码语言:txt
复制
/**
代码语言:txt
复制
 * 如果你在 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 主要接口,其他分支的不同域接口,服务端需要开启跨域白名单。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 先看下处理封装后的最终效果
  • 二次封装 axios 的 request 请求插件
  • 现在我们来一一解释 “request” 插件
    • 策略模式,不同环境的接口域名配置
      • 策略模式,扩展 $request.get 方法
        • 策略模式,根据接口返回的 “code” 进行全局弹框错误提示
          • Proxy 代理多个域
            • TS 环境下 global.d.ts 声明,让调用更方便
            • 注意
            领券
            问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档