2015-10-03 26 views
38

Tôi đã đấu tranh hàng giờ để tìm giải pháp cho vấn đề này ...Làm thế nào để xử lý các tác dụng phụ phức tạp trong Redux?

Tôi đang phát triển trò chơi có bảng điểm trực tuyến. Người chơi có thể đăng nhập và đăng xuất bất cứ lúc nào. Sau khi hoàn thành một trò chơi, người chơi sẽ thấy bảng điểm, và xem thứ hạng của riêng họ, và điểm số sẽ được gửi tự động.

Bảng điểm hiển thị xếp hạng của người chơi và bảng xếp hạng.

Screenshot

Bảng điểm được sử dụng cả khi người dùng kết thúc chơi (nộp một số điểm), và khi người dùng chỉ muốn kiểm tra xếp hạng của họ.

Đây là nơi logic trở nên rất phức tạp:

  • Nếu người dùng đang đăng nhập, sau đó cân bằng tỷ số sẽ được đệ trình đầu tiên. Sau khi bản ghi mới được lưu thì bảng điểm sẽ được tải.

  • Nếu không, bảng điểm sẽ được tải ngay lập tức. Người chơi sẽ được cung cấp một tùy chọn để đăng nhập hoặc đăng ký. Sau đó, điểm số sẽ được gửi, và sau đó bảng điểm sẽ được làm mới một lần nữa.

  • Tuy nhiên, nếu không có điểm để gửi (chỉ cần xem bảng điểm số cao). Trong trường hợp này, bản ghi hiện tại của người chơi được tải xuống đơn giản. Nhưng vì hành động này không ảnh hưởng đến bảng điểm, cả bảng điểm và bản ghi của người chơi sẽ được tải xuống đồng thời.

  • Có một số lượng không giới hạn các cấp. Mỗi cấp độ có một bảng điểm khác nhau. Khi người dùng xem bảng điểm, thì người dùng đang ‘quan sát’ bảng điểm đó. Khi nó được đóng lại, người dùng ngừng quan sát nó.

  • Người dùng có thể đăng nhập và đăng xuất bất cứ lúc nào. Nếu người dùng đăng xuất, xếp hạng của người dùng sẽ biến mất và nếu người dùng đăng nhập dưới dạng tài khoản khác, thì thông tin xếp hạng cho tài khoản đó sẽ được tìm nạp và hiển thị.

    ... nhưng việc tìm nạp thông tin này chỉ nên diễn ra cho bảng điểm mà người dùng hiện đang quan sát.

  • Để xem hoạt động, kết quả sẽ được lưu trong bộ nhớ, để nếu người dùng đăng ký lại vào cùng một bảng điểm, sẽ không có tìm nạp. Tuy nhiên, nếu có điểm số được gửi, bộ nhớ cache sẽ không được sử dụng.

  • Bất kỳ hoạt động mạng nào trong số này có thể không thành công và trình phát phải có khả năng thử lại chúng.

  • Các hoạt động này phải là nguyên tử. Tất cả các tiểu bang nên được cập nhật một lần (không có trạng thái trung gian).

Hiện tại, tôi có thể giải quyết vấn đề này bằng cách sử dụng Bacon.js (thư viện lập trình phản ứng chức năng), như hỗ trợ cập nhật nguyên tử. Mã này khá ngắn gọn, nhưng ngay bây giờ nó là một mã spaghetti không thể đoán trước được.

Tôi bắt đầu xem xét Redux.Vì vậy, tôi đã cố gắng để cấu trúc các cửa hàng, và đến với một cái gì đó như thế này (trong cú pháp YAMLish):

user: (user information) 
record: 
    level1: 
    status: (loading/completed/error) 
    data: (record data) 
    error: (error/null) 
scoreboard: 
    level1: 
    status: (loading/completed/error) 
    data: 
     - (record data) 
     - (record data) 
     - (record data) 
    error: (error/null) 

Vấn đề trở nên: nơi tôi đặt các tác dụng phụ.

Đối với các tác vụ phụ không có tác dụng, điều này trở nên rất dễ dàng. Ví dụ: trên hành động LOGOUT, trình giảm tốc record có thể đơn giản là xóa tất cả các bản ghi.

Tuy nhiên, một số hành động có tác dụng phụ. Ví dụ: nếu tôi chưa đăng nhập trước khi gửi điểm, thì tôi đăng nhập thành công, hành động SET_USER sẽ lưu người dùng vào cửa hàng.

Nhưng vì tôi có điểm số để gửi, hành động SET_USER này cũng phải làm cho yêu cầu AJAX được kích hoạt và đồng thời, đặt record.levelN.status thành loading.

Câu hỏi đặt ra là: làm cách nào để biểu thị rằng tác dụng phụ (gửi điểm) sẽ diễn ra khi tôi đăng nhập theo cách nguyên tử?

Trong kiến ​​trúc Elm, trình cập nhật cũng có thể phát ra các tác dụng phụ khi sử dụng dạng Action -> Model -> (Model, Effects Action), nhưng trong Redux, chỉ là (State, Action) -> State.

Từ tài liệu Async Actions, cách họ đề xuất là đặt chúng vào trình tạo tác vụ. Điều này có nghĩa là logic của việc gửi điểm số sẽ phải được đưa vào tác vụ tạo hành động cho một hành động đăng nhập thành công không?

function login (options) { 
    return (dispatch) => { 
    service.login(options).then(user => dispatch(setUser(user))) 
    } 
} 

function setUser (user) { 
    return (dispatch, getState) => { 
    dispatch({ type: 'SET_USER', user }) 
    let scoreboards = getObservedScoreboards(getState()) 
    for (let scoreboard of scoreboards) { 
     service.loadUserRanking(scoreboard.level) 
    } 
    } 
} 

Tôi tìm thấy điều này hơi lạ, vì mã chịu trách nhiệm về phản ứng dây chuyền này ngay bây giờ tồn tại trong 2 nơi:

  1. Trong giảm. Khi hành động SET_USER được gửi đi, trình giảm tốc record cũng phải đặt trạng thái của các bản ghi thuộc các bảng điểm quan sát thành loading.
  2. Trong tác vụ tạo tác vụ thực hiện tác dụng phụ thực tế của việc tìm nạp/gửi điểm.

Dường như tôi phải theo dõi tất cả các nhà quan sát đang hoạt động theo cách thủ công. Trong khi đó, trong phiên bản Bacon.js, tôi đã làm một cái gì đó như thế này:

Bacon.once() // When first observing the scoreboard 
.merge(resubmit口) // When resubmitting because of network error 
.merge(user川.changes().filter(user => !!user).first()) // When user logs in (but only once) 
.flatMapLatest(submitOrGetRanking(data)) 

Mã Bacon thực tế là còn rất nhiều, vì tất cả các quy định phức tạp trên, khiến phiên bản Bacon chỉ có thể đọc được.

Nhưng Bacon tự động theo dõi tất cả đăng ký đang hoạt động. Điều này khiến tôi bắt đầu đặt câu hỏi rằng nó có thể không đáng để chuyển đổi, bởi vì việc viết lại nó cho Redux sẽ đòi hỏi rất nhiều việc xử lý thủ công. Bất cứ ai có thể đề nghị một số con trỏ?

Trả lời

50

Khi bạn muốn phụ thuộc không đồng bộ phức tạp, chỉ cần sử dụng Bacon, Rx, kênh, sagas hoặc một trừu tượng không đồng bộ khác. Bạn có thể sử dụng chúng có hoặc không có Redux. Ví dụ với Redux:

observeSomething() 
    .flatMap(someTransformation) 
    .filter(someFilter) 
    .map(createActionSomehow) 
    .subscribe(store.dispatch); 

Bạn có thể soạn những hành động không đồng bộ của bạn bất cứ cách nào-một phần quan trọng duy nhất là cuối cùng họ biến thành store.dispatch(action) cuộc gọi.

Redux Thunk là đủ cho các ứng dụng đơn giản, nhưng khi nhu cầu không đồng bộ của bạn trở nên tinh vi hơn, bạn cần sử dụng tóm tắt thành phần không đồng bộ thực và Redux không quan tâm bạn sử dụng cái nào.


Cập nhật: Một số thời gian đã trôi qua, và một số giải pháp mới đã xuất hiện. Tôi khuyên bạn nên kiểm tra Redux Saga đã trở thành một giải pháp khá phổ biến cho luồng điều khiển không đồng bộ trong Redux.

+3

Trường hợp thành phần không đồng bộ này nên cư trú bình thường? Tôi có nghĩa là, một nơi nào đó bên ngoài của redux hoặc nó phù hợp hơn với middleware? –

+4

Middleware là để chuyển đổi đơn giản các hành động — không phải cho thành phần phức tạp. Chỉ cần đặt nó bên ngoài Redux như các hàm thường xuyên trả về các quan sát, dù sao bạn thích. Bạn cũng có thể sử dụng một cái gì đó như https://github.com/acdlite/redux-rx. –

+1

Cảm ơn bạn. Cuối cùng tôi đã quyết định rằng tôi sẽ chỉ sử dụng Bacon cho logic không đồng bộ phức tạp, nhưng đối với phần chỉ lưu trữ dữ liệu, tôi sử dụng mẫu của Redux. Bây giờ, triển khai thực hiện lưu trữ Redux của tôi trông như sau: 'const state 川 = action 川 => action 川 .scan (INITIAL_STATE, reducer)', và bây giờ tôi có thể kiểm tra các bộ giảm tốc này, và nó cũng có thể kiểm tra những dòng này những hành động này một cách riêng biệt. Bây giờ, tôi nghĩ nó tóm tắt về cách tổ chức mã Bacon/Rx đúng cách, đó là một thách thức trong chính nó. Cảm ơn một lần nữa! – Thai

16

Sửa: có bây giờ là một dự án redux-saga lấy cảm hứng từ những ý tưởng

Dưới đây là một số tài nguyên đẹp


Flux/Redux được lấy cảm hứng từ dòng xử lý sự kiện phụ trợ (bất kể tên là: eventsourcing, CQRS, CEP, kiến ​​trúc lambda ...).

Chúng tôi có thể so sánh ActionCreators/Hành động của thông lượng với các lệnh/sự kiện (thuật ngữ thường được sử dụng trong các hệ thống phụ trợ).

Trong các kiến ​​trúc phụ trợ này, chúng tôi sử dụng mẫu thường được gọi là Saga hoặc Trình quản lý quy trình. Về cơ bản nó là một phần trong hệ thống nhận các sự kiện, có thể quản lý trạng thái riêng của nó, và sau đó có thể ra lệnh mới. Để làm cho nó đơn giản, nó là một chút giống như thực hiện IFTTT (If-This-Then-That).

Bạn có thể thực hiện điều này với FRP và BaconJS, nhưng bạn cũng có thể thực hiện điều này trên đầu trang của Redux reducers.

function submitScoreAfterLoginSaga(action, state = {}) { 
    switch (action.type) { 

    case SCORE_RECORDED: 
     // We record the score for later use if USER_LOGGED_IN is fired 
     state = Object.assign({}, state, {score: action.score} 

    case USER_LOGGED_IN: 
     if (state.score) { 
     // Trigger ActionCreator here to submit that score 
     dispatch(sendScore(state.score)) 
     } 
    break; 

    } 
    return state; 
} 

Để làm cho nó rõ ràng: tình trạng tính từ gia giảm để lái xe Phản ứng render absolutly nên giữ sự trong trắng! Nhưng không phải tất cả trạng thái của ứng dụng đều có mục đích kích hoạt hiển thị và đây là trường hợp ở đây, nơi cần phải đồng bộ hóa các phần tách rời khác nhau của ứng dụng của bạn với các quy tắc phức tạp. Trạng thái của "saga" này không nên kích hoạt render React!

Tôi không nghĩ Redux cung cấp bất kỳ thứ gì để hỗ trợ mẫu này nhưng bạn có thể dễ dàng thực hiện nó một cách dễ dàng.

Tôi đã thực hiện việc này trong số startup framework và mẫu này hoạt động tốt. Nó cho phép chúng tôi xử lý các thứ IFTTT như:

  • Khi người dùng tích hợp hoạt động và người dùng đóng Popup1, sau đó mở Popup2 và hiển thị chú giải công cụ gợi ý.

  • Khi người dùng là sử dụng trang web di động và mở Menu2, sau đó đóng Menu1

QUAN TRỌNG: nếu bạn đang sử dụng undo tính năng/redo/phát lại của một số khuôn khổ như Redux, điều quan trọng là trong quá trình một replay của một bản ghi sự kiện, tất cả các sagas này không được yêu cầu, bởi vì bạn không muốn các sự kiện mới được kích hoạt trong quá trình phát lại!

+0

Cảm ơn bạn đã trả lời! Vì vậy, chúng tôi có một trình giảm tốc "saga" khác để quản lý tương tác/phối hợp này và cũng có thể kích hoạt các tác dụng phụ. Để làm điều này [Tôi có lẽ phải tham gia vào mặt tối của Flux và cho phép các cửa hàng kích hoạt các tác dụng phụ] (http://jamesknelson.com/join-the-dark-side-of-the-flux-responding-to-actions -với diễn viên /). Tôi quyết định đi với Bacon thay vì (sử dụng bộ giảm tốc trong các bộ phận không đồng bộ), nhưng câu trả lời của bạn cũng rất hữu ích! Cảm ơn. – Thai

+0

@Thai có ý tưởng của tôi khá giống với liên kết bên tối của bạn. Tôi sẽ không gọi đây là một mặt tối, bởi vì các bộ giảm mà tính toán trạng thái được sử dụng để dựng hình vẫn không có bất kỳ tác dụng phụ nào. Loại giảm tốc mới này chỉ có vai trò phối hợp cho "các giao dịch dài hạn" và trạng thái của chúng hoàn toàn không được sử dụng để thúc đẩy các kết xuất. –

+0

Có liên quan về mẫu saga với redux: http://stackoverflow.com/a/33829400/82609 –

Các vấn đề liên quan