Workflow Core¶
本页提供了 Workflow Core 的高级概述,它是工作流库核心的、与 UI 无关的 Swift 和 Kotlin 运行时。参见Workflow UI,了解配套的 Android 和 iOS 特有模块。
什么是工作流?¶
一个工作流定义了特定类型组件的可能状态和行为。一个工作流的整体状态包含两部分
任何时候,都可以要求工作流将其当前的 Props 和 State 转换为适合外部消费的Rendering(渲染)。Rendering 通常是一个简单的结构体,包含显示数据以及可以触发Workflow Actions(工作流行为)的事件处理函数——这些函数更新 State,并可能同时发出Output(输出)事件。
例如,一个运行简单游戏的工作流可能使用参与游戏的 Players
描述作为其 Props 进行配置,在被要求渲染时构建 GameScreen
结构体,并发出一个 GameOver
事件作为 Output,以表示已完成。
一个工作流 Rendering 通常在 iOS 或 Android 应用中充当视图模型,但这并非强制要求。再次强调,本页不包含任何关于如何驱动平台特定 UI 代码的细节。关于这方面的讨论,请参见Workflow UI。
注意
有 Android 开发背景的读者应注意“view model”的 v 和 m 是小写的——这个概念与 Jetpack 的 ViewModel
无关。
组合工作流¶
工作流以树形结构运行,其中一个根工作流声明其在特定状态下拥有任意数量的子节点,每个子节点又可以声明自己的子节点,依此类推。以这种方式组合工作流最常见的原因是,用小的工作流构建大的视图模型(Renderings)。
例如,考虑一个概览/详情分屏,就像一个邮件应用,左侧是邮件列表,右侧是选中邮件的正文。这可以建模为三个工作流组成的三元组:
InboxWorkflow
- 期望接收一个
List<MessageId>
作为其 Props - Rendering 是一个
InboxScreen
,一个包含从其 Props 派生的可显示信息的结构体,以及一个onMessageSelected()
函数 - 当调用
onMessageSelected()
时,会执行一个 WorkflowAction,它发出给定的MessageId
作为 Output - 没有私有 State
MessageWorkflow
- 需要一个
MessageId
Props 值来生成一个MessageScreen
Rendering - 没有私有 State,也不发出 Output
EmailBrowserWorkflow
- State 包含一个
List<MessageId>
和选中的MessageId
- Rendering 是一个
SplitScreen
视图模型,由其他两个工作流的 Renderings 组合而成 - 不接受 Props,也不发出 Output
当 EmailBrowserWorkflow
被要求提供其 Rendering 时,它会反过来向其两个子节点请求 Renderings。
- 它将其 state 中的
List<MessageId>
作为EmailInboxWorkflow
的 Props 提供,并返回接收到一个InBoxScreen
rendering。该InboxScreen
成为SplitScreen
Rendering 的左侧面板。 - 对于
SplitScreen
的右侧面板,浏览器工作流将当前选中的MessageId
作为输入提供给EmailMessageWorkflow
,以获取一个MessageScreen
rendering。
注意
注意,这两个子节点,即 EmailInboxWorkflow
和 EmailMessageWorkflow
,彼此互不了解,也不知道它们运行的上下文。
InboxScreen
rendering 包含一个 onMessageSelected(MessageId)
函数。当调用该函数时,EmailInboxWorkflow
会将一个 Action 函数放入队列,该函数将给定的 MessageId
作为 Output 发出。EmailBrowserWorkflow
接收到该 Output,并将另一个 Action 放入队列,该 Action 会相应地更新其 State 中的 selection: MessageId
。
每当这样的Workflow Action 级联触发时,根工作流会被要求提供一个新的 Rendering。就像之前一样,EmailBrowserWorkflow
将其 Renderings 的生成委托给它的两个子节点,这次将 selection
的新值作为更新后的 Props 提供给 MessageWorkflow
。
工作流为何以这种方式工作?¶
构建工作流是为了应对 Square 庞大的 Android 和 iOS 应用带来的组合和导航挑战。它让我们能够编写复杂、集中且经过良好测试的代码,封装了贯穿数百个独立屏幕的流程。如今,尽管有无数棵“树”,我们仍能看到并塑造整个“森林”。
我们在构建时牢记了两个核心设计原则
- 单向数据流是构建 UI 时保持理智的最佳方式
- 声明式编程是定义单向数据流的最佳方式
这实际上意味着什么?
单向数据流¶
网络上有大量关于单向数据流的信息,但它非常简单地意味着存在一条单一路径,数据沿着该路径从业务逻辑流向 UI,事件则从 UI 流向业务逻辑,它们总是且仅沿着这条路径单向流动。对于工作流而言,这还意味着 UI 是(几乎)无状态的,并且你应用中重要的状态是集中的,没有重复。
在实践中,这使得程序流程更容易理解,因为无论应用中何时发生什么,都消除了关于导致该事件的状态来自何处、哪些组件接收了哪些事件以及实际发生了哪些因果序列的问题。它使得单元测试更容易,因为状态和事件是明确的,并且始终位于同一位置并通过相同的 API 流动,因此单元测试主要只需要测试状态转换即可。
声明式 vs 命令式¶
传统上,大多数移动代码是“命令式”的——它包含关于如何构建和显示 UI 的指令。这些指令可以包含诸如循环之类的控制流。命令式代码通常是有状态的,状态通常分散在各处,并且倾向于关注实例和身份。阅读命令式代码时,你几乎必须运行一个解释器,并在脑中记住所有状态片段才能弄清楚它在做什么。
Web UI 传统上是声明式的——它描述要渲染什么,以及如何渲染的一些方面(风格),但没有说明如何实际绘制它。声明式代码通常比命令式代码更容易阅读。它描述了它产生什么,而不是如何生成它。声明式代码通常更关心纯值而不是实例身份。然而,由于计算机最终仍需要实际指令,声明式代码需要其他东西(通常是命令式的,可以是编译器或解释器)来实际执行操作。
工作流代码使用常规的 Kotlin 或 Swift 编写,它们都是命令式语言,但该库鼓励你以声明式和函数式的风格编写逻辑。该库为你管理状态和事件处理的连接,因此你唯一需要编写的代码就是对你的具体问题真正有意义的代码。
关于函数式编程的说明
Kotlin 和 Swift 并非严格意义上的函数式编程语言,但它们都具有允许你编写函数式风格代码的特性。函数式代码不鼓励副作用,并且通常比面向对象代码更容易测试。函数式和声明式编程相辅相成,工作流鼓励你编写此类代码。