Блог на ReactJS #3. Redux, Middleware, json-server, fetch

Этот урок входит в цикл "Блог на React". В предыдущем уроке мы добились компиляции стилей в один файл и освоили props (jsfiddle). Осталось самое сложное, но и самое интересное - настроить mock-сервер и научится с ним общаться. Что такое Mock-сервер? C английского Mock server - ложный/фиктивный сервер. Такой сервер отвечает точно так же, как и реальный бэкенд, но в нём нет своей логики. Возьмём json-server - это хороший мок-сервер, требующий минимум настроек и работающий в соответствии со спецификацией REST. npm install json-server --save-dev И напишем для него файл, служащий исходной базой данных: db.json
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
{
  "posts": [
    {
      "id": 0,
      "image": "/images/1.jpg",
      "name": "How much countries in Africa",
      "text": "It contains 54 fully recognised sovereign states (countries), nine territories and two de facto independent states with limited or no recognition."
    },
    {
      "id": 1,
      "image": "/images/2.jpg",
      "name": "How big is Africa?",
      "text": "It covers 6% of Earth's total surface area and 20.4% of its total land area"
    },
    {
      "id": 2,
      "image": "/images/3.jpg",
      "name": "What the largest country in Africa?",
      "text": "Algeria is Africa's largest country by area, and Nigeria is its largest by population"
    },
    {
      "id": 3,
      "image": "/images/4.jpg",
      "name": "When comes winter in Africa?",
      "text": "Winter in Africa starts 21 june, and ends after 23 september. Temperature can change from  2C to 26C during the winter according to the location"
    }
  ]
}
Теперь добавим запуск сервера в package.json, чтобы потом просто выполнить npm run api package.json
1
2
3
4
5
6
7
8
{
  "scripts": {
    "serve": "webpack-dev-server --watch",
    "api": "json-server --watch db.json",
    "build": "webpack"
  },
 ...
}
Теперь, если мы запустим сервер - npm run api, то по URL: http://localhost:3000/ будет доступна главная страница нашего сервера. А по http://localhost:3000/posts уже будет отдаваться лист наших постов! Неужели это не прекрасно! p.s. Также работают и методы GET/DELETE http://localhost:3000/posts/1 и POST http://localhost:3000/posts/ прямо из коробки. Никаких дополнительных настроек не требуется. Теперь у нас есть сервер, куда можно слать запросы. Осталось их отправить. В этом нам поможет Redux и его Middleware. Что такое Redux? Redux - это JS-библиотека, основанная на принципе Flux. В основе Redux лежат, так называемые, reducer'ы. Редюсеры - простые JS-функции. Редюсеры нужны для обработки и сохранения данных в Store. Store - это хранилище. Это, на первый взгляд, кажется сложным, но если взглянуть на пример, то станет понятнее. Для того, чтобы нам воспользоваться Redux, нужно его установить: npm install redux react-redux --save И давайте напишем наш первый reducer и store: reducer.js
1
2
3
4
5
6
7
8
9
10
11
12
13
const reducer = (state, action) => {
   switch(action.type) {
    case 'POSTS/LOAD':
      return {
        ...state,
        posts: action.result || [],
      };
    default:
      return state;
  }
};
 
export default reducer;
Видим, что редюсер принимает два параметра: state и action. В state будет хранится предыдущее значение состояния. После возвращения return в редюсере, state станет равным возвращаемому объекту. В action хранится информация о действии (его тип и результат). Тут использована новая фича из ES6 - деструктор ...state Чтобы с ним проект собрался, нужно установить плагин для Babel: babel-preset-stage-2 npm install --save-dev babel-preset-stage-2 И добавить его в конфиг webpack: webpack.config.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
var path = require('path');
var webpack = require('webpack');
var ExtractTextPlugin = require('extract-text-webpack-plugin');
 
module.exports = {
  entry: [
    './main.jsx',
    './styles.scss'
  ],
  output: {path: __dirname, filename: 'bundle.js'},
  module: {
    loaders: [
      {
        test: /.jsx?$/,
        loader: 'babel-loader',
        exclude: /node_modules/,
        query: {
          presets: ['es2015', 'react', 'stage-2']
        }
      },
      {
        test: /\.scss$/,
        loader: ExtractTextPlugin.extract('css-loader!sass-loader')
      }
    ]
  },
  plugins: [
    new ExtractTextPlugin('bundle.css', {allChunks: true})
  ]
};
Установили плагин, теперь проект снова будет собираться.

У нас есть редюсер, но нет его вызовов, и даже добавления в приложение ещё не сделано. Редюсеры добавляются совместно с store. Создадим store для нашего приложения: main.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore } from 'redux';
import { connect, Provider } from 'react-redux';
import Head from './head.jsx';
import reducer from './reducer.js';
 
const Main = connect(store => store)(
   class extends React.PureComponent {
      render() {
         return <Head color="red">Hey, Africa!</Head>;
      }
   }
);
 
const applicationStore = createStore(reducer, { posts: [], loading: false });
 
ReactDOM.render(
  <Provider store={applicationStore}>
    <Main />
  </Provider>,
  document.getElementById('main')
);
store у нас есть, в него добавили наш единственный редюсер и предоставили приложению. Заметьте, что добавились методы Provider и connect из ReactRedux:
  • Provider служит для предоставления store в компоненты React.
  • connect указывает, где они потребуются, т.е. куда "подключить" store.
Теперь у нас есть методы для сохранения данных, осталось их(данные) получить с бэкенда и передать в редюсер. Для запросов к бэкенду Redux предлагает использовать Middleware. Что такое Middleware? Это функции, которые выполняются после выполнения действий(dispatch) редюсера. Middleware тоже может диспатчить действия в reducer. Давайте напишем Middleware для нашего редюсера: middleware.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
export default function middleware() {
  return (next) => (action) => {
    // Получаем promise из action(параметров)
    const { promise, ...rest } = action;
 
    // Если его нет, то пропускаем обработку
    if (!promise) {
      return next(action);
    }
 
    // Если найден, то переводим загрузку в состояние loading
    // и по завершении загрузки переводим загрузку в success или failure
    // все действия возвращаем в наш action (из которого получен promise)
    next({ ...rest, readyState: 'loading' });
    return promise.then(
      success =>
         success.json().then(
          result => next({ ...rest, result, readyState: 'success' }),
          error => next({ ...rest, error, readyState: 'failure' })
        ),
      error => next({ ...rest, error, readyState: 'failure' })
    );
  };
}
Как видите, middleware - небольшие функции для обработки наших action На этом основные элементы системы написаны. Наладим полный рабочий цикл приложения в main.jsx main.jsx
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
import React from 'react';
import ReactDOM from 'react-dom';
import { createStore, applyMiddleware } from 'redux';
import { connect, Provider } from 'react-redux';
import Head from './head.jsx';
import Item from './item.jsx';
import reducer from './reducer.js';
import middleware from './middleware.js';
 
const Main = connect(store => store)(
  class extends React.PureComponent {
    componentDidMount() {
      this.props.dispatch({
        type: 'POSTS/LOAD',
        promise: fetch('http://localhost:3000/posts')
      });
    }
 
    render() {
      const { posts } = this.props;
      return (
        <div>
          <Head>Hey, Africa!</Head>
          {
            posts.length
              ? posts.map(post => <Item key={post.id} {...post} />)
              : <Item name="Loading posts..." />
          }
        </div>
      );
    }
  });
 
const applicationStore =
  applyMiddleware(middleware)(createStore)(
    reducer,
    { posts: [], loading: false }
  );
 
ReactDOM.render(
  <Provider store={applicationStore}>
    <Main />
  </Provider>,
  document.getElementById('main')
);
Что нового: Для отображения компонента Item не хватает файла: item.jsx
1
2
3
4
5
6
7
8
import React from 'react';
 
export default props => (
  <div className="item">
    <h2>{props.name}</h2>
    <p>{props.text}</p>
  </div>
);
Полный цикл, таким образом, завершен! Выполняем в разных консолях параллельно (если ещё не запустили): npm run api npm run serve И смотрим в браузере http://localhost:8080/ p.s. Если возникли проблемы, смотрите внизу статьи рабочий пример на jsfiddle

Итого:
  • Шапка рендерится в head.jsx, а каждый из постов в item.jsx
  • Все компоненты собираются в main.jsx
  • При componentDidMount создается action (с type + promise) и отправляется в reducer
  • Мы получаем данные с бэкенда с помощью fetch
  • В Middleware обрабатываем результат и диспатчим в редюсер то же действие, но уже с result или error
  • В редюсере получается action.result и сохраняется в store.posts
  • Из store в компонент Main приходит массив posts
  • Main с помощью map заменяет все элементы массива на компоненты Item с параметрами из массива
  • Рендер выводит Head и все Item'ы в теле Main
  • Profit!
Как обещал - примерчик в jsfiddle. Немного не такой, как описано в статье, т.к. fiddle вводит свои ограничения:
  • Весь код в одном файле
  • Нет json-server. Вместо него запрос отправляется на /echo/json
В остальном всё точно также
p.s. Что ещё полезно сделать? - структурировать данные. Расположите данные так, как вам удобно. Даже когда проект небольшой, чувствуется небольшая сумбурность, если держать все исходники в одной папке. Общепринятое место скомпилированного приложения - папка dist/ Компоненты можно вынести в отдельную папку src. p.p.s Если Вам лень самим всё выносить или что-то не получилось сделать, можете скачать готовый архив. Для запуска выполнить из папки с исходниками:
  • npm install
  • npm run serve
  • npm run api
Спасибо за внимание.
Если статья Вам показалась незаконченной или Вы знаете как её улучшить, пожалуйста сообщите мне e@gohtml.ru