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.
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:
- Trong giảm. Khi hành động
SET_USER
được gửi đi, trình giảm tốcrecord
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ànhloading
. - 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ườ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? –
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. –
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