点亮 ⭐️ Star · 照亮开源之路
https://github.com/apache/dolphinscheduler
动态任务组件是 Apache DolphinScheduler 以动态表单渲染(它包含表单结构的渲染,表单的配置项等)的方式实现工作流 Dag节点配置渲染。区别于现有任务组件的实现方式,动态任务组件的实现在于通过接口获取任务的分类、任务节点以及每个任务对应的表单结构。
本文将介绍 Apache DolphinScheduler 初版动态任务组件设计的实现方案,希望帮助大家更高效地理解和使用海豚调度。
01
初衷
Apache DolphinScheduler 之所以通过动态任务组件的方式实现工作流 Dag节点配置渲染,是基于以下三点考虑:
减少前端部分的代码,缩小打包后的体积
尽可能在新增、编辑和删除一个任务组件时无需重新打包构建前端代码
便于后端开发者快速且无障碍的对任务组件进行开发(无需关注前端代码逻辑)
02
设计方案
由于该方案是一个长期计划,所以我们目前只实现了初版的动态任务组件的功能,它包含以下几点功能和变化:
增加系统设置页面(该页面目前包含接口默认请求的超时时间配置和日志接口轮询的时间间隔配置),在页面中增加实验性功能配置,其中包含动态任务组件的开关选项(当开关打开时,会在工作流定义页面中出现一个创建工作流(动态)的黄色按钮,点击后跳转新的画布页面);
画布的小地图功能;
左侧的任务组件列表由任务组件分类和查询当前分类下的任务组件两个接口的数据进行前端页面的渲染(任务组件的名称、默认图标和鼠标悬浮的图标都以静态资源文件形式存放在后端资源目录中);
当拖拽
Shell
任务到画布的过程中调用一个接口,该接口会返回此任务的表单结构数据(表单结构数据是以JSON
文件的形式存放在后端资源目录中)。
目前这个方案依旧在初版阶段,所以需要注意以下几点:
该方案的实现是以兼容模式进行处理,既要保证现有的逻辑能够平稳运行,又要保证新功能的不断迭代;
初版不支持跑通整个工作流的业务;
初版不支持画布中节点与节点之间的连接线功能;
初版只支持
Shell
组件中的四个必填字段;初版的表单项只支持
studio
、input
和select
,其中select
支持动态options
和非动态options
。
03
表单结构化数据剖析
为了最小化的验证该方案的可行性并考虑到兼容性的问题,我们并没有对后端做较大的改动。为此,我们在表单结构化的数据形式做了一些妥协。
01
基础结构
name:主要用于表示任务组件的名称。当然,它也有一些其它的用途,例如我们采用了动态任务组件的方案,网页的文档结构是根据数据进行渲染的,所以为了方便
E2E
测试,我们会用此字段和forms
中的field
字段进行拼接作为标签的class
属性的值;locales:目前
DolphinScheduler
的项目只支持中文和英语两种语言,所以locales
中只存在这两个对象结构;apis:用于存放需要调用的接口;
forms: 用于存放表单项的数据。
{
"name": "",
"locales": {
"zh_CN": {},
"en_US": {}
},
"apis": {},
"forms": []
}
02
locals
在需要 使用 多语言的地方,需要增加
task_components
前缀,如果不增加前缀则不会进行多语言的处理;多语言的内容需要根据表单项进行调整。
{
"zh_CN": {
"node_name": "节点名称",
"script": "脚本"
},
"en_US": {
"node_name": "Node Name",
"script": "Script"
}
}
03
apis
初版动态任务组件的设计方案中针对动态的接口请求只支持
select
组件的动态options
;初版的接口请求方式只支持
get
;初版的接口不支持传参,且返回类型为
["aaa", "bbb", "ccc"]
形式;对于
url
字段,开发者无需增加dolphinscheduler
前缀,因为它已经内置在请求拦截器中了。
{
"apiName": {
"url": "",
"method": "get"
}
}
04
forms
基础节点配置
基础节点配置为每个表单项都必定存在的配置。
注:这里为了介绍得更清晰,所以将其独立出来,在使用中基础节点配置与 input
或 select
节点的对象要合并在一起。
label(支持多语言):表单项的名称(如果不增加
task_components
前缀,则中英文下都会显示为node_name
)type:表单项的类型,初版只支持
studio
、input
和select
field:提交表单时接口需要绑定的字段(如果存在接口入参格式是嵌套的情况
{a: {b: 1}}
则field
的值可写为"a.b"
, 对应的结构解析器、校验解析器和值解析器会同步处理该情况。但是初版只支持两层的对象嵌套,且初版有嵌套对象的表单项为脚本项)defaultValue:默认值,
input
组件的默认值为""
,select
组件的默认值为null
validate:表单项的校验规则
validate.required:是否必填
validate.trigger:校验触发的方式,
input
为值发生改变时触发,blur
为失焦时触发validate.type:校验类型(初版只支持非空校验)
validate.message(支持多语言):校验不通过时页面展示的提示信息
{
"label": "task_components.node_name",
"type": "input",
"field": "",
"defaultValue": "",
"validate": {
"required": true,
"trigger": [
"input",
"blur"
],
"type": "non-empty",
"message": ""
}
}
Studio
studio
类型没有任何特殊配置,使用基础节点配置已能满足需求,所以不做过多介绍。
Input
clearable:是否在有内容时显示清空按钮
placeholder(支持多语言):
input
框内的提示信息textarea:是否显示为文本域
{
"clearable": true,
"placeholder": "",
"textarea": false
}
Select
静态
-
clearable:是否在有内容时显示清空按钮
placeholder(支持多语言):
select
框内的提示信息options:
select
中的选项数据,为空数组时则没有下拉数据options.label(支持多语言):页面中展现的下拉数据
options.value:该条下拉数据对应的唯一值
{
"clearable": true,
"placeholder": "",
"options": [
{
"label": "",
"value": 1
},
{
"label": "",
"value": 2
}
]
}
动态
API:动态 options
需要调用的接口名称(需要在基础结构 apis
字段中做对应),并将请求后的数据覆盖到 select
的 options
字段中
{
"api": "apiName"
}
04
解析器
为了能让表单结构化数据以浏览器能识别的 HTML
、 CSS
和 JavaScript
进行渲染,为此,我们设计了一些解析器(由于是初版的原因,我们大体上对解析器做了一些设计,使它更加灵活的同时也为了方便后续的拓展)
注:由于多语言和表单校验是在组件的 setup
生命周期中进行注册和绑定,但是 vue3
的生命周期钩子最早的是 onBeforeMount
且该数据是异步接口获取的数据 。同时,解析器的代码重在展示逻辑,为此会使用 any
类型。
01
多语言解析器
多语言解析器用于解析基础结构中的 locales
字段,它会与本地的 vue-i18n
进行并集处理
useI18n().mergeLocaleMessage('zh_CN', { task_components: locales.zh_CN })
useI18n().mergeLocaleMessage('en_US', { task_components: locales.en_US })
02
值解析器
值解析器会将基础结构中 forms
字段中的 field
字段解析成要提交的表单格式
解析
解析并处理 field
字段(需要对嵌套对象结构进行处理)。
const model: any = {}
forms.forEach((f: any) => {
if (f.field.indexOf('.') >= 0) {
const hierarchy = f.field.split('.')
model[hierarchy[0]] = {
[hierarchy[1]]: setField(f.defaultValue, f.type)
}
} else {
model[f.field] = setField(f.defaultValue, f.type)
}
})
赋值
为解析后的 model
进行赋值操作(如果 defaultValue
存在则赋值为 defaultValue
的值,否则会根据 input
或 select
进行默认值)。
import { ref } from 'vue'
import type { Ref } from 'vue'
const setField = (value: string, type: string): Ref<null | string> => {
return ref(value ?
value :
type === 'select' ?
null :
''
)
}
03
校验解析器
用于解析基础结构中 forms
字段中的 validate
字段。
解析
解析并处理 validate
字段,在 validate
不存在或 validate
是空对象或 required
为 false
或 type
不存在时,将不会处理当前表单项的校验逻辑。
注:校验解析器在处理嵌套 field
的逻辑时与值解析器不同。
const validate: any = {}
forms.forEach((f: any) => {
if (
!f.validate &&
Object.keys(f.validate).length <= 0 &&
!f.validate.required &&
!f.validate.type
) return
if (f.field.indexOf('.') >= 0) {
const hierarchy = f.field.split('.')
validate[hierarchy[1]] = setValidate(f.validate, f.field)
} else {
validate[f.field] = setValidate(f.validate)
}
})
包装
在这里我们要先构建好一个能够让表单进行验证的数据结构 data
,并将不会发生变化的 required
和 trigger
赋值进去。当该方法有 field
参数传递进来时代表当前表单项的 type
为 studio
,对于这个类型并不是原生的表单项,所以需要通过直解析器处理后的值进行判断。
const setValidate = (validate: any, field?: any): object => {
const data: any = {
required: validate.required,
trigger: validate.trigger
}
if (validate.type === 'non-empty') {
data['validator'] = (rule: FormItemRule, value: string) => {
if (field) {
const hierarchy = field.split('.')
if (!model[hierarchy[0]][hierarchy[1]]) {
return Error(t(validate.message))
}
} else {
if (!value) {
return Error(t(validate.message))
}
}
}
}
return data
}
04
请求解析器
用于动态请求 select
的 options
选项。
基础请求方法
区别于硬编码形式,动态请求我们会预留一个这样的基础请求方法,然后将参数传递进来,这样可以不改造项目集中封装的 axios
。
const reqFunction = (url: string, method: 'get') => {
return axios({
url,
method
})
}
解析
循环查找表单项中的 api
字段值是否在 apis
中存在。
export function useFormRequest(apis: any, forms: Array<any>): Array<any> {
forms.map(f => {
if (f.api && apis[f.api]) {
reqFunction(apis[f.api].url, apis[f.api].method).then((res: any) => {
f.options = res.map((r: any) => {
return { label: r, value: r }
})
})
}
})
return forms
}
05
结构解析器
结构解析器是 最后一个执行 的解析器,它的作用在于过滤掉 forms
中的对于浏览器页面渲染和逻辑无关的参数(过滤掉这些无用的参数,能在浏览器渲染中节省一部分时间)。
export function useFormStructure(forms: Array<any>): Array<any> {
return forms.map((f: any) => {
delete f.validate
delete f.api
if (f.field.indexOf('.') >= 0) {
const hierarchy = f.field.split('.')
f.field = hierarchy[1]
}
return f
})
}
05
表单效果和配置数据
01
表单效果
表单的最终展现效果会根据配置数据进行渲染,修改表单信息只需要修改后端的 json 数据配置即可,而无需改动前端代码。
02
配置数据
json
{
task: 'shell',
locales: {
zh_CN: {
node_name: '节点名称',
node_name_tips: '请输入节点名称',
node_name_validate_message: '节点名称不能为空',
script_validate_message: '脚本内容不能为空',
task_priority: '任务优先级',
highest: '最高',
high: '高',
medium: '中',
low: '低',
lowest: '最低',
worker_group: 'Worker 分组',
script: '脚本'
},
en_US: {
node_name: 'Node Name',
node_name_tips: 'Please entry node name',
node_name_validate_message: 'Node name cannot be empty',
script_validate_message: 'Script content cannot be empty',
task_priority: 'Task Priority',
highest: 'Highest',
high: 'High',
medium: 'Medium',
low: 'Low',
lowest: 'Lowest',
worker_group: 'Worker Group',
script: 'Script'
}
},
apis: {
getWorkerGroupList: {
url: '/worker-groups/all',
method: 'get'
}
},
forms: [
{
label: 'task_components.node_name',
type: 'input',
field: 'name',
defaultValue: '',
clearable: true,
placeholder: 'task_components.node_name_tips',
validate: {
required: true,
trigger: ['input', 'blur'],
type: 'non-empty',
message: 'task_components.node_name_validate_message'
}
},
{
label: 'task_components.task_priority',
type: 'select',
field: 'taskPriority',
options: [
{ label: 'task_components.highest', value: 'HIGHEST' },
{ label: 'task_components.high', value: 'HIGH' },
{ label: 'task_components.medium', value: 'MEDIUM' },
{ label: 'task_components.low', value: 'LOW' },
{ label: 'task_components.lowest', value: 'LOWEST' }
],
optionsLocale: true,
defaultValue: 'MEDIUM',
validate: {
required: true,
trigger: ['input', 'blur']
}
},
{
label: 'task_components.worker_group',
type: 'select',
field: 'workerGroup',
options: [],
optionsLocale: false,
defaultValue: 'default',
api: 'getWorkerGroupList',
validate: {
required: true,
trigger: ['input', 'blur']
}
},
{
label: 'task_components.script',
type: 'studio',
field: 'taskParams.rawScript',
defaultValue: '',
validate: {
required: true,
trigger: ['input', 'blur'],
type: 'non-empty',
message: 'task_components.script_validate_message'
}
}
]
}
06
结语
这就是 Apache DolphinScheduler 针对初版的动态任务组件的设计和实现方式。在未来,我们还会新增更多的表单种类,如单选、多选和开关等组件,也会有更多的交互逻辑的表达(比如可能会发布一套模板语法,用 lodash
或 AST
进行解析),同样地,我们的解析器也会面临着诸多改造以支持更广泛的任务组件的需求。所以,如果读到这篇文章的你有 DolphinScheduler 动态任务组件实现
的更好的思路,也非常欢迎加入我们的贡献者种子群(联系人: easyworkflow)与我们交流, 我们相信贡献者的力量!
参与贡献
随着国内开源的迅猛崛起,Apache DolphinScheduler 社区迎来蓬勃发展,为了做更好用、易用的调度,真诚欢迎热爱开源的伙伴加入到开源社区中来,为中国开源崛起献上一份自己的力量,让本土开源走向全球。
参与 DolphinScheduler 社区有非常多的参与贡献的方式,包括:
贡献第一个PR(文档、代码) 我们也希望是简单的,第一个PR用于熟悉提交的流程和社区协作以及感受社区的友好度。
社区汇总了以下适合新手的问题列表:https://github.com/apache/dolphinscheduler/issues/5689
非新手问题列表:https://github.com/apache/dolphinscheduler/issues?q=is%3Aopen+is%3Aissue+label%3A%22volunteer+wanted%22
如何参与贡献链接:https://dolphinscheduler.apache.org/zh-cn/community/development/contribute.html
来吧,DolphinScheduler开源社区需要您的参与,为中国开源崛起添砖加瓦吧,哪怕只是小小的一块瓦,汇聚起来的力量也是巨大的。
参与开源可以近距离与各路高手切磋,迅速提升自己的技能,如果您想参与贡献,我们有个贡献者种子孵化群,可以添加社区小助手微信(Leonard-ds) ,手把手教会您( 贡献者不分水平高低,有问必答,关键是有一颗愿意贡献的心 )。
添加社区小助手微信(Leonard-ds)
添加小助手微信时请说明想参与贡献。
来吧,开源社区非常期待您的参与。
< 🐬🐬 >
更多精彩推荐
☞Apache DolphinScheduler 社区年度总结
☞Apache Dolphinscheduler 任务插件版图再添 Linkis,大幅提高计算治理能力
☞DolphinScheduler 快速构建 Hugging Face 文本分类工作流,基于工作流的机器学习训练部署太强了!
☞Apache DolphinScheduler 任务调度3.1.0版本源码剖析
☞名额已排到10月 | Apache DolphinScheduler Meetup分享嘉宾继续火热招募中
☞【Meetup讲师】您有一张社区认证讲师证书未领取,点击领取!
☞PyDolphinScheduler 4.0.0 正式发布
我知道你在看哟