Этот урок входит в цикл "Блог на 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