前面说的话
本文主要讲述在项目中遇到的一些业务场景,并提炼出来的解决方案。供小伙伴们参考~
在一个项目中,我们可能会遇到这样子的场景,项目请求的接口如
https://a.com/xxx
,由于业务的交集,可能还需要请求第二个域名的接口,如https://b.com/xxx
针对这种场景,我们可能会想到几个方案:
(注意:由于浏览器同源策略,一个前端工程在打包发布之后,通常我们会把资源放在与后端接口服务同一个域下。所以当有第二个域接口时,就会出现跨域请求导致请求失败。)
- 后端处理请求 “第二个域接口”,相当于代理动作。这样子前端就不会有跨域问题,无需做其他事。
存在问题:如果只是单纯的做代理,个人觉得有一种耦合的感觉,方法较为不优雅。
- 在前端请求两个不同域的接口。
存在问题:
- 由于浏览器同源策略,必须会有一个域的接口跨域,后端需要设置允许跨域白名单。
- 一般来说我们会对请求框架进行封装,类似
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
方法,sifou
,jianshu
为其属性作为两个不同域接口的方法,从而实现了我们在一个前端工程中请求多个不同域接口。接下来让我们看看实现的相关代码吧(当前只展示部分核心代码)~
二次封装 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(
{
timeout: 20000,
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,仅测试显示全局错误弹框,
*/
codeValue: 1
},
{
strategy: 'NOT_AUTH',
codeKey: 'err_no',
/*
配置 code 错误时值为3000,如果返回 3000 就会自动跳转至登录页。
想要看到效果的话,可以改为 0,仅测试跳转至登录页
*/
codeValue: 3000
}
])
.add('sifou', [
{
strategy: 'BUSINESS_ERROR',
codeKey: 'status',
// 配置 code 错误时值为1
codeValue: 1
},
{
strategy: 'NOT_AUTH',
codeKey: 'status',
codeValue: 3000
}
]);
/* ...更多域相关配置... */
// .add();
export default validator;
因为不同域的接口,可能是不同的后端开发人员开发,所以出参风格不一致是一个很常见的问题,这里采用了策略模式来进行一个灵活的配置。在后端返回业务逻辑错误时,就可以进行 全局性的错误提示 或** 统一跳转至登录页** 。整个前端工程达成更好的统一化。
Proxy 代理多个域
本地开发 node 配置代理应该是每个小伙伴的基本操作吧。现在我们在本地开发时,不管后端是否开启跨域,都给每个域加上代理,这步也是为了达成一个统一。目前我们需要代理三个域:
// vue.config.js
// ...
const proxy = {
'/master': {
target: apiConfig(MY_ENV, 'master'),
secure: true,
changeOrigin: true,
// 代理的时候路径是有 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
。
不同的项目业务场景复杂程度不一致,可能还需要更多的封装,针对业务的抽象架构才是不耍流氓的架构。
以上只是阐述了一些核心代码,具体还是要看源码才能更加了解,点我查看源码。