Understand dependencies(理解依赖关系)¶
Understanding "why things happen" in Slate¶
In Slate, all widgets, functions, variables, and queries are modeled as nodes in a graph. Each node evaluates to a JSON output and other nodes are templated to reference that output. These references define dependencies between nodes, which Slate uses to understand when to re-evaluate nodes when something changes (i.e. new user input; query executes; variable value updates).
A primary process of “developing” your Slate application will be the process of defining these nodes by adding widgets - for displaying outputs and capturing inputs; functions - for lightly manipulating data for display and handling application login; and queries - for making queries for data or connections to other Foundry service APIs, and then configuring their dependencies through Handlebar syntax.
Dependency graph¶
You can always view a representation of this dependency graph using the Dependencies tab, to understand how Slate interprets the relationships you've configured through your Handlebar references. This graph representation is your application - if it represents something that seems "wrong", then your starting point is to understand why rather than assume the graph is misrepresenting your application.
Whenever something unexpected happens in your application, start with the Dependencies tab to understand the upstream nodes from whatever widget is misbehaving. Look for unexpected relationships that may cause a node to re-evaluate based on an unrelated query or user input.
It is sometimes useful to think of the dependency graph as "lazy" - it avoids unnecessary work by only re-evaluating a node when the upstream references have changed value. This results in potentially confusing behavior when an upstream node updates to the same value as it previously was - downstream queries won't execute and downstream widgets won't re-render. This is commonly encountered in patterns that involve resetting user input - search your Foundry for the Intro to the Dependency Graph and Events Framework tutorial and the Resetting Widget Selections example to see this in action and how to work around it using Math.random() to inject entropy into the dependency graph evaluation and ensure nodes re-evaluate.
Events¶
With the Dependency Graph, Slate handles the complexity of determining when functions should recompute or when queries should rerun based on the state of their upstream dependencies. Most workflows can be configured using this functionality and it should always be the first impulse as a Slate developer to rely on the Dependency Graph wherever possible.
There are situations, however, where it makes sense to supplement the Dependency Graph with explicit trigger/action pairs, which Slate calls “Events”. Some simple cases are to trigger the display of a banner or toast when a query finishes, open a dialog when a user clicks something to view more details, or run a query from a button click after configuring a number of inputs.
Learn more about the Event framework and examples of common basic event patterns.
:::callout{theme="warning" title="Event madness"} It can be tempting, once you've realized the power of the Events framework, to see every development task in Slate through the lens of an event-based solution. This can lead to a common failure mode where a single app contains hundreds of event/action pairs. While this is technically possible, this complexity makes it difficult to step through the expected behavior of your application and can preclude even simple debugging.
As a rule of thumb, keep your event “chains” as short as possible and bias towards solutions that rely on the Dependency Graph rather than the Events. If you find your app growing to >50 events, especially if those events are doing more than just triggering toasts or initiating queries, it's a good time to pause and think carefully through your application architecture for opportunities to refactor and simplify your development or further decompose your application into sub-applications. :::
Custom event triggers¶
In addition to the built-in event types, you can create custom events in HTML and Table type widgets to allow different elements in these widgets to broadcast different events from a user click.
The configuration of these custom triggers takes 2 steps:
- Define the trigger in HTML element using the
slClickEventproperty:
<div class='pt-button' slClickEvent='delete'>Delete Me</div>
- Register the trigger in the widget configuration by adding the name to Click Event Names array.
Once the new event is registered, you'll see a new option wherever you can select an event trigger like myWidgetName.click.delete. Note that even if you dynamically generate the HTML to define the click events, the registration of those events still needs to be hard-coded so that Slate can keep track of the potential triggers, even if the conditions are such that you don't actually have that button generated at the moment.
You can use these events to build out custom button groups in HTML or combine them with event values, discussed below, to create lists or tables where each item has its own set of actions.
:::callout{title="Conditional events"}
If you need to add extra conditions (beyond the simple trigger firing) you can use the JavaScript portion of the Event configuration to execute additional logic. Evaluate your logic and if the event should not execute the action, return the special {{slDisableAction}} value. This value does not permanently disable the event, but rather interrupts that specific instance of the event action.
As a best practice, refrain from developing complex logic inside your Event JavaScript - there is no linting or error reporting so any mistakes or uncovered edge cases can cause your code to silently fail and lead to unexpected behavior. Instead, consider implementing an function that encapsulates the logic for event validity - then your event JavaScript can be as simple as:
unless ({{f_checkValidAction}})
return {{slDisableAction}}
You can, however, put debugger() statements in your Events and then use the Chrome Developer Tools to catch their execution and step through your logic.
:::
Passing event values¶
In addition to the slClickEvent property, you can define a slClickEventValue for a given HTML element. Whenever this event is triggered, you can reference the associated value in your event JavaScript using the special {{slEventValue}} variable.
This opens up a range of patterns for dynamic interactions:
- Create a function that generates the HTML for your Text or Table widget and defines the
slClickEventandslClickEventValuefor each element. - Create the necessary events and use those events to set the value of one or more single variables to the value from the event.
- Trigger some further action - most commonly a query - off the
variable.changedevent trigger and reference the value of the variable.
For instance, you can use this pattern to add a small 'x' next to rows in a table in an element where slClickEvent is 'delete' and the slClickEventValue is the row primary key. You can then set the value of a variable called v_idToDelete to the event value when myTable.click.delete event fires. Then run a q_deleteRow query based on v_idToDelete.changed event to complete this little chain.
Be wary of making chains much longer or more complex than this - see the caution on circular dependencies below - and favor short, distinct events rather than large, nested, or otherwise convoluted arrangements as these become difficult to reason about and debug and often lead to unexpected behavior.
:::callout{title="Events and circular dependencies"} The Slate dependency graph is technically a 'Directed Acyclic Graph' (DAG), which means that the node relationships are directed (meaning that there is a hierarchy from root nodes to leaf nodes and node resolution happens in that direction) and that the entire graph must be acyclic, meaning Slate will attempt to prevent you from configuring any “loops” - no matter how long - in your graph of dependencies.
However, using Events and setting variable values or triggering queries, it's possible to add a loop to your application, which will result in non-deterministic behavior. If you have a workflow that seems to necessitate a circular dependency, take a step back and consider alternative patterns - there's almost always a way to achieve the desired workflow functionality. :::
Managing query execution¶
One implication of the dependency graph framework, is that by default the entire dependency graph will resolve on page load. This means that every query and function will run is the necessary order for each node in the graph to resolve. In practice, this often means that if you haven't been paying attention during development, you'll slowly notice your page load performance decrease until you start getting intermittent timeouts. The root cause is almost always many queries that are “root” nodes in that they have no upstream query dependencies - this means all of these queries will attempt to run simultaneously. This can lead to queuing and load shedding from so many simultaneous browser connections, so many simultaneous queries to the same table(s), and so much network traffic returning to the application. The end state non-deterministic query failures and an application that feels sluggish, if not entirely broken, on page load.
:::callout{title="Connection pools and queueing with Postgate"} In Postgate, each user of your application has a connection pool specifically created for them (or restored from a cache if they already used the application recently). Each connection pool is limited to 10 connections at once, which prevents any one application instance from overloading Postgate, as it's shared by all users of your application and by any other applications using data that's been synced. Performance wise, this limit actually results in better performance that allowing all the queries through at once (if there are more than 10) as Postgres is faster globally the fewer simultaneous queries it's responding to.
Once all 10 connections are used up, the remaining query requests queue up, each one taking a connection once it becomes available. However, rather than allowing a connection to wait forever, to connection pool will time out on queries that have been waiting for a connection for five seconds. This means if you have a number of queries that take several seconds saturating the pool, queries that may have been fast - for instance, when you use the Test button to run it in isolation, may take a long time or even be timed out. :::
The solution here has several parts:
- Develop your application workflow so that queries are spaced out and depend on user input. A common pattern is to run a few small queries against pre-aggregated or DISTINCT tables to populate a set of filters, but “force” the user to make some selections and click a Submit button to run queries that actually fetch the data.
- Develop queries cautiously with an eye towards balancing the total number of queries - especially the total number of “heavy” (average >5s to return) that run on page load, with the amount of data brought back from each query. Make sure your queries are not doing unnecessary work - see the sections on Indexes and Schema and Postgres Tuning above.
- Control when your queries execute by either specifying additional criteria that must be met before the query executes or by setting the query to
manualand then triggering the query with an event - commonly a button click.
:::callout{title="Parsimonious queries"} In all development, you should strive towards parsimonious code - your code should do just as much work is necessary in the most efficient way possible. This is frequently a downfall of Slate queries, which spend lots of time doing the same work over and over. Consider this simple query, to populate a dropdown for a user to select a category:
SELECT DISTINCT(COALESCE(category_col)) FROM "my_table"
This means that on every page load Postgres has to repeat the work of looking at the category_col value in every row to see if it is null and then generate a list of all the distinct values. Using a transform and a new derived dataset, this work could be done once whenever there is new data added upstream and then the query is simply:
SELECT * FROM "my_table_categories"
Even if you can't fully remove SQL expressions, for instance where you need to accommodate users input, you should strive to factor out as much work as possible into upstream transformations and then build simpler queries. Always keep in mind this principal of “doing work” as few times as possible, and you will be on your way to more stable and performant applications. :::
For the third part, let's look at the two common patterns in more detail. Using these in your application can greatly reduce the number of queries running on page load and ensure that your application behaves as expected.
Conditional queries¶
Conditional Queries provide the most fine-grained control for query execution and can be combined with "Event triggered" or "Manual" configuration (described below) to add logical conditions that must be met for a query to execute. Turn on execution conditions for a query by checking the In addition... box under the dropdown next to Run.
In the simplest case the All dependencies are not null option prevents the query from executing if any of the handlebar expressions in the query or its partials do not have specified values. This is particularly useful for queries that must have some amount of user input before being meaningful and prevents unnecessary round-trips to the database for queries that will fail because the WHERE statement is incomplete.
Manual queries¶
A simple strategy for controlling when your queries run is to set them to manual, using the checkbox in the dropdown next to the Run button.
A query set to run manually will only execute when it's triggered by an event or a manually-specified dependency.
This is the correct pattern for queries that should be triggered by a user action, but can lead to unnecessary complexity if you start “chaining” together too many - see the note in the Events section above. You can still rely on the dependency graph to take care of all the nodes downstream of your manual query - after the query runs from its trigger, any downstream dependencies will update automatically; no need to add complexity by adding events based on the q_queryName.success event.
Best practices and common patterns for JavaScript functions¶
Functions provide the ability to lightly manipulate data or perform logic based on user selection and your application state. A couple of often overlooked patterns and features can make your work with functions cleaner and simpler.
Avoid duplicate work by returning complex objects¶
If you look at the example of Validating User Input, you'll notice that rather than returning a single value, the validation function returns a JavaScript Object. Slate can parse these objects and makes them available through Handlebars anywhere else in your application.
This means that if, for instance you need to derive a few new display columns to use in different widget, instead of making a function for each one, you can build a single function that creates all the display values needed in different formats, and return them all in an object. We'll take a closer look at this example below.
There's always a balance between consolidating logic into fewer functions, which can help reduce duplicated work and keep similar work consolidated, and breaking logic apart, which can help encapsulate discrete operations and make it easier to find just the necessary code. Keep this trade off in mind and periodically refactor your functions as you refine your application.
Generating display columns¶
Often you'll find that you need to do a little extra work to shape your data for display in a chart or generate some HTML for display in a table. Where possible, for example in the case of formatting dates, you should explore doing this work as part of the data transformation and preparation phase - this follows the principle of doing work as few times as possible (i.e. once in a transform rather than on every page load for every user).
Where that's not possible, you can write a simple function to map through each value in a column and do some work to generate a new value, all while preserving the data structure that Slate expects.
`// f_deriveDisplayColumns
var data = {{q_myQuery.results.[0]}}
data.newColA = _.map(data.primaryKey, (value,key,index) => {
// Work to derive new column
return newColValue
});
return data;`
This snippet uses the Lodash ↗ version of the _.map()function; you could equally use the build in Array.map() for this case. It is generally useful to familiarize yourself with Lodash helper functions as they are tremendously helpful in manipulating JavaScript objects and often implement complex algorithms that could be bug-prone to re-implement from scratch.
With the function complete, rather than referencing {{q_myQuery}} directly, you can instead reference {{f_deriveDisplayColumns}} anywhere in your app, since it has the same structure as the original query response object, just with additional “columns” added in.
Factoring code with custom libraries¶
If you do find that you need to implement a rather complex piece of JavaScript, consider moving it to a custom library at the bottom left of the function editor. Libraries make pure JavaScript functions available in any Slate “function”, so you can avoid repeatedly implementing the same helper functions multiple times.
Since Libraries are pure JavaScript, you can't use Handlebars inside them; rather like normal JavaScript you can pass parameters into the library functions when they are called from a Slate “function”. These parameters can then be filled with Handlebars or otherwise dynamically generated.
Documenting your functions¶
In both your Library code and your functions, always take the time to organize your code cleanly and document the potentially unintuitive sections. You can always strive to write “self-documenting” code, but it is always kind to future developers and maintainers to be in the habit of cleanly and succinctly documenting your logic.
中文翻译¶
理解依赖关系¶
理解 Slate 中"事件发生的原因"¶
在 Slate 中,所有组件(widget)、函数(function)、变量(variable)和查询(query)都被建模为图中的节点(node)。每个节点求值(evaluate)为 JSON 输出,其他节点则通过模板(templated)来引用(reference)该输出。这些引用定义了节点之间的依赖关系(dependencies),Slate 利用这些依赖关系来判断当某些内容发生变化时(例如新的用户输入、查询执行、变量值更新),何时需要重新求值(re-evaluate)节点。
"开发" Slate 应用程序的主要过程就是定义这些节点的过程:通过添加组件来显示输出和捕获输入;添加函数来对数据进行轻量处理以便显示和处理应用程序登录;添加查询来查询数据或连接到其他 Foundry 服务 API,然后通过 Handlebar 语法配置它们的依赖关系。
依赖关系图¶
您始终可以使用依赖关系选项卡查看此依赖关系图的表示形式,以了解 Slate 如何解释您通过 Handlebar 引用配置的关系。此图表示就是您的应用程序——如果它表示的某些内容看起来"错误",那么您的起点应该是理解为什么,而不是假设该图错误地表示了您的应用程序。
每当您的应用程序中出现意外情况时,请从"依赖关系"选项卡开始,从行为异常的组件出发,了解其上游节点。查找可能导致节点基于无关的查询或用户输入而重新求值的意外关系。
有时将依赖关系图视为"惰性"的会很有帮助——它只在上游引用的值发生变化时才重新求值节点,从而避免不必要的工作。这可能导致令人困惑的行为:当上游节点更新为与之前相同的值时,下游查询不会执行,下游组件也不会重新渲染。这在涉及重置用户输入的模式中经常遇到——请在 Foundry 中搜索 依赖关系图和事件框架入门 教程以及 重置组件选择 示例,了解实际效果以及如何使用 Math.random() 向依赖关系图求值中注入熵(entropy)以确保节点重新求值。
事件¶
借助依赖关系图,Slate 能够根据上游依赖关系的状态,自动处理确定函数何时应重新计算或查询何时应重新运行的复杂性。大多数工作流都可以使用此功能进行配置,并且作为 Slate 开发者,始终应首先考虑尽可能依赖依赖关系图。
然而,在某些情况下,使用显式的触发/动作对来补充依赖关系图是合理的,Slate 将这些称为"事件"。一些简单的用例包括:在查询完成时触发显示横幅或提示信息(toast);当用户点击某内容以查看更多详细信息时打开对话框;或在配置多个输入后通过按钮点击运行查询。
:::callout{theme="warning" title="事件滥用"} 一旦意识到事件框架的强大功能,很容易倾向于通过基于事件的解决方案来看待 Slate 中的每个开发任务。这可能导致一种常见的失败模式:单个应用程序包含数百个事件/动作对。虽然这在技术上是可行的,但这种复杂性使得难以逐步理解应用程序的预期行为,甚至可能妨碍简单的调试。
作为经验法则,请保持事件"链"尽可能短,并优先选择依赖依赖关系图而非事件的解决方案。如果您发现应用程序中的事件数量超过 50 个,尤其是当这些事件不仅仅用于触发提示信息或启动查询时,那么是时候暂停一下,仔细思考您的应用程序架构,寻找重构和简化开发的机会,或者进一步将应用程序分解为子应用程序。 :::
自定义事件触发器¶
除了内置的事件类型之外,您还可以在 HTML 和 表格 类型组件中创建自定义事件,以允许这些组件中的不同元素通过用户点击广播不同的事件。
配置这些自定义触发器需要两个步骤:
- 使用
slClickEvent属性在 HTML 元素中定义触发器:
<div class='pt-button' slClickEvent='delete'>删除我</div>
- 通过将名称添加到 点击事件名称 数组中,在组件配置中注册该触发器。
一旦新事件注册完成,您将在任何可以选择事件触发器的地方看到一个新的选项,例如 myWidgetName.click.delete。请注意,即使您动态生成 HTML 来定义点击事件,这些事件的注册仍然需要硬编码,以便 Slate 能够跟踪潜在的触发器,即使当前条件导致该按钮实际上并未生成。
您可以使用这些事件在 HTML 中构建自定义按钮组,或者将它们与下面讨论的事件值(event values)结合使用,以创建列表或表格,其中每个项目都有自己的一组操作。
:::callout{title="条件事件"}
如果您需要添加额外的条件(超出简单的触发器触发),您可以使用事件配置的 JavaScript 部分来执行额外的逻辑。评估您的逻辑,如果事件不应执行该动作,则返回特殊的 {{slDisableAction}} 值。此值不会永久禁用该事件,而是中断该事件动作的特定实例。
作为最佳实践,请避免在事件 JavaScript 内部开发复杂的逻辑——因为没有代码检查或错误报告,任何错误或未覆盖的边缘情况都可能导致代码静默失败并导致意外行为。相反,考虑实现一个封装事件有效性逻辑的函数——这样您的事件 JavaScript 可以简单如下:
unless ({{f_checkValidAction}})
return {{slDisableAction}}
但是,您可以在事件中放置 debugger() 语句,然后使用 Chrome 开发者工具来捕获其执行并逐步调试您的逻辑。
:::
传递事件值¶
除了 slClickEvent 属性之外,您还可以为给定的 HTML 元素定义 slClickEventValue。每当此事件被触发时,您可以在事件 JavaScript 中使用特殊的 {{slEventValue}} 变量来引用关联的值。
这为动态交互开辟了一系列模式:
- 创建一个函数,为您的文本或表格组件生成 HTML,并为每个元素定义
slClickEvent和slClickEventValue。 - 创建必要的事件,并使用这些事件将一个或多个单一变量的值设置为来自事件的值。
- 基于
variable.changed事件触发器触发进一步的操作——最常见的是查询——并引用该变量的值。
例如,您可以使用此模式在表格行的旁边添加一个小型 'x',其中 slClickEvent 为 'delete',slClickEventValue 为该行的主键。然后,当 myTable.click.delete 事件触发时,将名为 v_idToDelete 的变量的值设置为该事件值。接着,基于 v_idToDelete.changed 事件运行 q_deleteRow 查询,以完成这个小链条。
请注意避免使链条比这更长或更复杂——请参阅下面关于循环依赖的警告——并倾向于使用简短、明确的事件,而不是大型、嵌套或以其他方式复杂化的安排,因为这些安排难以推理和调试,并且常常导致意外行为。
:::callout{title="事件与循环依赖"} Slate 依赖关系图在技术上是一个"有向无环图"(DAG),这意味着节点关系是有方向的(即存在从根节点到叶节点的层次结构,节点解析沿此方向进行),并且整个图必须是无环的,这意味着 Slate 将尝试阻止您在依赖关系图中配置任何"循环"——无论多长。
然而,通过使用事件并设置变量值或触发查询,可能会为您的应用程序添加循环,这将导致非确定性行为。如果您的工作流似乎需要循环依赖,请退一步考虑替代模式——几乎总是有方法可以实现所需的工作流功能。 :::
管理查询执行¶
依赖关系图框架的一个含义是,默认情况下,整个依赖关系图将在页面加载时解析。这意味着每个查询和函数都将按必要的顺序运行,以便图中的每个节点都能解析。在实践中,这通常意味着如果您在开发过程中没有注意,您会慢慢注意到页面加载性能下降,直到开始出现间歇性超时。根本原因通常是许多查询是"根"节点,即它们没有上游查询依赖——这意味着所有这些查询将尝试同时运行。这可能导致来自如此多同时浏览器连接的排队和负载丢弃,对同一张或多张表的如此多同时查询,以及返回应用程序的如此多网络流量。最终状态是查询的非确定性失败,以及应用程序在页面加载时感觉迟钝,甚至完全无法使用。
:::callout{title="Postgate 中的连接池与排队"} 在 Postgate 中,应用程序的每个用户都有一个专门为他们创建的连接池(如果用户最近使用过该应用程序,则从缓存中恢复)。每个连接池一次最多限制 10 个连接,这防止了任何单个应用程序实例使 Postgate 过载,因为 Postgate 由您的应用程序的所有用户以及任何其他使用已同步数据的应用程序共享。从性能角度来看,此限制实际上比允许所有查询同时通过(如果超过 10 个)能带来更好的性能,因为 Postgres 响应的同时查询越少,全局性能越好。
一旦所有 10 个连接都被使用,剩余的查询请求就会排队,每个请求在连接可用时获取一个连接。然而,连接池不会让连接无限期等待,对于等待连接超过五秒的查询,连接池会将其超时。这意味着如果您有一些耗时数秒的查询占满了连接池,那些本来可能很快的查询——例如,当您使用测试按钮在隔离环境中运行时——可能会花费很长时间甚至超时。 :::
解决方案包含几个部分:
- 开发您的应用程序工作流,使查询分散开并依赖于用户输入。一种常见模式是针对预聚合或 DISTINCT 表运行几个小查询来填充一组筛选器,但"强制"用户进行一些选择并点击提交按钮来运行实际获取数据的查询。
- 谨慎开发查询,注意平衡查询总数——尤其是在页面加载时运行的"重型"(平均返回时间 >5 秒)查询总数——与每个查询返回的数据量。确保您的查询没有执行不必要的工作——请参阅上面的索引与模式和Postgres 调优部分。
- 通过指定查询执行前必须满足的额外条件,或将查询设置为
manual(手动)并通过事件(通常是按钮点击)触发查询,来控制查询的执行时机。
:::callout{title="精简查询"} 在所有开发中,您都应追求精简的代码——您的代码应以最高效的方式完成恰好必要的工作。这通常是 Slate 查询的弱点,它们花费大量时间重复执行相同的工作。考虑这个简单的查询,用于填充下拉菜单供用户选择类别:
SELECT DISTINCT(COALESCE(category_col)) FROM "my_table"
这意味着在每次页面加载时,Postgres 都必须重复检查每一行中的 category_col 值以查看其是否为空,然后生成所有不同值的列表。使用转换和新的派生数据集,这项工作可以在上游添加新数据时一次性完成,然后查询简化为:
SELECT * FROM "my_table_categories"
即使您无法完全移除 SQL 表达式,例如在需要适应用户输入的情况下,您也应努力将尽可能多的工作分解到上游转换中,然后构建更简单的查询。始终牢记"尽可能少地执行工作"这一原则,您将朝着构建更稳定、性能更高的应用程序迈进。 :::
对于第三部分,让我们更详细地了解两种常见模式。在您的应用程序中使用这些模式可以大大减少页面加载时运行的查询数量,并确保您的应用程序按预期运行。
条件查询¶
条件查询为查询执行提供了最精细的控制,并且可以与"事件触发"或"手动"配置(如下所述)结合使用,以添加查询执行必须满足的逻辑条件。通过选中 运行 旁边下拉菜单下的 此外... 复选框来启用查询的执行条件。
在最简单的情况下,所有依赖项均不为空 选项可防止查询在其查询或其部分中的任何 handlebar 表达式没有指定值时执行。这对于那些在获得一定用户输入之前没有意义的查询特别有用,并且可以防止因 WHERE 语句不完整而导致查询失败的不必要的数据库往返。
手动查询¶
控制查询执行时机的一个简单策略是使用 运行 按钮旁边下拉菜单中的复选框,将它们设置为 manual(手动)。
设置为手动运行的查询仅在由事件或手动指定的依赖项触发时才会执行。
这是应由用户操作触发的查询的正确模式,但如果您开始将太多查询"链接"在一起,可能会导致不必要的复杂性——请参阅上面事件部分的说明。您仍然可以依赖依赖关系图来处理手动查询下游的所有节点——在查询从其触发器运行后,任何下游依赖项都会自动更新;无需通过基于 q_queryName.success 事件添加事件来增加复杂性。
JavaScript 函数的最佳实践与常见模式¶
函数提供了根据用户选择和应用程序状态对数据进行轻量处理或执行逻辑的能力。一些经常被忽视的模式和功能可以使您使用函数的工作更清晰、更简单。
通过返回复杂对象避免重复工作¶
如果您查看验证用户输入的示例,您会注意到验证函数返回的不是单个值,而是一个JavaScript 对象。Slate 可以解析这些对象,并通过 Handlebars 在应用程序中的任何其他地方使用它们。
这意味着,例如,如果您需要派生几个新的显示列以在不同的组件中使用,您不必为每个列创建一个函数,而是可以构建一个单一函数,该函数以不同格式创建所有需要的显示值,并将它们全部返回到一个对象中。我们将在下面更详细地查看此示例。
在将逻辑整合到更少的函数中(这有助于减少重复工作并保持相似工作集中)和将逻辑分解开(这有助于封装离散操作并更容易找到必要的代码)之间总是需要权衡。请记住这种权衡,并在完善应用程序时定期重构您的函数。
生成显示列¶
您经常会发现需要做一些额外的工作来格式化数据以便在图表中显示,或生成一些 HTML 以便在表格中显示。在可能的情况下,例如格式化日期,您应该探索将此工作作为数据转换和准备阶段的一部分来完成——这遵循了尽可能少地执行工作的原则(即在转换中执行一次,而不是在每个用户的每次页面加载时都执行)。
在无法做到这一点的情况下,您可以编写一个简单的函数来映射列中的每个值并执行一些工作以生成新值,同时保留 Slate 期望的数据结构。
`// f_deriveDisplayColumns
var data = {{q_myQuery.results.[0]}}
data.newColA = _.map(data.primaryKey, (value,key,index) => {
// 派生新列的工作
return newColValue
});
return data;`
此代码片段使用了 Lodash ↗ 版本的 _.map() 函数;您也可以在此情况下使用内置的 Array.map()。熟悉 Lodash 辅助函数通常很有用,因为它们在操作 JavaScript 对象方面非常有用,并且通常实现了复杂的算法,从头重新实现这些算法容易出错。
函数完成后,您无需直接引用 {{q_myQuery}},而是可以在应用程序中的任何位置引用 {{f_deriveDisplayColumns}},因为它具有与原始查询响应对象相同的结构,只是添加了额外的"列"。
使用自定义库分解代码¶
如果您确实需要实现相当复杂的 JavaScript 代码,请考虑将其移动到函数编辑器左下角的自定义库中。库使纯 JavaScript 函数在任何 Slate "函数"中可用,因此您可以避免多次重复实现相同的辅助函数。
由于库是纯 JavaScript,您不能在它们内部使用 Handlebars;相反,像普通 JavaScript 一样,您可以在从 Slate "函数"调用库函数时将参数传递给它。然后,这些参数可以使用 Handlebars 填充或以其他方式动态生成。
记录您的函数¶
在您的库代码和函数中,始终花时间清晰地组织代码并记录可能不直观的部分。您始终可以努力编写"自文档化"的代码,但养成清晰简洁地记录逻辑的习惯,对于未来的开发者和维护者来说总是一种善意。