跳到内容

Workflow Core

本页提供了 Workflow Core 的高级概述,它是工作流库核心的、与 UI 无关的 Swift 和 Kotlin 运行时。参见Workflow UI,了解配套的 Android 和 iOS 特有模块。

什么是工作流?

一个工作流定义了特定类型组件的可能状态和行为。一个工作流的整体状态包含两部分

任何时候,都可以要求工作流将其当前的 Props 和 State 转换为适合外部消费的Rendering(渲染)。Rendering 通常是一个简单的结构体,包含显示数据以及可以触发Workflow Actions(工作流行为)的事件处理函数——这些函数更新 State,并可能同时发出Output(输出)事件。

Workflow schematic showing State as a box with Props entering from the top, Output exiting from the top, and Rendering exiting from the left side, with events returning to the workflow via Rendering

例如,一个运行简单游戏的工作流可能使用参与游戏的 Players 描述作为其 Props 进行配置,在被要求渲染时构建 GameScreen 结构体,并发出一个 GameOver 事件作为 Output,以表示已完成。

Workflow schematic with State type GameState, Props type Players, Rendering type GameScreen, and Output type GameOver. The workflow is receiving an onClick() event.

一个工作流 Rendering 通常在 iOS 或 Android 应用中充当视图模型,但这并非强制要求。再次强调,本页不包含任何关于如何驱动平台特定 UI 代码的细节。关于这方面的讨论,请参见Workflow UI

注意

有 Android 开发背景的读者应注意“view model”的 vm 是小写的——这个概念与 Jetpack 的 ViewModel 无关。

组合工作流

工作流以树形结构运行,其中一个根工作流声明其在特定状态下拥有任意数量的子节点,每个子节点又可以声明自己的子节点,依此类推。以这种方式组合工作流最常见的原因是,用小的工作流构建大的视图模型(Renderings)。

例如,考虑一个概览/详情分屏,就像一个邮件应用,左侧是邮件列表,右侧是选中邮件的正文。这可以建模为三个工作流组成的三元组:

InboxWorkflow

  • 期望接收一个 List<MessageId> 作为其 Props
  • Rendering 是一个 InboxScreen,一个包含从其 Props 派生的可显示信息的结构体,以及一个 onMessageSelected() 函数
  • 当调用 onMessageSelected() 时,会执行一个 WorkflowAction,它发出给定的 MessageId 作为 Output
  • 没有私有 State

Workflow schematic showing InboxWorkflow

MessageWorkflow

  • 需要一个 MessageId Props 值来生成一个 MessageScreen Rendering
  • 没有私有 State,也不发出 Output

Workflow schematic showing MessageWorkflow

EmailBrowserWorkflow

  • State 包含一个 List<MessageId> 和选中的 MessageId
  • Rendering 是一个 SplitScreen 视图模型,由其他两个工作流的 Renderings 组合而成
  • 不接受 Props,也不发出 Output

Workflow schematic showing MessageWorkflow

EmailBrowserWorkflow 被要求提供其 Rendering 时,它会反过来向其两个子节点请求 Renderings。

  • 它将其 state 中的 List<MessageId> 作为 EmailInboxWorkflow 的 Props 提供,并返回接收到一个 InBoxScreen rendering。该 InboxScreen 成为 SplitScreen Rendering 的左侧面板。
  • 对于 SplitScreen 的右侧面板,浏览器工作流将当前选中的 MessageId 作为输入提供给 EmailMessageWorkflow,以获取一个 MessageScreen rendering。

Workflow schematic showing a parent EmailBrowserWorkflow providing Props to its children, InboxWorkflow and MessageWorkflow, and assembling their renderings into a SplitScreen(InboxScreen, MessageScreen)

注意

注意,这两个子节点,即 EmailInboxWorkflowEmailMessageWorkflow,彼此互不了解,也不知道它们运行的上下文。

InboxScreen rendering 包含一个 onMessageSelected(MessageId) 函数。当调用该函数时,EmailInboxWorkflow 会将一个 Action 函数放入队列,该函数将给定的 MessageId 作为 Output 发出。EmailBrowserWorkflow 接收到该 Output,并将另一个 Action 放入队列,该 Action 会相应地更新其 State 中的 selection: MessageId

Workflow schematic showing EmailBrowserWorkflow rendering by delegating to two children, InboxWorkflow and MessageWorkflow, and assembling their renderings into its own.

每当这样的Workflow Action 级联触发时,根工作流会被要求提供一个新的 Rendering。就像之前一样,EmailBrowserWorkflow 将其 Renderings 的生成委托给它的两个子节点,这次将 selection 的新值作为更新后的 Props 提供给 MessageWorkflow

工作流为何以这种方式工作?

构建工作流是为了应对 Square 庞大的 Android 和 iOS 应用带来的组合和导航挑战。它让我们能够编写复杂、集中且经过良好测试的代码,封装了贯穿数百个独立屏幕的流程。如今,尽管有无数棵“树”,我们仍能看到并塑造整个“森林”。

我们在构建时牢记了两个核心设计原则

  • 单向数据流是构建 UI 时保持理智的最佳方式
  • 声明式编程是定义单向数据流的最佳方式

这实际上意味着什么?

单向数据流

网络上有大量关于单向数据流的信息,但它非常简单地意味着存在一条单一路径,数据沿着该路径业务逻辑流向 UI,事件则 UI 流向业务逻辑,它们总是且仅沿着这条路径单向流动。对于工作流而言,这还意味着 UI 是(几乎)无状态的,并且你应用中重要的状态是集中的,没有重复。

在实践中,这使得程序流程更容易理解,因为无论应用中何时发生什么,都消除了关于导致该事件的状态来自何处、哪些组件接收了哪些事件以及实际发生了哪些因果序列的问题。它使得单元测试更容易,因为状态和事件是明确的,并且始终位于同一位置并通过相同的 API 流动,因此单元测试主要只需要测试状态转换即可。

声明式 vs 命令式

传统上,大多数移动代码是“命令式”的——它包含关于如何构建和显示 UI 的指令。这些指令可以包含诸如循环之类的控制流。命令式代码通常是有状态的,状态通常分散在各处,并且倾向于关注实例和身份。阅读命令式代码时,你几乎必须运行一个解释器,并在脑中记住所有状态片段才能弄清楚它在做什么。

Web UI 传统上是声明式的——它描述要渲染什么,以及如何渲染的一些方面(风格),但没有说明如何实际绘制它。声明式代码通常比命令式代码更容易阅读。它描述了它产生什么,而不是如何生成它。声明式代码通常更关心纯值而不是实例身份。然而,由于计算机最终仍需要实际指令,声明式代码需要其他东西(通常是命令式的,可以是编译器或解释器)来实际执行操作。

工作流代码使用常规的 Kotlin 或 Swift 编写,它们都是命令式语言,但该库鼓励你以声明式和函数式的风格编写逻辑。该库为你管理状态和事件处理的连接,因此你唯一需要编写的代码就是对你的具体问题真正有意义的代码。

关于函数式编程的说明

Kotlin 和 Swift 并非严格意义上的函数式编程语言,但它们都具有允许你编写函数式风格代码的特性。函数式代码不鼓励副作用,并且通常比面向对象代码更容易测试。函数式和声明式编程相辅相成,工作流鼓励你编写此类代码。