前言
前面一章我们详细讲述了,基于请求库的封装。
从系统角度来说,基本满足正常的使用功能以及扩展能力。
对于不同的业务平台,可以进行数据的处理、定制以及异常的处理且支持多种不同的请求方式。
今天我们要讲的,是介于请求封装库与接口管理之间的管理器,来维系接口管理和请求之间的关系。
目的有2个:
- 简化请求方式
- 接口权限控制,可以控制到页面级别
设计
如何简化请求方式呢?
针对我们二次封装的Axios
库,请求对象需要进行配置
- 请求方式、url全路径、参数
- Promise的then和catch接收数据和异常处理
import { Request } from '@basic-library';
Request.$http({
url: '/api/getScoreInfo',
data,
method: 'post',
})
.then((res) => {
....
})
.catch((e) => {
return Promise.reject(e);
});
如果业务侧需要进行进一步简化,则可以去掉请求方式,路径优化
优化后的方式:
import { Service } from '@basic-library'
const result = await Service.useHttp('接口别名', 接口参数)
// get请求目前支持:支持字符串和json对象 ?aa=22&cc=33 {aa:'222'}
Service.useHttp('interfaceAlias',`pageId=${currentItem.pageId}`)
Service.useHttp('interfaceAlias',{pageId:'3333'})
//post请求支持:json对象
Service.useHttp('interfaceAlias',{pageId:'3333'})
-
接口参数部分做了兼容,支持对象或者参数拼写(需要识别请求类型)
-
接口URL进行简化为别名方式,如getScoreInfo,就需要有一个对象进行存储 ,以key和value方式对应,
别名对应真实的请求URL
如
// 服务代理名称
const porxyAPi = 'systemApi'
const version = 'v1'
{
"interfaceAlias": "queryPageInterface",
"interfaceUrlKey": `/${porxyAPi}/common/${version}/PageService/queryPageInterface`,
"interfaceType": "get"
}
key: queryPageInterface
value:/${porxyAPi}/common/${version}/PageService/queryPageInterface
配置说明
参数 | 说明 | 类型 |
---|---|---|
interfaceAlias | 接口别名 | string |
interfaceUrlKey | 接口URL key | string |
interfaceType | 请求方式 GET/POST | string |
isTenant | true/false 是否携带租户,默认false | boolean |
isWhite | 1-是 白名单-系统层的请求 | Number |
接口别名对应请求URL配置
1、可以静态存储在系统中,初始化加载到对象中
2、服务端进行维护利用接口方式加载到系统中
如果系统全局加载,需要保证别名Key一致性。
import globalApi from '@/service'
/**
* 接口注册
* @param {*} No
* 场景: 系统加载
*/
function registerInterFace() {
// 接口注册--白名单接口、代理PROXY设置
Service.register(globalApi.API_STORE, {
proxy: CONFIG.INTERFACE_PROXY,
unique: true,
errorListen: true,
})
}
如何接口权限控制到页面级别?
实现策略:
对于上面的接口别名配置部分,与后端进行约定,传入当前页面ID,返回当前页面所有的接口配置,注册到系统中。进入页面之前,请求接口即可
对于实现部分,可以参考之前的文章,对于Loader设计Router相关的,摘录核心部分
/**
* registerPageMount 全局页面挂载监听
* @param {*} router 路由
* 场景:
* 1、页面内获取接口请求标记-内置全局实例
* 2、页面内错误捕获
*/
function registerPageMount(router){
// 进入页面
router.beforeResolve(async (to, from, next) => {
document.title = Config.BSConfig.systemName || '';
try {
if(Config.BSConfig?.IS_AUTH_INTERFACE){
const cItem= BaseStore.auth.getInfoByRoute(to.path)
if(cItem){
const result = await Service.useHttp('interfaceService',`pageId=${cItem.pageId}`)
cache.setCache('_EDU_CUR_PAGE_INFO',cItem,'session')
if(result?.success){
cache.setCache('_EDU_CUR_INTERFACE_INFO',result?.returnObj,'session')
Service.initData(result?.returnObj)
}else{
console.warn(`当前页面${cItem.pageId}接口未授权!`)
}
}
}
next()
} catch (error) {
console.warn(error)
next()
}
});
上文中提到了
Service.register
和Service.initData
,这2个API,主要是系统初始化注入的部分和动态注入
源码实践
入口文件:
import interfaceReq from './interface';
const BaseService = (function () {
if (window._INTERFACE_) {
return window._INTERFACE_;
} else {
window._INTERFACE_ = interfaceReq;
return interfaceReq;
}
})();
export default BaseService;
interface.js
/**
* 接口管理器
*/
import produce, { enableMapSet } from 'immer';
import { cache, SocketEmitter } from '@basic-utils'
import Request from '../service';
//导入进度条插件
import NProgress from 'nprogress'
NProgress.configure({ease:'ease',speed: 500, showSpinner: false})
enableMapSet();
// interfaceStore
const _INTERFACE_API_ = {
defaultMap: new Map(),
dataMap: new Map(),
};
let interfaceStore = produce(_INTERFACE_API_, () => { });
window._INTERFACE_API_ = interfaceStore;
function formatData(features) {
const formatMap = [];
features.forEach((feature) => {
formatMap.push([feature.interfaceAlias, feature]);
});
return formatMap;
}
function responseDataMiddleware({data, config}){
if(!data.success){
return Promise.reject({
code: data.msgCode,
message: data.prompt || data.msgContent,
config,
status: 666,
});
}
return data
}
class interfaceRequest {
constructor() {
this.updateTime = 0;
// 请求唯一
this.unique = false;
// 代理
this.proxy = '';
this.errorListen = false;
this.REQ_EMIT = 'feature_req_update'
SocketEmitter.remove(this.REQ_EMIT)
}
updateDepHolder() {
this.updateTime = Date.now();
}
onErrorStatus(featureUpdate) {
const fn = (res) => featureUpdate(res);
SocketEmitter.on(this.REQ_EMIT, fn)
}
isObject(value) {
return value !== null && typeof value === "object";
}
get useBusinessApi() {
console.debug('当前页面接口权限最近更新时间->', this.updateTime);
return Array.from(interfaceStore.dataMap.values());
}
get useBaseApi() {
return Array.from(interfaceStore.defaultMap.values());
}
/**
* 根据接口别名获取获取请求URL
* @param {*} alias 接口别名
* @param {*} params 参数
* @returns 请求URL
*/
getUrl(alias, params){
let result = this.findInterfaceStore(alias)
let _suffix = ''
const requestType = result?.interfaceType || 'get'
if (!result) return null
if (requestType.toLowerCase() == 'get') {
_suffix = this._getParamsFusion(params)
}
return this._getComposeURL(result, _suffix)
}
parse(alias, params){
return this.getUrl(alias, params)
}
HParse(alias, params, host = true){
let _params = ''
let result = this.findInterfaceStore(alias)
const requestType = result?.interfaceType || 'get'
if (!result) return null
if (requestType.toLowerCase() == 'get') {
_params = this._getParamsFusion(params)
}
// 租户ID
const tenantId = cache.getCache('tenantId', 'local')
if(host){
const protocol = window.location.protocol
const host = window.location.host
return `${protocol}//${host}/${tenantId}${result.interfaceUrlKey}${_params}`
}
return `/${tenantId}${result.interfaceUrlKey}${_params}`
}
httpRequest(type = '$http', options, params) {
NProgress.start()
let alias = ''
let headers = {
Authorization: cache.getCache('token', 'session')
}
if(typeof options == 'string'){
alias = options
}else{
alias = options.name
options.headers && (headers = {...headers,...options.headers})
}
let result = this.findInterfaceStore(alias)
let data = null
if (!result) {
return new Promise((resolve, reject) => {
NProgress.done()
this.errorListen && SocketEmitter.emit(this.REQ_EMIT, 'Interface without permission')
console.error(`${alias}接口请求无效,未授权,请检查配置!`)
reject(`No Permission ${alias}`)
})
}
const requestType = result?.interfaceType || 'get'
if (requestType.toLowerCase() == 'post') {
data = params || null
}
return Request[type]({
url: this.getUrl(alias, params),
method: requestType,
headers,
data
}).then((res)=>{
NProgress.done()
return type == '$httpXMLInstance' ? res: responseDataMiddleware(res)
}).catch((err)=>{
NProgress.done()
this.errorListen && SocketEmitter.emit(this.REQ_EMIT, err)
return {
returnObj : null,
success: false,
code: err?.code
}
});
}
/**
* 正常请求
* @param {*} options 请求配置 支持对象或者字符串 options.alias || alias
* @param {*} params 请求参数 支持对象或者字符串
* @returns
*/
useHttp(options, params) {
return this.httpRequest('$http', options, params)
}
/**
* 上传使用
* @param {*} options 请求配置 支持对象或者字符串 options.alias || alias
* @param {*} params 请求参数 支持对象或者字符串
* @returns
*/
useMultiPart(options, params) {
return this.httpRequest('$httpMultiPartInstance', options, params)
}
/**
* 导出
* @param {*} options 请求配置 支持对象或者字符串 options.alias || alias
* @param {*} params 请求参数 支持对象或者字符串
* @returns
*/
useHttpXML(options, params) {
return this.httpRequest('$httpXMLInstance', options, params)
}
/**
* 请求类型-参数拼装--GET请求
* @param {*} data
* @returns
*/
_getParamsFusion(data) {
let result = ""
if (data && this.isObject(data)) {
let strFix = []
for (const key in data) {
strFix.push(`${key}=${data[key]}`)
}
result = strFix.length ? '?' + strFix.join('&') : ''
} else if (data && !this.isObject(data)) {
result = data.lastIndexOf('?') > -1 ? data.substr(data.lastIndexOf('?')) : '?' + data
}
return result
}
/**
* 插入代理地址【线上默认加入proxy】
* @param {*} url
* @param {*} proxy
* @param {*} pos
* @returns
*/
_inserFix(url, proxy, pos = 2) {
if (!proxy) return url
let inserProxy = '/' + proxy
if(pos == 1){
return inserProxy + url
}else{
const fIndex = url.indexOf('/')
const tIndex = url.substr(fIndex + 1).indexOf('/') + 1
return url.slice(0, tIndex) + inserProxy + url.slice(tIndex)
}
}
/**
* 返回请求的URL地址
* @param {*} api 接收aliasItem
* @param {*} suffix 接收的参数
* @param {*} suffix 接收的参数
* @returns
*/
_getComposeURL(api, suffix, date = Date.now()) {
let temp = ''
// 租户ID
const tenantId = cache.getCache('tenantId', 'local')
let url = `${api.interfaceUrlKey}${suffix}`
// 白名单+租户信息 后期统一放开
if(api.isAuth == 1 && api.isTenant){
url = this._inserFix(url, tenantId)
}else if(api.isAuth != 1 && !api.isTenant){
// 业务部分,线上业务逻辑【api代理+interfaceUrlKey】
url = this._inserFix(url, this.proxy, 1)
}
// 追加时间戳
if(this.unique){
temp = url.lastIndexOf('?') === -1 ? `?ts=${date}` : `&ts=${date}`
}
return url + temp
}
/**
* 获取请求接口信息
* @param {*} alias 接口别名
* @returns
*/
findInterfaceStore(alias) {
let result = interfaceStore.defaultMap.get(alias)
if (!result) {
result = interfaceStore.dataMap.get(alias)
}
return result
}
/**
* 初始化接口数据
* @param {*} menus
* @param {*} privileges
*/
register(features = [], {proxy = 'api',unique = false, errorListen = false}) {
if (!features?.length) return
interfaceStore = produce(interfaceStore, (draftState) => {
draftState.defaultMap = new Map(formatData(features));
});
this.updateDepHolder();
this.proxy = proxy
this.unique = unique
this.errorListen = errorListen
}
/**
* 初始化接口数据
* @param {*} menus
* @param {*} privileges
*/
initData(features = []) {
if (!features) {
console.error('当前页面暂无接口权限!')
return
}
interfaceStore = produce(interfaceStore, (draftState) => {
draftState.dataMap = new Map(formatData(features));
});
this.updateDepHolder();
}
}
export { interfaceStore };
export default new interfaceRequest();
API
参数 | 说明 |
---|---|
useHttp | 接口请求 |
useMultiPart | 文件流请求 |
useHttpXML | 请求返回文件流-导出 |
register | 初始化接口数据 |
initData | 加载接口数据 |
parse | 根据接口别名返回解析后的URL |
HParse | 根据接口别名返回解析后的URL--http地址 |
源码解读:
1、导入进度条插件 NProgress.configure
2、代码中核心部分在于 httpRequest
,函数内,findInterfaceStore
,根据接口别名,查找请求相关信息(请求方式、请求URL),获取到后,getUrl
对参数进行兼容处理
3、异常处理部分,还是在catch部分内,使用SocketEmitter.emit
进行向外发送错误信息的通知
老铁们,我们一起关注系列设计:
深入挖掘前端基础服务&中间件设计-basic-library
深入挖掘前端基础服务&中间件设计-字典设计
深入挖掘前端基础服务&中间件设计-配置设计
深入挖掘前端基础服务&中间件设计-用户信息
深入挖掘前端基础服务&中间件设计-权限控制
# 深入挖掘前端基础服务&中间件设计-请求封装