WordPress iOS GitHub

Mục tiêu thiết kế chính của việc này là tách rời cách thu thập dữ liệu, cách dữ liệu được sửa đổi và ai quan tâm đến dữ liệu

Mục lục

Làm thế nào nó hoạt động

WordPress iOS GitHub

Cửa hàng là nơi chứa dữ liệu và logic nghiệp vụ về một miền cụ thể (bài đăng, nhận xét, plugin,…) trong ứng dụng. Chỉ Cửa hàng mới được phép sửa đổi dữ liệu để đáp ứng với Hành động. Cửa hàng luôn quyết định khi nào bắt đầu yêu cầu mạng, khi một Hành động sửa đổi dữ liệu hoặc dựa trên các Truy vấn đang hoạt động

Bộ điều phối là thứ mà các thành phần khác nhau sử dụng để phát thông báo đến các thành phần khác. Điều này rất giống với tinh thần của

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 })
    }
}
4, nhưng được đánh máy và viết mạnh mẽ với Swift trong tâm trí. Có một
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 })
    }
}
5 toàn cầu được các chế độ xem sử dụng để gửi hành động đến các cửa hàng

Truy vấn là thành phần mô tả tập hợp con dữ liệu mà chế độ xem hoặc thành phần khác muốn từ cửa hàng. Cửa hàng sẽ quyết định những gì cần tìm nạp từ mạng để đáp ứng các truy vấn đang hoạt động

Cách sử dụng

Bước đầu tiên là xác định phân lớp Cửa hàng của bạn và trạng thái bên trong của nó. Chúng tôi sẽ sử dụng nhận xét làm ví dụ

// 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]()
}

Bước tiếp theo là xác định một số bộ chọn để lấy dữ liệu từ cửa hàng. Đối với kho lưu trữ nhận xét của chúng tôi, chúng tôi sẽ cần yêu cầu danh sách nhận xét cho một trang web và nhận xét cụ thể

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 })
    }
}

Sau đó, chúng ta cần xác định tất cả các hành động có thể được thực hiện trên các bình luận

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
        })
    }
}

Thay vì phải gọi

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 })
    }
}
6 mỗi khi bạn sửa đổi trạng thái, WordPressFlux cung cấp một lớp khác có tên
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 })
    }
}
0 sẽ làm điều đó cho bạn, miễn là bạn giữ tất cả trạng thái bên trong một biến
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 })
    }
}
1

struct CommentsStoreState {
    // Stored comments keyed by site
    var comments = [Int: [Comment]]()
    // Fetching state per site
    var fetching = [Int: Bool]()
}

class CommentsStore: StatefulStore {
    init() {
        super.init(initialState: CommentsStoreState())
    }
}

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 })
    }
}
0 cũng cung cấp một trình trợ giúp để nhóm một số thay đổi trạng thái vào cùng một sự kiện thay đổi. Điều này hữu ích nếu bạn muốn tránh thông báo cho người tiêu dùng cửa hàng về những thay đổi một phần. Chẳng hạn, hãy triển khai các phương thức để tìm nạp và nhận nhận xét từ 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
        }
    }
}

Cuối cùng, chúng ta cần xác định các truy vấn mà lớp Chế độ xem có thể sử dụng để yêu cầu dữ liệu. Để quản lý các truy vấn, cửa hàng của chúng tôi cần kế thừa từ

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 })
    }
}
3 và triển khai phương thức
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 })
    }
}
4. Khi phương thức này được gọi, cửa hàng cần đánh giá danh sách
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 })
    }
}
5 và
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 })
    }
}
1 của nó và quyết định xem có cần yêu cầu bất kỳ dữ liệu nào từ mạng không

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 })
    }
}
2

Mô hình xem hoặc Trình điều khiển xem sau đó sẽ chạy truy vấn trên cửa hàng và giữ biên nhận

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 })
    }
}
3

Các ví dụ sử dụng này nhằm giới thiệu các thành phần khác nhau từ WordPressFlux và quá đơn giản để sử dụng thực tế. Một số ví dụ về những điều họ không giải quyết

  • Phân biệt giữa “chưa nói chuyện với máy chủ” và “Tôi đã hỏi và không có dữ liệu”
  • xử lý lỗi
  • Persistence/caching và cách tích hợp với Core Data
  • Tránh các yêu cầu nếu chúng tôi có đủ dữ liệu mới

chúng ta đến từ đâu

Trước đó, ứng dụng WordPress đã xử lý dữ liệu thông qua lớp

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 })
    }
}
7. Một lớp
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 })
    }
}
7 sẽ cung cấp tất cả các hoạt động liên quan đến một thực thể cụ thể, như đồng bộ hóa hoặc sửa đổi các đối tượng. Các dịch vụ sẽ chịu trách nhiệm gọi các lớp
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 })
    }
}
9 thích hợp khi họ cần nói chuyện với mạng

Các phiên bản của lớp Dịch vụ có nghĩa là tồn tại trong thời gian ngắn và Trình điều khiển Chế độ xem sẽ khởi tạo một phiên bản, thực hiện một yêu cầu và loại bỏ nó. Điều này làm cho việc sử dụng trở nên dễ dàng, nhưng điều đó có nghĩa là không có trạng thái chia sẻ và không có gì để ngăn các yêu cầu trùng lặp hoặc xung đột

Kịch bản phổ biến nhất để tải các đối tượng từ mạng sẽ là

  1. Trình điều khiển xem được khởi tạo và trình bày
  2. Từ phương thức
    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
            })
        }
    }
    0 của nó, nó khởi tạo một
    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 })
        }
    }
    7 và gọi phương thức
    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
            })
        }
    }
    2 của nó
  3. 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 })
        }
    }
    7 khởi tạo một
    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 })
        }
    }
    9 và yêu cầu dữ liệu từ mạng
  4. Khi dữ liệu đến,
    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 })
        }
    }
    7 hợp nhất kết quả với dữ liệu hiện có trong Dữ liệu chính
  5. 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
            })
        }
    }
    6 của Trình điều khiển Chế độ xem thông báo các đối tượng được quản lý đã thay đổi và yêu cầu Trình điều khiển Chế độ xem cập nhật

Điều này thường hoạt động, ngoại trừ một số trường hợp.

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
        })
    }
}
6 hoạt động tốt với
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
        })
    }
}
8, nhưng không có cách nào tương đương để quan sát các thực thể đơn lẻ. Ngoài ra, không có gì để ngăn chặn các yêu cầu trùng lặp. mỗi khi bạn điều hướng khỏi danh sách và quay lại, dữ liệu sẽ được yêu cầu lại

Ngoài ra, trong khi bộ điều khiển kết quả cung cấp các bản cập nhật cho danh sách các thực thể, không có cách nào tốt để thông báo các thay đổi trạng thái khác như lỗi cho các bên quan tâm, trừ khi họ bắt đầu yêu cầu

Ý tưởng tương lai

Có một vài điều còn sót lại trong quá trình triển khai ban đầu. Một số là do những hạn chế hiện tại của Swift và một số khác yêu cầu một số cách sử dụng thực tế hơn trước khi chúng tôi có thể đánh giá mức độ hữu ích của chúng

Thay đổi trạng thái cân bằng.

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 })
    }
}
0 cung cấp một bộ điều phối thay đổi thứ cấp sẽ bao gồm các trạng thái cũ và mới khi gọi
struct CommentsStoreState {
    // Stored comments keyed by site
    var comments = [Int: [Comment]]()
    // Fetching state per site
    var fetching = [Int: Bool]()
}

class CommentsStore: StatefulStore {
    init() {
        super.init(initialState: CommentsStoreState())
    }
}
0. Kịch bản lý tưởng là điều này sẽ chỉ được gọi nếu trạng thái cũ và trạng thái mới thực sự khác nhau. Điều này có thể thực hiện được nếu
struct CommentsStoreState {
    // Stored comments keyed by site
    var comments = [Int: [Comment]]()
    // Fetching state per site
    var fetching = [Int: Bool]()
}

class CommentsStore: StatefulStore {
    init() {
        super.init(initialState: CommentsStoreState())
    }
}
1 phù hợp với
struct CommentsStoreState {
    // Stored comments keyed by site
    var comments = [Int: [Comment]]()
    // Fetching state per site
    var fetching = [Int: Bool]()
}

class CommentsStore: StatefulStore {
    init() {
        super.init(initialState: CommentsStoreState())
    }
}
2, nhưng việc triển khai điều đó có thể yêu cầu rất nhiều bản soạn sẵn và đó là một sự tối ưu hóa mà chúng tôi không biết liệu mình có cần hay không. Nhanh 4. 1 sẽ bao gồm tổng hợp tuân thủ Equatable/Hashable giúp việc này trở nên dễ dàng hơn nhiều

Truy vấn với bộ chọn. Hiện tại, một

struct CommentsStoreState {
    // Stored comments keyed by site
    var comments = [Int: [Comment]]()
    // Fetching state per site
    var fetching = [Int: Bool]()
}

class CommentsStore: StatefulStore {
    init() {
        super.init(initialState: CommentsStoreState())
    }
}
3 cụ thể được xác định bởi ứng dụng, thậm chí không có giao thức cho nó giống như chúng tôi có cho
struct CommentsStoreState {
    // Stored comments keyed by site
    var comments = [Int: [Comment]]()
    // Fetching state per site
    var fetching = [Int: Bool]()
}

class CommentsStore: StatefulStore {
    init() {
        super.init(initialState: CommentsStoreState())
    }
}
4. Một ý tưởng là có một truy vấn xác định một
struct CommentsStoreState {
    // Stored comments keyed by site
    var comments = [Int: [Comment]]()
    // Fetching state per site
    var fetching = [Int: Bool]()
}

class CommentsStore: StatefulStore {
    init() {
        super.init(initialState: CommentsStoreState())
    }
}
5. Bằng cách này, một cửa hàng có thể ánh xạ bất kỳ thay đổi trạng thái nào thành kết quả truy vấn và chỉ thông báo cho người quan sát truy vấn khi kết quả thay đổi. Đây là một tính năng thú vị, nhưng có vấn đề khi triển khai theo cách chung chung. Vấn đề chính là lưu trữ các truy vấn không đồng nhất với các loại
struct CommentsStoreState {
    // Stored comments keyed by site
    var comments = [Int: [Comment]]()
    // Fetching state per site
    var fetching = [Int: Bool]()
}

class CommentsStore: StatefulStore {
    init() {
        super.init(initialState: CommentsStoreState())
    }
}
6 khác nhau. Nó vẫn chưa bị loại trừ là không thể, nhưng chúng ta cần sử dụng điều này nhiều hơn và quyết định xem việc tối ưu hóa có xứng đáng hay không