Redux-saga
Естественный код
Нажми пробел чтобы продолжить
Стрелка вправо — для быстрой промотки
Проблема управления состоянием UI
- React — View
- Redux store — Model
- And Controller to rule them all ...
FLUX — таблетка от боли
Боль ослабла, но осталась
-
Store раздувается
- запрос уже отправлен?
- а какой ответ получен?
- а что делать дальше?
-
Асинхронный код разбит на части и "размазан"
- не видно общей картины
- сложно дорабатывать
Встречайте — coroutines !
function* runAndWait() {
console.log('Готов работать. Жду подтверждения')
let startCommand = yield
console.log('Начинаю работать работу...')
// TODO: сделать что-то мега полезное
console.log('Выполнено. Жду подтверждения...')
let nextCommand = yield workResult
// TODO: продолжить делать что-то полезное
// ...
}
- Запускаешь
- Говоришь "дождись того-то"
- А затем сделай вот это
Redux-saga — это набор инструментов
для работы с coroutines
Долгоиграющий процесс
-
отобразить прелоадер
-
загрузить заказ с сервера
-
показать форму редактирования заказа
-
дождаться ввода пользователя
-
отправить данные на сервер
-
отобразить ошибки
-
repeat until
Сделаем страницу заказа
// 1. отобразим прелоадер
riot.mount('#container', 'preloader-page', {
msg: 'Загружаем заказ'
})
// 2. загрузим заказ с сервера
let response = yield fetch(`/order/${id}`)
let order = yield response.json()
// создадим канал для событий - действий пользователя
let pageChannel = channel()
let store = createStore(reduceFormErrors)
// 3. отобразим страницу с заказом
riot.mount('#container', 'order-page', {
pageChannel,
order
})
Естественная запись кода
<!-- Тег Riot.js, живет в отдельном файле -->
<order-page>
<h1>Заказ #{opts.order.id}</h1>
<form onsubmit={onSubmit}>
<!-- TODO здесь разместить поля HTML формы -->
<button type="submit">Отправить</button>
</form>
</order-page>
View
<script type="text/javascript">
this.onSubmit = event => {
let formData = jQuery(event.target).serializeArray()
opts.pageChannel.put(formData)
}
</script>
while(true){
// 4. дождемся ввода пользователя
let formData = yield take(pageChannel)
Естественная запись кода. ч2
// 5. отправим данные на сервер
let response = yield fetch(`/order/${id}`, {
method: 'POST',
body: JSON.stringify(formData),
})
let errors = yield response.json()
// если все ОК - переправим на новую страницу
if (!errors.length)
return window.location = `/order/${id}/success`
// 6. отобразим ошибки
store.dispatch({
type: 'ERRORS_OCCURRED',
errors
})
}
Реальный проект
как мерило эффективности
Веб-сервис МойМеханик
Автомеханик приедет к вам домой и на работу
Что нам нужно чтобы чинить автомобили?
45 разных страниц
Многовато... React нас спасёт!
Будет жестокая битва за конверсию
Придется строить
много велосипедов...
Еще тут сложная бизнес-логика
What?!
И нужно сделать несколько интерфейсов
- Мобильная + десктоп веб-версия
- Виджет на сайтах партнеров
- PhoneGap приложение
А после возьмемся за приложение для мастеров
И оно в 3 раза больше
Главные правила
использования саг
#1 По саге на страницу
#2 MVC Controller живет
в саге
-
View НЕ знает о store.dispatch
-
View НЕ знает о сагах
-
View читает из store и пускает события в channel
#3 Единственный store — это
путь в АД
-
Один глобальный store
-
Локальный store для каждой страницы
-
ошибки заполнения форм
-
прелоадеры
-
...
-
#4 Используй исключения, Люк!
-
OrderNotFoundError
-
Redirect2URL
-
RequestToAPIError
-
ServerError
-
....
Рецепт #1. Router
По изменению window.location включаем нужную сагу
import runOrderPage from './pages/order'
// ... TODO: импортировать другие страницы/саги
URL схема
// URL схема приложения
const routes = [
['/order/*', runOrderPage],
// ... TODO: дописать другие URLs и саги для них
]
function* router(routes){
// запускается единожды при инициализации приложения
let routerChannel = channel()
Роутер
// настраиваем riot.router
function registerRoute([urlPattern, saga]){
riot.route(urlPattern, ()=>routerChannel.put(saga))
}
_.forEach(routes, registerRoute)
// при каждом событии в routerChannel
// останавливаем предыдущую сагу и запускаем новую
yield* takeLatest(routerChannel, function*(saga){
yield* saga()
})
}
// останавливаем предыдущую сагу и запускаем новую
yield* takeLatest(routerChannel, function*(saga){
try {
yield* saga()
} catch (error){
// проглатываем исключение чтобы
// не порушить все приложение
Raven.captureException(error)
console.error(error)
}
})
Повысим устойчивость приложения
Рецепт #2. Прелоадер
Схема данных
const PRELOADER_INITIAL_STATE = {
isRunning: false,
}
function reduce(state=PRELOADER_INITIAL_STATE, action){
switch (action.type) {
case 'TASK_STOPPED':
return { isRunning: false }
default:
return state
}
}
case 'TASK_STARTED':
return { isRunning: true }
function* runOrderPage(store){
riot.mount('#container', 'order-page')
Использование
store.dispatch({type: 'TASK_STARTED'})
let response = yield fetch('/api/order/${id}')
// TODO: обработать ответ сервера и возможные ошибки
store.dispatch({type: 'TASK_STOPPED'})
}
Повысим устойчивость
// ...
try {
let response = yield fetch('/api/order/${id}')
// TODO: обработать ответ сервера и возможные ошибки
} finally {
store.dispatch({type: 'TASK_STOPPED'})
}
// ...
Оформим в виде декоратора
const PreloaderWrapper = store => saga => function*(...args){
store.dispatch({type: 'TASK_STARTED'})
try {
return yield* saga(...args)
finally {
store.dispatch({type: 'TASK_STOPPED'})
}
}
function* runOrderPage(store){
riot.mount('#container', 'order-page')
yield* PreloaderWrapper(store)(function*(){
let response = yield fetch('/api/order/${id}')
// TODO: обработать ответ сервера и возможные ошибки
})()
}
Запишем короче
const fetchOrder = decorate(
PreloaderWrapper(store),
function*(id){
let response = yield fetch('/api/order/${id}')
// TODO: обработать ответ сервера и возможные ошибки
return responseData
})
)
function* runOrderPage(store){
riot.mount('#container', 'order-page')
let order = yield* fetchOrder(id)
}
Анонс
Как в МойМеханик ускоряют
разработку?
Hot Module Replacement
Middlewares
const routesMiddlewares = [
showPlugIfFailure,
// перехватывает все ошибки,
// отображает страницу заглушку и стучит в Sentry.io
redirectsHandler.handler,
// ловит RedirectException, меняет window.location
handler404.handler,
// ловит Order404Exception и пр.
// отображает страницу заглушку
handleAPIAuthErrors.handler,
// перенаправляет на /signin
]
Reusable apps
App structure
-
Model
-
reducer
-
selectors
-
-
View
-
React.js
-
Riot.js
-
Vue.js
-
-
Controller:
-
decorators
-
middlewares
-
page saga
-
Apps
-
forms
-
config
-
preloader
-
tracker
-
auth
-
orders
-
...
Generic controllers
Меньше boilerplate — больше смысла
Спасибо!
Какие есть вопросы?
Автор доклада: Евгений Евсеев
CTO веб-сервиса МойМеханик.рф
email pelid80@gmail.com
skype evseev.evgeny
Код из доклада есть в репозитории на GitHub
Redux-saga
By Evgeny Evseev
Redux-saga
Естественный способ записи асинхронного кода
- 2,641