深入挖掘前端基础服务&中间件设计-接口管理

124 阅读5分钟

前言

前面一章我们详细讲述了,基于请求库的封装。

从系统角度来说,基本满足正常的使用功能以及扩展能力。
对于不同的业务平台,可以进行数据的处理、定制以及异常的处理且支持多种不同的请求方式。

image.png

今天我们要讲的,是介于请求封装库与接口管理之间的管理器,来维系接口管理和请求之间的关系。

image.png

目的有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"
}

keyqueryPageInterface
value/${porxyAPi}/common/${version}/PageService/queryPageInterface

配置说明

参数说明类型
interfaceAlias接口别名string
interfaceUrlKey接口URL keystring
interfaceType请求方式 GET/POSTstring
isTenanttrue/false 是否携带租户,默认falseboolean
isWhite1-是 白名单-系统层的请求Number

接口别名对应请求URL配置
1、可以静态存储在系统中,初始化加载到对象中
2、服务端进行维护利用接口方式加载到系统中

如果系统全局加载,需要保证别名Key一致性。

image.png

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.registerService.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
深入挖掘前端基础服务&中间件设计-字典设计
深入挖掘前端基础服务&中间件设计-配置设计
深入挖掘前端基础服务&中间件设计-用户信息
深入挖掘前端基础服务&中间件设计-权限控制
# 深入挖掘前端基础服务&中间件设计-请求封装