测试已测试 | ✓ |
语言语言 | SwiftSwift |
许可证 | MIT |
发布上次发布 | 2017年1月 |
SwiftSwift版本 | 3.0 |
SPM支持SPM | ✗ |
由Vadym Markov维护。
Aftermath是一个基于Swift的无状态消息驱动的微框架,它基于单向数据流架构的概念。
乍一看,Aftermath可能只是发布-订阅消息模式的类型安全实现,但实际上它可以被认为是应用程序设计中的一种独特的思维模型,与熟悉的MVC、MVVM或MVP方法不同。通过利用事件溯源和Flux模式背后的想法,它有助于隔离关注点,减少代码依赖性,并使数据流动更加可预测。
以下图表详细说明了Aftermath架构中的数据流
命令是一个消息,包含了一系列描述执行相应行为意图的指令。命令可能导致数据检索、数据更改以及任何类型的同步或异步操作,这些操作会产生更新应用程序/视图状态的所需输出。
每个命令只能产生一个唯一的
命令处理器层负责应用程序中的业务逻辑。命令提交给命令处理器,它通常执行短期或长期操作,例如网络请求、数据库查询、缓存读/写处理等。命令处理器可以同步并立即发布结果。另一方面,这是应用程序中编写异步代码的最佳位置。
限制是针对每个命令只创建一个命令处理器。
命令处理器负责发布将由反应器消耗的事件。有三种类型的事件
progress
事件指示由命令触发的操作已开始,目前处于挂起状态。data
事件保存由命令执行产生的输出。error
事件表示命令执行过程中发生了错误。反应 响应命令处理器发布的事件。它应该通过在每个场景中描述所需行为来处理 3 种可能的事件类型
wait
函数响应事件的 progress
类型。consume
函数响应事件的 data
类型。rescue
函数是在接收到 error
事件时的回退选项。通常,反应执行 UI 更新,但也可以用于其他类型的输出处理。
通过对前面描述的 4 个核心组件,我们可以构建一个简化的数据流版本
第一步是声明一个命令。您的命令类型必须符合 Aftermath.Command
协议,而 Output
类型必须隐式指定。
假设我们想要从某个不可信的资源中获取书籍列表,并更正标题和作者姓名中的拼写错误
// This is our model we are going to work with.
struct Book {
let id: Int
let title: String
let author: String
}
struct BookListCommand: Command {
// Result of this command will be a list of books.
typealias Output = [Book]
}
struct BookUpdateCommand: Command {
// Result of this command will be an updated book.
typealias Output = Book
// Let's pass the entire model to the command to simplify this example.
// Ideally we wouldn't do that because a command is supposed to be as simple
// as possible, only with attributes that are needed for handler.
let book: Book
}
注意 任何类型都可以扮演 Output
的角色,因此如果我们想给我们的 BookUpdateCommand
添加日期,它可能看起来像以下这样
typealias Output = (Book, Date)
为了执行命令,您必须符合 CommandProducer
协议
class ViewController: UITableViewController, CommandProducer {
// Fetch a list of books.
func load() {
execute(command: BookListCommand())
}
// Update a single book with corrected title and/or author name.
func update(book: Book) {
execute(command: BookUpdateCommand(book: book))
}
}
命令是一种意图,需要由处理器将其转换为动作。命令处理器负责发布事件以通知该操作的执行结果。命令处理器类型必须符合 Aftermath.CommandHandler
协议,该协议需要了解它将要处理的命令类型
struct BookListCommandHandler: CommandHandler {
func handle(command: BookListCommand) throws -> Event<BooksCommand> {
// Start network request to fetch data.
fetchBooks { books, error in
if let error = error {
// Publish error.
self.publish(error: error)
return
}
// Publish fetched data.
self.publish(data: books)
}
// Load data from local database/cache.
let localBooks = loadLocalBooks()
// If the list is empty let the listeners know that operation is in the process.
return Book.list.isEmpty ? Event.progress : Event.data(localBooks)
}
}
注意 每个命令处理器都需要在 Aftermath Engine 上注册。
Engine.shared.use(handler: BookListCommandHandler())
最后一步但并非最不重要的是,响应命令处理器发布的事件。只需符合 ReactionProducer
协议,实现反应行为,然后您就可以开始了
class ViewController: UITableViewController, CommandProducer, ReactionProducer {
var books = [Book]()
deinit {
// Don't forget to dispose all reaction tokens.
disposeAll()
}
override func viewDidLoad() {
super.viewDidLoad()
// React to events.
react(to: BookListCommand.self, with: Reaction(
wait: { [weak self] in
// Wait for results to come.
self?.refreshControl?.beginRefreshing()
},
consume: { [weak self] books in
// We're lucky, there are some books to display.
self?.books = books
self?.refreshControl?.endRefreshing()
self?.tableView.reloadData()
},
rescue: { [weak self] error in
// Well, seems like something went wrong.
self?.refreshControl?.endRefreshing()
print(error)
}))
}
// ...
}
重要的是 在 ReactionProducer
实例即将被删除或需要取消订阅事件时,销毁所有反应令牌。
// Disposes all reaction tokens for the current `ReactionProducer`.
disposeAll()
// Disposes a specified reaction token.
let token = react(to: BookListCommand.self, with: reaction)
dispose(token: token)
动作 是命令的一种变体,可以自行处理。这在命令本身或业务逻辑很小的时候可以简化代码。不需要注册动作,它将自动添加到正在运行的命令处理器列表中,当它作为一个命令执行时。
import Sugar
struct WelcomeAction: Action {
typealias Output = String
let userId: String
func handle(command: WelcomeAction) throws -> Event<WelcomeAction> {
fetchUser(id: userId) { user in
self.publish(data: "Hello \(user.name)")
}
return Event.progress
}
}
// Execute action
struct WelcomeManager: CommandProducer {
func salute() {
execute(action: WelcomeAction(userId: 11))
}
}
事实类似于通知,不涉及异步操作。当不需要处理程序生成输出时可以使用它。事实本身就是输出,所以你想要的只是通知所有订阅者系统中发生了什么,他们会相应地做出反应。从这个意义上讲,它更接近于Notification
类型安全的替代品。
struct LoginFact: Fact {
let username: String
}
class ProfileController: UIViewController, ReactionProducer {
override func viewDidLoad() {
super.viewDidLoad()
// React
next { (fact: LoginFact) in
title = fact.username
}
}
}
struct AuthService: FactProducer {
func login() {
let fact = LoginFact(username: "John Doe")
// Publish
post(fact: fact)
}
}
中间件是一个层,可以在命令和事件到达其听者之前拦截它们。
这意味着你可以在命令处理程序处理之前,在命令中间件中修改/取消/扩展要执行的命令
或者,你可以在发布的窃听者接收到事件之前在事件中间件中执行适当的操作。
它适用于日志记录、崩溃报告、终止特定命令或事件等。
// Command middleware
struct ErrorCommandMiddleware: CommandMiddleware {
func intercept(command: AnyCommand, execute: Execute, next: Execute) throws {
do {
// Don't forget to call `next` to invoke the next function in the chain.
try next(command)
} catch {
print("Command failed with error -> \(command)")
throw error
}
}
}
Engine.shared.pipeCommands(through: [ErrorCommandMiddleware()])
// Event middleware
struct LogEventMiddleware: EventMiddleware {
// Don't forget to call `next` to invoke the next function in the chain.
func intercept(event: AnyEvent, publish: Publish, next: Publish) throws {
print("Event published -> \(event)")
try next(event)
}
}
Engine.shared.pipeEvents(through: [LogEventMiddleware()])
注意在构建自定义中间件时,有必要调用next
以调用链中的下一个函数。
AnyCommand
和AnyEvent
是特殊协议,每个Command
或Event
都符合这些协议。它们主要用于middleware
中,以克服使用具有associatedtype
的Swift泛型协议的限制。
引擎是Aftermath配置的主入口点。
Engine.shared.use(handler: BookListCommandHandler())
// Commands
Engine.shared.pipeCommands(through: [LogCommandMiddleware(), ErrorCommandMiddleware()])
// Events
Engine.shared.pipeEvents(through: [LogEventMiddleware(), ErrorEventMiddleware()])
struct EngineErrorHandler: ErrorHandler {
func handleError(error: Error) {
if let error = error as? Failure {
print("Engine error -> \(error)")
} else if let warning = error as? Warning {
print("Engine warning -> \(warning)")
} else {
print("Unknown error -> \(error)")
}
}
}
Engine.shared.errorHandler = EngineErrorHandler()
Engine.shared.invalidate()
命名很困难。拥有像BookListCommand
、BookListCommandHandler
和BookListWhatever
这样的名字感觉不太对,对吧?如果你同意,那么你可以通过引入一个新想法来解决这个问题。你可以将所有相关类型组合成故事,使流程更加具体。
struct BookListStory {
struct Command: Aftermath.Command {
// ...
}
struct Handler: Aftermath.CommandHandler {
// ...
}
}
从这个意义上讲,它与在敏捷软件开发方法中使用的用户故事非常相似。
你可以在AftermathNotes演示项目中找到更多详细示例。
某些故事可能看起来非常相似。那么根据DRY原则使它们更具通用性和可重用性是有意义的。例如,假设我们有通过id获取单个资源的流程。
import Aftermath
import Malibu
// Generic feature
protocol DetailFeature {
associatedtype Model: Entity
var resource: String { get }
}
// Command
struct DetailCommand<Feature: DetailFeature>: Aftermath.Command {
typealias Output = Feature.Model
let id: Int
}
// Command handler
struct DetailCommandHandler<Feature: DetailFeature>: Aftermath.CommandHandler {
typealias Command = DetailCommand<Feature>
let feature: Feature
func handle(command: Command) throws -> Event<Command> {
fetchDetail("\(feature.resource)/\(command.id)") { json, error in
if let error = error {
self.publish(error: error)
return
}
do {
self.publish(data: try Feature.Model(json))
} catch {
self.publish(error: error)
}
}
return Event.progress
}
}
// Concrete feature
struct BookFeature: ListFeature, DeleteFeature, CommandProducer {
typealias Model = Todo
var resource = "books"
}
// Execute command to load a single resource.
execute(command: DetailCommand<BookFeature>(id: id))
// Register reaction listener.
react(to: DetailCommand<BookFeature>.self, with: reaction)
你可以在AftermathNotesPlus演示项目中找到更多详细示例。
我们认为,在iOS应用程序中,大多数情况下,没有真正需要单个全局状态(单一真相来源)或分布在各个商店之间的多个子状态。数据存储在本地持久化层,如数据库和缓存中,或者从网络上获取。然后,这个内容,从不同的来源组装成一个一个的部分,被转化为“视图状态”,它可以被视图读取以在屏幕上渲染。这个“视图状态”保持内存有效,直到我们切换上下文并且当前视图被释放。
记住这一点,与保留在任何形式的全局状态中不再使用的复制相比,丢弃属于视图的“视图状态”更有意义。
通过重新播放历史中的先前事件来恢复状态应该足以
Aftermath的优势
**Aftermath** 的缺点
备注 即使目前 Aftermath 是一个无状态框架,我们也有计划引入某种可选的存储以更好地管理状态。这可能将是 v2 中的新功能,请保持关注。
// Commands
Engine.sharedInstance.pipeCommands(through: [LogCommandMiddleware(), ErrorCommandMiddleware()])
// Events
Engine.sharedInstance.pipeEvents(through: [LogEventMiddleware(), ErrorEventMiddleware()])
Aftermath 通过 CocoaPods 提供。要安装它,只需将以下行添加到您的 Podfile 中
pod 'Aftermath'
Aftermath 也可以通过 Carthage 安装。为了安装,只需将其写入您的 Cartfile
github "hyperoslo/Aftermath"
Aftermath 也可以手动安装。只需下载并将在项目中放置 Sources
文件夹。
iOS Playground 利用交互式 Playgrounds 的实时视图来展示如何从网络获取数据并在 UITableView
中显示。
AftermathNotes 是一个简单应用,演示了如何使用 Aftermath 设置网络堆栈和缓存层。它使用 故事 概念将相关类型分组,使 命令 -> 事件
流更容易阅读。
AftermathNotesPlus 是一个更高级的示例,它扩展了 AftermathNotes 的演示。它玩弄泛型,并引入了 功能 概念,以重用视图控制器和 RESTful 网络请求。
此存储库旨在成为框架的核心实现,但也有一系列扩展,它们将 Aftermath 与其他库集成并增加更多功能
AftermathCompass 是一个基于 Aftermath 和 Compass 的消息驱动路由系统。
AftermathSpots 是为了改善使用 Spots 跨平台视图控制器框架构建组件化 UI 时的开发流程。它提供了自定义反应和可注入行为,将代码重用性提升到新的水平,并使您的应用更加解耦和灵活。
仍不确定如何管理状态?涵盖所有场景并找到所有场合的银弹并不容易。但如果您认为现在是时候打破常规并在您的下一个应用程序中尝试新的架构,这里有一些进一步的阅读和研究链接。
fantastic-ios-architecture - 一份与 iOS 架构主题相关的资源列表。
Hyper Interaktiv AS,[邮箱地址被隐藏]
后果 受益于Flux中单向数据流的概念,并利用了事件溯源中的命令序列和事件等一些概念。
我们非常欢迎您为 后果 贡献,有关更多信息,请查阅贡献指南文件。
后果 提供 MIT 许可。有关更多信息,请参阅许可证文件。