注意:以下翻译的准确性尚未经过验证。这是使用 AIP ↗ 从原始英文文本进行的机器翻译。

理解依赖关系

理解Slate中的“为什么会发生”

在Slate中,所有微件、函数、变量和查询都被建模为图中的节点。每个节点评估为一个JSON输出,其他节点模板化引用该输出。这些引用定义了节点之间的依赖关系,Slate使用这些依赖关系来理解当某些内容更改时(即新的用户输入、查询执行、变量值更新)何时需要重新评估节点。

“开发”您的Slate应用程序的主要过程将是通过添加微件来定义这些节点的过程——用于显示输出和捕获输入;函数——用于轻量操纵数据以便显示和处理应用程序登录;以及查询——用于查询数据或连接到其他Foundry服务API,然后通过Handlebar语法配置它们的依赖关系。

依赖关系图

您始终可以使用依赖关系标签查看此依赖关系图的表示,以理解Slate如何通过您的Handlebar引用解释您配置的关系。此图表示_即是_您的应用程序——如果它代表了一些看似“错误”的内容,那么您的起点是理解_为什么_而不是假设图错误地表示了您的应用程序。

每当您的应用程序中发生意外情况时,从依赖关系标签开始,了解任何行为异常的微件的上游节点。寻找可能导致节点基于不相关查询或用户输入重新评估的意外关系。

有时将依赖关系图视为“懒惰”是有用的——它通过仅在上游引用更改值时才重新评估节点来避免不必要的工作。当上游节点更新为与之前相同的值时,这会导致潜在的混淆行为——下游查询不会执行,下游微件不会重新渲染。这通常出现在涉及重置用户输入的模式中——在您的Foundry中搜索依赖关系图和事件框架简介教程和重置微件选择示例以查看其操作方式,以及如何使用Math.random()注入熵到依赖关系图评估中并确保节点重新评估。

事件

通过依赖关系图,Slate处理何时函数应该重新计算或查询应该根据其上游依赖关系的状态重新运行的复杂性。大多数工作流程可以使用此功能进行配置,作为Slate开发人员,应该始终首先依赖于依赖关系图。

然而,有些情况需要使用显式触发/操作对来补充依赖关系图,Slate称之为“事件”。一些简单的例子是当查询完成时触发横幅或提示的显示,当用户点击某物以查看更多详细信息时打开对话框,或在配置多个输入后通过按钮点击运行查询。

了解有关事件框架和常见基本事件模式示例的更多信息。

事件狂热

一旦您意识到事件框架的强大功能,可能会倾向于通过事件驱动的解决方案视角来看待Slate中的每个开发任务。这可能导致一个常见的失败模式,其中一个应用程序包含数百个事件/操作对。虽然技术上可行,但这种复杂性使得很难逐步了解您的应用程序的预期行为,甚至可能妨碍简单的调试。

作为经验法则,保持事件“链”尽可能短,并倾向于依赖于依赖关系图的解决方案而不是事件。如果您发现您的应用程序增长到>50个事件,特别是如果这些事件不仅仅是触发提示或启动查询时,是时候暂停并仔细思考您的应用程序架构是否有机会重构和简化您的开发,或者进一步将您的应用程序分解为子应用程序。

自定义事件触发器

除了内置的事件类型外,您还可以在HTML表格类型微件中创建自定义事件,以允许这些微件中的不同元素从用户点击中广播不同的事件。

这些自定义触发器的配置分为两个步骤:

  1. 在HTML元素中使用slClickEvent属性定义触发器:
Copied!
1 2 3 4 5 <div class='pt-button' slClickEvent='delete'>Delete Me</div> <!-- 这是一个HTML代码片段,创建了一个具有类名'pt-button'的<div>元素。 该元素绑定了一个自定义事件'slClickEvent',事件名为'delete'。 点击该按钮时,应该会触发与'delete'事件关联的处理逻辑。 按钮上显示的文本是“Delete Me”。 -->
  1. 在微件配置中通过将名称添加到 Click Event Names 数组来注册触发器。

一旦新事件注册完成,您将在可以选择事件触发器的地方看到一个新选项,例如 myWidgetName.click.delete。注意,即使您动态生成HTML来定义点击事件,这些事件的注册仍然需要硬编码,以便Slate能够跟踪潜在触发器,即使条件是您目前实际上没有生成该按钮。

您可以使用这些事件在HTML中搭建自定义按钮组,或者将它们与下面讨论的事件值结合,创建每个项目都有自己一套操作的列表或表格。

条件事件

如果您需要添加额外条件(超出简单的触发器触发),可以使用事件配置中的JavaScript部分来执行附加逻辑。评估您的逻辑,如果事件不应该执行操作,返回特殊值 {{slDisableAction}}。该值不会永久禁用事件,而是中断该事件操作的特定实例。

作为最佳实践,避免在事件JavaScript中开发复杂逻辑 - 由于没有代码审查或错误报告,任何错误或未覆盖的边缘情况都可能导致您的代码静默失败并导致意外行为。相反,考虑实现一个封装事件有效性逻辑的函数 - 然后您的事件JavaScript可以简化为:

Copied!
1 2 3 4 5 unless ({{f_checkValidAction}}) return {{slDisableAction}} // unless 是一种控制结构,相当于 if 的反义用法 // 如果 {{f_checkValidAction}} 不为真,则执行 return {{slDisableAction}}

您可以在事件中插入 debugger() 语句,然后使用 Chrome 开发者工具捕获其执行并逐步调试您的逻辑。

传递事件值

除了 slClickEvent 属性之外,您还可以为给定的 HTML 元素定义一个 slClickEventValue。每当该事件被触发时,您可以在事件 JavaScript 中使用特殊的 {{slEventValue}} 变量引用关联的值。

这为动态交互打开了一系列模式:

  1. 创建一个函数,用于生成您的 文本表格 微件的 HTML,并为每个元素定义 slClickEventslClickEventValue
  2. 创建必要的事件,并使用这些事件将一个或多个单个变量的值设置为事件中的值。
  3. 触发进一步的操作——通常是查询——基于 variable.changed 事件触发,并引用变量的值。

例如,您可以使用此模式在表格中的行旁边添加一个小 'x',其中 slClickEvent 为 'delete',slClickEventValue 为行的主键。然后,当 myTable.click.delete 事件触发时,将名为 v_idToDelete 的变量的值设置为事件值。接着运行基于 v_idToDelete.changed 事件的 q_deleteRow 查询,以完成这条小链。

注意不要使链条比这更长或更复杂——请参阅下面关于循环依赖的注意事项——并且倾向于使用简短、独立的事件,而不是大型、嵌套或其他复杂的安排,因为这些变得难以推理和调试,并且经常导致意外行为。

事件与循环依赖

Slate 依赖图技术上是一个“有向无环图”(DAG),这意味着节点关系是有向的(意味着从根节点到叶节点有一个层次结构,节点解析按该方向发生),并且整个图必须是无环的,这意味着 Slate 会尝试阻止您在依赖关系图中配置任何“循环”——无论多长。

然而,使用 事件 并设置变量值或触发查询,可能会在您的应用程序中添加一个循环,这将导致不确定性行为。如果您的工作流程似乎需要循环依赖,请退一步考虑替代模式——几乎总有办法实现所需的工作流程功能。

管理查询执行

依赖图框架的一个影响是,默认情况下,整个依赖图将在页面加载时解析。这意味着每个查询和函数将以必要的顺序运行,以便图中的每个节点解析。实际上,这通常意味着如果您在开发过程中没有注意,您会慢慢注意到页面加载性能下降,直到您开始遇到间歇性超时。根本原因几乎总是许多查询是“根”节点,因为它们没有上游查询依赖关系——这意味着所有这些查询将尝试同时运行。这可能导致由于太多同时的浏览器连接,太多同时查询相同表,太多网络流量返回到应用程序而出现排队和负载分流。最终状态是不确定的查询失败,并且在页面加载时应用程序感觉迟缓,如果不是完全崩溃的话。

Postgate 的连接池与排队

在 Postgate 中,您的应用程序的每个用户都有一个专门为他们创建的 连接池(如果他们最近已经使用过该应用程序,则从缓存中恢复)。每个连接池一次限制为 10 个连接,这防止了任何一个应用程序实例过载 Postgate,因为它是由您的应用程序的所有用户共享的,以及任何其他使用已同步数据的应用程序共享的。在性能方面,这个限制实际上导致了更好的性能,因为当响应的同时查询数量更少时,Postgres 的全局速度更快。

一旦所有 10 个连接都被使用,剩余的查询请求会排队,每个连接在可用时获取一个连接。然而,连接池不会让连接无限期等待,而是在查询等待连接五秒钟后超时。这意味着如果您有多个查询需要几秒钟来填满池,可能很快的查询——例如,当您使用 测试 按钮在隔离中运行它时,可能需要很长时间甚至超时。

解决方案包括以下几个部分:

  1. 开发您的应用程序工作流程,使查询间隔开来并依赖于用户输入。一个常见模式是对预聚合或 DISTINCT 表运行一些小查询以填充一组筛选,但“强迫”用户进行一些选择并点击 提交 按钮以运行实际获取数据的查询。
  2. 谨慎开发查询,注意平衡页面加载时运行的查询总数——尤其是“繁重”(平均 >5 秒返回)查询总数,与每个查询带回的数据量。确保您的查询没有做不必要的工作——请参阅上面关于 索引和架构 以及 Postgres 调优 的部分。
  3. 控制查询执行时间,通过指定查询执行前必须满足的附加条件,或者将查询设置为 manual,然后通过事件——通常是按钮点击——触发查询。
简洁的查询

在所有开发中,您应该力求简洁的代码——您的代码应以最有效的方式做必要的工作。这常常是 Slate 查询的一个缺点,它们花费大量时间重复相同的工作。考虑这个简单的查询,为用户选择类别填充下拉菜单:

Copied!
1 SELECT DISTINCT(COALESCE(category_col)) FROM "my_table"

在这段SQL代码中:

  • SELECT DISTINCT 用于选择唯一的值,确保返回的结果集中没有重复的行。
  • COALESCE(category_col) 函数用于返回 category_col 列中第一个非空值。若 category_col 的值为空,则 COALESCE 会尝试返回其他提供的参数中的第一个非空值(当前代码中没有提供其他参数)。
  • "my_table" 是查询数据的表名。

因此,这段代码会从 "my_table" 表中选择 category_col 列的所有非重复且非空的值。 这意味着在每次页面加载时,Postgres 都必须重复检查每一行中的 category_col 值是否为空,然后生成所有不同值的列表。使用变换和一个新的派生数据集,这项工作可以在上游添加新数据时一次性完成,然后查询就变得非常简单:

Copied!
1 2 SELECT * FROM "my_table_categories" -- 选择表 "my_table_categories" 中的所有列和行

即使您无法完全删除SQL表达式,例如需要适应用户输入的情况,也应努力将尽可能多的工作提取到上游变换中,然后构建更简单的查询。始终牢记“尽可能少次完成工作”这一原则,您将迈向更稳定和高效的应用。

对于第三部分,让我们更详细地看看两种常见模式。在您的应用中使用这些模式可以大大减少页面加载时运行的查询数量,并确保您的应用按预期运行。

条件查询

条件查询为查询执行提供了最细粒度的控制,可以与“事件触发”或“手动”配置(如下所述)结合使用,以添加查询执行必须满足的逻辑条件。通过勾选运行旁边下拉菜单中的此外...框来开启查询的执行条件。

在最简单的情况下,所有依赖项都不为空选项可防止查询在查询或其部分中的任何Handlebar表达式没有指定值时执行。这对于在有意义之前必须有一定用户输入的查询特别有用,并且防止了由于WHERE语句不完整而导致的查询不必要的数据库往返。

手动查询

控制查询运行时间的简单策略是将其设置为手动,使用运行按钮旁边下拉菜单中的复选框。

设置为手动运行的查询将在事件触发或手动指定的依赖项触发时执行。

对于应由用户操作触发的查询,这是正确的模式,但如果开始将太多查询“链接”在一起,可能导致不必要的复杂性——请参见上面事件部分中的注释。您仍然可以依赖依赖图来处理手动查询的所有下游节点——在查询从其触发事件运行后,任何下游依赖项将自动更新;无需通过基于q_queryName.success事件添加事件来增加复杂性。

JavaScript函数的最佳实践和常见模式

函数提供了根据用户选择和您的应用状态轻度操作数据或执行逻辑的能力。一些常被忽视的模式和特性可以使您与函数的工作更清晰和简单。

通过返回复杂对象避免重复工作

如果您查看验证用户输入的示例,您会注意到,验证函数返回的是一个JavaScript Object,而不是单个值。Slate可以解析这些对象,并通过Handlebars在应用的其他地方提供。

这意味着,例如如果您需要派生一些新的显示列以用于不同的微件,而不是为每一个创建一个函数,您可以构建一个创建所有所需显示值的函数,并以不同格式返回它们。我们将在下面更详细地查看此示例。

在将逻辑整合到较少的函数中(这有助于减少重复工作并将类似工作整合在一起)和将逻辑分开(这有助于封装离散操作并更容易找到所需代码)之间总是存在平衡。请牢记这种权衡,并在精炼应用时定期重构函数。

生成显示列

通常您会发现需要进行一些额外工作以整理数据以在图表中显示或生成HTML以在表格中显示。尽可能地,例如在格式化日期的情况下,您应该探索在数据变换和准备阶段进行此工作——这遵循尽可能少次完成工作的原则(即在一次变换中完成,而不是每次页面加载时为每个用户完成)。

如果无法做到这一点,您可以编写一个简单的函数来遍历列中的每个值并进行一些工作以生成新值,同时保留Slate期望的数据结构。

Copied!
1 2 3 4 5 6 7 8 `// f_deriveDisplayColumns var data = {{q_myQuery.results.[0]}} data.newColA = _.map(data.primaryKey, (value,key,index) => { // 计算新列的值 return newColValue }); return data;`

在这里,_.map 方法用于遍历 data.primaryKey,并对每个元素进行处理以生成新的列 newColA。具体的处理逻辑需要在 return newColValue 部分实现。 此代码片段使用了 Lodash ↗ 版本的 _.map()函数;在这种情况下,你也可以同样使用内置的 Array.map()。通常,熟悉 Lodash 的辅助函数是很有用的,因为它们在操作 JavaScript 对象时非常有帮助,并且通常实现了复杂的算法,这些算法如果从头实现可能容易出错。

在函数完成后,与其直接引用 {{q_myQuery}},你可以在应用程序中的任何地方引用 {{f_deriveDisplayColumns}},因为它与原始查询响应Object具有相同的结构,只是增加了额外的“列”。

使用自定义库重构代码

如果你发现需要实现一段相当复杂的JavaScript代码,考虑将其移动到函数编辑器左下角的自定义库中。库使纯JavaScript函数在任何Slate“函数”中可用,这样你可以避免多次重复实现相同的辅助函数。

由于库是纯JavaScript,你不能在其中使用 Handlebars;而是像正常的JavaScript一样,你可以在从Slate“函数”调用库函数时传递参数。这些参数可以使用 Handlebars 填充或动态生成。

为你的函数编写文档

在库代码和函数中,总是花时间干净地组织代码并为可能不直观的部分编写文档。你可以努力编写“自我记录”的代码,但养成干净且简洁地记录你的逻辑的习惯总是对未来的开发人员和维护人员友好的。