Redux-saga

Естественный код

Нажми пробел чтобы продолжить

Стрелка вправо — для быстрой промотки

Проблема управления состоянием UI

  1. React — View
  2. Redux store — Model
  3. And Controller to rule them all ...

FLUX — таблетка от боли

Боль ослабла, но осталась

  1. Store раздувается
    • ​запрос уже отправлен?
    • а какой ответ получен?
    • а что делать дальше?
       
  2. Асинхронный код разбит на части и "размазан"
    • не видно общей картины
    • сложно дорабатывать

Встречайте — coroutines !

function* runAndWait() {

  console.log('Готов работать. Жду подтверждения')

  let startCommand = yield
  console.log('Начинаю работать работу...')
  // TODO: сделать что-то мега полезное
  console.log('Выполнено. Жду подтверждения...')

  let nextCommand = yield workResult
  // TODO: продолжить делать что-то полезное
  // ...
}
  1. Запускаешь
  2. Говоришь "дождись того-то"
  3. А затем сделай вот это

Redux-saga — это набор инструментов
для работы с coroutines

Долгоиграющий процесс

  1. отобразить прелоадер
     

  2. загрузить заказ с сервера
     

  3. показать форму редактирования заказа
     

  4. дождаться ввода пользователя
     

  5. отправить данные на сервер
     

  6. отобразить ошибки
     

  7. 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?!

И нужно сделать несколько интерфейсов

  1. Мобильная + десктоп веб-версия
  2. Виджет на сайтах партнеров
  3. PhoneGap приложение

А после возьмемся за приложение для мастеров

И оно в 3 раза больше

Главные правила

использования саг

#1 По саге на страницу

#2 MVC Controller живет
в саге

  1. View НЕ знает о store.dispatch

  2. View НЕ знает о сагах

  3. View читает из store и пускает события в channel

#3 Единственный store  это
путь в АД

  1. Один глобальный store

  2. Локальный store для каждой страницы

    • ошибки заполнения форм

    • прелоадеры

    • ...

#4 Используй исключения, Люк!

  1. OrderNotFoundError

  2. Redirect2URL

  3. RequestToAPIError

  4. ServerError

  5. ....

Рецепт #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

Made with Slides.com