乔达 试图以声明方式进行

每当我需要坐下来创建一个新的应用程序时,我都会陷入困境。 我急于选择这段时间来选择哪个库或框架。 我上一次在X库上编写代码,但是现在Y框架已经长大并移交了,仍然有一个很棒的UI Kit Z,并且以前的项目还剩下很多工作。


从某种意义上说,我意识到框架并不重要-我需要什么,我可以在任何一个框架上进行。 在这里看来您应该很高兴,在github上放满星星,然后冷静下来。 但是同样,不断出现一种不可抗拒的愿望,去做自己的东西,自己的自行车。 那好 有关此主题的一些一般想法和名为Chorda的框架正在削减您的时间。


实际上,麻烦不是别人的决定是不好的还是无效的。 不行 问题是,别人的决定使我们以可能对我们不方便的方式进行思考。 但是等等。 “不便便便”是什么意思,这甚至会影响发展吗? 回想一下,实际上有DX之类的东西-一套既定的个人习惯和普遍接受的惯例。 从这里我们可以说,当我们自己的DX与库或框架作者的DX重合时,这对我们来说很方便。 当它们分开时,非常不适,烦躁并寻找新事物。


一点历史


在为企业应用程序开发UI时,您将面对大量的用户表单。 有一天,一个绝妙的想法突然浮现在脑海:为什么我每次都可以简单地列出JSON中的字段并将结果结构提供给生成器时创建一个Web表单? 而且,尽管这种方法在血腥的企业世界中效果不是很好(为什么,这是一次单独的对话),但是从命令式样式转换为声明式样式的想法通常也不错。 大量的Web表单,页面甚至整个站点的生成器都可以在Web上轻松找到,这证明了这一点。


因此,在某些时候,由于过渡到声明性,我对改进我的代码并不陌生。 但是,一旦我们不仅需要标准的html元素,而且还需要复杂的交互式小部件组件,我们就无法使用简单的生成器。 快速增加了对代码可重用性,可集成性,可扩展性等的要求。 用声明性API开发自己的组件库很快就到了。


但是在这里,幸福并没有发生。 最好的情况可能反映出我的同事的意见,该同事将使用创建的库。 他在文档中查看了这些示例,并说:“该库很酷。漂亮,动态。但是,我现在如何从这一切中创建一个应用程序?” 他是对的。 事实证明,制作一个组件与将多个组件组合在一起并使它们无缝工作并不相同。


从那时起,已经过去了很多时间。 当我再次被汇聚思想和发展的渴望所吸引时,我决定采取一些不同的行动,而不是自下而上,而是自上而下。


应用程序管理==状态管理


我更习惯将应用程序视为具有某些克隆状态集的有限状态机。 应用程序的工作是从一个状态到另一种状态的过渡集,其中更改模型会导致创建新版本的视图。 将来我将一些与其唯一表示形式相关的固定数据 (对象,数组,原始类型等)称为文档


有一个明显的问题-对于模型的许多值,有必要描述文档的许多选项。 这里通常使用两种方法:


  1. 模板。 我们使用我们最喜欢的标记语言,并用分支和循环指令对其进行补充。
  2. 功能介绍 我们在函数中以最喜欢的编程语言描述分支和循环。

通常,这两种方法都声明为声明性的。 第一个被认为是声明性的,因为尽管它虽然略微扩展,但却基于标记语言的规则。 第二个-因为它着重于功能的组成,其中许多功能充当规则。 值得注意的是,现在模板和函数之间没有明确的界限。


一方面,我喜欢模板,但另一方面,我想以某种方式使用javascript的功能。 例如,如下所示:


createFromConfig({ data: { name: 'Alice' }, tag: 'div', class: 'clickable box', onClick: function () { alert('Click') } }) 

结果是描述整个一个特定状态的JS配置。 为了描述许多状态,有必要实现此配置的可扩展性。 使一组选项可扩展的最便捷方法是什么? 我们这里不会发明任何东西-重载选项已经存在很长时间了。 在其带有Option API的Vue示例中可以看到它的工作方式。 但是,与同一个Vue不同,我想知道是否可以用相同的方式描述完整状态,包括数据和文档。


应用程序结构和声明性


“组件”一词变得含糊不清,尤其是在所谓的 功能组件。 在继续进行应用程序的结构时,我将组件称为结构元素

很快,我得出的结论是,结构元素(组件)不是文档元素,而是某些实体,该实体:


  1. 结合数据和文档(绑定和事件)
  2. 与其他类似实体连接(树形结构)

正如我之前指出的,如果您将应用程序视为一组状态,那么对于这些​​状态,您必须具有描述方法。 而且,有必要找到一种这样的方法,使其不包含“虚假”命令式运算符。 我们正在谈论引入模板的那些非常辅助的元素- #if# elsifv-for等。 我想很多人已经知道了解决方案-必须将逻辑转移到模型,在表示层保留一个API,该API允许您通过简单的数据类型来控制结构元素。


通过管理层,我了解可变性和周期性的存在。


可变性(如果为其他)


让我们看看如何使用Chorda中的卡片组件示例控制显示选项:


 const isHeaderOnly = true const card = new Html({ $header: { /*  */ }, $footer: { /*  */ }, components: {header: true, footer: !isHeaderOnly} //    }) 

通过设置组件选项的值,您可以控制显示的组件。 而且,在将组件与反应性存储链接时,我们将获得结构化数据管理。 有一个警告-对象用作值,并且其中的键没有排序,这对组件施加了一些限制。


周期(用于)


处理仅在运行时知道数量的数据将需要遍历列表。


 const drinks = ['Coffee', 'Tea', 'Milk'] const html = new Html({ html: 'ul', css: 'list', defaultItem: { html: 'li', css: 'list-item' }, items: drinks }) 

items选项的值分别是Array,我们得到一组有序的组件。 与组件绑定一样,将项目绑定到存储将控制权转移到数据。


结构元素在树层次结构中相互连接。 如果我们结合前面的示例,则要在卡的主体中显示列表,我们将得到以下内容:


 //   const state = { struct: { header: true, footer: false, }, drinks: ['Coffee', 'Tea', 'Milk'] } //  const card = new Html({ $header: { /*  */ }, $content: { html: 'ul', css: 'list', defaultItem: { html: 'li', css: 'list-item' }, items: state.drinks }, $footer: { /*  */ }, components: state.struct }) 

大约以此方式,创建了应用程序的数据结构。 拥有两种类型的生成器就足够了-基于对象和基于数组。 仍然只有了解如何将结构元素转换为文档。


当一切都已经为我们发明


总的来说,我赞成文档呈现系统应在浏览器级别(尽管至少是相同的VDOM)实现的事实。 我们的任务只是将其小心地连接到组件树。 毕竟,无论库的速度增长了多少,浏览器还是拥有更多。


老实说,我有时会尝试使用渲染功能,但是一段时间后我放弃了,因为我的绘制速度无法比VanillaJS快(可悲!)。 现在,使用VDOM进行渲染已经成为一种时尚,而且其实现甚至可能很多。 因此,再加上虚拟树的另一种实现,我决定不将其添加到github的存钱罐中-仅使用下一个框架就足够了。


最初,在Chorda中创建了用于Maquette库的适配器以进行渲染,但是一旦“来自现实世界”的任务开始出现,事实证明在React上放置一个抽屉更为实用。 例如,在这种情况下,您可以简单地使用现有的React DevTools,而不能编写自己的工具。


要将VDOM与结构元素连接,您需要诸如layout之类的东西。 可以将其称为结构元素的文档功能。 重要的是纯函数。


考虑一个带有标题,主体和地下室的卡的示例。 前面已经提到过,这些组件是无序的,即 如果我们在操作过程中开始打开/关闭组件,则它们将以新的顺序每次出现。 让我们看看如何通过布局解决此问题:


 function orderedByKeyLayout (h, type, props, components) { return h(type, props, components.sort((a, b) => a.key - b.key).map(c => c.render())) } const html = new Html({ $header: {}, $content: {}, $footer: {}, layout: orderedByKeyLayout //     }) 

该布局允许您配置所谓的 与组件关联的宿主元素及其子元素( 项目组件 )。 通常,标准布局也是足够的,但是在某些情况下,布局需要存在包装元素(例如,用于网格)或分配特殊类,而我们不希望将其带到组件级别。


少量反应


声明并绘制了组件的结构后,我们获得了与一个特定数据集相对应的状态。 接下来,我们需要描述许多数据集及其对变化的反应。


在处理数据时,我不喜欢两件事:


  • 免疫力。 跟踪更改的一件好事是为穷人提供版本控制,该版本在原始对象和扁平对象上非常有用。 但是,一旦结构变得更加复杂并且投资数量增加,就很难保持复杂对象的免疫力。
  • 换人。 如果将某些对象放入数据仓库中,那么当我索要它时,我可以返回该对象的副本或通常具有与其结构相似性的另一个对象或代理。

我想拥有一个行为不可变的存储库,但其中包含可变数据,该数据还可以保持链接的持久性。 在理想情况下,它看起来像这样:我创建一个存储库,向其中写入一个空对象,开始从应用程序表单输入数据,然后单击“提交”按钮后,我得到具有填充属性的相同对象(链接相同!)。 我将这种情况称为理想情况,因为这种情况很少会发生,即存储模型与展示模型匹配。


需要解决的另一个任务是将数据从存储传递到结构元素。 同样,我们不会发明任何东西,而是使用连接到公共上下文的方法 。 就Chorda而言,我们无法访问上下文本身,而只能访问其显示(称为scope) 。 此外,组件的范围是其子组件的上下文。 这种方法允许您在我们应用程序的任何级别上缩小,扩展或替换相关数据,并且这些更改将被隔离。


上下文数据如何在组件树中分布的一个示例:


 const html = new Html({ //     scope: { drink: 'Coffee' }, $component1: { scope: { cups: 2 }, $content: { $myDrink: { //      ,    drinkChanged: function (v) { //    drink   text this.opt('text', v) } }, $numCups: { cupsChanged: function (v) { this.opt('text', v + ' cups') } } } }, $component2: { scope: { drink: 'Tea' //      drink }, drinkChanged: function (v) { //    drink   text this.opt('text', v) } } }) //    // <div> // <div> // <div> // <div>Coffee</div> // <div>2 cups</div> // </div> // </div> // <div>Tea</div> // </div> 

最难理解的时刻是每个组件都有自己的上下文,而不是我们在使用模板时通常会在结构的最顶层声明的上下文。


期权超载呢?


当然,您会遇到一种情况,即组件很大,因此有必要在内部深处的某个地方更改小的嵌套组件。 他们说,造粒和成分应对此有所帮助。 而且,必须立即设计组件和体系结构。 如果大型组件不是您自己的,而是属于另一个团队甚至一个独立社区开发的图书馆的一部分,那么情况将变得非常可悲。 即使他们不是最初计划的,也可以轻松地对基础组件进行更改怎么办?


通常,库中的组件被设计为类,然后它们可以用作创建新组件的基础。 但是这里隐藏了一个我从未喜欢过的小功能:有时我们创建一个类只是将其应用在一个地方。 真奇怪 例如,我习惯于使用类进行输入,在对象组之间建立关系,而不是使用它们来解决分解问题。


让我们看看类如何在Chorda中与配置一起工作。


 //      class Card extends Html { config () { return { css: 'box', $header: {}, $content: {}, $footer: {} } } } const html = new Html({ css: 'panel', $card: { as: Card, $header: { //       title $title: { css: 'title', text: 'Card title' } } } }) 

我喜欢此选项,而不是创建一个仅会使用一次的特殊TitleedCard类。 而且,如果您需要包括一部分选项,则可以使用杂质机制。 好吧,没有人取消Object.assign。


在乔达语中,一类实质上是用于配置的容器,并扮演一种特殊杂质的角色。


为什么要使用另一个框架?


我重申,我认为该框架更多是关于思维和经验的方式,而不是技术。 我的习惯和DX在JS中要求声明性,而在其他解决方案中找不到。 但是,一项功能的实施带来了新的功能,不久之后,它们就不再适合专用库的框架。


目前,Chorda正在积极开发中。 主要方向已经可见,但是细节在不断变化。


感谢您阅读到最后。 我很乐意评论。


我在哪里可以看到?


该文件


GitHub来源


CodePen示例

Source: https://habr.com/ru/post/zh-CN480856/


All Articles