Этот урок входит в цикл "Блог на React".
Теперь добавим запуск сервера в package.json, чтобы потом просто выполнить npm run api
package.json
Теперь, если мы запустим сервер - 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
Видим, что редюсер принимает два параметра: 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
Установили плагин, теперь проект снова будет собираться.
У нас есть редюсер, но нет его вызовов, и даже добавления в приложение ещё не сделано. Редюсеры добавляются совместно с store.
Создадим store для нашего приложения:
main.jsx
store у нас есть, в него добавили наш единственный редюсер и предоставили приложению. Заметьте, что добавились методы Provider и connect из ReactRedux:
Как видите, middleware - небольшие функции для обработки наших action
На этом основные элементы системы написаны. Наладим полный рабочий цикл приложения в main.jsx
main.jsx
Что нового:
Полный цикл, таким образом, завершен!
Выполняем в разных консолях параллельно (если ещё не запустили):
npm run api
npm run serve
И смотрим в браузере http://localhost:8080/
p.s. Если возникли проблемы, смотрите внизу статьи рабочий пример на jsfiddle
Итого:
- Блог на ReactJS #1. Компоненты. npm, jsx, webpack, webpack-dev-server
- Блог на ReactJS #2. Несколько компонентов, props, state. npm run, конфигурация webpack
- Блог на ReactJS #3. Redux, Middleware, json-server, fetch
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" } ]} |
1 2 3 4 5 6 7 8 | { "scripts": { "serve": "webpack-dev-server --watch", "api": "json-server --watch db.json", "build": "webpack" }, ...} |
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; |
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}) ]}; |
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')); |
- Provider служит для предоставления store в компоненты React.
- connect указывает, где они потребуются, т.е. куда "подключить" store.
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' }) ); };} |
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', }); } 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 - новый компонент, отображающий каждый пост (описан ниже)
- componentDidMount - одна из встроенных функций компонентов React. Выполняется, когда компонент первый раз отрендерился. Полный список этих функций на оф. сайте.
- fetch - запрос к бэкенду. Появился в ES6. Возвращает Promise. Много полезной информации по ES6 (learnjs)
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>); |
- Шапка рендерится в 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!
- Весь код в одном файле
- Нет json-server. Вместо него запрос отправляется на /echo/json
- npm install
- npm run serve
- npm run api