前端工程丨Vue3丨TS丨封裝請(qǐng)求多個(gè)不同域的接口

前面說(shuō)的話

本文主要講述在項(xiàng)目中遇到的一些業(yè)務(wù)場(chǎng)景田轧,并提煉出來(lái)的解決方案。供小伙伴們參考~

在一個(gè)項(xiàng)目中鞍恢,我們可能會(huì)遇到這樣子的場(chǎng)景傻粘,項(xiàng)目請(qǐng)求的接口如 https://a.com/xxx每窖,由于業(yè)務(wù)的交集,可能還需要請(qǐng)求第二個(gè)域名的接口弦悉,如 https://b.com/xxx

針對(duì)這種場(chǎng)景窒典,我們可能會(huì)想到幾個(gè)方案:
(注意:由于瀏覽器同源策略,一個(gè)前端工程在打包發(fā)布之后稽莉,通常我們會(huì)把資源放在與后端接口服務(wù)同一個(gè)域下瀑志。所以當(dāng)有第二個(gè)域接口時(shí),就會(huì)出現(xiàn)跨域請(qǐng)求導(dǎo)致請(qǐng)求失敗污秆。)

  1. 后端處理請(qǐng)求 “第二個(gè)域接口”劈猪,相當(dāng)于代理動(dòng)作。這樣子前端就不會(huì)有跨域問(wèn)題良拼,無(wú)需做其他事战得。

存在問(wèn)題:如果只是單純的做代理,個(gè)人覺(jué)得有一種耦合的感覺(jué)庸推,方法較為不優(yōu)雅常侦。

  1. 在前端請(qǐng)求兩個(gè)不同域的接口。

存在問(wèn)題:

  • 由于瀏覽器同源策略予弧,必須會(huì)有一個(gè)域的接口跨域刮吧,后端需要設(shè)置允許跨域白名單。
  • 一般來(lái)說(shuō)我們會(huì)對(duì)請(qǐng)求框架進(jìn)行封裝掖蛤,類(lèi)似 request.get('getUser')杀捻,我們還會(huì)設(shè)置一個(gè) “baseURL” 為默認(rèn)域名,如 https://a.com蚓庭。這樣子 “request” 默認(rèn)發(fā)起的請(qǐng)求都是 https://a.com 下的相關(guān)接口致讥。
    那請(qǐng)求域名 https://b.com 相關(guān)接口我們?cè)撛鯓舆M(jìn)行封裝呢?

針對(duì)以上的兩個(gè)方案分析器赞,我們得出了一個(gè)較優(yōu)的處理方案垢袱,請(qǐng)繼續(xù)往下看:

先看下處理封裝后的最終效果

本文 demo 以請(qǐng)求 掘金,思否港柜,簡(jiǎn)書(shū) 的接口來(lái)為例请契。

// ...
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 作為主要對(duì)象,并擴(kuò)展 .get 方法夏醉,sifou爽锥,jianshu 為其屬性作為兩個(gè)不同域接口的方法,從而實(shí)現(xiàn)了我們?cè)谝粋€(gè)前端工程中請(qǐng)求多個(gè)不同域接口畔柔。接下來(lái)讓我們看看實(shí)現(xiàn)的相關(guān)代碼吧(當(dāng)前只展示部分核心代碼)~

二次封裝 axios 的 request 請(qǐng)求插件

這里我們拿 axios 為例氯夷,先對(duì)它進(jìn)行一個(gè)封裝:

// 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 = '';
  // 開(kāi)發(fā)模式開(kāi)啟代理
  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(
        {
          timeout: 20000,
          headers: {
            'Content-Type': 'application/json',
            token: 'xxx'
          }
        },
        { baseURL },
        config
      )
    )
    .then(res => {
      const data = res.data;
      if (data && res.status === 200) {
        // 開(kāi)始驗(yàn)證請(qǐng)求成功的業(yè)務(wù)錯(cuò)誤
        validator.start(config.branch!, data, config);
        return data;
      }
      return Promise.reject(new Error('Response Error'));
    })
    .catch(error => {
      // 網(wǎng)絡(luò)相關(guān)的錯(cuò)誤,這里可用彈框進(jìn)行全局提示
      return Promise.reject(error);
    });
};

/**
 * @desc 請(qǐng)求方法類(lèi)封裝
 */
class Request {
  private extends: any;
  // request 要被作為一個(gè)插件靶擦,需要有 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;

現(xiàn)在我們來(lái)一一解釋 “request” 插件

策略模式腮考,不同環(huán)境的接口域名配置

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: 'http://www.reibang.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);

使用策略模式添加不同域接口的 測(cè)試/正式環(huán)境 域名雇毫。

策略模式,擴(kuò)展 $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 之類(lèi)的憑證
        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;

通過(guò) Request 類(lèi)的 extend 方法踩蔚,我們就可以進(jìn)行擴(kuò)展 $request 的 get 方法棚放,實(shí)現(xiàn)優(yōu)雅的調(diào)用其他域接口。

策略模式寂纪,根據(jù)接口返回的 “code” 進(jìn)行全局彈框錯(cuò)誤提示

import validator from './validator';

考慮到不同域接口的出參 “code” 的 key 和 value 都不一致席吴,如掘金的 code 為 err_no,思否的 code 為 status捞蛋,但是簡(jiǎn)書(shū)卻沒(méi)有設(shè)計(jì)返回的 code ~

讓我們仔細(xì)看兩段代碼(當(dāng)前只展示部分核心代碼):

// src/plugins/request/strategies
import { parseCode, showMsg } from './helper';
import router from '@/router';
import { IStrategieInParams, IStrategieType } from './index.type';
/**
 * @desc 請(qǐng)求成功返回的業(yè)務(wù)邏輯相關(guān)錯(cuò)誤處理策略
 */
const strategies: Record<
  IStrategieType,
  (obj: IStrategieInParams) => string | undefined
> = {
  // 業(yè)務(wù)邏輯異常
  BUSINESS_ERROR({ data, codeKey, codeValue }) {
    const message = '系統(tǒng)異常孝冒,請(qǐng)稍后再試';
    data[codeKey] = parseCode(data[codeKey]);
    if (data[codeKey] === codeValue) {
      showMsg(message);
      return message;
    }
  },
  // 沒(méi)有授權(quán)登錄
  NOT_AUTH({ data, codeKey, codeValue }) {
    const message = '用戶未登錄,請(qǐng)先登錄';
    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 錯(cuò)誤時(shí)值為1拟杉,如果返回 1 就會(huì)全局彈框顯示庄涡。
        想要看到效果的話,可以改為 0搬设,僅測(cè)試顯示全局錯(cuò)誤彈框,
       */
      codeValue: 1
    },
    {
      strategy: 'NOT_AUTH',
      codeKey: 'err_no',
      /* 
        配置 code 錯(cuò)誤時(shí)值為3000穴店,如果返回 3000 就會(huì)自動(dòng)跳轉(zhuǎn)至登錄頁(yè)。
        想要看到效果的話拿穴,可以改為 0泣洞,僅測(cè)試跳轉(zhuǎn)至登錄頁(yè)
       */
      codeValue: 3000
    }
  ])
  .add('sifou', [
    {
      strategy: 'BUSINESS_ERROR',
      codeKey: 'status',
      // 配置 code 錯(cuò)誤時(shí)值為1
      codeValue: 1
    },
    {
      strategy: 'NOT_AUTH',
      codeKey: 'status',
      codeValue: 3000
    }
  ]);
/* ...更多域相關(guān)配置... */
// .add();
export default validator;

因?yàn)椴煌虻慕涌冢赡苁遣煌暮蠖碎_(kāi)發(fā)人員開(kāi)發(fā)默色,所以出參風(fēng)格不一致是一個(gè)很常見(jiàn)的問(wèn)題球凰,這里采用了策略模式來(lái)進(jìn)行一個(gè)靈活的配置。在后端返回業(yè)務(wù)邏輯錯(cuò)誤時(shí)腿宰,就可以進(jìn)行 全局性的錯(cuò)誤提示 或** 統(tǒng)一跳轉(zhuǎn)至登錄頁(yè)** 呕诉。整個(gè)前端工程達(dá)成更好的統(tǒng)一化。

Proxy 代理多個(gè)域

本地開(kāi)發(fā) node 配置代理應(yīng)該是每個(gè)小伙伴的基本操作吧〕远龋現(xiàn)在我們?cè)?strong>本地開(kāi)發(fā)時(shí)甩挫,不管后端是否開(kāi)啟跨域,都給每個(gè)域加上代理椿每,這步也是為了達(dá)成一個(gè)統(tǒng)一伊者。目前我們需要代理三個(gè)域:

// vue.config.js
// ...
const proxy = {
  '/master': {
    target: apiConfig(MY_ENV, 'master'),
    secure: true,
    changeOrigin: true,
    // 代理的時(shí)候路徑是有 master 的,因?yàn)檫@樣子就可以針對(duì)代理间护,不會(huì)代理到其他無(wú)用的删壮。但實(shí)際請(qǐng)求的接口是不需要 master 的,所以在請(qǐng)求前要把它去掉
    pathRewrite: {
      '^/master': ''
    }
  },
  '/jianshu': {
    target: apiConfig(MY_ENV, 'jianshu'),
    // ...
  },
  '/sifou': {
    target: apiConfig(MY_ENV, 'sifou'),
    // ...
  }
};
// ...

TS 環(huán)境下 global.d.ts 聲明兑牡,讓調(diào)用更方便

// src/global.d.ts
import { ComponentInternalInstance } from 'vue';
import { AxiosRequestConfig } from 'axios';
declare global {
  interface IAxiosRequestConfig extends AxiosRequestConfig {
    // 標(biāo)記當(dāng)前請(qǐng)求的接口域名是什么,默認(rèn)master税灌,不需要手動(dòng)控制
    branch?: string;
    // 全局顯示 loading均函,默認(rèn)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 的寫(xiě)法亿虽,需要再新增這段聲明
 *
 * @example
 * created(){
 *  this.$request.get();
 *  this.$request.get.sifou();
 *  this.$request.get.jianshu();
 * }
 */
declare module '@vue/runtime-core' {
  export interface ComponentCustomProperties {
    $request: IRequest;
  }
}
export {};

注意

項(xiàng)目正式上線時(shí),除了 master 主要接口苞也,其他分支的不同域接口洛勉,服務(wù)端需要開(kāi)啟跨域白名單。

總結(jié)

本文為一個(gè)前端項(xiàng)目請(qǐng)求多個(gè)不同域的接口如迟,提供了封裝的思路收毫,基礎(chǔ)框架為 Vue3+TS
不同的項(xiàng)目業(yè)務(wù)場(chǎng)景復(fù)雜程度不一致殷勘,可能還需要更多的封裝此再,針對(duì)業(yè)務(wù)的抽象架構(gòu)才是不耍流氓的架構(gòu)。
以上只是闡述了一些核心代碼玲销,具體還是要看源碼才能更加了解输拇,點(diǎn)我查看源碼

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末贤斜,一起剝皮案震驚了整個(gè)濱河市策吠,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌瘩绒,老刑警劉巖猴抹,帶你破解...
    沈念sama閱讀 218,607評(píng)論 6 507
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異锁荔,居然都是意外死亡蟀给,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,239評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門(mén)堕战,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)坤溃,“玉大人,你說(shuō)我怎么就攤上這事嘱丢⌒浇椋” “怎么了?”我有些...
    開(kāi)封第一講書(shū)人閱讀 164,960評(píng)論 0 355
  • 文/不壞的土叔 我叫張陵越驻,是天一觀的道長(zhǎng)汁政。 經(jīng)常有香客問(wèn)我,道長(zhǎng)缀旁,這世上最難降的妖魔是什么记劈? 我笑而不...
    開(kāi)封第一講書(shū)人閱讀 58,750評(píng)論 1 294
  • 正文 為了忘掉前任,我火速辦了婚禮并巍,結(jié)果婚禮上目木,老公的妹妹穿的比我還像新娘。我一直安慰自己懊渡,他們只是感情好刽射,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,764評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布军拟。 她就那樣靜靜地躺著,像睡著了一般誓禁。 火紅的嫁衣襯著肌膚如雪懈息。 梳的紋絲不亂的頭發(fā)上,一...
    開(kāi)封第一講書(shū)人閱讀 51,604評(píng)論 1 305
  • 那天摹恰,我揣著相機(jī)與錄音辫继,去河邊找鬼。 笑死俗慈,一個(gè)胖子當(dāng)著我的面吹牛姑宽,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播姜盈,決...
    沈念sama閱讀 40,347評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼低千,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了馏颂?” 一聲冷哼從身側(cè)響起示血,我...
    開(kāi)封第一講書(shū)人閱讀 39,253評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎救拉,沒(méi)想到半個(gè)月后难审,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,702評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡亿絮,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,893評(píng)論 3 336
  • 正文 我和宋清朗相戀三年告喊,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片派昧。...
    茶點(diǎn)故事閱讀 40,015評(píng)論 1 348
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡黔姜,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出蒂萎,到底是詐尸還是另有隱情秆吵,我是刑警寧澤,帶...
    沈念sama閱讀 35,734評(píng)論 5 346
  • 正文 年R本政府宣布五慈,位于F島的核電站纳寂,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏泻拦。R本人自食惡果不足惜毙芜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,352評(píng)論 3 330
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望争拐。 院中可真熱鬧腋粥,春花似錦、人聲如沸。這莊子的主人今日做“春日...
    開(kāi)封第一講書(shū)人閱讀 31,934評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)。三九已至对嚼,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間绳慎,已是汗流浹背纵竖。 一陣腳步聲響...
    開(kāi)封第一講書(shū)人閱讀 33,052評(píng)論 1 270
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留杏愤,地道東北人靡砌。 一個(gè)月前我還...
    沈念sama閱讀 48,216評(píng)論 3 371
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像珊楼,于是被迫代替她去往敵國(guó)和親通殃。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 44,969評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 前端與服務(wù)端數(shù)據(jù)交互時(shí)厕宗,涉及到跨域的一些問(wèn)題画舌。JavaScript出于安全的考慮,禁止了跨域調(diào)用其他頁(yè)面的對(duì)象已慢,也...
    啊燦2580閱讀 639評(píng)論 0 0
  • 轉(zhuǎn)載自:https://segmentfault.com/a/1190000007326671bo 前端跨域整理 ...
    天字一等閱讀 483評(píng)論 0 3
  • 作者:Qyouu鏈接:https://www.imooc.com/article/70123來(lái)源:慕課網(wǎng) 本文對(duì)原...
    六個(gè)周閱讀 1,305評(píng)論 0 14
  • 一. 瀏覽器安全策略 同源策略 瀏覽器的安全都是以同源為基礎(chǔ)曲聂,它是瀏覽器最核心也最基本的安全功能 同源策略規(guī)定:不...
    菊花泡茶閱讀 532評(píng)論 0 0
  • 推薦指數(shù): 6.0 書(shū)籍主旨關(guān)鍵詞:特權(quán)、焦點(diǎn)佑惠、注意力朋腋、語(yǔ)言聯(lián)想、情景聯(lián)想 觀點(diǎn): 1.統(tǒng)計(jì)學(xué)現(xiàn)在叫數(shù)據(jù)分析膜楷,社會(huì)...
    Jenaral閱讀 5,721評(píng)論 0 5