GraphQL请求详解(v4)

阅读之前

什么是GraphQL?

Oinone官方解读
GraphQL入门

可视化请求工具

insomnia下载

概述

(以下内容简称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
  • 动作 action
    • enable:启用 提交动作 ServerAction
    • disable:禁用 提交动作 ServerAction

服务端定义元数据生成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:平台内置了多种QueryMutation定义,通过模型继承关系将自动生成,无需手动定义。比如Query定义包括queryOnequeryPage等;Mutation定义包括createupdate等。特殊情况下,默认逻辑无法满足时,服务端通常采用函数重载的方式进行替换,客户端则无需关心。

生成规则

  • type DemoModel:通过模型编码demo.DemoModel.分隔后的最后一位,并转换为大驼峰格式。字段与声明类型一致。
  • type DemoModelInput动作入参定义,未做特殊声明的情况下与模型定义一致。
  • type DemoModelQuerytype DemoModelMutationQueryMutation为固定后缀,分别生成动作相关类型。当函数类型为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请求规则

  • querymutationGQL默认关键字,若为query则可缺省。
  • demoModelMutation:通过DemoModelMutation类型定义转换为小驼峰获取。Query类似。
  • enable:通过服务端定义的函数名称获取,以上示例中函数名称Java函数名称一致,特殊情况下,服务端可使用@Function(name = "")注解或@Action.Advanced(name = "")注解进行修改。disable类似。
  • (data: { id: ${id} })data通过服务端定义的参数名称获取,与Java函数参数名称一致。值类型与服务端声明类型一致,对象类型采用{}包裹,对象中的每个属性类型与服务端声明类型同样一致。此处服务端声明的data名称的入参仅包含DemoModelInput中定义的属性。GQL是通过参数名称进行参数映射的,与参数位置无关。
  • 响应字段声明:idnameisEnabled为本次请求后获取到的相关字段,不在响应字段声明中的属性无法被获取。

PS:GQL在客户端发起的请求中提供了VariablesFragmentInline 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-switchermenu等,这些组件需要发起相关请求,但其元数据无法在页面中获取。

不仅如此,这种请求方式只有在业务场景确定模型不再发生变更,并且确定该服务与权限相关功能完全无关时才可以使用。

由于我们无法提前预知该服务的相关变化,这些问题是无法避免的:

  • 当模型字段发生变化时,在代码中定义的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低代码应用平台体验

(0)
数式-海波的头像数式-海波数式管理员
上一篇 2023年6月20日 pm4:07
下一篇 2023年11月2日 pm1:58

相关推荐

  • 【界面设计器】树形表格

    阅读之前 你应该: 熟悉模型的增删改查相关内容。【界面设计器】模型增删改查基础

    2024年4月19日
    86300
  • 如何增加页面消息通知轮询的间隔或者关闭轮询

    场景 oinone的前端页面默认自带了消息通知功能,在顶部状态栏可以看到消息的查看入口,默认每隔5秒查询一次最新的消息,我们可以通过自定义消息组件增加该间隔或者是关闭轮询 示例代码 修改轮询间隔 import { MaskWidget, NotificationWidget, SPI } from '@kunlun/dependencies'; @SPI.ClassFactory(MaskWidget.Token({ widget: 'notification' })) export class DemoNotificationWidget extends NotificationWidget { /** * 轮询间隔时间,单位: 秒 * @protected */ protected msgDelay = 30000; } 关闭轮询 import { MaskWidget, NotificationWidget, SPI } from '@kunlun/dependencies'; @SPI.ClassFactory(MaskWidget.Token({ widget: 'notification' })) export class DemoNotificationWidget extends NotificationWidget { protected mounted() { super.mounted(); // 清除轮询的定时器 this.msgTimer && clearInterval(this.msgTimer); // 挂载后手动查询一次消息 this.getMsgTotal(); } }

    2024年8月20日
    81700
  • 表格主题配置(v4)

    TableThemeConfig /** * 表格主题配置 */ export interface TableThemeConfig { border: boolean | string; stripe: boolean; isCurrent: boolean; isHover: boolean; /** * 表格列主题配置 */ column: Partial<TableColumnThemeConfig>; } /** * 表格列主题配置 */ export interface TableColumnThemeConfig { /** * <h3>最小宽度</h3> * <ul> * <li>boolean: enabled column width auto compute</li> * <li>number: using css width (default: px)</li> * <li>string: using css width</li> * <li> * object: auto compute width for label by default function * <ul> * <li>min: min min width (default: 120)</li> * <li>max: max min width (default: 432)</li> * <li>chineseWidth: chinese width (default: 14 -> fontSize: 14px)</li> * <li>otherWidth: non chinese width (default: 9 -> fontSize: 14px)</li> * <li>sortableFixWidth: sortable handle width (default: 40)</li> * <li>nonSortableFixWidth: non sortable fix width (default: 22)</li> * </ul> * </li> * <li>function: auto compute width for label by function</li> * </ul> */ minWidth: boolean | number | string | Partial<TableColumnMinWidthComputeConfig> | TableColumnMinWidthComputeFunction; /** * 操作列 */ operation: { /** * 宽度 (default: 165) */ width?: number | string; /** * 最小宽度 (default: 120) */ minWidth?: number | string; }; } export interface TableColumnMinWidthComputeConfig { min: number;…

    2023年11月1日
    80200
  • 【前端】低无一体部署常见问题

    如何检查上传的SDK是否有效? 1. 在任意页面刷新后,查看是否发起【查询SDK组件】的请求。 2. 在返回的js和css列表中是否能找到在界面设计器上传的js和css文件。 3. 检查浏览器的Console中是否有组件相关报错。 4. 检查sdk中是否包含了启动工程未加入的包依赖。 启动工程包依赖:main.ts VueOioProvider( { dependencies: { vue: import('vue'), lodashEs: import('lodash-es'), antDesignVue: import('ant-design-vue'), elementPlusIconsVue: import('@element-plus/icons-vue'), elementPlus: import('element-plus'), kunlunDependencies: import('@kunlun/dependencies'), kunlunVueUiAntd: import('@kunlun/vue-ui-antd'), kunlunVueUiEl: import('@kunlun/vue-ui-el') } } ); SDK依赖:rollup.config.ts const globals = { vue: 'vue', 'lodash-es': 'lodashEs', 'ant-design-vue': 'antDesignVue', '@element-plus/icons-vue': 'elementPlusIconsVue', 'element-plus': 'elementPlus', '@kunlun/dependencies': 'kunlunDependencies', '@kunlun/vue-ui-antd': 'kunlunVueUiAntd', '@kunlun/vue-ui-el': 'kunlunVueUiEl', '@kunlun/mobile-dependencies': 'kunlunMobileDependencies', '@kunlun/vue-ui-mobile-vant': 'kunlunVueUiMobileVant' }; 上述两个文件配置的依赖和对应名称必须匹配才能在sdk上传后正常运行,否则会出现内存变量无法共享的问题。 当未发起【查询SDK组件】的请求时如何处理? 1. 在任意页面刷新后,查看manifest.js加载路径。 业务工程通常为:http://${host}:${port}/manifest.js 设计器镜像中通常为:http://${host}:${port}/config/manifest.js 2. 若未正确加载manifest.js,则在dist目录中根据请求路径添加manifest.js文件。此文件称为运行时配置文件,可点击查看参考文档。 runtimeConfigResolve({ plugins: { usingRemote: true } });

    低无一体 2023年11月1日
    1.2K00
  • 在前端视图添加自定义的区域块

    添加自定义区域块 平台提供了一系列默认的视图布局,可以帮助开发人员快速构建出复杂的企业应用系统。当然,我们可以使用自定义区域块来扩展表格、表单、画廊、树形等视图。 自定义区域块概述 平台视图布局都是通过XML配置实现的。在视图布局中,我们可以使用一些特定的元素标签来构建视图的表头、表单、搜索区域等部分。而自定义区域块,就是这些元素标签之外的部分。我们可以通过在视图布局的XML中添加自定义区域块,来扩展页面功能。 视图类型及相关元素 视图类型分为表格(TABLE)、表单(FORM)、画廊(GALLERY)、树形(TREE)等。不同类型的视图布局,包含的元素也有所不同。 下面是几种视图类型及其对应的元素: 表格:搜索区域、表格主体,其中表格主体包含了表格上面的动作、表格区域等部分。 表单:表单区域,包含了表单动作、表单区域等部分。 画廊:动作、卡片详细信息。 在表格页面添加自定义区域块 以下是一个示例,演示如何在表格页面顶部添加自定义区域块。 1. 修改视图布局XML 首先,我们需要修改表格视图的XML布局,添加自定义区域块元素标签。 <view type="TABLE"> <!– 这是搜索区域 –> <pack widget="group"> <view type="SEARCH"> <element widget="search" slot="search" slotSupport="field" /> </view> </pack> <!– 这是表格主体 –> <pack widget="group" slot="tableGroup"> <!– 在这里添加自定义区域块元素标签 –> <element widget="MyCustomElement"></element> <!– 这是表格上面的动作 –> <element widget="actionBar" slot="actionBar" slotSupport="action"> <xslot name="actions" slotSupport="action" /> </element> <!– 这是表格区域 –> <element widget="table" slot="table" slotSupport="field"> <element widget="expandColumn" slot="expandRow" /> <xslot name="fields" slotSupport="field" /> <element widget="rowActions" slot="rowActions" slotSupport="action" /> </element> </pack> </view> 在上述代码中,我们添加了一个名为MyCustomElement的元素标签。这将作为我们自定义区域块的容器。 2. 创建自定义Vue组件 接下来,我们需要创建一个Vue组件,并将其指定为自定义元素标签MyCustomElement的模板。 <template> <div> <!– 在这里编写自定义区域块的内容 –> <p>Hello, world!</p> </div> </template> <script lang="ts"> import { defineComponent } from 'vue'; export default defineComponent({ components: { }, props: [], setup(props) { return {}; } }); </script> 在上述代码中,我们定义了一个非常简单的Vue组件,它在页面上显示一个“Hello, world!”的文本信息。 3. 创建自定义Element Widget 为了使自定义Vue组件与XML布局文件关联起来,我们需要创建一个对应的Element Widget。 import { BaseElementWidget, SPI, BaseElementViewWidget, Widget, ViewMode, FormWidget, BaseElementWidgetProps } from '@kunlun/dependencies'; import MyCustomElement from './MyCustomElement.vue'; @SPI.ClassFactory(BaseElementWidget.Token({ widget: 'MyCustomElementWidget' })) export class MyCustomElementWidget extends BaseElementWidget { public initialize(props: BaseElementWidgetProps): this { super.initialize(props) this.setComponent(MyCustomElement) return this } } 在上述代码中,我们继承了BaseElementWidget类,并在其中指定了Vue组件MyCustomElement。这样,XML布局文件中的元素标签就能够正确地与Vue组件关联起来。 4. 注册视图布局 最后,我们需要将上述代码配置注册。具体而言,我们需要调用registerLayout方法来将XML布局文件、模块名和视图类型进行关联。 import { registerLayout, ViewType } from '@kunlun/dependencies'; registerLayout( `<view type="TABLE"> <pack widget="group"> <view type="SEARCH"> <element widget="search" slot="search" slotSupport="field" />…

    2023年11月1日
    2.2K00

Leave a Reply

登录后才能评论