阅读之前
什么是GraphQL?
Oinone官方解读
GraphQL入门
可视化请求工具
概述
(以下内容简称GQL
)
众所周知,平台的所有功能都是通过一系列元数据定义来驱动的,而GQL
作为服务端和客户端交互协议,其重要性不言而喻。下面会从以下几个方面介绍GQL
在平台中是如何运作的:
- 服务端定义元数据生成
GQL
对应的schema
- 通过
HttpClient
发起一个GQL
请求 - 通过
window.open
使用GET
方式发起一个GQL
请求 - 客户端泛化调用任意API服务
- 客户端通过运行时上下文
RuntimeContext
发起GQL
请求
准备工作
在开始介绍GQL
之前,我们需要定义一个可以被GQL
请求的服务端函数,以此来介绍我们的相关内容。(对服务端不感兴趣的同学可以跳过代码部分)
DemoModel.java
@Model.model(DemoModel.MODEL_MODEL)
@Model(displayName = "演示模型", labelFields = {"name"})
public class DemoModel extends IdModel {
private static final long serialVersionUID = -7211802945795866154L;
public static final String MODEL_MODEL = "demo.DemoModel";
@Field(displayName = "名称")
private String name;
@Field(displayName = "是否启用")
private Boolean isEnabled;
}
DemoModelAction.java
@Model.model(DemoModel.MODEL_MODEL)
public class DemoModelAction {
@Action(displayName = "启用")
public DemoModel enable(DemoModel data) {
data.setIsEnabled(true);
data.updateById();
return data;
}
@Action(displayName = "禁用")
public DemoModel disable(DemoModel data) {
data.setIsEnabled(false);
data.updateById();
return data;
}
}
上面的java代码定义了演示模型
的字段
和动作
:
- 字段
field
- id:ID
整数 Integer
- name:名称
字符串 String
- isEnabled:是否启用
布尔 Boolean
- id:ID
- 动作
action
- enable:启用
提交动作 ServerAction
- disable:禁用
提交动作 ServerAction
- enable:启用
服务端定义元数据生成GQL
对应的schema
模型和字段
type DemoModel {
id: Long
name: String
isEnabled: Boolean
}
动作
type DemoModelInput {
id: Long
name: String
isEnabled: Boolean
}
type DemoModelQuery {
queryOne(query: DemoModelInput): DemoModel
......
}
type DemoModelMutation {
enable(data: DemoModelInput): DemoModel
disable(data: DemoModelInput): DemoModel
......
}
PS:平台内置了多种Query
和Mutation
定义,通过模型
继承关系将自动生成,无需手动定义。比如Query
定义包括queryOne
、queryPage
等;Mutation
定义包括create
、update
等。特殊情况下,默认逻辑无法满足时,服务端通常采用函数重载的方式进行替换,客户端则无需关心。
生成规则
type DemoModel
:通过模型编码demo.DemoModel
取.
分隔后的最后一位,并转换为大驼峰
格式。字段与声明类型一致。type DemoModelInput
:动作
入参定义,未做特殊声明的情况下与模型定义一致。type DemoModelQuery
和type DemoModelMutation
:Query
和Mutation
为固定后缀,分别生成动作
相关类型。当函数类型为QUERY
时,使用Query
后缀,其他情况使用Mutation
后缀。
(此处仅做简单解释,详细生成规则过于复杂,客户端无需关心)
Query
类型的GQL
示例
query {
demoModelQuery {
queryOne(query: { id: ${id} }) {
id
name
isEnabled
}
}
}
Mutation
类型的GQL
示例
mutation {
demoModelMutation {
enable(data: { id: ${id} }) {
id
name
isEnabled
}
}
}
GQL
请求规则
query
和mutation
:GQL
默认关键字,若为query
则可缺省。demoModelMutation
:通过DemoModelMutation
类型定义转换为小驼峰
获取。Query
类似。enable
:通过服务端定义的函数名称
获取,以上示例中函数名称
与Java函数名称
一致,特殊情况下,服务端可使用@Function(name = "")
注解或@Action.Advanced(name = "")
注解进行修改。disable
类似。(data: { id: ${id} })
:data
通过服务端定义的参数名称
获取,与Java函数参数名称
一致。值类型与服务端声明类型一致,对象类型采用{}
包裹,对象中的每个属性类型与服务端声明类型同样一致。此处服务端声明的data
名称的入参仅包含DemoModelInput
中定义的属性。GQL
是通过参数名称
进行参数映射的,与参数位置无关。- 响应字段声明:
id
、name
、isEnabled
为本次请求后获取到的相关字段,不在响应字段声明中的属性无法被获取。
PS:GQL
在客户端发起的请求中提供了Variables
、Fragment
和Inline Fragment
等方式进行GQL的复用。感兴趣的同学可在GQL官网自行学习。
TypeScript最佳实践
通常情况下,我们是不需要手动发起GQL的,但在某些特殊场景(无法获取元数据上下文等)中,这是无法避免的。
在我们学习如何发起GQL请求之前,我们需要达成一个基本共识,即ts类型声明
和API服务归集
。
ts类型声明
:需按照模型
定义类型声明
,通常我们将这些类型声明放在typing.ts
文件中进行管理。API服务归集
:需按照模型
对应的服务分别定义相关API服务的调用方法,并将这些服务放在service.ts
文件中进行管理。具体的方法根据实际情况进行判断,但不建议使用复杂入参定义服务方法。模块名称归集
:需将所有API中用到的模块名称统一放在module-name.ts
文件中进行管理。
工程结构
service
目录将为该工程提供全部的API服务。
这是我们建议的工程结构,具体工程结构也可以根据自身情况进行相应变更。接下来,我们会在通过HttpClient发起一个GQL请求
章节提供相关的示例代码作为参考。
├── src
│ ├── service
│ │ ├── demo-model
│ │ │ ├── index.ts
│ │ │ ├── service.ts
│ │ │ └── typing.ts
│ │ ├── model.ts
│ │ ├── module-name.ts
│ │ └── index.ts
│ ├── main.ts
│ └── shim-vue.d.ts
├── tsconfig.json
└── vue.config.js
通过HttpClient发起一个GQL请求
service/module-name.ts
export const DEMO = "demo";
service/index.ts
export * from './demo-model';
service/demo-model/typing.ts
export interface DemoModel {
id?: string;
name?: string;
isEnabled?: string;
}
service/demo-model/service.ts
import { HttpClient } from '@kunlun/dependencies';
import { DEMO } from '../module-name';
import { DemoModel } from './typing';
const http = HttpClient.getInstance();
export class DemoModelService {
public static async enable(id: string): Promise<DemoModel | undefined> {
const gql = `mutation {
demoModelMutation {
enable(data: {id: "${id}"}) {
id
name
isEnabled
}
}
}`;
const res = await http.mutate<DemoModel>(DEMO, gql);
return res.data.demoModelMutation.enable;
}
}
service/demo-model/index.ts
export * from './typing';
export * from './service';
PS:对于GQL字符串的构造,平台还提供了另一种构造方式,开发者可根据自身喜好进行选择。
service/model.ts(模型名称归集,与模块名称归集类似)
export const DEMO_MODEL_NAME = 'demoModel';
service/demo-model/service.ts
import { GQL } from '@kunlun/dependencies';
import { DEMO_MODEL_NAME } from '../model';
import { DEMO } from '../module-name';
import { DemoModel } from './typing';
export class DemoModelService {
public static async enable(id: string): Promise<DemoModel | undefined> {
return GQL.mutation(DEMO_MODEL_NAME, 'enable')
.buildRequest((builder) => builder.buildObjectParameter('data', (builder) => builder.stringParameter('id', id)))
.buildResponse((builder) => builder.parameter('id', 'name', 'isEnabled'))
.request(DEMO);
}
}
使用对应服务
import { DemoService } from './service';
// 1. async/await (推荐)
const res = await DemoService.enable('1');
console.log(res);
// 2. Promise#then
DemoService.enable('1').then((res) => {
console.log(res);
});
至此,我们已经可以发起任何GQL
请求了。
通过window.open使用GET
方式发起一个GQL
请求
使用HttpClient
发起的请求不论是使用query
还是mutate
,都是POST
方式的请求。对于文件下载等需要使用GET
方式的请求,我们通常使用window.open
方法,并打开新的浏览器窗口来实现这一功能。
const gql = ${GQL}
;
window.open(UrlHelper.appendBasePath(/pamirs/${MODULE_NAME}?query=${encodeURIComponent(gql)}
), '_blank');
客户端泛化调用任意API服务
当GQL
中定义的模型字段过多时,我们往往会发觉直接手写一个GQL
的难度是相当高的,为此,我们提供了一个较为简单的调用服务方式,以辅助生成我们所需的GQL
。
该方法采用动态获取元数据的方式实现的泛化调用,仅限于开发环境
使用,不适合生产环境
使用。获取元数据的相关API服务相对于业务来说是一些无意义的损耗,因此,我们通常建议在开发时通过该方式生成GQL
后,从请求体中拷贝该GQL
,并改为通过HttpClient
方式进行调用。一般而言,生成一次的GQL
后,在未来的变更中仅会有个别字段的变化,不会产生过大的变化。
service/model.ts(模型名称归集,与模块名称归集类似)
export const DEMO_MODEL = 'demo.DemoModel';
service/demo-model/service.ts
import { GenericFunctionService } from '@kunlun/dependencies';
import { DEMO_MODEL } from '../model';
import { DemoModel } from './typing';
export class DemoModelService {
public static async enable(id: string): Promise<DemoModel | undefined> {
return GenericFunctionService.INSTANCE.executeByName(DEMO_MODEL, 'enable', { deep: 1 }, { id });
}
}
这种方式需要使用模型编码
和函数名称
作为入参,deep
参数控制响应字段声明
的层级,最后的不定长参数作为函数的入参。
由于泛化调用服务会动态获取模型
、关联模型
、函数
等相关信息,函数名称
、参数名称
等信息无法提前预知,也就无法明确定义参数名称
,因此需要根据参数位置
进行入参匹配。
小结
以上两种方式均是在没有元数据的情况下使用的请求方式,从之前的文章中我们可以了解到,元数据仅在main-view
组件中才进行了收集,其他的情况是无法正常获取元数据的。
比如我们通常在mask
中定义的组件,app-switcher
、menu
等,这些组件需要发起相关请求,但其元数据无法在页面中获取。
不仅如此,这种请求方式只有在业务场景确定,模型不再发生变更,并且确定该服务与权限相关功能完全无关时才可以使用。
由于我们无法提前预知该服务的相关变化,这些问题是无法避免的:
- 当模型字段发生变化时,在代码中定义的
GQL
也需要发生相应的变更。 - 当权限限制该用户发起该请求时,请求结果只会报错,我们无法预知服务是否可以正常调用。
客户端通过运行时上下文RuntimeContext
发起GQL
请求
在可以获取到元数据的情况下,我们可以用下面这种方式发起GQL
请求。
service/demo-model/service.ts
import {
GenericFunctionService,
isRuntimeServerAction,
RuntimeContext,
RuntimeServerAction
} from '@kunlun/dependencies';
import { DemoModel } from './typing';
export class DemoModelService {
public static async enable(runtimeContext: RuntimeContext, id: string): Promise<DemoModel | undefined> {
const action = runtimeContext.model.modelActions.find(
(v) => isRuntimeServerAction(v) && v.functionDefinition?.name === 'enable'
) as RuntimeServerAction;
const functionDefinition = action?.functionDefinition;
if (!functionDefinition) {
return;
}
const requestModelFields = runtimeContext.getRequestModelFields();
return GenericFunctionService.INSTANCE.execute(
functionDefinition,
{
model: runtimeContext.model,
requestFields: requestModelFields
},
{ id }
);
}
}
在这个示例中,我们通过runtimeContext.model.modelActions
查找所需调用的动作,如果该动作
在当前运行时上下文中未找到,那我们就能知道这个动作
可能在当前页面中未配置
或无权限访问
。并且从运行时上下文中获取到的请求模型字段
,可以完全按照页面配置进行按需请求。这也是我们推荐使用的调用方式之一。
需要注意的是,在GenericFunctionService#execute
可选项中requestFields
属性优先于deep
属性。
Oinone社区 作者:数式-海波原创文章,如若转载,请注明出处:https://doc.oinone.top/frontend/45.html
访问Oinone官网:https://www.oinone.top获取数式Oinone低代码应用平台体验