前端工程丨Vue3丨TS丨封裝請求多個(gè)不同域的接口
(關(guān)注前端精,讓你前端越來越精 ~ )
前面說的話
本文主要講述在項(xiàng)目中遇到的一些業(yè)務(wù)場景,并提煉出來的解決方案。供小伙伴們參考~
在一個(gè)項(xiàng)目中,我們可能會(huì)遇到這樣子的場景,項(xiàng)目請求的接口如
https://a.com/xxx,由于業(yè)務(wù)的交集,可能還需要請求第二個(gè)域名的接口,如https://b.com/xxx
針對這種場景,我們可能會(huì)想到幾個(gè)方案:
(注意:由于瀏覽器同源策略,一個(gè)前端工程在打包發(fā)布之后,通常我們會(huì)把資源放在與后端接口服務(wù)同一個(gè)域下。所以當(dāng)有第二個(gè)域接口時(shí),就會(huì)出現(xiàn)跨域請求導(dǎo)致請求失敗。)
后端處理請求 “第二個(gè)域接口”,相當(dāng)于代理動(dòng)作。這樣子前端就不會(huì)有跨域問題,無需做其他事。
存在問題:如果只是單純的做代理,個(gè)人覺得有一種耦合的感覺,方法較為不優(yōu)雅。
在前端請求兩個(gè)不同域的接口。
存在問題:
由于瀏覽器同源策略,必須會(huì)有一個(gè)域的接口跨域,后端需要設(shè)置允許跨域白名單。 一般來說我們會(huì)對請求框架進(jìn)行封裝,類似 request.get('getUser'),我們還會(huì)設(shè)置一個(gè) “baseURL” 為默認(rèn)域名,如https://a.com。這樣子 “request” 默認(rèn)發(fā)起的請求都是https://a.com下的相關(guān)接口。
那請求域名https://b.com相關(guān)接口我們該怎樣進(jìn)行封裝呢?
針對以上的兩個(gè)方案分析,我們得出了一個(gè)較優(yōu)的處理方案,請繼續(xù)往下看:
先看下處理封裝后的最終效果
本文 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 作為主要對象,并擴(kuò)展 .get 方法,sifou,jianshu 為其屬性作為兩個(gè)不同域接口的方法,從而實(shí)現(xiàn)了我們在一個(gè)前端工程中請求多個(gè)不同域接口。接下來讓我們看看實(shí)現(xiàn)的相關(guān)代碼吧(當(dāng)前只展示部分核心代碼)~
二次封裝 axios 的 request 請求插件
這里我們拿 axios 為例,先對它進(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 = '';
// 開發(fā)模式開啟代理
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) {
// 開始驗(yàn)證請求成功的業(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 請求方法類封裝
*/
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)在我們來一一解釋 “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: '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);
使用策略模式添加不同域接口的 測試/正式環(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 之類的憑證
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 方法,我們就可以進(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,但是簡書卻沒有設(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 請求成功返回的業(yè)務(wù)邏輯相關(guān)錯(cuò)誤處理策略
*/
const strategies: Record<
IStrategieType,
(obj: IStrategieInParams) => string | undefined
> = {
// 業(yè)務(wù)邏輯異常
BUSINESS_ERROR({ data, codeKey, codeValue }) {
const message = '系統(tǒng)異常,請稍后再試';
data[codeKey] = parseCode(data[codeKey]);
if (data[codeKey] === codeValue) {
showMsg(message);
return message;
}
},
// 沒有授權(quán)登錄
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 錯(cuò)誤時(shí)值為1,如果返回 1 就會(huì)全局彈框顯示。
想要看到效果的話,可以改為 0,僅測試顯示全局錯(cuò)誤彈框,
*/
codeValue: 1
},
{
strategy: 'NOT_AUTH',
codeKey: 'err_no',
/*
配置 code 錯(cuò)誤時(shí)值為3000,如果返回 3000 就會(huì)自動(dòng)跳轉(zhuǎn)至登錄頁。
想要看到效果的話,可以改為 0,僅測試跳轉(zhuǎn)至登錄頁
*/
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)椴煌虻慕涌?,可能是不同的后端開發(fā)人員開發(fā),所以出參風(fēng)格不一致是一個(gè)很常見的問題,這里采用了策略模式來進(jìn)行一個(gè)靈活的配置。在后端返回業(yè)務(wù)邏輯錯(cuò)誤時(shí),就可以進(jìn)行 全局性的錯(cuò)誤提示 或 統(tǒng)一跳轉(zhuǎn)至登錄頁。整個(gè)前端工程達(dá)成更好的統(tǒng)一化。
Proxy 代理多個(gè)域
本地開發(fā) node 配置代理應(yīng)該是每個(gè)小伙伴的基本操作吧。現(xiàn)在我們在本地開發(fā)時(shí),不管后端是否開啟跨域,都給每個(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)檫@樣子就可以針對代理,不會(huì)代理到其他無用的。但實(shí)際請求的接口是不需要 master 的,所以在請求前要把它去掉
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)前請求的接口域名是什么,默認(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 的寫法,需要再新增這段聲明
*
* @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ù)端需要開啟跨域白名單。
總結(jié)
本文為一個(gè)前端項(xiàng)目請求多個(gè)不同域的接口,提供了封裝的思路,基礎(chǔ)框架為 Vue3+TS。
不同的項(xiàng)目業(yè)務(wù)場景復(fù)雜程度不一致,可能還需要更多的封裝,針對業(yè)務(wù)的抽象架構(gòu)才是不耍流氓的架構(gòu)。
以上只是闡述了一些核心代碼,具體還是要看源碼才能更加了解。
感謝您的點(diǎn)贊和在看 ??
↙↙↙點(diǎn)擊【閱讀原文】,查看源碼
