ZarahioN Presents

Answering why

Author Archive ZN Dev-Ops Head Monkey

Первые серьезные строки на React.js [Pt.3 – Finally starting for real]

В прошлых монотонных рассказах мы занимались долгой и утомительной настройкой вебпака и вообще «среды» для создания React приложений.

К счастью, это мучение наконец закончилось и мы можем приступить к созданию нашего приложения. Ура, свобода!

Впрочем, начнем с азов, затронутых еще в первом рассказе. Заглянем в файл index.js, он скорее всего находится в папке src, если ты продолжаешь с пути c-r-a, либо прямо в корне проекта, если решил в лоб принять удар тяжелой и героической самостоятельной настройки вебпака.

Мы завершили наше прошлое знакомство с кодом реакта на небольшом примере двух вариантов компонентов, умном и глупом, как я, пожалуй, буду дальше их называть. Первый по моей скромной (и скорее всего не очень надежной) терминологии имеет свою не визуальную логику, второй просто отображает разметку подставляя переданные параметры.
Как я уже говорил, существует еще как минимум пара типов и принципов разделения компонентов, но это разделение несет с собой несколько тонких граней, которые с наскока легко проморгать.

Для простоты подхода (и соблюдения традиций) мы начнем с глупых — stateless — компонентов.
На данный момент существует 3 способа их создания, впрочем последний (и исторически первый) через React.createClass мы не будем разбирать как технически устаревающий (и не зря же мы проходили через все страдания вебпака и бейбла)

class X extends React.Component

import React, { Component } from 'react'; 
import ReactDOM from 'react-dom'; 

class ColoredDiv extends React.Component {
    render() {
        const {color, fontSize: fz} = this.props;
        return <div style={{background: this.props.bgColor, color, fontSize: fz}}>Me colored, yay!</div>
    }
}

ReactDOM.render(<ColoredDiv bgColor="green" color="red" fontSize="14px"/>, document.getElementById('root'));

Синтаксис классов это одно из тех первых нововведений ES6, которые встречаешь начиная изучать реакт, он по сути является прямым наследником названного React.createClass. Как правило классы создают только для умных компонентов, однако технически наш ColoredDiv остается глупым, так как у него нет никакой логики или состояния, и сейчас его можно напрямую преобразовать в функциональный.

Разберем же наш простенький код по полочкам:
import X from 'x'; — механизм загрузки и сборки модулей, для которого мы и используем вебпак, так как это до сих пор достаточно далекий от браузерного стандарта функционал. Как можно правильно догадаться, import React from 'react' дает доступ к функционалу реакта посредством константы React (также любой импорт говорит вебпаку загрузить указанную библиотеку, что бывает важно в некоторых случаях).
class X extends React.Component — синтаксис для создания «класса» в ES6, при этом мы наследуем от базового класса реакта (потому что надо).
render() это метод нашего класса, который будет использовать реакт для создания разметки компонента — ее рендера.

this.props это специальный неизменяемый объект (свойство\параметр), в который реакт помещает переданные компоненту свойства (prop(ertie)s).

Непосредственно процесс передачи можно увидеть глянув на последнюю строчку — ReactDOM.render. Первым аргументом для него идет JSX разметка (технически возврат React.createElement, но это ненужные детали). Мы использовали имя созданного компонента (класса) и вставили несколько атрибутов: bgColor, color, fontSize эти атрибуты и попадают в объект props компонента.

Как можно заметить, в рендере самого компонента мы возвращаем JSX разметку подобную той, что передавали в ReactDOM.render (наблюдаешь связь?). И как можно заметить, фигурные скобки в атрибутах служат для передачи JS значений, включая объекты как в style (отчего там получились по две фигурные скобки, внешние для обозначения компилятору что идет JS код, внутренние уже для самого объекта), так и просто строки или числа.

Также я использовал сразу три способа использования переменных из свойств (props): доступ напрямую this.props.bgColor, и деструктуризации {color} = this.props и {fontSize: fz} = this.props, третья отличается от второй лишь тем, что мы переименовали переменную перед использованием. Как правило используется второй и третий тип, так как это позволяет сразу в начале рендера указать, какие свойства используются в компоненте.

Как можно догадаться, фигурные скобки позволяют использовать JS не только в атрибутах:

render() {
    const {color, fontSize: fz } = this.props;
    return (<div style={{background: this.props.bgColor, color, fontSize: fz}}>
        {Math.random() >= 0.5 ? 'Me colored, yay!' : 'Me no random!'}
    </div>)
}

Помимо скромных забав со случайными числами таким образом можно использовать любой JS код, который возвращает значения (строки, числа, элементы реакта (JSX) или массивы перечисленных типов), к повсеместно используемому примеру, Array.map:

class ColoredDiv extends React.Component{
    render() {
        const {color, fontSize: fz, rows } = this.props;
        return (<div style={{background: this.props.bgColor, color, fontSize: fz}}>
            {rows.map(row => <li>{row}</li>)}
        </div>);
    }
}

ReactDOM.render(<ColoredDiv 
    color="#ab00ff" 
    bgColor="white" 
    fontSize="14px" 
    rows={[
        'Me colored, yay!', 
        'Me colored, nope second try!', 
        'No random is it!'
    ]}
/>, document.getElementById('root'));

Как можно ожидать (или нет, с непривычки) — ColoredDiv выведет уже знакомый стилизованный div и li со строками из массива переданного через свойство rows (ReactDOM.render).
Если ты еще не ознакомился с функциональными методами массивов, постарайся разобраться хотя бы с минимальным набором map, filter, reduce, и опционально forEach, последний технически идентичен map, просто не возвращает значений. Но особенно непредсказуемо полезным может оказаться reduce. А первые два ты скорее всего будешь использовать повсеместно, работая с реактом (и очень вероятно без него тоже).

Помимо бездумного разглядывания кода, я предлагаю тебе самостоятельно побаловаться с нашим скромным примером, использовав все три перечисленных типа данных доступных для вывода, и в целом привыкнуть к подобию JSX на HTML и его отличительным особенностям, которые позволяют творить (иногда к сожалению) что угодно за счет вставки чистого JS кода в разметку. Особенно интересно становится, когда ты заканчиваешь играть со стандартными HTML елементами и начинаешь компоновать разметку компонентами реакта.

Итак, что мы успели сделать:
1. Создали класс-компонент ColoredDiv через class ColoredDiv extends React.Component { ... },
2. Добавили в него метод render() { ... } который возвращает JSX разметку (<div style={{ ... }}>...</div>),
3. Отрендерили (или правильнее сказать монтировали (mount)) созданный компонент посредством ReactDOM.render(<ColoredDiv ... />, realDOMElem),
4. Передали свойства (props) компоненту ColoredDiv через ReactDOM.render и использовали их.

Базовое знакомство с работой реакта? Завершено.

Хоть это и скучно, но на сейчас почти хватит, осталось только глянуть второй способ записи глупых компонентов, о котором я говорил ранее:

const X = (props) => …

Как можно заметить, имея опыт с ES6, это просто «стрелочная» (arrow) функция. Такой тип записи компонентов называется функциональным (duh!) и является более простой и удобной заменой относительно громоздкому классу. К слову, использовать стандартные функции теоретически тоже можно, но лично я такое не практикую.

Также я упоминал, что наш чрезмерно неумный компонент можно без дополнительных манипуляций преобразовать в функциональный:

const ColoredDiv = ({color, fontSize: fz, rows, ...props }) => 
<div style={{background: props.bgColor, color, fontSize: fz}}>
    {rows.map(row => <li>{row}</li>)}
</div>

Как всегда немного магии деструктуризации и условно громоздкая конструкция класса «сокращается» в два раза. Условно и кавычки подсказывают, что все не так просто. Куда большую пользу функциональные компоненты несут своей семантикой — они по определению не имеют своего состояния и поэтому они должны быть просты..
но.. никто не помешает сделать «умный» компонент-обертку который передает все данные и обработчики «глупому» функциональному компоненту.. который тем не менее содержит сотню строк разметки перемешанной с данными и обработчиками. И технически будет прав, но работать с этим все равно будет не очень приятно.
Что я хочу сказать — нельзя прятаться за терминами и определениями, оправдывая не очень хороший код (duh!x2), нужно стараться писать хороший код, помогая себе определенными терминами и абстракциями или шаблонами скрывающимися за ними.

На этой недовольной лирической ноте мы завершаем знакомство с практическими азами реакта.. и начинаем наш для некоторых короткий, а для некоторых очень долгий путь познания современных реалий веб-разработки с React.js.

Благость под названием create-react-app [Pt.2 – Automatic React Installation (and how to break it)]

Что же, в прошлый раз мы исследовали прелестное искусство мазохизма и настраивали своеобразный синоним среды разработки самостоятельно — с вебпаком, бейблом и ручной настройкой всего и вся. Но еще в самом начале прошлого рассказа я упомянул, о куда более простой альтернативе:

create-react-app

Что же это? По факту cli — command line interface для создания базового шаблона стандартного приложения на реакте.

В начале своего пути реакт не имел подобных инструментов (да и вообще разрабатывался «по приколу» и «для себя» фейсбуком). Однако, со временем и появлением других подобных библиотек на базе или доработке идей виртуального DOM-а и наличием у них подобных инструментов упрощения жизни разработчиков, сообщество реакта и фейсбук (вроде) наконец созрели для создания своего инструмента.

Почему не сразу? Все просто — чтобы научиться что-то чинить, нужно или это что-то сломать, или построить свой вариант. Ломать это, конечно, весело, но без хоть какого-либо понимания, процесс обратной починки может затянуться, а то и вовсе по факту не начаться. Если у тебя что-то сломается при использовании шаблона созданного c-r-a — ты хотя бы примерно сможешь ориентироваться в каком из узлов всей цепочки сборки и компиляции это произошло и где искать помощи.

Ну и плюс, без боли и часов потраченных на самостоятельную настройку и подгонку проекта под свои пожелания, прелести готового и хорошо работающего шаблона выглядят не так офигенно.

Впрочем, завершим затянувшееся лирическое отступление и приступим к делу:

Работа с create-react-app

Есть два пути: старый-добрый-глобальный, если у тебя npm ниже версии 5.2, и новый-модный npx, если выше. Отличаются они в одну строчку и кучку деталей (про которые можно почитать здесь(англ) или здесь(рус)), поэтому посмотрим оба (, а я таки обновлю свою стареющую ноду 8.5.0 версии (хотя это делать оказалось необязательно)).

Классический способ это установка cli в глобальную директорию npm:

npm i -g create-react-app

После чего можно использовать: (если это первая глобальная установка, возможно придется добавить путь в PATH на Win или в /bin (или куда нужно) на *nix системах)

create-react-app app-folder-name

Это создаст проект в «app-folder-name» по текущему пути консоли.

Модный способ это использование вышеназванного npx, который поставляется с 5.2 версий npm:

npm install npm@latest -g

Это обновит npm, на случай, если ты хочешь использовать npx, но npm уже старенький. Напоминаю, что проверить версии можно с помощью npm(node) -v

npx create-react-app app-folder-name

Это опять же создаст проект в «app-folder-name» по текущему пути консоли.

Удобство npx состоит в том, что он позволяет как запускать локально установленные версии инструментов (модулей), так и использовать «одноразовые» инструменты вроде create-react-app без их установки в глобальную директорию. Что в последнем случае очень удобно — не приходится следить за версией названных инструментов.

И.. собственно все, в этот мы справились куда быстрее, не так ли? Мы можем запустить вебпак через yarn start и начать создавать!

Но это скучно.

Поэтому, приготовься покинуть кабину, мы катапультируемся!

(Я знаю, что eject это не совсем катапультироваться, но тем не менее)

create-react-app это очень базовый, стандартизированный и проверенный способ начинать работу с реактом, поэтому в нем нет всех тех прикольных штук, которые тебе возможно захочется использовать. К примеру последний писк моды и ES8 с компиляцией от бейбла, или немногословный и немножко страшный, но такой удобный stylus, или тебе просто, как и мне, интересно поковыряться и узнать как они все это настроили.

Поэтому без промедления (и находясь в папке с созданным проектом):

yarn eject

Ах, сколько новых папочек и файлов

К слову, если ты (как любой адекватный человек, да-да) держишь проект в гит-репо, c-r-a не даст тебе катапультироваться без сохранения последних изменений, что очень мило с их стороны, на случай если тебе не понравится опыт досрочного кхем выпуска.

Что же, разберемся по-подробнее с появившимся разнообразием конфигурационных файлов. В особенности меня сейчас интересует configs/webpack.config.dev.js — он содержит конфигурацию для — yarn start — процесса разработки, а ближайшее время мы будем заниматься только им.
К слову, в нем достаточно много подробных комментариев, так что приглашаю заглянуть в него вместе, не дожидаясь моих пояснений.
Также помимо самого файла конфигурации, может быть полезно заглянуть в configs/paths.js — он содержит распознаватель путей и непосредственно их самих — полезно примерно прикидывать что куда указывает.

module.exports = {
  entry: [
    require.resolve('react-dev-utils/webpackHotDevClient'),
    paths.appIndexJs,
  ],
  output: {
    pathinfo: true,
    filename: 'static/js/bundle.js',
    chunkFilename: 'static/js/[name].chunk.js',
    publicPath: '/',
  },
  resolve: {
    modules: ['node_modules', paths.appNodeModules].concat(
      process.env.NODE_PATH.split(path.delimiter).filter(Boolean)
    ),
    extensions: ['.web.js', '.mjs', '.js', '.json', '.web.jsx', '.jsx'],
  },
  module: {
    strictExportPresence: true,
    rules: [
      {
        test: /\.(js|jsx|mjs)$/,
        enforce: 'pre',
        use: [
          {
            options: {
              formatter: eslintFormatter,
              eslintPath: require.resolve('eslint'),
            },
            loader: require.resolve('eslint-loader'),
          },
        ],
        include: paths.appSrc,
      },
      {
        oneOf: [
          {
            test: [/\.bmp$/, /\.gif$/, /\.jpe?g$/, /\.png$/],
            loader: require.resolve('url-loader'),
            options: {
              limit: 10000,
              name: 'static/media/[name].[hash:8].[ext]',
            },
          },
          {
            test: /\.(js|jsx|mjs)$/,
            include: paths.appSrc,
            loader: require.resolve('babel-loader'),
            options: {
              cacheDirectory: true,
            },
          },
          {
            test: /\.css$/,
            use: [
              require.resolve('style-loader'),
              {
                loader: require.resolve('css-loader'),
                options: {
                  importLoaders: 1,
                },
              },
              {
                loader: require.resolve('postcss-loader'),
                options: {
                  ident: 'postcss',
                  plugins: () => [
                    require('postcss-flexbugs-fixes'),
                    autoprefixer({ ... }),
                  ],
                },
              },
            ],
          },
          {
            exclude: [/\.(js|jsx|mjs)$/, /\.html$/, /\.json$/],
            loader: require.resolve('file-loader'),
            options: {
              name: 'static/media/[name].[hash:8].[ext]',
            },
          },
        ],
      },
    ],
  },
};

Я, как можно заметить, обрезал комментарии и некоторые слишком уж специфичные элементы оптимизации или работы вебпака, с которыми шансы столкнуться приближается к скромному нулю (плюс плагины, опции загрузчиков и бонусы для react-native).

Как можно сразу заметить, entry и output слегка отличаются от нашего самопала, однако большей частью это лишь косметические изменения:

entry может принимать не единственную строку, как в нашем случае, а массив или даже объект строк (и массивов, вложенность, ура), в данном случае помимо нашего приложения вебпак просто обрабатывает сервер для разработки и набор заполнителей выбранный c-r-a; output же использует несколько доп опций:
pathinfo — включает в конечную сборку данные о путях, модулях и прочем,
publicPath — позволяет указать корень выхода — папку, в которую помещаются все сгенерированные файлы, мы не использовали его, так как наш проект содержал всего один файл
chunkFilename — указывает как называть чанки — куски кода оптимизированные для отдельной загрузки и кеширования.

В свою очередь resolve (за исключением страшной конструкции сбора путей и плагина) остался таким же, немного расширив набор поддерживаемых путей и расширений.

И наконец module. Помимо самих загрузчиков, присутствует один параметр strictExportPresence — он указывает вебпаку расценивать не найденный модуль (импорт) как ошибку и сразу же прекращать дальнейшую сборку, что очень полезно, так как сразу же кричит о проблеме, не дожидаясь перезагрузки пака и порой, если используется роутер или компонент загружается не сразу, попадания на пропущенный импорт.

С загрузчиками, впрочем, все достаточно просто и знакомо (спасибо вебпаку за стандартизацию конфигурации):
Первым идет eslint — линтер для JS. Линтер — это «инструмент статического анализа» — или говоря по-человечьи, подсветка ошибок и несоответствий выбранному набору правил написания кода. Очень полезный инструмент, который, что особенно приятно, как правило доступен в виде расширения для редактора.
При этом загрузчик линтера использует специальный модификатор enforce — он нужен, чтобы линтер всегда обрабатывал исходный код до его преобразования бейблом. Модификатор (параметр) enforce задает категорию порядка применения загрузчика, всего доступно 4 опции (pre, inline, normal, post), о которых можно почитать здесь.

После линтера идет основной набор, объединенный через модификатор oneOf — он позволяет обработать файл только одним (первым подходящим) загрузчиком. В конце находится загрузчик просто предоставляющий доступ к файлу, не затрагивая при этом исходный код и стандартные медиа-форматы — png, jpg, т.д — за счет их расположения выше по списку и соответственно порядку загрузки.

На чем наш расковыряный файл конфигурации заканчивается. Я, опять же, мягко указываю тебе покопаться в конфигурации самостоятельно и, к примеру, добавить поддержку любимого css препроцессора или пресета для бейбла. Процесс достаточно простой, и я буду разбирать его в будущем, когда приложению понадобится стилизация (а делать ее на css или тем более встроенных стилях я отказываюсь), но всегда полезно попробовать самостоятельно (имея бекап), прежде чем идти читать руководство.
Также должен заметить, что учитывая базовость и частое нежелание катапультироваться из c-r-a, создали кучку способов изменения или расширения конфигурации, оставаясь технически с c-r-a. Но я сам больше склоняюсь к eject-у, нежели использованию таких инструментов.

На сим я оставляю тебя, и даже могу пообещать, что в следующий раз мы наконец займемся самим реактом.

Разбираемся с React(Webpack)-ом [Pt. 1 — Manual React Installation (and pain of doing so)]

Что вообще такое React(.js)?

По уверениям создателей это просто быстрая, декларативная библиотека для создания пользовательских интерфейсов, и технически это сущая правда, однако.. насколько я заметил, реакт (как и многие подобные фреймворки) стал практически синонимом SPA (Single Page Application) — одностраничным приложениям, которые загружаются один раз и дальше работают без фактической перезагрузки страницы.

И это большей частью хорошо, не учитывая какой вес дополнительных библиотек и страшных слов тянет за собой эта аббревиатура. (Да-да, я знаю насколько многие боятся redux, не говоря о redux-saga, которые бок о бок частенько ковыляют на просторах вакансий React.js разработчиков). Впрочем, все не так страшно(, и часто очень даже приятно). Поэтому пристегиваемся и отправляемся в путь, ну а чтобы поездка прошла легко, начнем со слона аккуратно прячущегося на заднем плане.

Начало работы с React.js

Есть два (с половиной) основных способа начать использовать реакт, не учитывая банальную загрузку готовой библиотеки, но этот способ не для нас.

Первым и уже практически ставшим стандартом, является использование утилиты create-react-app, которая создаст базовую установку для практически мгновенного начала работы со стандартным и привычным набором инструментов.

Половиной подхода я бы назвал стареющую альтернативу — поиск шаблонов «по параметрам» на гитхабе или же просторах интернета, но этот способ немного сложнее и заставляет рассчитывать на сознательность индивидуала или команды, а не целой группы разработчиков присматривающих за официальным инструментом (чему тоже не всегда стоит полностью доверять, но это уже на гранях паранойи).

Однако, мы пока не станем пользоваться их помощью, и пройдем упрощенную версию их пути сами, наткнувшись на многочисленные камни и набив шишек по дороге и настругав мозолей. Да вебпак, я помню насколько весело было разбираться с твоей конфигурацией в ранних версиях.

Что же нам нужно?

Внимание, путь дальше очень страшен и труден, придется столкнутся с терминалом или командной строкой!
Существует альтернативный простой путь, но я все же рекомендую пройти базовую подготовку и «боевое крещение».

Node.js и npm, а для любителей котиков и чуть более упорных людей — еще и yarn

Процесс установки обоих достаточно прост и стандартизирован для главных ОС. После установки можно убедиться в успешности введя в терминал или командную строку:

node -v
npm -v

И для вышеназванных любителей котиков:

yarn -v

Если все команды успешно выполнились и вывели ожидаемые версии — успех! Первый шаг сделан.

Для чего нам Node.js?

Во-первых — простота и удобство добавления библиотек.
Во-вторых — сервер разработки на будущее, избавляющий от необходимости держать локальный apache, nginx либо альтернативы (не говоря о прелестях запуска файлов напрямую в браузере).
В-третьих и несерьезно — «это ж модно».

Следующим шагом нам нужно установить сам реакт и пару сопутствующих инструментов:

npm(yarn) init
Круглые скобки подсказывают взаимозаменяемость команд или флагов

Это создаст специальный файл package.json, в котором хранится описание, версия и набор необходимых для приложения библиотек. Во время инициализации скорее всего придется ответить на пару вопросов о приложении.

npm i[nstall] -S(--save) react react-dom либо yarn add react react-dom
Квадратные скобки подсказывают полное название команды или флага

Это должно установить библиотеки react и react-dom, обе необходимые для работы реакта в браузере. И технически мы могли бы начать работу (на некоторых версиях реакта) прямо сейчас, используя src="node_modules/react/dist/..." но это не очень удобно и совсем не то, как мы будем в будущем работать.

Следующим шагом будет установка специального инструмента — «переводчика» (transpiler) или же просто компилятора babel. Он нужен главным образом из-за использованного в реакте языка JSX, который представляет собой XML с вписанным в атрибуты обычным JavaScript-ом. О нем мы еще позже подробно поговорим, но просто знай, что он помогает упростить жизнь, переводя более современный и удобный вариант JS в поддерживаемый повсеместно (и в некоторых областях сильно устаревший) JS.

Мы могли бы установить инструмент babel-cli, который позволяет в реальном времени преобразовывать код, однако, в мире мало кто использует его таким образом для чего-то серьезнее прототипа или приложения на вечер. Поэтому помимо babel мы добавим еще одно некогда страшное слово в список библиотек — webpack.

Webpack позволяет собирать сложные проекты из множества файлов-модулей и используемых библиотек в один большой исполняемый файл, который можно загрузить на страницу.
(При необходимости можно собирать несколько небольших исполняемых файлов и используемые библиотеки выносить отдельно, но это уже вопрос оптимизации процесса сборки)

npm i -D(--save-dev) webpack@3.11.0 babel-loader babel-core babel-preset-react-app
либо
yarn add -D(--dev) webpack@3.11.0 babel-loader babel-core babel-preset-react-app
-D или --dev это флаг для записи библиотек в условный раздел devDependencies нашего package.json. Условный он потому, что для запуска приложения нам не нужны и зависимости из dependencies — они будут собраны в готовый файл в будущем. Однако это полезное разделение на относящиеся непосредственно к работе приложения модули и просто необходимые для сборки (и желательно тестирования) инструменты.

Это загрузит непосредственно webpack и babel. Важный момент — проверь, что вебпак установился нужной версии. На протяжении его разработки было несколько критических изменений в структуре файла конфигурации, которые отказывались от обратной совместимости и нужно было обновлять файл конфигурации. Если у тебя установится версия, которая внесла подобные изменения в отношении к указанной, предложенный далее файл конфигурации может не работать. Плюс конфигурация вебпака была в некоторой мере «неодназначной» и в связи с обилием примеров на просторах интернета под прошлые версии — сломать что-то и гадать, почему оно не работает, было очень легко.
Сейчас ситуация стала куда лучше, но избежать возможных проблем на первых шагах все же не помешает.

Что же заглянем на страницу документации настройки вебпака и.. да…

Но, к счастью, большая часть настроек нам не понадобиться в ближайшее (и, скорее всего, дальнейшее) время, однако знать, куда попробовать заглянуть в случае чего — весьма полезно. Полный процесс настройки я, возможно, распишу как-нибудь в другой раз, сейчас же хватит простенького конфига для сборки базового приложения на реакте. (Мы почти готовы писать код спустя пол часа подготовки!)

@webpack.config.js
var path = require('path');
var webpack = require('webpack');

module.exports = {
    entry: './index.js',
    output: {
        path: path.resolve(__dirname, 'public'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /\.jsx?$/,
                loader: 'babel-loader',
                options: {
                    presets: ['react-app']
                }
            }
        ]
    },
    resolve: {
        modules: [path.resolve(__dirname, 'node_modules')],
        extensions: ['.js', '.jsx']
    },
};

Для начала что такое require и module.exports — это функционал по соответственно импорту и экспорту модулей и библиотек. Он начал свой путь с библиотек вроде RequireJS, потом перекочевал в Node.js и JS как расширенная часть языка и наконец был принят в стандарте ES6. (Или как-то так, я не слишком хорошо осведомлен о истории модулей).

Далее непосредственно конфиг:
entry — файл, с которого начинать сборку, с него по сути начинает работу приложение — загружает при необходимости библиотеки (реакт, к примеру) и что-то делает со страницей.
output — путь и название конечного файла (или файлов) в которые соберется весь необходимый код приложения.
module — «логика» (rules) вебпака — какие файлы обрабатывать, через какие загрузчики (loader) их пропускать, какие параметры передать этим загрузчикам и так далее.
— — test — регулярное выражение (как правило) по которому проверять, какие файлы должен обрабатывать этот загрузчик
— — loader — непосредственно название библиотеки загрузчика
— — options — набор параметров, который нужно передать данному загрузчику, каждый загрузчик принимает свой собственный набор параметров
resolve — какие пути (modules) проверять при импорте модулей в коде и какие расширения файлов считать импортируемыми (extensions) таким образом. Это немного своеобразный элемент, так как он (должен) работать только для импорта библиотек, но можно настроить подгрузку своих файлов, чтобы не нужно было прописывать глубоких путей наверх в некоторых случаях. Это своеобразный вопрос конфигурации и я, возможно, разберу его при необходимости в будущем.

И.. на этом все с настройкой. Теперь мы наконец можем начать создавать наше приложение!
Но, отставив шутки, если ты высидел и прошел весь этот путь, постаравшись разобраться что и зачем нужно — ты герой. Многих пугает современная разработка на JS большой частью из-за всего этого скопища полезных и порой необходимых инструментов, которая позволила удобно преобразовать простенький скриптовый язык в что-то чуть более серьезное и уверенное. (Ну или демонического монстра, разрушающего будущее веб-разработки, кому что больше по душе)

Но, мы еще не закончили, последний шаг.

Hello World with React.js (and bunch of not so fancy stuff)

Создадим два пустых файла index.html и index.js (который мы указали в entry конфига вебпака). В body html файла нужно добавить две строки:

<div id="root"></div>
<script src="bundle.js"></script>

Элемент div нужен как контейнер (или обертка) для указания реакту, куда вставлять (render) разметку его компонентов. Скрипт же очевидным образом загружает bundle.js, который мы сейчас соберем вебпаком из index.js. Переходя к которому:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

const StateLessComponent = ({text}) =>
<div>
    <span>{text}</span>
</div>;

class StateFullComponent extends Component {
    state = {
        text: 'Heya Earthlings!'
    }
    render(){
        return (
            <StateLessComponent text={this.state.text}/>
        );
    }
}

ReactDOM.render(<StateFullComponent/>, document.getElementById('root'));

И примерно так выглядит весь React-код. Конечно же, разметки, логики и финтифлюшек в реальной жизни куда больше, но база остатется той же. Смотрим снизу-вверх:

ReactDOM.render(JSXComponent, DOMNode) — вставляет (монтирует) собранный реакт(jsx)-компонент в указанный элемент DOM-а страницы. То есть, мы находим какой-то элемент (контейнер) на реальной странице и вставляем в него наш компонент (или дерево вложенных компонентов, как в нашем скромном случае).

class StateFullComponent extends Component — это ES6 синтаксис-обертка для создания функции-конструктора и по совместительству в реалиях реакта — полноценный компонент.
Полноценный означает что он может, помимо простого отображения разметки (метод render), иметь некую логику (свои методы\функции), иметь состояние (state) и использовать некоторые «события» реакта. (lifecycle hooks, технически это не события, но тем не менее, практическая суть достаточно близка).
О всех этих несомненно интересных словах мы поговорим, уже разбирая непосредственно внутренности реакта.

Как можно догадаться, есть и «неполноценные» или же как их чаще называют — функциональные компоненты — они могут только отображать разметку и переданные данные (включая возможность привязывать обработчики событий). Чем, собственно и является:

const StateLessComponent = ({text}) => <div>...

(arguments) => (returnedValue) это небольшое но приятное дополнение ES6 имеющее, помимо некоторых очень нужных и важных свойств, возможность записывать функции в коротком виде.

Разделение на statefull и stateless, «умные» и «глупые», контейнеры и компоненты мелькает очень часто, однако простая теоретическая суть — умные имеют логику, данные, и всякое и раскидывают это все по глупым, которые занимаются только отображением — очень часто не суть реальная. Подходов и способов работы со всей этой кутерьмой множество и основную часть мы рассмотрим, как выше было сказано, ковыряя сам реакт.

Последние строки:

import React, { Component } from 'react';
import ReactDOM from 'react-dom';

Это уже названный выше функционал модулей в ES6 версии JS — он немного отличается от использованного в конфиге вебпака из-за того, что даже Node.js еще не начал полноценное использование этого стандарта.
(Насколько я знаю*. Достаточно скоро или уже Node.js может полностью перейти к этому стандарту, но до тех пор там используется CommonJS версия импорта и экспорта — require() и module.)

Помимо синтаксического отличия я использовал именованный импорт — он позволяет запросить только нужные части из модуля, без всего модуля.
И опять же требуется пометка — насколько я знаю, на данный момент для такого (реально) частичного импорта нужно использовать «обрезание» кода и дерева модулей — «dead code elimination» и «tree shaking», по этим словам ты скорее всего найдешь информацию, если тебя интересует вопрос оптимизации размеров твоего приложения. Я же, возможно, в будущем сам расскажу подробнее про эти возможности, но учитывая какой уровень дополнительной сложности вносит только базовый вебпак, оптимизация — это разговор на другой раз.

Поэтому мы наконец завершаем наше затянувшееся путешествие, во время которого мы успели
1. [Поставить Node.js, npm и, опционально, но мягко рекомендуемо, yarn.](#Что же нам нужно?)
2. Создать пустой проект через yarn init и добавить в него реакт через yarn add
3. Немного разобрать что есть babel и webpack и зачем они нужны
4. [И даже написать первые строки приложения на реакте](#Hello World with React.js (and bunch of not so fancy stuff))

На этом я ненадолго оставляю тебя с, надеюсь, немного подкипевшей головой и списком материала на вечернее чтение:
npm, yarn, webpack, babel и сам реакт
Это сайты документации или помощи инструментов, которыми мы будем очень часто пользоваться на пути создания реакт приложений и я, как бы не хотел, не смогу ответить на все вопросы — иногда придется расчехлить навыки поиска и покопаться в официальных источниках (ну или заглянуть на stackoverflow\подобные) в поисках нужного ответа.

GraphQL + Apollo + Express = ? [Pt. 1 — Installation adventure]

Заглянув таки пару минут назад на страницу с установкой аполло, я слегка ужаснулся..
тому, насколько давно я с ним не сталкивался.

Я как-то работал на проекте, у которого уже был базовый функционал и работал граф(кюэль? я продолжу называть GraphQL — граф а GraphiQL — графи, так проще, не спрашивай). Это был недолгий опыт, но именно тогда я по-настоящему влюбился в граф (и аполло) и все ждал возможности поиграть с ними еще немного.

Такая возможность наконец пришла и я оказался на удивление смел и решил создать пустенький проект самостоятельно, вместо привычного поиска готового шаблона.
(Спойлер: это оказалось приятной ошибкой и теперь я пишу эту запись, чтобы не повторять ее)

Конечно же я помнил все необходимые библиотеки (express, apollo-server, duh) и быстро установив их задумался, «А как его вообще подключать?». По дороге я вспомнил, что мне слишком лень настраивать еще и клиента и будет неплохо подумать над будущей моделью имея интерактив графи без необходимости начинать корячить само приложение. Оказавшись в тупике я ушел в гугл и быстро вернулся со страничкой гитхаба, где было описано, как подключить графи (с самим аполло я же влегкую справлюсь, да?)

Первая проблема бросилась в глаза быстро — аполло ставится под конкретный фреймворк (surprise!) а чистый apollo-server скорее всего предназначен для чистого Node.js сервера либо кастомной настройки. Откатываем установку apollo-server, ставим apollo-server-express…

И вспоминаем что синтаксис запуска экспресса я с коленки тоже не помню… Ладно, гугл, сайт экспресса, ctrl+c, ctrl+v.

Так, экспресс поставили, графи подключили, кажется все?

Нет.. нужно запустить сам граф, чтобы было что проверять (surprise! x2). Процесс знаешь? Не угадал, страница гитхаба про аполло и графи лежит в репо самого аполло и.. прямо на главной странице для самых умелых они разместили полный шаблон для старта экспресс-сервера с аполло, графи и граф-ом (оригинал можно найти по ссылке). Моя полноценная копия:

[cc lang=»javascript»]
import express from ‘express’;
import bodyParser from ‘body-parser’;
import { graphqlExpress, graphiqlExpress } from ‘apollo-server-express’;

const app = express();

app.get(‘/’, (req, res) => res.send(‘Bye-bye World!’))

app.use(‘/graphql’, bodyParser.json(), graphqlExpress({ schema: myGraphQLSchema }));
app.get(‘/graphiql’, graphiqlExpress({ endpointURL: ‘/graphql’ }));

const port = 81;
app.listen(port, () => console.log(‘Example app listening on port ‘ + port))
[/cc]

yarn start — and fail.
Мораль сей басни? Мы (я) забыли схему, точнее просто отвлеклись на погоню за облаками.

Импортируем будущую схему и собираем ее через небольшую утилиту (или пишем ручками, кому как нравится):

[cc lang=»javascript»]
@index.js
..
import myGraphQLSchema from ‘./schema’;

@schema.js
import { makeExecutableSchema } from ‘graphql-tools’;

import typeDefs from ‘./types’;
import resolvers from ‘./resolvers’;

const schema = makeExecutableSchema({
typeDefs,
resolvers,
});

export default schema;

@types.js
const Recipe = `
type Recipe {
id: Int!
message: String
author: String
}
`;

const Query = `
type Query {
recipies: [Recipe]
}
`

export default [Query, Recipe];

@resolvers.js
export default {
Query: {
recipies: () => [’empty’,’empty’,’empty’,]
}
}

[/cc]

Все? Все, не учитывая ленивый и пустой обработчик (resolver), но это на будущее.

Что мы сегодня узнали? Дети, используйте готовые шаблоны или придется писать технически бесполезные блог-посты. Ситуация не так уж и пичальна, впрочем — экспресс запущен, аполло «настроен» и все практически работает.

Ссылки на документацию для самых интересующихся (и тех, кто использует koa, hapi, restify, lambda, micro, azure-functions, adonis и хочет особенного отношения):
Apollo main doc
Apollo GraphiQL
How to Apollo GraphQL Schema
Express blank (Bye World) app

React-Saga «продвинутый» пример.

За время отсутствия записей я какое-то время посвятил созданию небольшого приложения-таймера, логику которого я реализовал практически полностью с помощью саг.

And that was a pleasant walk in a lush park, I must say.

Приступим:

export const types = {
    'start': 'StartTimer@Front',
    'stop': 'StopTimer@Front',
    'pause': 'PauseTimer@Front',
    'resume': 'ResumeTimer@Front',
    'stopped': 'TimerStopped@Saga',
    'timeUpdated': 'TimerUpdated@Saga',
}

Для начала действия: всего пользователь может отправить 4 действия (они помечены сферой @Front[end]) — старт и стоп таймера, чтобы начать его выполнение с самого начала, или пауза-продолжение — чтобы соответсвенно приостановить, без сбрасывания счетчика, и продолжить его выполнение. Также есть 2 действия (@Saga), которые производит сама сага: стоп, чтобы оповестить редуктор и сам таймер что пора остановиться и обнулить состояние, и обновление текущего времени.

Можно заметить, что я как и ранее работаю вопреки принятому стандарту в виде капитализированных констант. Также я использую два типа наименования: verb + noun — для действий, которые начинают какой-то процесс, как правило приходящие от пользователя, и noun(gerund) + past verb — для действий, которые должны напрямую влиять на состояние приложения — когда действие уже произошло и нужно оповестить элементы приложения и пользователя.

Feel free to translate it into classic redux types definitions for yourself.

Next stop:

function* ticking(action) {
    try {
        let timeElapsed, pomoTime, restTime;
        while (true){
            const { started } = yield race({
                started: take(types.start),
                unpaused: take(types.resume),
            });
            if (started) {
                pomoTime = started.pomoTime;
                restTime = started.restTime;
                timeElapsed = 0;
            }
            while (true) {
                let timePrev = Date.now();
                const { cancelled } = yield race({
                    ticked: call(delay, Math.round(Math.random() * 8) + 16),
                    cancelled: take([types.pause, types.stopped]),
                });
                if (cancelled)
                    break;
                timeElapsed += Date.now() - timePrev;
                if (timeElapsed > (pomoTime + restTime) * 1000)
                    yield fork(stopTimer);
                yield put({ type: timeUpdated, elapsed: timeElapsed });
            }
        }
    } catch (e) {
        yield put({type: "Welp, we did it"});
    }
}

Нас встречает уже знакомая конструкция:

const { started } = yield race({
    started: take(start),
    unpaused: take(types.resume),
});

Для запуска таймера мы ожидаем одно из двух действий — он только начал работу (types.start) или продолжил с паузы (types.resume). При этом в первом случае мы берем себе действие, чтобы знать о необходимости обнулить состояние таймера.

if (started) {
    pomoTime = started.pomoTime;
    restTime = started.restTime;
    timeElapsed = 0;
}

Помимо взятия данных с действия (чего делать не стоит, но на момент написания кода я не удосужился ознакомиться с одним из эффектов saga) мы обнуляем текущее значение таймера из настроек.
Я остановился на этом моменте для одного уточнения — у saga есть вспомогательная функция select, которая позволяет получить текущее состояние приложения.

Текущее с поправками — из-за принципа работы saga select выполняется после обработки действия редуктором, если он конечно есть, что может привести к неожиданному результату при получении данных. Подробнее об этом.

let timePrev = Date.now();
const { cancelled } = yield race({
    ticked: call(delay, Math.round(Math.random() * 8) + 16),
    cancelled: take([pause, types.stopped]),
});

race2; it was, yeah it was until 63aa3….

Мы создаем «гонку», чтобы дать возможность пользователю остановить таймер. Элемент случайности добавлен сугубо для эстетического эффекта — цифры увеличиваются в хаотичном порядке, а не одинаковыми множителями в 8\16\33 миллисекунды.
Завершение парада:

if (cancelled)
    break;
timeElapsed += Date.now() - timePrev;
if (timeElapsed > (pomoTime + restTime) * 1000)
    yield fork(stopTimer);
yield put({ type: timeUpdated, elapsed: timeElapsed });

Если мы получили остановку — выйти из цикла работы таймера while(true). Я использую Date.now() вместо простой арифметики для капельку большей точности — если планировщик saga пропустит пару тиков из-за пика загрузки у пользователя, таймер продолжит работать с относительной точностью. Это сугубо экспериментальный ход, так как я не тестировал ни эффективность планировщика saga, ни работоспособности и точности таймера при пиковых нагрузках.

В конце мы, в случае превышения установленного пользователем времени, завершаем работу таймера самостоятельно или просто отчитываемся редуктору, что время на таймере чуть-чуть увеличилось.

Я использую fork из-за одной интересной особенности кода и планировщика саг — если просто использовать call и остановить таймер, то сигнал о остановке может придти слишком рано и тогда наш таймер его пропустит, заметит что время позднее и надо останавливать уже во второй раз, пропустит сигнал снова и снова попытается остановится — и таким образом повесит всю страницу.
Поэтому приходится использовать небольшую задержку — достаточно большую, чтобы все эффекты saga успели отправиться, но достаточно маленькую, чтобы таймер не успел подумать что его никто не будет останавливать (16-24 мс в нашем случае).
Как часто бывает, я уверен что есть более элегантное решение этой проблемы, но на момент написания статьи я его не придумал, если знаешь чем можно помочь — добро пожаловать в комментарии или репо.

function* stopTimer() {
    yield delay(5);
    yield put({type: timeUpdated, elapsed: 0});
    yield put({type: types.stopped, finished: true});
}

Таким образом мы можем быть уверены, что таймер будет готов выслушать нас и остановиться по types.stopped.

Что же, на этом пока все, весь код доступен в репо, а на рабочий экземпляр посмотреть (и сломать) можно на этой страничке.

Эффекты и саги. React + Redux-saga. [Pt. 3]

Я должен признаться, я был не совсем откровенен, называя call или put вспомогательными функциями (и насчет еще некоторых моментов, но забудем пока про это).

call, put и многие другие являются фабриками Эффектов (Effect) — специальных объектов, напоминающих действия (action) из redux, только предназначенные для самой библиотеки saga.

Почему эффекты? Я бы сказал та же причина, что и у самого редакса — простота, плоскость и тестируемость — за счет мгновенного возвращения простого объекта эффекты позволяют тестировать только код генератора, без необходимости ожидать выполнения запросов или их подмены.

Как я уже упомянул, эффекты сильно напоминают собой действия:

{
  CALL: {
    fn: log,
    args: [42],
    context: null
  }
}

Так, к примеру, выглядит возврат из call (немного обрезанный, но тем не менее). Как видно «тип» эффекта прописан ключом объекта.

Впрочем к чему весь этот разговор — эффекты, вспомогательные функции или фабрики, какая разница?

Знакомьтесь, наша новая фабрика take:

function* typing(action) {
    try {
        yield put({type: typingStart});
        yield take(typingStopped);
        yield put({type: typingEnd});
    } catch (e) {
        yield put({type: "Welp, we did it"});
    }
}

После написания этого примера, я начинаю понимать, насколько

В любом случае, фабрика («вспомогательная функция» мне нравилось больше) take позволяет дождаться действия редакса изнутри обработчика. Применения? Бесконечны! О чем и говорит мой креативный пример.

Как? Мы же в мире генераторов. Исходный код я, конечно же, не смотрел, но предполагаю что saga перенаправляет вызов .next нашего обработчика-генератора на появление указанного действия и «БАМ!» совершенно магическим образом мы получаем это действие внутри обработчика как только оно появляется благодаря dispatch-у.

На самом деле это очень удобный инструмент для поддержания нашей иллюзии синхронности — мы можем взять любое действие — то же начало ввода пользователем — сделать что-то — изменить интерфейс, к примеру — и продолжить выполнение саги уже после другого действия. Более привычный подход заключается в разделении логики на два обработчика — вход в состояние по первому действию и выход из него по второму (более сложная десятистраничная логика приветствуется с распростертыми объятиями).

Однако, текущий вариант не слишком отличается от классического подхода и достаточно бесполезен в чистом виде, но это не конец. saga позволяет помимо прочего, использовать элементы «многопоточного» программирования — fork или race, помимо некоторых других.

Мы даже уже использовали all, который позволяет дождаться выполнения всех эффектов (обещаний) и получить все результаты в один массив. В saga есть его близкий родственник — race. Они похожи, только вот race ждет завершения любого из эффектов и возвращает объект с именованным результатом:

function* typing(action) {
try {
    while (true){
        yield take(textUpdated);
        yield put({type: typingStarted});
        while (true){
            const {updated, timeouted} = yield race({
                updated: take(textUpdated),
                timeouted: call(delay, 1000),
            })
            if (timeouted) {
                yield put({type: typingEnded});
                break;
            }
        }
    }
} catch (e) {
    yield put({type: "Welp, we did it"});
}
}

Как ни пугающе, но у нас теперь два бесконечных цикла (и скорее всего способ переписать это в более презентабельном виде).

Первый цикл нужен для прослушивания действия textUpdated. Как только мы его получили, можно вступить во второй цикл, перед которым мы отправляем в редакс сообщение, что пользователь начал печатать. Внутри мы создаем условие гонки — или пользователь продолжает печатать — updated— и мы ничего не делая перезапуская внутренний цикл, или проходит 1 секунда — delay(1000[ms]) — и мы отправляем сообщение, что пользователь перестал печатать и выходим во внешний цикл прослушивания.

Почему это лучше?

Несколько причин:

  1. Нам больше не нужно такое в компоненте:
start="1.">
clearTimeout(this.typingTimeout);
this.typingTimeout = setTimeout(() => typingStopped(), 1000);

И нам даже не нужно вмешиваться в обработчик в компоненте — он точно так же просто продолжает создавать действие, что был введен новый текст, и ничего более.

  1. Код отвечающий за состояние печатания выглядит проще (за исключением двух циклов while(true) которые все же немного портят картину) и находится в одном единственном месте.
  2. Нам не нужна еще одна библиотека чтобы сделать задержку или отложенное действие. К примеру, если использовать all вместо race то в результате у нас получится массив со всеми собранными за секунду действиями — очень напоминает буфер из RxJS и подобных. Комбинируя несколько простых эффектов можно получить неплохой набор часто используемых инструментов.

start="2.">

Почему нет?

Я не уверен, есть ли у меня ответ. Избавление от таймера и вообще любых помех в теле компонента выглядит слишком привлекательно для меня на данный момент. Если у тебя найдутся возражения или предложения того, как это лучше реализовать, всегда готов выслушать.

На этом, я думаю, наш небольшой цикл уроков по redux-saga завершается. Осталось пару фабрик и вспомогательных функций, которые мы не разобрали, вроде fork, join или cancel, однако они имеют схожее поведение и очень похожи на свои аналоги из мира многопоточного программирования. Если нужда в их разборе появится, то мы несомненно этим займемся.

Опять же — код доступен в репоа документация по сагам на их сайте.

Отправка параметров и несколько одновременных запросов. React + Redux-saga. [Pt. 2]

В прошлой части  мы узнали о сагах, почему их можно использовать вместо альтернативного подхода внедрения запросов прямо в тело компонента и как создать простое действие с помощью саг.

Но это скучно.

Ведь мы все хотим совершать десятки запросов по несколько мегабайт каждый. Приступим.

function* fetchQuote(action) {
    try {
        const quote = yield call(requestYodified, action.text);
        const spellcheckedQuote = yield call(spellcheck, quote.data);

        yield put({type: success, quote: quote.data, spellcheckData: spellcheckedQuote.data});
    } catch (e) {
        yield put({type: fail, message: e.message});
    }
}

Как можно заметить, изменения по сравнению с прошлой версией огромны.

Поставив шутки в угол, кроме небольшого переименования переменных, добавлена всего одна строка. При этом мы вернулись к call и даже передали оба раза второй аргумент — переменную, которую мы хоть отдать функции совершающей запрос. Если мы захотим добавить больше данных — передаем больше переменных или оборачиваем все в объект — к чему душа больше лежит.

К сожалению апи от Йоды опять полетел именно в тот момент, когда я попытался проверить работу последовательных запросов.

Ненадежен, Йода, ты же как…

Поэтому почему бы не расширить немного обработку ошибок — к примеру, чтобы совсем не разочаровывать пользователя сообщением о ошибке, мы проверим как у него с орфографией!

...
catch (e) {
    try {
        const spellcheckedQuote = yield call(spellcheck, action.text);
        yield put({
            type: spellchecked, results: spellcheckedQuote.data
        });
    }
    finally {
        yield put({type: fail, message: e.message});
    }
}

Таким образом мы можем, к примеру, вывести результат выполнения части запросов, если это более выгодно по сравнению с полной остановкой из-за ошибки где-то на промежуточном этапе работы. А за время работы над «неудачным» сценарием, Йода поднялся и приготовился к работе — можно отдать ему какой-нибудь текст для перевода в человеческий — к примеру, заголовок нашего приложения «Welcome to React sagas!», спустя несколько долгих секунд раздумий Йода наконец отвечает «To react sagas welcome! Herh herh herh.». Вот и замечательно.

    {
    type: 'QuoteFetchRequest@Front',
    text: 'Welcome to React sagas!'
    }

    {
    type: 'QuoteFetchSucceeded@Saga',
    quote: 'To react sagas welcome! Herh herh herh.',
    spellcheckData: {
        original: 'To react sagas welcome! Herh herh herh.',
        suggestion: 'To react sagas welcome Hersh her her.',
        corrections: {
        Herh: [
        'Hersh', ...
        ],
        herh: [
        'her.', ...
        ] } }
    }

С последовательными запросами мы разобрались — добавить еще десяток-другой в цепочку, чтобы заставить пользователя ждать минутами, дело техники и сокровенной комбинации ctrl+c -> ctrl+v. Осталось научиться делать одновременно много, как некогда почивший салат.

function* fetchQuoteWhilSpellchecking(action) {
    try {
        const [quote, spellcheckedText] = yield all([
            call(requestYodified, action.text),
            call(spellcheck, action.text)
        ]);

        yield put({type: success, quote: quote.data, spellcheckData: spellcheckedText.data});
    } catch (e) {
        yield put({type: fail, message: e.message});
    }
}

Как вновь видно — изменений очень много.

Функция all — очередной вспомогательный элемент saga, позволяет создать несколько запросов и, дождавшись выполнения каждого, продолжить выполнение саги с полученными значениями. Запись вида const [a, b, c] = [...] — альтернатива деструктуризации для массивов, ожидаемо переводит массив вида [3, 2, 4] в три переменные — a = 3, ...

Наяривание до десятка запросов опять же происходит старым, добрым и немого уставшим ctrl+c -> ctrl+v. Но помни, пользователь очень любит ждать и обладает наилучшим железом, чем больше количество и чем толще запросы выше качество запросов — тем лучше.

Совмещение последовательного и параллельного выполнения опять же банально — получив результат параллельной обработки создаем новый запрос, и так далее, смешивая в нужных пропорциях.

В следующем выпуске — Саги: интересные части. Эффекты, отложенное исполнение, комбинирование и предсказуемое ожидание действий.

Код опять же доступен в репо, помимо полного кода, там появился «индикатор» загрузки.

Почему саги? React + Redux-saga. [Pt. 1]

Учитывая, что ты заглянул в запись с таким названием, что такое React уже знаешь. Шутка, не зря сами создатели назвали библиотеку redux-saga, а не react-saga — многие принципы и способ работы базируется вокруг именно редакса, который, является достаточно свободной библиотекой. Однако, чтобы понять принципы работы достаточно знать только о транзакциях (action\effect).

Поэтому, надеюсь ты знаешь что такое Redux.
(Но ничего, даже если React и Redux вызывают только дрожь и страдания, о них тоже будет понятно и доходчиво, как-нибудь в будущем)

Но что же такое (redux-)saga и зачем она нужна? Это, как говорят сами разработчики, попытка упростить вызов изменений в приложении использующем redux (или подобную ему модель в общем случае). Вместе с упрощением приходит и удобство тестирования, лучшая обработка ошибок и в целом более эффективная работа всего приложения.

И вместе с тем дополнительные проблемы для разработчиков, ибо саги хотят генераторы:

function* fetchQuote(action) {
    try {
        const quote = yield call(apiCall, action.data);
        yield put({type: "QuoteFetchSucceeded@Saga", quote: quote});
    } catch (e) {
        yield put({type: "QuoteFetchFailed@Saga", message: e.message});
    }
}

В глаза сразу бросается вопрос — «за что?!».

В глаза сразу бросаются пару моментов: — function* и yield. Впрочем, все достаточно тихо и миро — * просто указывает что это «генератор»(конструктор) генераторов — специальных конструкций (типо функций), которые могут возвращать несколько результатов на протяжении своей жизни, сохраняя свое состояние в месте возврата последнего значения.
(Или как-то так)

К примеру такой простой счетчик:

function* counter(){
    let i = 1;
    while(true)
        yield i++;
}
let count = counter();
var a = setInterval(() => console.log(count.next()), 500)

Как можно заметить, счетчик работает, при том, что у нас нет глобальной переменной с его значением (при желании код вызова генератора можно обернуть в замыкание, closure, избавляясь от инициализрованного экземпляра генератора, или даже полностью переделать через замыкание без генераторов, но это не суть в данном примере). Также можно на практике убедиться, что функция со * это не сам генератор, а «конструктор» — для работы нам нужна конкретная сущность отдельно взятого генератора.

Впрочем, мы не будем углубляться в суть генераторов, подметив лишь интересный для нас момент — конструкция, (объект) созданный генератором, позволяет нам получать промежуточные результаты выполнения, возвращаемые через подобное return-у ключевое слову yield, сохраняя свое внутреннее состояние на протяжении этого процесса. При этом повторять этот процесс можно сколь угодно долго — как видно на примере счетчика, он будет идти бесконечно поскольку у нас нет завершающего условия.

Учитывая эти два элемента — приостанавливаемое выполнение и сохранение при этом состояния исполнения — можно легко понять почему генератор был выбран для написания саг. Сохранение состояний позволяет писать комплексные запросы совершающие несколько действий, а возможно использовать паузу дает чувство ностальгии по великолепному sleep(1).

Что же, после этого короткого посвящения в ужасы прелести генераторов можно вернуться к сагам:

function* fetchQuote(action) {
    try {
        const quote = yield call(apiCall, action.data);
        yield put({type: "QuoteFetchSucceeded@Saga", quote: quote});
    } catch (e) {
        yield put({type: "QuoteFetchFailed@Saga", message: e.message});
    }
}

Первая остановка — yield call() — мы хотим сделать долгий запрос — отправляем функцию на исполнение, а, когда придет результат, продолжаем обработку, за счет возврат результата в quote (в генератор можно отправить что-то, вызвав .next(variable), значение при этом возвращается из yield на котором остановился генератор).

Таким образом мы приостанавливаем работу генератора пока идет долгий запрос, а когда он наконец завершается, saga (библиотека) доброжелательно возвращает нам результат выполнения запроса.

Call — просто вспомогательная функция от библиотеки, которая выполняет переданную в нее функцию сразу же возвращая нам простой объект (Эффект — Effect), что может быть важно, к примеру, при юнит-тестах. Никто не мешает просто напрямую сделать запрос.

Следующая остановка — yield put({type: "QuoteFetchSucceeded@Saga", quote: quote});

А вот это уже похоже на отправку Redux действия (dispatch(action)). Функция put указывает saga что нужно создать действие для редакса — вызвать dispatch с переданным объектом. Ожидаемо действие проходит, передавая с собой «цитату»:

{
    "type(pin)": "QuoteFetchFailed@Saga",
    "quote(pin)": "UnAPI-ed quote text, cause Yoda text converter we wanted to use got bonked and refused to respond with anything other than 503"
}

К сожалению, мы протестировали наш try-catch блок, к счастью — он работает.

Как только мы подходим к put в saga, мы переходим в царство редакса — а на его тему нужны совсем другие уроки. Впрочем, остался еще один «непростой» момент в работе с сагами — выбор селектора.

function* mySaga() {
    yield takeLatest("QuoteFetchRequest@Saga", fetchQuote);
    // vs
    yield takeEvery("QuoteFetchRequest@Saga", fetchQuote);
}

export default mySaga;

Есть два стандартных варианта: takeLatest и takeEvery. Их сигнатуры совпадают (ActionTypeHandler, WorkerGenerator), первое — название действия, которое ты будешь создавать из редакса, второе — генератор, который мы недавно разбирали — функция, которая будет последовательно обрабатывать это действие.

Согласно названиям они имеют важное отличие — takeLatest обработает (возьмет) только результат последнего отправленного запроса, takeEvery — все результаты. Это бывает полезно, когда пользователи спамят кнопки, а тебе слишком лень блокировать кнопку на время запроса.

…И присоединение этого селектора к redux store:

import mySaga from './saga'
// 1.
const sagaMiddleware = createSagaMiddleware()

let composer = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
    reducer,
    composer(
        // 2.
        applyMiddleware(
            sagaMiddleware
        )
    ),
)
// 3.
sagaMiddleware.run(mySaga)

Что же, пора приступить к решающим вопросам: почему saga и почему не saga?

Почему саги

В этом будет легко убедить стоящих горой за TDD, последовательность, читаемость и развернутость — используя саги нужно хорошенько постараться, чтобы написать запутанный и логически сложный код, следуя хоть каким-то стандартам.

Однако, в «правильном» виде требуется отдельный файл под саги, отдельный «рабочий» — обработчик и создатель событий — это опять же увеличивается развернутость и многословность кода; однако, сам редакс грешит этим в такой мере, что добавление еще одного мушкетера к многословной троице скорее всего не станет проблемой, при этом:

Сага решает проблему сложных действий внутри компонента:

handleButtonClick(){
    let data = this.state.variable;
    request.post(API_URL, data)
    //+ установить в состояние что идет работа и возможно произвести другие вызовы к интерфейсу
    .then(response => this.props.responseRecieved(response))
    .catch(errorHandler)
}
connect(
    ...,
    ..responseRecieved(data) => dispatch({type: RESP_TYPE, data})
)()
@render
<button onClick={handleButtonClick}>Press me!</button>

// или снося вес в действие
connect(
    ...,
    ..requestStarted(data) => (dispatch) => {
        apiCall(data)
        .then(response => dispatch({type: REQ_TYPE, data}))
        .catch(errorHandler)
    }
)
@render
<button
    onClick={
        () => this.props.requestStarted(this.state.variable)
    }
>Press me!</button>

После скромного опыта с сагами мне как-то.. некомфортно было это писать, мне даже кажется что с thunk и запросами в компоненте это делается по-другому…

Альтернатива с использованием саги:

@render
<button
    onClick={() => this.props.requestStarted(this.state.variable)}
>Press me!</button>

connect(
    ...,
    ..requestStarted(data) => dispatch({type: REQ_TYPE, data})
)

@saga
function* requestData(action) {
    try {
        const result = yield call(apiCall, action.data);
        yield put({type: RESP_TYPE, quote: quote});
    } catch (e) { errorHandler(e) }
}

function* reqSaga() {
    yield takeLatest(REQ_TYPE, requestData);
}

Почему не саги

  • Генераторы это «страшно»
  • Многословность и лишний файл, от этого никак практически не избавиться.
  • Это лишняя библиотека.

Буду ждать больше причин за и против в комментариях, ну а я, если в будущем меня столкнет с Redux, точно возьму с собой и Saga.

Помимо показанного и поверхностного использования саг для более «простого» вызова действий в редаксе, ты можешь почитать о их внутренней работе, многих вспомогательных функциях и способах обработок в официальной документации или наших последующих уроках.

Исходный код полностью доступен в репо, история коммитов представляет порядок создания приложения вместе с написанием статеек.