WordPressFlux - 一个受 Flux 启发的数据流架构
WordPressFlux 提供了构建类似 [Flux][1] 的数据流架构的工具。
此设计的主要目标是解耦数据获取、修改以及谁对数据感兴趣的方式。
目录
如何工作
Store 是存储特定领域(帖子、评论、插件等)数据和业务逻辑的地方,只有 Store 在响应 Actions 时才允许修改数据。Store 总是决定何时启动网络请求,无论是 Action 修改数据还是基于活动的 Queries。
Dispatcher 是不同组件用于向其他组件广播消息的工具。这在精神上很像 (NS)NotificationCenter
,但它是强类型的,且是用 Swift 编写的。有一个全局的 ActionDispatcher
,由视图用来向 stores 发送动作。
查询是描述视图或其他组件从存储中获取数据子集的组件。存储将决定需要从网络上获取什么以满足活跃的查询。
使用
第一步是定义您的Store子类及其内部状态。我们将评论作为示例。
// Simplified comment model
struct Comment {
let id: Int
let parent: Int
let content: String
}
class CommentsStore: Store {
// Stored comments keyed by site
private var comments = [Int: [Comment]]()
// Fetching state per site
private var fetching = [Int: Bool]()
}
下一步是定义一些选择器来从存储中获取数据。对于我们的评论存储,我们需要为网站请求评论列表和特定评论。
extension CommentStore {
func getComments(siteID: Int) -> [Comment] {
return comments[siteID]
}
func getComment(id: Int, siteID: Int) -> Comment {
return comments[siteID].first(where: { $0.id == id })
}
}
然后我们需要定义可以在评论上执行的所有操作。
enum CommentAction: Action {
case delete(siteID: Int, id: Int)
case reply(siteID: Int, parent: Int, content: String)
}
extension CommentStore {
override func onDispatch(_ action: Action) {
guard let commentAction = action as? CommentAction else {
return
}
switch commentAction {
case .delete(let siteID, let id):
deleteComment(siteID: siteID, id: id)
case .reply(let siteID, let parent, let content):
replyToComment(siteID: siteID, parent: parent, content: content)
}
}
func deleteComment(siteID: Int, id: Int) {
guard let index = comments.index(where: { $0.id == id }) else {
return
}
comments.remove(at: index)
emitChange()
CommentsApi().deleteComment(id: id, siteID: siteID)
}
func replyToComment(siteID: Int, parent: Int, content: String) {
let comment = Comment(id: -1, parent: parent, content: Content)
comments.append(comment)
CommentsApi().postComment(comment: comment, success: {
// Update the stored comment once we have an ID
})
}
}
为了避免每次修改状态时都要调用emitChange()
,WordPressFlux提供了一个名为StatefulStore
的另一个类,它会为您完成这项工作,只要您将所有状态都保留在state
变量中。
struct CommentsStoreState {
// Stored comments keyed by site
var comments = [Int: [Comment]]()
// Fetching state per site
var fetching = [Int: Bool]()
}
class CommentsStore: StatefulStore<CommentsStoreState> {
init() {
super.init(initialState: CommentsStoreState())
}
}
StatefulStore
还提供了一个辅助工具,可以将多个状态更改组合到同一更改事件中。如果您想避免通知存储消费者部分更改,这很有用。例如,让我们实现从API获取和接收评论的方法。
extension CommentsStore {
func fetchComments(siteID: Int) {
state.fetching[siteID] = true
CommentsApi().getPlugins(
siteID: siteID,
success: { [weak self] (comments) in
self?.receiveComments(siteID: siteID, comments: comments)
},
failure: { (error) in
// Dispatch an error
})
}
func receiveComments(siteID: Int, comments: [Comment]) {
transaction { (state) in
state.plugins[siteID] = plugins
state.fetching[siteID] = false
}
}
}
最后,我们需要定义我们的查询,视图层可以使用这些查询来请求数据。为了管理查询,我们的存储需要继承自QueryStore
并实现一个queriesChanged()
方法。当此方法被调用时,存储需要评估activeQueries
列表和其state
,并决定是否需要从网络上请求任何数据。
// If there are several types of queries, an enum is usually a
// better fit, but the struct keeps the example code simpler.
struct CommentsQuery {
let siteID: Int
}
class CommentsStore: QueryStore<CommentsStoreState, CommentsQuery> {
init() {
super.init(initialState: CommentsStoreState())
}
override func queriesChanged() {
// This is a very naive approach that would cause too many
// network requests.
// The state should consider when the data was last fetched,
// and provide an option to override that when the user requests
// an explicit refresh.
activeQueries
.map({ $0.siteID })
.filter({ !state.fetching[$0] })
.forEach { (siteID) in
fetchComments(siteID: siteID)
}
}
}
然后,视图模型或视图控制器将在存储中运行查询并保留收据
class CommentListViewModel {
var queryReceipt: Receipt
init(siteID: Int, store: CommentsStore) {
queryReceipt = store.query(CommentsQuery(siteID: siteID))
}
}
这些使用示例旨在展示WordPressFlux的不同组件,并且在实际使用中过于简单。以下是他们不处理的一些示例
- 区分“尚未与服务器通信”和“已请求但无数据”。
- 错误处理。
- 持久性/缓存以及如何与Core Data集成。
- 如果有足够新鲜的数据,则避免请求数据。
我们来自哪里
在这之前,WordPress应用程序通过Service
类处理数据。一个Service
类将提供与特定实体相关的所有操作,例如同步或修改对象。当需要与网络通信时,服务将负责调用适当的Remote
类。
Service类实例的期望是短暂的,视图控制器将实例化一个,执行请求并销毁它。这使得使用变得容易,但是这意味着没有共享状态,并且没有阻止重复或冲突请求的东西。
从网络加载数据的最常见场景将是
- 实例化并显示视图控制器
- 从其
viewDidLoad
方法开始,它会实例化一个Service
并调用它的sync
方法。 Service
实例化一个Remote
并从网络上请求数据。- 当数据到达时,
Service
会将结果与 Core Data 中现有的数据合并。 - 视图控制器中的
NSFetchedResultsController
注意到管理对象已更改,并要求视图控制器进行更新。
这通常可以工作,但是有几个例外。NSFetchedResultsController
与 UITableView
合作得很好,但没有用于观察单个实体的等价方案。此外,无法防止重复请求数据:每次您从列表中导航离开并返回时,都会重新请求数据。
此外,虽然结果控制器提供了对实体列表的更新,但如果没有预先发起请求,就没有很好的方法将错误之类的其他状态变化传达给相关利益方。
未来想法
有一些事情在初始实现中被忽略了。其中一些是 Swift 当前限制的后果,而另一些则需要在使用之前先进行一些实际的应用。
等价的状态变化。StatefulStore
提供了一个次要的更改分配器,当调用 onStateChange(_:)
时,它将包括旧的和新的状态。理想的情况是,这只有在旧的和新的是实际不同的情况下才会调用。如果 State
符合 Equatable
,这是可以做到的,但实现它需要大量的样板代码,而且我们还不知道是否需要这种优化。Swift 4.1 将包括 Equatable/Hashable 合规性合成,这将使这变得更加容易。
具有选择器的查询。目前,一个特定的 Query
由应用程序定义,没有类似于 Action
的协议。有一个想法是要有一个查询定义 selector: (State) -> Result
。这样,存储库就可以将任何状态变化映射到查询结果,并且只有在结果改变时才通知查询观察者。这是一个有趣的功能,但在通用方式实现时存在一些问题。主要问题是存储异构查询与不同的 Result
类型。这还没有被排除在外,但我们需要更多地使用它,并决定这种优化是否值得。