WordPressFlux 1.0.1-beta.1

WordPressFlux 1.0.1-beta.1

由以下人员维护:Jeremy MasselLorenzo MatteiYael RubinsteinOlivier HalligonGiovanni LodiAutomattic Mobile



WordPressFlux 1.0.1-beta.1

WordPressFlux - 一个受 Flux 启发的数据流架构

WordPressFlux 提供了构建类似 [Flux][1] 的数据流架构的工具。

此设计的主要目标是解耦数据获取、修改以及谁对数据感兴趣的方式。

目录

如何工作

Flux diagram

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类实例的期望是短暂的,视图控制器将实例化一个,执行请求并销毁它。这使得使用变得容易,但是这意味着没有共享状态,并且没有阻止重复或冲突请求的东西。

从网络加载数据的最常见场景将是

  1. 实例化并显示视图控制器
  2. 从其 viewDidLoad 方法开始,它会实例化一个 Service 并调用它的 sync 方法。
  3. Service 实例化一个 Remote 并从网络上请求数据。
  4. 当数据到达时,Service 会将结果与 Core Data 中现有的数据合并。
  5. 视图控制器中的 NSFetchedResultsController 注意到管理对象已更改,并要求视图控制器进行更新。

这通常可以工作,但是有几个例外。NSFetchedResultsControllerUITableView 合作得很好,但没有用于观察单个实体的等价方案。此外,无法防止重复请求数据:每次您从列表中导航离开并返回时,都会重新请求数据。

此外,虽然结果控制器提供了对实体列表的更新,但如果没有预先发起请求,就没有很好的方法将错误之类的其他状态变化传达给相关利益方。

未来想法

有一些事情在初始实现中被忽略了。其中一些是 Swift 当前限制的后果,而另一些则需要在使用之前先进行一些实际的应用。

等价的状态变化StatefulStore 提供了一个次要的更改分配器,当调用 onStateChange(_:) 时,它将包括旧的和新的状态。理想的情况是,这只有在旧的和新的是实际不同的情况下才会调用。如果 State 符合 Equatable,这是可以做到的,但实现它需要大量的样板代码,而且我们还不知道是否需要这种优化。Swift 4.1 将包括 Equatable/Hashable 合规性合成,这将使这变得更加容易。

具有选择器的查询。目前,一个特定的 Query 由应用程序定义,没有类似于 Action 的协议。有一个想法是要有一个查询定义 selector: (State) -> Result。这样,存储库就可以将任何状态变化映射到查询结果,并且只有在结果改变时才通知查询观察者。这是一个有趣的功能,但在通用方式实现时存在一些问题。主要问题是存储异构查询与不同的 Result 类型。这还没有被排除在外,但我们需要更多地使用它,并决定这种优化是否值得。