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