committed by
GitHub
9 changed files with 425 additions and 3 deletions
@ -0,0 +1,287 @@ |
|||
/* |
|||
Reference: |
|||
*/ |
|||
|
|||
import ApiService from './serviceBase' |
|||
import { Method } from 'axios' |
|||
|
|||
const sourceUrl = '/api/abp/api-definition' |
|||
const serviceUrl = process.env.VUE_APP_BASE_API |
|||
|
|||
export default class DynamicApiService { |
|||
/** 获取api代理信息 |
|||
* @param includeTypes 包括类型信息 |
|||
*/ |
|||
public static get(includeTypes = false) { |
|||
const _url = sourceUrl + '?includeTypes=' + includeTypes |
|||
return ApiService.Get<ApplicationApiDescriptionModel>(_url, serviceUrl) |
|||
} |
|||
} |
|||
|
|||
export class UrlBuilder { |
|||
public static generateUrlWithParameters(action: ActionApiDescriptionModel, methodArguments: any, apiVersion: ApiVersionInfo) { |
|||
let urlBuilder = action.url |
|||
urlBuilder = this.replacePathVariables(urlBuilder, action.parameters, methodArguments, apiVersion) |
|||
urlBuilder = this.addQueryStringParameters(urlBuilder, action.parameters, methodArguments, apiVersion) |
|||
return urlBuilder |
|||
} |
|||
|
|||
private static replacePathVariables( |
|||
urlBuilder: string, |
|||
actionParameters: ParameterApiDescriptionModel[], |
|||
methodArguments: any, |
|||
apiVersion: ApiVersionInfo) { |
|||
const pathParameters = actionParameters.filter(x => x.bindingSourceId === ParameterBindingSources.path) |
|||
if (pathParameters.length === 0) { |
|||
return urlBuilder |
|||
} |
|||
if (pathParameters.some(x => x.name === 'apiVersion')) { |
|||
urlBuilder = urlBuilder.replace("{apiVersion}", apiVersion.version) |
|||
} |
|||
pathParameters |
|||
.filter(x => x.name !== 'apiVersion') |
|||
.forEach(pathParameter => { |
|||
const value = HttpActionParameterHelper.findParameterValue(methodArguments, pathParameter) |
|||
if (!value) { |
|||
if (pathParameter.isOptional) { |
|||
urlBuilder = urlBuilder.replace(`{{${pathParameter.name}}}`, '') |
|||
} else if (pathParameter.defaultValue) { |
|||
urlBuilder = urlBuilder.replace(`{{${pathParameter.name}}}`, String(pathParameter.defaultValue)) |
|||
} else { |
|||
throw new Error(`Missing path parameter value for ${pathParameter.name} (${pathParameter.nameOnMethod})`) |
|||
} |
|||
} else { |
|||
urlBuilder = urlBuilder.replace(`{{${pathParameter.name}}}`, String(value)) |
|||
} |
|||
}) |
|||
return urlBuilder |
|||
} |
|||
|
|||
private static addQueryStringParameters( |
|||
urlBuilder: string, |
|||
actionParameters: ParameterApiDescriptionModel[], |
|||
methodArguments: any, |
|||
apiVersion: ApiVersionInfo |
|||
) { |
|||
const bindSources = [ParameterBindingSources.modelBinding, ParameterBindingSources.query] |
|||
const queryStringParameters = actionParameters.filter(x => bindSources.some(b => b === x.bindingSourceId)) |
|||
let isFirstParam = true |
|||
queryStringParameters |
|||
.forEach(queryStringParameter => { |
|||
const value = HttpActionParameterHelper.findParameterValue(methodArguments, queryStringParameter) |
|||
if (!value) { |
|||
return |
|||
} |
|||
urlBuilder = this.addQueryStringParameter(urlBuilder, isFirstParam, queryStringParameter.name, value) |
|||
isFirstParam = false |
|||
}) |
|||
if (apiVersion.shouldSendInQueryString()) { |
|||
urlBuilder = this.addQueryStringParameter(urlBuilder, isFirstParam, 'api-version', apiVersion.version) |
|||
} |
|||
return urlBuilder |
|||
} |
|||
|
|||
private static addQueryStringParameter( |
|||
urlBuilder: string, |
|||
isFirstParam: boolean, |
|||
name: string, |
|||
value: any |
|||
) { |
|||
urlBuilder += isFirstParam ? '?' : '&' |
|||
if (Array.isArray(value)) { |
|||
let index = 0 |
|||
value.forEach(val => { |
|||
urlBuilder += `${name}[${index++}]=` + encodeURI(val) |
|||
}) |
|||
urlBuilder.substring(0, urlBuilder.length - 1) |
|||
} else { |
|||
urlBuilder += `${name}=` + encodeURI(value) |
|||
} |
|||
return urlBuilder |
|||
} |
|||
} |
|||
|
|||
export class HttpActionParameterHelper { |
|||
public static findParameterValue(methodArguments: any, apiParameter: ParameterApiDescriptionModel) { |
|||
const methodArgKeys = Object.keys(methodArguments) |
|||
const keyIndex = apiParameter.name === apiParameter.nameOnMethod ? |
|||
methodArgKeys.findIndex(key => apiParameter.name.toLowerCase() === key.toLowerCase()) : |
|||
methodArgKeys.findIndex(key => apiParameter.nameOnMethod.toLowerCase() === key.toLowerCase()) |
|||
|
|||
let value = methodArguments[methodArgKeys[keyIndex]] |
|||
if (!value) { |
|||
return null |
|||
} |
|||
|
|||
if (apiParameter.name === apiParameter.nameOnMethod) { |
|||
return value |
|||
} |
|||
|
|||
const inputKeys = Object.keys(value) |
|||
const inputKeyIndex = inputKeys.findIndex(key => key.toLowerCase() === apiParameter.name.toLowerCase()) |
|||
value = inputKeyIndex < 0 ? null : value[inputKeys[inputKeyIndex]] |
|||
|
|||
return String(value) |
|||
} |
|||
} |
|||
|
|||
export class ApiVersionInfo { |
|||
bindingSource?: string |
|||
version!: string |
|||
|
|||
constructor ( |
|||
version: string, |
|||
bindingSource?: string |
|||
) { |
|||
this.version = version |
|||
this.bindingSource = bindingSource |
|||
} |
|||
|
|||
public shouldSendInQueryString() { |
|||
return ['Path'].some(x => x === this.bindingSource) |
|||
} |
|||
} |
|||
|
|||
export enum ParameterBindingSources { |
|||
modelBinding = 'ModelBinding', |
|||
query = 'Query', |
|||
body = 'Body', |
|||
path = 'Path', |
|||
form = 'Form', |
|||
header = 'Header', |
|||
custom = 'Custom', |
|||
services = 'Services' |
|||
} |
|||
|
|||
export class ControllerInterfaceApiDescriptionModel { |
|||
type!: string |
|||
} |
|||
|
|||
export class ReturnValueApiDescriptionModel { |
|||
type!: string |
|||
typeSimple!: string |
|||
} |
|||
|
|||
export class MethodParameterApiDescriptionModel { |
|||
name!: string |
|||
typeAsString!: string |
|||
type!: string |
|||
typeSimple!: string |
|||
isOptional!: boolean |
|||
defaultValue?: any |
|||
} |
|||
|
|||
export class ParameterApiDescriptionModel { |
|||
nameOnMethod!: string |
|||
name!: string |
|||
type!: string |
|||
typeSimple!: string |
|||
isOptional!: boolean |
|||
defaultValue?: any |
|||
constraintTypes = new Array<string>() |
|||
bindingSourceId?: string |
|||
descriptorName?: string |
|||
} |
|||
|
|||
export class PropertyApiDescriptionModel { |
|||
name!: string |
|||
type!: string |
|||
typeSimple!: string |
|||
isRequired!: boolean |
|||
} |
|||
|
|||
export class ActionApiDescriptionModel { |
|||
uniqueName!: string |
|||
name!: string |
|||
httpMethod!: Method |
|||
url!: string |
|||
supportedVersions = new Array<string>() |
|||
parametersOnMethod = new Array<MethodParameterApiDescriptionModel>() |
|||
parameters = new Array<ParameterApiDescriptionModel>() |
|||
returnValue = new ReturnValueApiDescriptionModel() |
|||
} |
|||
|
|||
export class ControllerApiDescriptionModel { |
|||
controllerName!: string |
|||
type!: string |
|||
interfaces = new Array<ControllerInterfaceApiDescriptionModel>() |
|||
actions: {[key: string]: ActionApiDescriptionModel} = {} |
|||
|
|||
public static getAction( |
|||
actionName: string, |
|||
actions: {[key: string]: ActionApiDescriptionModel}) { |
|||
const actionKeys = Object.keys(actions) |
|||
const index = actionKeys.findIndex(key => { |
|||
const a = actions[key] |
|||
if (a.name.toLowerCase() === actionName.toLowerCase()) { |
|||
return a |
|||
} |
|||
}) |
|||
if (index < 0) { |
|||
throw new Error(`没有找到名为 ${actionName} 的方法定义!`) |
|||
} |
|||
return actions[actionKeys[index]] |
|||
} |
|||
} |
|||
|
|||
export class ModuleApiDescriptionModel { |
|||
rootPath = 'app' |
|||
remoteServiceName = 'Default' |
|||
controllers: {[key: string]: ControllerApiDescriptionModel} = {} |
|||
|
|||
public static getController( |
|||
controllerName: string, |
|||
controllers: {[key: string]: ControllerApiDescriptionModel}) { |
|||
const controllerKeys = Object.keys(controllers) |
|||
const index = controllerKeys.findIndex(key => { |
|||
const c = controllers[key] |
|||
if (c.controllerName.toLowerCase() === controllerName.toLowerCase()) { |
|||
return c |
|||
} |
|||
}) |
|||
if (index < 0) { |
|||
throw new Error(`没有找到名为 ${controllerName} 的接口定义!`) |
|||
} |
|||
return controllers[controllerKeys[index]] |
|||
} |
|||
} |
|||
|
|||
export class TypeApiDescriptionModel { |
|||
baseType!: string |
|||
isEnum!: boolean |
|||
enumNames = new Array<string>() |
|||
enumValues = new Array<any>() |
|||
genericArguments = new Array<string>() |
|||
properties = new Array<PropertyApiDescriptionModel>() |
|||
} |
|||
|
|||
export class ApplicationApiDescriptionModel { |
|||
modules: {[key: string]: ModuleApiDescriptionModel} = {} |
|||
types: {[key: string]: TypeApiDescriptionModel} = {} |
|||
|
|||
public static getAction( |
|||
remoteService: string, |
|||
controllerName: string, |
|||
actionName: string, |
|||
modules: {[key: string]: ModuleApiDescriptionModel}) { |
|||
const module = this.getModule(remoteService, modules) |
|||
const controller = ModuleApiDescriptionModel.getController(controllerName, module.controllers) |
|||
return ControllerApiDescriptionModel.getAction(actionName, controller.actions) |
|||
} |
|||
|
|||
private static getModule( |
|||
remoteService: string, |
|||
modules: {[key: string]: ModuleApiDescriptionModel}) { |
|||
const moduleKeys = Object.keys(modules) |
|||
const index = moduleKeys.findIndex(key => { |
|||
const m = modules[key] |
|||
if (m.remoteServiceName.toLowerCase() === remoteService.toLowerCase()) { |
|||
return m |
|||
} |
|||
}) |
|||
if (index < 0) { |
|||
throw new Error(`没有找到名为 ${remoteService} 的服务定义!`) |
|||
} |
|||
return modules[moduleKeys[index]] |
|||
} |
|||
} |
|||
@ -0,0 +1,71 @@ |
|||
import ApiService from '@/api/serviceBase' |
|||
import { PagedResultDto } from '@/api/types' |
|||
import { Component, Vue } from 'vue-property-decorator' |
|||
import { HttpProxyModule } from '@/store/modules/http-proxy' |
|||
import { |
|||
ActionApiDescriptionModel, |
|||
ApiVersionInfo, |
|||
ParameterBindingSources, |
|||
UrlBuilder, |
|||
ApplicationApiDescriptionModel |
|||
} from '@/api/dynamic-api' |
|||
/** |
|||
* 动态Http代理组件 |
|||
*/ |
|||
@Component({ |
|||
name: 'HttpProxyMiXin' |
|||
}) |
|||
export default class HttpProxyMiXin extends Vue { |
|||
protected pagedRequest<TResult>(options: { |
|||
service: string, |
|||
controller: string, |
|||
action: string, |
|||
data?: any, |
|||
params?: any |
|||
}) { |
|||
return this.request<PagedResultDto<TResult>>(options) |
|||
} |
|||
|
|||
protected request<TResult>(options: { |
|||
service: string, |
|||
controller: string, |
|||
action: string, |
|||
data?: any |
|||
}) { |
|||
const action = ApplicationApiDescriptionModel |
|||
.getAction( |
|||
options.service, options.controller, options.action, |
|||
HttpProxyModule.applicationApiDescriptionModel.modules) |
|||
const apiVersion = this.getApiVersionInfo(action) |
|||
let url = process.env.REMOTE_SERVICE_BASE_URL || '' |
|||
url = this.ensureEndsWith(url, '/') + UrlBuilder.generateUrlWithParameters(action, options.data, apiVersion) |
|||
|
|||
return ApiService.HttpRequest<TResult>({ |
|||
url: url, |
|||
method: action?.httpMethod, |
|||
data: options.data |
|||
}) |
|||
} |
|||
|
|||
private getApiVersionInfo(action: ActionApiDescriptionModel) { |
|||
const apiVersion = this.findBestApiVersion(action) |
|||
const versionParam = |
|||
action.parameters.find(p => p.name === 'apiVersion' || p.bindingSourceId === ParameterBindingSources.path) ?? |
|||
action.parameters.find(p => p.name === 'api-version' || p.bindingSourceId === ParameterBindingSources.query) |
|||
return new ApiVersionInfo(apiVersion, versionParam?.bindingSourceId) |
|||
} |
|||
|
|||
private findBestApiVersion(action: ActionApiDescriptionModel) { |
|||
if (action.supportedVersions.length === 0) { |
|||
return '1.0' |
|||
} |
|||
return action.supportedVersions[action.supportedVersions.length - 1] |
|||
} |
|||
|
|||
private ensureEndsWith(str: string, c: string) { |
|||
if (str.lastIndexOf(c) === str.length - c.length) { |
|||
return str |
|||
} |
|||
return str + c |
|||
} |
|||
} |
|||
@ -0,0 +1,46 @@ |
|||
import { VuexModule, Module, Mutation, Action, getModule } from 'vuex-module-decorators' |
|||
import store from '@/store' |
|||
import { getItem, setItem } from '@/utils/localStorage' |
|||
import DynamicApiService, { ApplicationApiDescriptionModel } from '@/api/dynamic-api' |
|||
|
|||
export interface IHttpProxy { |
|||
applicationApiDescriptionModel: ApplicationApiDescriptionModel |
|||
} |
|||
|
|||
const dynamicApiKey = 'abp-api-definition' |
|||
|
|||
@Module({ dynamic: true, store, name: 'http-proxy' }) |
|||
class HttpProxy extends VuexModule implements IHttpProxy { |
|||
public applicationApiDescriptionModel = new ApplicationApiDescriptionModel() |
|||
|
|||
@Mutation |
|||
private SET_API_DESCRIPTOR(apiDescriptor: ApplicationApiDescriptionModel) { |
|||
this.applicationApiDescriptionModel = apiDescriptor |
|||
} |
|||
|
|||
@Action({ rawError: true }) |
|||
public async Initialize() { |
|||
const dynamicApiJson = getItem(dynamicApiKey) |
|||
if (dynamicApiJson) { |
|||
this.SET_API_DESCRIPTOR(JSON.parse(dynamicApiJson)) |
|||
return |
|||
} |
|||
const dynamicApi = await DynamicApiService.get() |
|||
this.SET_API_DESCRIPTOR(dynamicApi) |
|||
setItem(dynamicApiKey, JSON.stringify(dynamicApi)) |
|||
} |
|||
|
|||
@Action({ rawError: true }) |
|||
public findAction(service: { |
|||
name: string, |
|||
controller: string, |
|||
action: string |
|||
}) { |
|||
return ApplicationApiDescriptionModel |
|||
.getAction( |
|||
service.name, service.controller, service.action, |
|||
this.applicationApiDescriptionModel.modules) |
|||
} |
|||
} |
|||
|
|||
export const HttpProxyModule = getModule(HttpProxy) |
|||
Loading…
Reference in new issue