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

相关推荐

  • 前端日期组件国际化支持方案

    在 oinone 平台中,系统默认支持基础的国际化翻译功能。但由于日期时间组件的国际化依赖对应语言包,而全量引入语言包会显著增加打包体积,因此前端默认仅集成了中、英文的日期时间支持。若需为日期时间组件扩展其他语言(如日语)的国际化支持,需手动导入对应语言包并完成配置,具体步骤如下: 假设我们现在国际化翻译切换成了日语,那么我们在日期时间也要支持日语,那么需要如下操作: 1: 重写 RootWidget 继承平台默认的 RootWidget,SPI 注册条件保持跟平台一致即可覆盖平台默认的RootWidget // CustomRootWidget.ts import { RootComponentSPI, RootWidget, SPIFactory } from '@oinone/kunlun-dependencies'; import Root from './Root.vue'; // 通过SPI注册覆盖平台默认的root组件 @SPIFactory.Register(RootComponentSPI.Token({ widget: 'root' })) export class CustomRootWidget extends RootWidget { public initialize() { super.initialize(); this.setComponent(Root); return this; } } 2: 覆盖 Root 组件的 Vue 文件 自定义的 Vue 文件需负责导入目标语言(如日语)的语言包,并根据当前语言环境动态切换配置。这里需要同时处理 ant-design-vue、element-plus 组件库及 dayjs 工具的语言包,确保日期组件的展示和交互统一适配目标语言。 <!– Root.vue –> <template> <a-config-provider :locale="antLocale"> <el-config-provider :locale="eleLocale"> <match :rootToken="root"> <template v-for="page in pages" :key="page.widget"> <route v-if="page.widget" :path="page.path" :slotName="page.slotName" :widget="page.widget"> <slot :name="page.slotName" /> </route> </template> <route :path="pagePath" slotName="page" :widgets="{ page: widgets.page }"> <slot name="page" /> </route> <route path="/" slotName="homePage"> <slot name="homePage" /> </route> </match> </el-config-provider> </a-config-provider> </template> <script lang="ts"> import { CurrentLanguage, EN_US_CODE, UrlHelper, ZH_CN_CODE } from '@oinone/kunlun-dependencies'; import { ConfigProvider as AConfigProvider } from 'ant-design-vue'; import { ElConfigProvider } from 'element-plus'; import dayjs from 'dayjs'; // 导入ant-design-vue语言包 import enUS from 'ant-design-vue/es/locale/en_US'; import zhCN from 'ant-design-vue/lib/locale/zh_CN'; import jaJP from 'ant-design-vue/lib/locale/ja_JP'; // 新增:日语语言包 // 导入 dayjs的语言包 import 'dayjs/locale/zh-cn'; import 'dayjs/locale/ja'; // 新增:日语语言包 // 导入element-plus语言包 import elEn from 'element-plus/dist/locale/en.mjs'; import elZhCn from 'element-plus/dist/locale/zh-cn.mjs'; import elJaJP from 'element-plus/dist/locale/ja.mjs'; // 新增:日语语言包 import { computed, defineComponent, onMounted,…

    2025年8月13日
    76400
  • Oinone平台可视化调试工具

    为方便开发者定位问题,我们提供了可视化的调试工具。
    该文档将介绍可视化调试工具的基本使用方法。

    2024年4月13日
    1.5K00
  • 在前端视图添加自定义的区域块

    添加自定义区域块 平台提供了一系列默认的视图布局,可以帮助开发人员快速构建出复杂的企业应用系统。当然,我们可以使用自定义区域块来扩展表格、表单、画廊、树形等视图。 自定义区域块概述 平台视图布局都是通过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.8K00
  • 【动作】-路由动作跳转后如何主动刷新页面数据

    介绍 当我们使用多tab组件的时候,如果一个viewAction已经打开了一个tab页,再次用该viewAction打开页面的时候,会发现不会根据路由上的业务参数(如详情和编辑页的id参数)主动刷新数据,这个时候可以通过以下方法解决该问题 // 该方法可以在进入新路由页面后刷新数据,推荐将该方法放到工具类 function refreshViewAction(action: any) { const onRefreshTabWithActive = (manager: MultiTabsManager, instance: MultiTabInstance) => { // 进入路由后刷新页面数据 manager.refresh(instance.key); manager.clearOnActive(onRefreshTabWithActive); }; MultiTabsManager.INSTANCE.onActive(onRefreshTabWithActive); executeViewAction(action); } 将原本调用executeViewAction的方法改为refreshViewAction 如果需要扩展executeViewAction的其他入参请自行拓展refreshViewAction的入参

    2024年6月18日
    1.4K00
  • 如何在多标签页切换时自动刷新视图

    在日常项目中,常常会遇到多视图(Multi-View)标签的场景,用户在切换不同视图时,可能需要刷新当前活动标签内的视图数据或状态。本文将详细解析下面这段代码,并说明如何利用它在视图切换时刷新对应的视图。 下列代码写在ss-boot里面的main.ts import { VueOioProvider } from '@kunlun/dependencies'; import { delay } from 'lodash-es'; VueOioProvider( { … 自己的配置 }, [ () => { setTimeout(() => { subscribeRoute( (route) => { const page = route.segmentParams.page || {}; // 如果不是表格类型,则不刷新(根据自己的需求判断) if (page.viewType !== ViewType.Table) { return; } const { model, action } = page; const multiTabsManager = MultiTabsManager.INSTANCE; delay(() => { const tab = multiTabsManager.getActiveTab(); if (tab?.key && tab.stack.some((s) => s.parameters?.model === model && s.parameters?.action === action)) { multiTabsManager.refresh(tab.key); } }, 200); }, { distinct: true } ); }, 1000); } ] ); 1. VueOioProvider 及其作用 首先,代码通过 VueOioProvider 初始化应用程序或组件,并传入两部分参数: 配置对象:可以根据实际业务需求进行自定义配置; 回调函数数组:这里传入了一个匿名函数,用于在应用初始化后执行额外的逻辑 2. 延时执行与路由监听 在回调函数中,使用了 setTimeout 延时 1000 毫秒执行,目的通常是为了确保其他组件或全局状态已经初始化完毕,再开始进行路由监听。 随后,代码调用 subscribeRoute 来监听路由的变化。subscribeRoute 接收两个参数: 回调函数:每次路由变化时都会触发该函数,并将最新的 route 对象传递给它; 配置对象:此处使用 { distinct: true } 来避免重复的触发,提高性能。 3. 判断视图类型 在路由回调函数内部,首先通过 route.segmentParams.page 获取当前页面的配置信息。通过判断 page.viewType 是否等于 ViewType.Table,代码可以确定当前视图是否为“表格类型”: 如果不是表格类型:则直接返回,不做刷新操作; 如果是表格类型:则继续执行后续刷新逻辑。 这种判断机制保证了只有特定类型的视图(例如表格)在切换时才会触发刷新,避免了不必要的操作 4. 多视图标签的刷新逻辑 当确认当前视图为表格类型后,从 MultiTabsManager 中获取当前活动标签: MultiTabsManager.INSTANCE.getActiveTab() 返回当前活动的标签对象; 如果 key 存在,并且激活的标签内部存储的action跟url一致, 就调用 multiTabsManager.refresh(key) 方法来刷新当前标签内的视图。

    2025年3月13日
    1.2K00

Leave a Reply

登录后才能评论