前面說(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)求失敗污秆。)
- 后端處理請(qǐng)求 “第二個(gè)域接口”劈猪,相當(dāng)于代理動(dòng)作。這樣子前端就不會(huì)有跨域問(wèn)題良拼,無(wú)需做其他事战得。
存在問(wèn)題:如果只是單純的做代理,個(gè)人覺(jué)得有一種耦合的感覺(jué)庸推,方法較為不優(yōu)雅常侦。
- 在前端請(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)我查看源碼。