Open pengx17 opened 5 years ago
大佬你好,在借鉴demo的时候遇到一些问题,想请教一下。
大佬你好,在借鉴demo的时候遇到一些问题,想请教一下。
- 想要获取yaml中光标所在位置对应的path,但是getDocumentSymbols这个方法返回的一直是[]
- 关于monaco-editor 中的model、token、symbol、editor这些概念不清楚,请问有推荐的文档吗?
您好,关于这一块的内容我太久没看过了,给不到什么有价值的信息 😥 我记得当时做了很多debug工作、查看源码才搞通了一些疑难杂症。
大佬你好,在借鉴demo的时候遇到一些问题,想请教一下。
- 想要获取yaml中光标所在位置对应的path,但是getDocumentSymbols这个方法返回的一直是[]
- 关于monaco-editor 中的model、token、symbol、editor这些概念不清楚,请问有推荐的文档吗?
您好,关于这一块的内容我太久没看过了,给不到什么有价值的信息 😥 我记得当时做了很多debug工作、查看源码才搞通了一些疑难杂症。
嗯嗯,多谢。我也啃源码debug去。
简介
Kubernetes 集群的使用者日常工作中经常需要与 Deployment 等 Kubernetes 对象接触。熟悉 Kubernetes 的朋友都会知道,Kubernetes 的对象结构虽然重视可移植性,不同的对象有着相似的设计理念,但就算是最熟练的系统运维或者开发工程师也不一定能将 Kubernetes 的对象玩转。 为了使用户更方便的操作 Kubernetes 对象,灵雀云的 Kubernetes 发行版前端界面提供了对用户友好的 UI 表单解决方案。用户可以通过 UI 表单更容易的编辑 Kubernetes 对象,同时还提供了 YAML 格式与 UI 表单实时互转的功能。
读者可以先从这个Deployment表单demo里面感受一下实时互转的效果(注:这个 demo 是为这篇文章单独开发,与灵雀云产品无关):
难点
YAML 是 Kubernetes 对象最常见的展现和修改形式。对于前端来说,假如我们需要同时支持表单和 YAML 的方式编辑 Kubernetes 资源,那么最终我们都得落回到编辑 YAML 上。 从数据表现格式上来看,表单的模型数据是 JSON 对象,而 YAML 为字符串。YAML 与表单数据的互转,类似于序列化与反序列化的过程,我们需要在转化的边界借助如js-yaml之类的库进行转换。 实际上数据形式的问题比较好解决,难点在于 YAML 所代表的 Kubernetes 资源对象模型与表单数据进行转换的问题。实践过程中,表单数据与 K8S 对象互转有不少问题需要攻克。比如:
实现方案推导
模板驱动表单
每个 Angular 表单的开发者应该都接触过模板驱动表单与响应式表单这两个不同的Angular表单实现思路。有些开发者可能会把响应式表单与动态表单混淆,实际上这两个概念没有什么联系。不熟悉的同学可以看看 Angular 官网这篇关于表单的介绍,其给出如下两者的区别:
用模板驱动表单写前端表单确实很容易:给定任意一个数据对象,将需要的字段与模板的表单控件通过 [(ngModel)] 指令进行数据绑定;根据实际需要,再绑定一下诸如 required 的表单验证指令就完事了。鹅妹子嘤!
不过一旦这么做,用户就将数据的“权威”就交给了模板,脱离了数据的实际控制权,也就只能被动的接受来自于模板的数据更新、表单状态与生命周期、数据验证等事件。对于复杂表单的业务逻辑,你很难通过这种模式扩展到大规模而复杂的表单数据逻辑处理之中。
响应式表单与受控组件
使用 Angular 响应式表单对于初学者来说有些啰嗦和麻烦:为了维护表单的状态,我们需要显式地创建一套完整的表单控制器对象层级结构,并将此对象通过 FormGroup / FormControl 之类的指令绑定到模板上的表单控件上。初看 Angular 的响应式表单的思想,似乎有点违背如今 MV* 的设计模式,因为它把一些本来可以通过框架隐式管理的工作暴露给了开发者自己,额外的增加了不少工作量。
熟悉 React 的表单控件实现的人应该了解React 的受控组件和非受控组件概念。通过受控组件,用户可以通过单项数据流的思路,掌握表单控件数据的实际控制权。不过对于实际的完整表单应用场景,用户还需要处理表单的提交状态、表单验证逻辑等信息。Angular 框架内置了一套相对成熟稳定的响应式表单解决方案,帮助开发者更可控的管理表单的状态的同时进行表单相关业务的开发。
Angular 表单控件的根基:ControlValueAccessor
Angular 的表单控件的魔法与 React 的受控组件的思路十分类似,是典型的单项数据流的处理模式。另外,不管是模板驱动表单还是响应式表单,都是围绕着 ControlValueAccessor 接口设计实现的。假如一个组件提供了 NG_VALUE_ACCESSOR 令牌注入到模板的 DI 上下文,并实现了 ControlValueAccessor 接口,那么这个组件就可以绑定任意 Angular 的表单指令。
ControlValueAccessor 最关键的有两点: registerOnChange 和 writeValue,这两个函数分别对应了单项数据流从表单内到外和从表单外到内两个方向的数据变化。
适配器模式
聪明的朋友一定会注意到,ControlValueAccessor 接口并没有要求 onChange 与 writeValue 调用的时候表单数据格式与输入一致。因此我们可以在一个业务表单控件组件内实现局部的资源对象与UI表单数据转换的逻辑。比方说上面提到的,我们可以通过它实现一个键值对表单控件。
它对外暴露为正常的键值对控件,值类型为 { [key: string]: string }。 数据由外到内时,可以通过 writeValue 将键值对通过 Object.entries 改变为 [string, string] 的数组,最后将绑定到表单内部的 FormArray 控件上;同时将内部状态改变时,在调用 onChange 之前将 [string, string][] 转化为外部的 { [key: string]: string } 对象类型。
假如我们以表单控件为界限,这个界限的两侧分别为 Host 上绑定表单控件 NgControl 与表单控件内部的响应式表单。借助 onChange 和 writeValue,我们可以围绕着 ControlValueAccessor 实现一个局部的适配器模式。
通过这个思路,我们可以继续引申:
由于有了基于适配器模式的数据转化思路, 对于每一个表单控件,我们可以通过 onChange 和 writeValue 这两个接口进行数据与表单的模型变化,实现UI的数据模型和实际对外暴露的数据模型的不一致需求。
针对于一个复杂的资源对象表单,我们可以把问题拆解为多子表单控件组件,每一个子表单控件组件都去实现适配器模式。同时,每个子表单控件组件依然可以用同样方式进行拆解组合。这样,通过将资源对象不断往下递归拆解,借助表单的组合和嵌套,我们可以最终实现一个复杂的表单树。
内嵌表单的实现隔离了复杂表单的实现逻辑。每一个子表单控件虽然对外暴露是一个表单控件数据,但其内部是一个完整的表单。父级(host)组件可以完全不了解子表单组件控件内部处理逻辑,比如表单的错误处理、数据转换等。同时由于K8S的设计,许多子表单是可以在不同的资源里复用的,减轻了我们的开发成本。
表单控件本身在提供 K8S 数据的同时,也可以表现为一个独立的 K8S 对象资源。我们可以把局部相关的业务逻辑完整的封装在此表单控件组件内部,做到神行合一。这点很重要,通过这一点,我们可以更容易的划分出 K8S 资源的问题范围,更快做出代码结构的判断,减少开发的脑力负担和维护成本。坊间也有其他开发者倾向于将业务处理逻辑独立出来,不放到组件内部,这样组件就可以只负责薄薄一层视图渲染逻辑。我认为不是不可行,不过在复杂表单组件嵌套和复用角度,可能本文采用的方式更容易维护。
由于上述实现思路过程有比较规范的思路,我们可以设计出来一个标准的开发 Kubernetes 资源对象表单的实现范式。这个范式可以大大降低开发人员对于开发、维护复杂表单实现的思维负担。以上的过程借助递归的思路,把一个问题拆解的简单又有效:
Kubernetes对象的响应式表单开发范式
中心思想
我总结了这个开发范式里几个关键点:
流程
为任意一个 Kubernetes 对象开发表单的过程可以总结如下:
稍后我们会以部署表单为例,详细说明流程细节。
用例分析: Deployment 表单
熟悉 Deployment 对象的结构
首先参考官网对于 Deployment 的 API 文档,输出一套 TypeScript 的接口,方便后续参照:
部署表单拓扑
对于部署表单,我们拆分为3个主要表单:
art: http://asciiflow.com/
<deployment-form [(ngModel)]="deployment">
const metadataForm = this.fb.group({ name: ['', [Validators.required]], namespace: ['', [Validators.required]], labels: [{}], annotations: [{}] }); const specForm = this.fb.group({ selector: this.fb.group({ matchLabels: [{}] }), template: this.fb.group({ spec: [{}] }) }); const deployForm = this.fb.group({ metadata: metadataForm, spec: specForm, });
setForm setResource | | | +------+ +------>>>+ Form | +------+