Middleware trong Redux là gì?
Nếu đã từng làm việc với các ngôn ngữ lập trình phía server-side hẳn bạn không lạ gì với khái niệm middleware. Middleware là 1 lớp nằm giữa ứng dụng và network request, là nơi bạn có thể thêm vào CORS headers, logging,... Thậm chí bạn có thể bố trí middleware trong ứng dụng theo 1 chuỗi tương tự ...
Nếu đã từng làm việc với các ngôn ngữ lập trình phía server-side hẳn bạn không lạ gì với khái niệm middleware. Middleware là 1 lớp nằm giữa ứng dụng và network request, là nơi bạn có thể thêm vào CORS headers, logging,... Thậm chí bạn có thể bố trí middleware trong ứng dụng theo 1 chuỗi tương tự như sau:
Middleware 1: Logging -> Middleware 2: Authentication -> Middleware 3: Reporter
Trong Redux, khái niệm middleware cũng tồn tại và giữ vai trò tương tự, nhưng vấn đề mà middleware trong redux giải quyết thì có điểm khác biệt so với server-side. Trong Redux, middleware là lớp nằm giữa Reducers và Dispatch Actions. Vị trí mà Middleware hoạt động là trước khi Reducers nhận được Actions và sau khi 1 Action được dispatch(). Middleware trong Redux được biết đến nhiều nhất trong việc xử lý ASYNC Action - đó là những Action không sẵn sàng ngay khi 1 Action Creator được gọi tới, thông thường ở đây là các API request.
Trong bài viết này, chúng ta sẽ cùng nhau tìm hiểu về concept cũng như tư tưởng của Middleware được triển khai trong Redux.
Không gì tốt hơn việc tìm hiểu 1 mô hình bằng cách thử xây dựng lại nó. Chúng ta sẽ cùng nhau code 2 middlewares là:
- Logging Middleware: Ghi lại lịch sử khi 1 action được dispatch, ghi lại state của hệ thống tại thời điểm đó.
- Crash Reporter Middleware: Thông báo khi hệ thống có lỗi đến 1 hệ thống riêng biệt nằm bên ngoài ứng dụng của chúng ta.
Logging Middleware
Kết quả của middleware này sau khi hoàn thành nhằm in ra những thông báo như sau trên màn hình console (hoặc file log - tuỳ vào bạn)
1st Attempt
Chúng ta bắt đầu với cách tiếp cận vấn đề đơn giản nhất, thêm trực tiếp lệnh log vào trước sau mỗi khi dispatch action
function dispatchAndLog(store, action) { console.log('dispatching', action) store.dispatch(action) console.log('next state', store.getState()) }
Khi sử dụng cần truyền store hiện tại vào làm đối số.
dispatchAndLog(store, addTodo('Use Redux'))
Cons: thấy rõ sự bất cập ngay là thay vì mỗi lần thay vì viết 1 câu lệnh, ta cần đến 3. Thêm nữa ta cần import hàm này vào mỗi khi cần ghi log.
Attempt #2: Monkeypatching Dispatch
Monkey patching là kỹ thuật mà ta sẽ chỉnh sửa, mở rộng 1 phần của ứng dụng mà không làm thay đổi hệ thống. Áp dụng kỹ thuật này vào đây ta sẽ "patching" lại hàm dispatch của Redux
let next = store.dispatch store.dispatch = function dispatchAndLog(action) { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result }
Cons: Mọi thứ có vẻ ổn ? không hề, Money Patching là 1 bad practice mà bạn cần tránh khi code https://en.wikipedia.org/wiki/Monkey_patch#Pitfalls Tuy vậy, chúng ta sẽ chuyển sang middleware thứ 2 trước khi quay lại giải quyết các pitfalls của Logging Middleware này.
Crash Reporting
Rõ ràng trong Production, việc dựa vào sự kiện onError của Javascript là không đủ giá trị / tin tưởng để Debug cũng như tracking quá trình cua bugs. Sử dụng 1 dịch vụ third party như Sentry mang lại hiệu quả cao hơn nhiều. Dù vậy việc dựa vào external API/ external module như vậy thì cần thiết cho chúng ta viết những interface có khả năng tháo lắp, thay đổi dễ dàng - hãy nghĩ tới Adapter Design Pattern. Bằng không ta sẽ không thể xây dựng được 1 loose eco-system. Điều này cũng đúng với Logging Middleware ở phía trên, nghĩ theo hướng này ta cần thay đổi các middleware trở nên tách biệt với nhau:
function patchStoreToAddLogging(store) { let next = store.dispatch store.dispatch = function dispatchAndLog(action) { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } } function patchStoreToAddCrashReporting(store) { let next = store.dispatch store.dispatch = function dispatchAndReportErrors(action) { try { return next(action) } catch (err) { console.error('Caught an exception!', err) Raven.captureException(err, { extra: { action, state: store.getState() } }) throw err } } }
Attempt #3: Loại bỏ Monkeypatching
Kỹ thuật monkeypatch mang lại nhiều rủi ro, bởi vậy thay vì trực tiếp thay thế hàm dispatch, ta sẽ trả về 1 hàm dispatch mới wrap bên ngoài hàm dispatch có sẵn của Redux.
function logger(store) { let next = store.dispatch // Previously: // store.dispatch = function dispatchAndLog(action) { return function dispatchAndLog(action) { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } }
Ta cần 1 helper để apply các hàm dispatch này vào lớp Middleware của Redux - thực tế thì Redux có helper này nhưng interface sẽ khác với ví dụ chúng ta làm.
function applyMiddlewareByMonkeypatching(store, middlewares) { middlewares = middlewares.slice() middlewares.reverse() // Transform dispatch function with each middleware. middlewares.forEach(middleware => store.dispatch = middleware(store) ) } applyMiddlewareByMonkeypatching(store, [logger, crashReporter])
Tuy vậy, khi vào forEach chạy xong, hàm dispatch của store lúc này chỉ lấy giá trị của function cuối cùng được trả về từ mảng middlewares. Để Middlewares hoạt động như 1 chuỗi tuần tự, ta cần chỉ định hàm dispatch sẽ được 1 middleware wrap bên ngoài phải là hàm dispatch đã được wrap bởi middleware trước đó như sau:
middleware2(middleware1)) // Middleware 2 : Logging -> Middleware 1: Crash reporter
Để làm được điều đó, các middleware thay vì đọc hàm dispatch trực tiếp từ store, thì ta sẽ truyền tham số next vào thể hiện cho hàm dispatch đã được middleware trước đó trong chuỗi wrap lại.
function logger(store) { return function wrapDispatchToAddLogging(next) { return function dispatchAndLog(action) { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } } }
Sử dụng ES6 cho code khô thoáng hơn bằng arrow function
const logger = store => next => action => { console.log('dispatching', action) let result = next(action) console.log('next state', store.getState()) return result } const crashReporter = store => next => action => { try { return next(action) } catch (err) { console.error('Caught an exception!', err) Raven.captureException(err, { extra: { action, state: store.getState() } }) throw err } }
Attempt #4: Applying the Middleware
Để apply middles vào Redux store, ta sẽ viết 1 helper nhận vào các Middlewares cũng như Store ban đầu, Kết quả trả về là store mới, với hàm dispatch đã được wrap lại bởi chuỗi Middleware
function applyMiddleware(store, middlewares) { middlewares = middlewares.slice() middlewares.reverse() let dispatch = store.dispatch middlewares.forEach(middleware => dispatch = middleware(store)(dispatch) ) return Object.assign({}, store, { dispatch }) }
Hàm applyMiddleware thật trong Redux API tương đương chức năng như hàm chúng ta vừa viết nhưng tất nhiên là toàn năng hơn:
- Nó đảm bảo chỉ cung cấp 1 số API cần thiết cho các Middleware truy cập ( hàm chúng ta vừa viết thì middleware toàn quyền truy cập vào store luôn )
- Cung cấp tính năng nếu bạn gọi trực tiếp store.dispatch() bên trong middleware thì action sẽ chạy lại từ đầu chuỗi Middleware (quan trọng để implement ASYNC middleware)
- Đảm bảo chỉ được cung cấp middleware 1 lần duy nhất
Thông qua bài viết này, bạn sẽ hiểu đây chính xác là cách Redux triển khai middleware eco-system của mình - gọi là eco-system bởi nó mang lại khả năng mở rộng rất lớn, hiện nay đã có rất nhiều thư viện như: redux-thunk, redux-promise, redux-saga ... Chúng đều là các middleware cuả Redux, và tất nhiên các thư viện này cũng có bản chất như những gì chúng ta đã làm trong bài này, cũng không quá phức tạp như những gì bạn nghĩ ban đầu phải không? Bài viết có tham khảo từ: http://redux.js.org/docs/advanced/Middleware.html