table of contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5...

78

Upload: others

Post on 15-Oct-2020

0 views

Category:

Documents


0 download

TRANSCRIPT

Page 1: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents
Page 2: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

1.1

1.2

1.2.1

1.2.2

1.2.3

1.2.4

1.2.5

1.2.6

1.3

1.3.1

1.3.2

1.3.3

1.3.4

1.3.5

1.3.6

1.3.7

1.3.8

1.3.9

1.3.10

1.3.11

1.3.12

1.3.13

1.4

TableofContentsВступление

Подготовка

Созданиеpackage.json

Первыешаги

ES2015,ReactHMR

ESLint

Установказависимостейнаавтомате

Reactdevtools

Создание

ОсновыRedux(теория)

Точкавхода

НастройкаStore

СозданиеReducer

Присоединениеданных(connect)

Комбинированиередьюсеров

Контейнерыикомпоненты

Созданиеactions

Константы

Наводимпорядок

Middleware(усилители).Логгер

Асинхронныеactions

ВзаимодействуемсVK

Заключение

2

Page 3: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

ReactRedux[RUtutorial]Всемдоброговременисуток.Вданномучебномкурсевынайдете2раздела:

1. Подготовка2. Теорияreduxисозданиевеб-приложенияпошагам

Есливаминтересно,именновыполнениеасинхронныхдействийиработасVKAPI,выможетепрочитатьсоответствующиеглавы.

Курспредополагает,чточитательужезнакомсReact.js.Есливынезнакомы,рекомендуюдляначалаознакомитьсяскурсомReact.jsдляначинающих.

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

Консультациииплатныеуслуги

Общениепоскайпу-1900руб/час,минимум-30минут.

Созданиесервисовсиспользованиемreact/redux,поискпроблемспроизводительностью,помощьвсобеседованииразработчиков-ценапозапросу.

Пишитена[email protected]стемой"КонсультацияReact/Redux"

Полезныессылки

React.js(EN)-офф.сайт,содержитпримерыдляизучения

React.jsдляначинающих(RU)-курснарусскомдлятех,ктохочетпостичьазыreact.js

Redux(EN)-офф.сайт,которыйполностьюохватываетвопросизученияreduxисодержитотличныепримеры.

Redux(RU)-переводофф.документациивкоторомпереведенопрактическивсе.

Webpackscreencast(RU)-скринкастотИльиКанторапоwebpack

Вступление

3

Page 4: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

Twitterаккаунтсоздателяredux,иегобесплатныйвидеокурс(EN)

МойTwitter-можетезадаватьвопросы.

Вступление

4

Page 5: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

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

созданиесписказависимостейпроектанастройкаWebpack

созданиеdev-сервера(Express.js)ES2015/ES7(Babel6)Babel6+ReactReacthotreloadESlint

Результатомподготовки,будетследующийкод.

Есливампонятенкодданногораздела,предлагаюсразупереходитькглаве"Создание".

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

Подготовка

5

Page 6: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

Созданиеpackage.jsonВдиректорииспроектомнеобходимвыполнить:

npminit

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

NPMсоздаcтдлянашегоприложенияфайл-описание.Онпригодитсянамдляуказаниязависимостейипрочихвкусностей.

Первымделоммыдобавимвpackage.jsonкомандудлястартасервера(которыйсоздадимнаследующемшаге).Дляэтогонеобходимодобавить:

"scripts":{

"start":"nodeserver.js"

}

Итоговыйфайлвыглядитпримернотак:

package.json

{

"name":"redux-ru-tutorial",

"version":"1.0.0",

"description":"ReduxRUtutorial",

"main":"index.js",

"scripts":{

"start":"nodeserver.js"

},

"author":"MaximPatsianskiy",

"license":"MIT"

}

Теперьеслинаписатьnpmstartбудетвыполненакомандаnodeserver.js,котораянампонадобитсяпозжедлязапускасервера.

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

Созданиеpackage.json

6

Page 7: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

Созданиеpackage.json

7

Page 8: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

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

npmiwebpackwebpack-dev-middlewarewebpack-hot-middleware--save-dev

Флаг--save-dev-добавитпакетwebpack(ипарочкувспомогательных)всписокзависимостейнашегопроекта.

Теперьнеобходимосоздатьконфигурационныйфайл

webpack.config.js

varpath=require('path')

varwebpack=require('webpack')

module.exports={

devtool:'cheap-module-eval-source-map',

entry:[

'webpack-hot-middleware/client',

'./src/index'

],

output:{

path:path.join(__dirname,'dist'),

filename:'bundle.js',

publicPath:'/static/'

},

plugins:[

newwebpack.optimize.OccurenceOrderPlugin(),

newwebpack.HotModuleReplacementPlugin(),

newwebpack.NoErrorsPlugin()

]

}

Дажедлятех,ктонезнакомсwebpack'ом,этотконфигпокажетсявполнепонятным.Вentry-указываетсяоткудаwebpack'уначинатьсборку,авoutput-кудасгенерировать.Вdevtoolуказываем,чтонамнуженsource-mapдляотладкикодаспаройнастроек(cheap,module,eval).

Интереспытливогочитателяможетвызватьстрока'webpack-hot-middleware/client'(NPM),котораянужнанамдляподдержкиhot-reload,вместесоднимизплагинов-webpack.HotModuleReplacementPlugin

Первыешаги

8

Page 9: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

ServerНампонадобитсясервердляразработки,дляэтогомыможемиспользоватьwebpack-dev-server,либобыстроразвернемсвой.Вданномруководстверассматриваетсявторойвариант.

Дляначалаустановимexpress

npmiexpress--save-dev

исоздадимнашweb-сервернаегооснове.

server.js

varwebpack=require('webpack')

varwebpackDevMiddleware=require('webpack-dev-middleware')

varwebpackHotMiddleware=require('webpack-hot-middleware')

varconfig=require('./webpack.config')

varapp=new(require('express'))()

varport=3000

varcompiler=webpack(config)

app.use(webpackDevMiddleware(compiler,{noInfo:true,publicPath:config.output.publi

cPath}))

app.use(webpackHotMiddleware(compiler))

app.get("/",function(req,res){

res.sendFile(__dirname+'/index.html')

})

app.listen(port,function(error){

if(error){

console.error(error)

}else{

console.info("==>Listeningonport%s.Openuphttp://localhost:%s/inyourbro

wser.",port,port)

}

})

Обратитевнимание,настроку

app.use(webpackHotMiddleware(compiler))

Первыешаги

9

Page 10: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

Наэтомшагедобавляетсянемногомагиикнашемусерверу,аименно:сервертеперьпринимаетуведомления,когдаглавныйjsскриптсобранивызываетобновлениямодулейнашегоприложения,востальномcервервсегонавсегоотдаетнамindex.html,вкотороммыподключаемфайл,сгенерированныйwebpack'ом.

index.html

<!DOCTYPEhtml>

<html>

<head>

<title>Redux[RU]Tutorial</title>

</head>

<body>

<divid="root">

</div>

<scriptsrc="/static/bundle.js"></script>

</body>

</html>

Добавимточкувходадляwebpack'а.Мыуказалиеевнашемwebpack.config.jsвнастройкеentry-index.js

src/index.js

document.getElementById('root').innerHTML='Привет,яготов.'

Проверим?

npmstart

http://localhost:3000/

Отлично,кое-чтоужезавелось.Ноеслисейчасобновитькодвфайлеindex.jsстраницавбраузереостанетсяпрежней.Хотяприэтомвконсолимыувидим,чтоwebpackчто-топересобрал.

HotReloadДеловтом,что"модуль"index.jsнеумеетсообщатьwebpack'укакимобразомонхотелбыобновиться.Сейчас,дляпростотыпримера,достаточнодобавитьстрокуmodule.hot.accept(),котораясообщаетwebpack'уследующуюинформацию:"Я(index.js)умеюhot-reloadсам,дляэтогопростовозьмииобновименявсгенерированномфайле(/static/bundle.js)."

Первыешаги

10

Page 11: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

src/index.js

document.getElementById('root').innerHTML='Привет,яготов!'

module.hot.accept()

Перезапуститесервер,обновитестраницубраузера.

Атеперьпоменяйтетекст,вindex.js-онтакжеобновитсянаэкранебраузера.Браузернеперезагрузитстраницу,каквслучаесlive-reload,асразуотобразиттольконужныйкусочек.Этогораздоудобнее!

Конечно,постоянноуказыватьmodule.hot.accept()неудобно,инеразумно.Следующийшагпоможетнамизбавитьсяот"ручной"настройкиhot-reloadдляreact.jsкода.

Первыешаги

11

Page 12: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

ES2015,ReactHMRДляиспользованиявозможностейES6(2015)иES7будемиспользоватьbabel.Свыходом6йверсии,онсталоченьмодульным,поэтомунепугайтесьбольшомуколичествузависимостей.

Babel6Всеначинаетсяс

npminstallbabel-corebabel-loader--save-dev

Далеенужнопоставитьпресеты(предустановки),которыенамнужны.

#ДляподдержкиES6/ES2015

npminstallbabel-preset-es2015--save-dev

#ДляподдержкиJSX

npminstallbabel-preset-react--save-dev

#ДляподдержкиES7

npminstallbabel-preset-stage-0--save-dev

Намоднозначнонуженполифил,чтобывсефичиработаливбраузере

npminstallbabel-polyfill--save

Инемногоулучшимвремясборки,добавивследующиепакеты

npminstallbabel-runtime--save

npminstallbabel-plugin-transform-runtime--save-dev

*Длянаписанияэтогонебольшоготекстапроbabel6,яиспользовалстатьюUsingES6andES7intheBrowser,withBabel6andWebpack*

Вданныймомент,унасдостаточно"пакетов",чтобыписать"современный"кодииспользоватьJSX.Давайтевэтомубедимся.

Во-первых,подправимконфигдляwebpack'а:

ES2015,ReactHMR

12

Page 13: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

webpack.config.js

varpath=require('path')

varwebpack=require('webpack')

module.exports={

devtool:'cheap-module-eval-source-map',

entry:[

'webpack-hot-middleware/client',

'babel-polyfill',

'./src/index'

],

output:{

path:path.join(__dirname,'dist'),

filename:'bundle.js',

publicPath:'/static/'

},

plugins:[

newwebpack.optimize.OccurenceOrderPlugin(),

newwebpack.HotModuleReplacementPlugin(),

newwebpack.NoErrorsPlugin()

],

module:{//Обновлено

loaders:[//добавилиbabel-loader

{

loaders:['babel-loader'],

include:[

path.resolve(__dirname,"src"),

],

test:/\.js$/,

plugins:['transform-runtime'],

}

]

}

}

Добавиласьзаписьвсекцииloaders.Теперьвсеjsфайлывsrcдиректориибудутобрабатыватьсяbabel-loader'ом,которомумывсвоюочередьтожедолжныуказатьнастройки.Дляэтого,нужносоздатьфайл.babelrcсоследующимсодержимым:

{

"presets":["es2015","stage-0","react"]//поддержкаES2015,ES7иJSX

}

Есливызнакомысgulp,томожнопровестинекуюаналогию,междуплагинамиgulpилоадерами(loaders)webpack'a.Еслимыхотимделатькакие-топреобразованияскодомвнутрифайла,будьтоcss,jsиликартинки-мыиспользуемсоответсвующий

ES2015,ReactHMR

13

Page 14: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

loader.Причемсоздаватьдополнительныефайлынастроек,каквслучаесbabel,обычноненужно.

Ок,создадимReactкомпонент,незабывприэтомскачатьнужныепакеты:

npmireactreact-dom--save

src/index.js

import'babel-polyfill'

importReactfrom'react'

import{render}from'react-dom'

importAppfrom'./containers/App'

render(

<App/>,

document.getElementById('root')

)

src/containers/App.js

importReact,{Component,PropTypes}from'react'

exportdefaultclassAppextendsComponent{

render(){

return<div>ПриветизApp</div>

}

}

Перезапускаемсборку(npmstart).

ВеськоднатекущиймоментвыложенвспециальнуюветкунаGithub.Можетесверится,есличто-тонеработает.

React+HotReloadВозможно,вамвстретитсяаббревиатураHMR(hotmodulereplacement),чтовпринципеболееправильноотражаетсуть,поэтомуподhot-reloadяподразумеваюименноHMR;)

Каквыпомнитеизпрошлойглавы-мыдобавилиmodule.hot.accept(),длятого,чтобыwebpackобновлялкодизфайлаindex.jsвсборкебезперезагрузкистраницывбраузере.Еслисейчаспопробоватьизменитьчто-товApp.js-товрезультатеничего

ES2015,ReactHMR

14

Page 15: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

неслучится,ровнопотемжепричинам,чтоивпредыдушемслучае.Чтож,этопоправимоиблагодарядобрымлюдям,намненужносамимвписыватьacceptфункцию.Итак,встречайте(иустанавливайте):

npminstallreact-hot-loader--save-dev

Достаточнодобавитьещеодинloaderвконфигимыполучимhot-reloadдляReactкомпонентов.

varpath=require('path')

varwebpack=require('webpack')

module.exports={

devtool:'cheap-module-eval-source-map',

entry:[

'webpack-hot-middleware/client',

'babel-polyfill',

'./src/index'

],

output:{

path:path.join(__dirname,'dist'),

filename:'bundle.js',

publicPath:'/static/'

},

plugins:[

newwebpack.optimize.OccurenceOrderPlugin(),

newwebpack.HotModuleReplacementPlugin(),

newwebpack.NoErrorsPlugin()

],

module:{

loaders:[

{

loaders:['react-hot','babel-loader'],//добавилиloader'react-hot'

include:[

path.resolve(__dirname,"src"),

],

test:/\.js$/,

plugins:['transform-runtime'],

}

]

}

}

Перезапускаемсборкуипроверяем.ТеперьHMRработаетидляReactкомпонентов,еслиженет-сверьтесьсисходнымкодомданногораздела.

ES2015,ReactHMR

15

Page 16: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

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

P.S.ВофициальномрепозиторииReact-hot-reloader'aговоритсяотом,чтоготовитсяквыходуReactTransform,которыйстанетлогическимпродолжениемтекущихрешений.(31.01.2015)

P.P.S.Какговоритсоздательбиблиотекиreact-hot-reload,намбольшененужноиспользоватьwebpack.NoErrorsPlugin,которыйранеевыполнялследующее:есливсборкебылиошибки,оннеобновлялфайлсборки.Поэтомупростоудалитесоответствующуюстрокуизсекцииpluginsвнутриwebpack.config.js

ES2015,ReactHMR

16

Page 17: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

ESlintЕсливынезнакомыс"линтерами",товы,наверняка,знакомыстипичнымпоискомошибкивстилеmyVariableisundefinedиподобными.

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

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

Но,довольнотеории.

Поставимнужныепакеты:

npmibabel-eslinteslinteslint-plugin-react--save-dev

Теперь,хотяяиговорил,чтофайлы.xxxrcобычноненужны,дляESlintвсеженужносделатьтакой.Внеммыопишемправиладлясинтаксическойпроверки(lint)кода.

.eslintrc

ESLint

17

Page 18: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

{

"extends":"eslint:recommended",

"parser":"babel-eslint",

"env":{

"browser":true,

"node":true

},

"plugins":[

"react"

],

"rules":{

"no-debugger":0,

"no-console":0,

"new-cap":0,

"strict":0,

"no-underscore-dangle":0,

"no-use-before-define":0,

"eol-last":0,

"quotes":[2,"single"],

"jsx-quotes":[1,"prefer-single"],

"react/jsx-no-undef":1,

"react/jsx-uses-react":1,

"react/jsx-uses-vars":1

}

}

Самоеинтересное,конечноже,секцияrules,где:

0-правиловыключено1-правиловыдастпредупреждение2-правиловыдастошибку

Некоторыеправилапринимаютмассиваргументов,напримерquotes.Внашемслучае,именноэтоправиломожнопрочитатьтак:Показывайошибку,есливстретишьдвойнуюкавычку.

Списоквсехправилeslint-plugin-react.

ЧтобыESlintработалвавтоматическомрежиме,мыбудемвсетакжеиспользоватьwebpack.

Нарядуссекциейloaders,вwebpackестьсекция...preloaders(да-да,postloadersтожеесть).Ядумаюизназваниясекцийужевсепонятно:кодобрабатывается"до"и"после"loaders.ДляESlintнамподходитpreloaders.

Итак,поставимнужныйлоадер:

npmieslint-loader--save-dev

ESLint

18

Page 19: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

Поправимконфиг:

webpack.config.js

...

module:{

preLoaders:[//добавилиESlintвpreloaders

{

test:/\.js$/,

loaders:['eslint'],

include:[

path.resolve(__dirname,"src"),

],

}

],

loaders:[//всеостальноеосталосьнетронутым

{

loaders:['react-hot','babel-loader'],

include:[

path.resolve(__dirname,"src"),

],

test:/\.js$/,

plugins:['transform-runtime'],

}

]

}

...

Здесьивбудущем,ябудуиспользовать...-еслидаюфрагмент(ы)файла,аневеськодцеликом.ВеськодразделавсегдаестьнаGithub,ссылкауказанавконцестатьи.

Теперьдостаточноперезапуститьсборку.Должнополучитьсяследующее:

Линтерпоказыватенам,чтовфайлеsrc/containers/App.jsестьнеиспользуемаяпеременнаяPropTypes,хотяонаопределена.Этодействительнотак,поэтомудавайтепоправимкод:

ESLint

19

Page 20: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

src/Containers/App.js

importReact,{Component}from'react'

exportdefaultclassAppextendsComponent{

render(){

return<div>ПриветизApp</div>

}

}

Сохранивфайл,мыувидимвконсолиследующее:

Великолепно!Ошибокнет.Навсякийслучайдобавлю:сборкуwebpackненужнобылоперезапускать.Обычно,сборкунужноперезапускатьлишьпослеизмененийвwebpack.config.jsВостальныхслучаях,таккакунаснастроен"режимнаблюдения"-webpackсамперезапуститсяисгенерируетновыйфайлсборки.

Итого:наданныймоментмыможемписатьES2015/ES7код,использоватьJSXинепереживатьзаглупыеошибки,асвоевременноправитьих,используяподсказкиESlint.Webpackавтоматическипересобираетнашфайлсборки(/static/bundle.js),приэтоммыиспользуемвсюмощьHotModuleReplacement,иеслиизменимчто-либовjsкодеreact-компонентов-измененияприлетятсразужевбраузербезперезагрузкистраницы.Поздравляю,мыготовыскомфортомнаписатьReduxприложение.

Исходныйкодданногораздела

Длянаписанияэтогораздела,яиспользовалследующиематериалы:

LintinginWebpack(ENG,текст)

ESLint

20

Page 21: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

УстановказависимостейнаавтоматеВнутриэтогоразделаяставилвсезависимостиспомощьюnpminstall<имя_пакета>,ноестьболееудобныйспособ-использоватьплагиндляwebpack-npm-install-webpack-plugin

npminstallnpm-install-webpack-plugin--save-dev

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

.npmrc

save=true

save-exact=true

webpack.config.js

...

//добавьтеновуюзависимостьвначалеконфига

varNpmInstallPlugin=require('npm-install-webpack-plugin');

...

//добавьтеплагинвсекциюплагинов

plugins:[

newwebpack.optimize.OccurenceOrderPlugin(),

newwebpack.HotModuleReplacementPlugin(),

newNpmInstallPlugin()//<--

],

...

Далеевруководстве,явсеравнобудуписатьnpminstall,таккакэтовизуальнодаетхорошеепредставлениеотом,какиезависимостинамнужны.Есливыпоставилиинастроилиnpm-install-webpack-plugin,томожетенебеспокоитсяипропускатьэтистроки.

Установказависимостейнаавтомате

21

Page 22: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

ReactdevtoolsЕслиуваснеустановленодополнениедляконсолихрома(ровнокакисамGoogleChrome),рекомендуювамэтосделать,таккакиногдамыбудемобращатьсякэтиминструментам.

Reactdevtoolsвмагазинерасширений.

Есливыиспользуетевозможностьконсолихрома-$0,товероятновампонравитсяивозможность$r.

Какработает$r?(нижеgif-анимация)

Длятех,кточитаетвформатекниги:необходимокликнутьнакомпонентвконсолихрома,навкладкеReact,далеепереместитьсянавкладкуConsoleинаписать$r.Втакомслучае,вконсоликомпонентбудетпредставленвсвоейнативнойjs-реализации.

Reactdevtools

22

Page 23: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

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

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

Redux-приложениеэто:

состояние(state)приложенияводномместеоднонаправленныйпотокданных

ReduxвдохновленFluxметодологиейиязыкомпрограммированияElm

Создание

23

Page 24: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

Подкапотом,Reduxиспользуетслабодокументированнуюфичуреакта-context,которая,кслову,досихпорявляетсяunstable,иможетбытьизменена/удалена.Ксчастью,этогонепроисходитиврядлипроизойдет.

Файлыипапки:Изначальнонашеприложениевфайловомменеджередолжновыглядетьтак:

+--src

|+--actions

|+--components

|+--constants

|+--containers

|+--reducers

|+--index.js

+--index.html

+--package.json

+--server.js

+--webpack.config.js

Создание

24

Page 25: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

ОсновыRedux(теория)Курсрассчитаннасозданиеприложенияпошагам,аэтозначитмаксимумпрактикииминимумтеории.Тотсамыйминимум,передвами.

Давайтеещеразвзглянемнасхемунашегоприложения:

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

Вшапкесправа-ссылкавойти/выйти.

Представим,какдолжнывыглядетьданныедлятакойстраницы:

ОсновыRedux(теория)

25

Page 26: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

app:{

page:{

year:2016,

photos:[photo,photo,photo...]

},

user:{

name:'Имя',

...

}

}

Поздравляювас,мытолькочтоописаликакдолжновыглядетьсостояние(state)нашегоприложения.

Засодержаниевсегосостояниянашегоприложения,отвечаетобъектStore.Какуженеразупоминалось-этообычныйобъект.Важно,чтовотличииотFlux,вReduxтолькоодинобъектStore.

Нехочетсяоставлятьваснадолгобезпрактики,поэтомупроцесссозданияstoreинемногоподробностейпронегояаккуратновплетувследующиеглавы,апокадостаточнотого,что:store,"объединяет"редьюсер(reducer)идействия(actions),атакжеимеетнесколькочрезвычайнополезныхметодов,например:

getState()-позволяетполучитьсостояниеприложения;dispatch(actions)-позволяетобновлятьсостояния,путемвызовадействия;subcribe(listener)-регистрируетслушателей

Actions

Actionsописываютдействия.

Actions-этопростойобъект.Обязательноеполе-type.Также,еслиследоватьсоглашению,вседанные,которыепередаютсявместесдействием,кладутвнутрьсвойстваpayload.Такимобразом,длянашегоприложения,мыможемсоставить,напримертакуюпаруactions:

{

type:'ЗАГРУЗИ_ФОТО',

payload:2016//год

}

ОсновыRedux(теория)

26

Page 27: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

{

type:'ФОТО_ЗАГРУЖЕНЫ_УСПЕШНО',

payload:[массивфото]

}

Чтобывызватьactions,мыдолжнынаписатьфункцию,котораяврамкахFlux/Reduxназывается-ActionsCreator(создательдействия),нопередэтимстоитпринятьвовнимание,чтообычнотипдействия,описываюткакконстанту.Например,константывашегопроекта:

constGET_PHOTO_REQUEST='GET_PHOTO_REQUEST'

constGET_PHOTO_SUCCESS='GET_PHOTO_SUCCESS'

Возникаетвопрос,зачем?Вмаленькихпроектах-незачем.Вбольших-этоудобно.Пока,простозапомните.

Вернемся,кActionsCreator,одинизнаших"создателейдействий",выгляделбытак:

functiongetPhotos(year){

return{

type:GET_PHOTOS,

payload:year

}

}

Итого:actionsсообщаетнашемуприложению-"Эй,что-топроизошло!Иязнаю,чтоименно!"

Reducer

"Actionsописываетфакт,чточто-топроизошло,нонеуказывает,каксостояниеприложениядолжноизменитьсявответ,этоработадляReducer'а"-(офф.документация)

Нашеприложениененуждаетсявнесколькихредьюсерах,нокрайненеобходимопознакомитьчитателясreducercomposition,таккакэтофундаментальныйшаблонпостроенияreduxприложений:мыразбиваемнашеглобальноесостояниенакусочки,закаждыйкусочекотвечаетсвойreducer.КусочкиобъединяютсявКорневомРедьюсере(rootReducer).

ОсновыRedux(теория)

27

Page 28: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

Схематично,нашеприложениеможнопредставитьтак:

Таккакунасестьreducer'ыpageиuser,можнопредставитьследующийдиалог:

pageActions:Пришло123фото

Reducer(page):Ок,нужноположитьэти123фотовpage.photos

Анаjsвыгляделобытак:

functionpage(state=initialState,action){

switch(action.type){

caseGET_PHOTO_SUCCESS:

returnObject.assign({},state,{

photos:action.payload

})

default:

returnstate

}

}

Обратитевнимание,мынемутировалинашstate,мысоздалиновыйstate.Этоважно.Крайневажно.Вредьюсере,мывсегдадолжнывозвращатьновыйобъект,анеизмененныйпредыдущий.

ОсновыRedux(теория)

28

Page 29: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

Напрактике,ябудуиспользоватьobjectspreadsyntax,поэтомупредыдущуюфункциюсObject.assignможнопереписатьследующимобразом:

functionpage(state=initialState,action){

switch(action.type){

caseGET_PHOTO_SUCCESS:

return{...state,photos:action.payload}//Objectspreadsyntax

default:

returnstate

}

}

Объект,которыймывозвращаемвредьюсере,далееспомощьюфункцииconnect,превратитсявсвойствадлякомпонентов.Такимобразом,еслипродолжитьпримерсфото,томожнонаписатьтакойпсевдо-код:

<Pagephotos={reducerPage.photos}/>

Благодаряэтому,внутрикомпонента<Page/>,мысможемполучитьфото,какthis.props.photo

Япостаралсяоченькраткодатьсамуюважнуютеорию.

Есличто-тоосталосьнепонятным,непереживайте,напрактикемывсезакрепимитогдавсевстанетнасвоиместа.

ОсновыRedux(теория)

29

Page 30: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

ТочкавходаПодтянемReduxиreact-reduxвнашпроект:

npmireduxreact-redux--save

Какужебылоописановразделе"подготовка",точкавходавнашеприложение-src/index.js

Обновимегосодержание:

src/index.js

importReactfrom'react'

import{render}from'react-dom'

import{createStore}from'redux'

import{Provider}from'react-redux'

importAppfrom'./containers/App'

conststore=createStore(()=>{},{})//WAT;)

render(

<Providerstore={store}>

<App/>

</Provider>,

document.getElementById('root')

)

Итак,первыйкомпонентизмираRedux-<Provider/>([EN]документация).

Благодаряэтомукомпоненту,мысможемполучатьнеобходимыеданныеизstoreнашегоприложения,есливоспользуемсявспомогательнойфункциейconnect,речьокоторойпойдетдалее.Сейчаснамиполучатьнечего,таккакstoreунас-пустойобъект.

Давайтеподробнеепосмотримнастроку:

conststore=createStore(()=>{},{})

Во-первых,есливамтрудночитатьES2015код,топереводитееговпривычныйES5,спомощьюbabel-playground.

Точкавхода

30

Page 31: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

Во-вторых,давайтевзглянемнадокументациюметодаcreateStore:принимаетодинобязательныйаргумент(функциюreducer)ипарочкунеобязательных(начальноесостояниеи"усилители").

Теперьпереведемто,чтомынаписали,когдаприсваивалиstore:Возьмипустуюанонимнуюфункциювкачествередьюсераипустойобъектвкачественачальногосостояния.Есликоротко:возьминичегои"ничего"неделай.

Предлагаювынестисозданиеstoreвотдельныйфайл,длятого,чтобыдобавитьвозможностьHMRидляболееудобнойработысreducer'омиусилителями(enhancers).

src/index.js

importReactfrom'react'

import{render}from'react-dom'

import{Provider}from'react-redux'

importAppfrom'./containers/App'

importconfigureStorefrom'./store/configureStore'

conststore=configureStore()

render(

<Providerstore={store}>

<App/>

</Provider>,

document.getElementById('root')

)

Усилители-этоmiddlewareфункции.Есличитательзнакомсexpress.js,тоонзнакомсусилителямивredux.Дляостальных:типичныйусилитель-логгер(logger),которыйпростопишетвконсольвсечтопроисходитснаблюдаемымобъектом.

Еслипосмотретьвконсоль,webpackвсетакжеусердноработаетисообщаетобошибке:нетфайлаconfigureStore...Порасоздать,атекущийкодможновзятьздесь.

Точкавхода

31

Page 32: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

НастройкаStoreДляначала,повторимто,чтонамужеизвестнопроstore,ибытьможетдобавимчуть-чутьнового.Итак:

Storeхранитсостояниеприложения.Единственныйпутьизменитьstore-этоотправитьдействие(dispatchaction).

Store-этонекласс.Этообычныйобъектснесколькимиметодами,аименно:

getState()

dispatch(action)

subscribe(listener)

replaceReducer(nextReducer)

Создадимфункциюконфигурацииstore

store/configureStore.js

import{createStore,applyMiddleware}from'redux'

importrootReducerfrom'../reducers'

exportdefaultfunctionconfigureStore(initialState){

conststore=createStore(rootReducer,initialState)

returnstore

}

Ничегонеобычного,строгоподокументации:передаемrootReducerвтолькочтоимпортированнуюфункциюcreateStore.

ВReduxверсии2.x.xмыдолжныявноуказать,чтоreducersподдерживаютhotreload.Сделатьэтодостаточнопросто.Есливзглянутьвначалокода,видно,чтомыимпортируемтакназываемыйкорневойредьюсер(rootReducer),которыйпосутииотражаетвсесостояниенашегоприложения.Теперьпосмотримещевышепотуториалу-ага,уstoreестьподходящаяфункция-replaceReducer

ТеперьвзявзаосновуотличныйвидеоскринкастпроWebpack,мызнаем,чтоhotreloadожидаетотнасфункцииaccept.Вуаля,поравноситьправки.

store/configureStore.js

НастройкаStore

32

Page 33: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

import{createStore}from'redux'

importrootReducerfrom'../reducers'

exportdefaultfunctionconfigureStore(initialState){

conststore=createStore(rootReducer,initialState)

if(module.hot){

module.hot.accept('../reducers',()=>{

constnextRootReducer=require('../reducers')

store.replaceReducer(nextRootReducer)

})

}

returnstore

}

Ксожалению,нашкоддосихпорнеработает.Webpackругаетсянаотсутствующийreducer.Давайтеисправимэто,ияобещаю,наконец-томожнобудетпосмотретьнарезультатвбраузере.

НастройкаStore

33

Page 34: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

СозданиеReducerПо-моему,вэтомкоденечегодажекомментировать!

src/reducers/index.js

constinitialState={

user:'UnknownUser'

};

exportdefaultfunctionuserstate(state=initialState){

returnstate;

}

Пораоткрытьбраузерипосмотретьвконсольразработчика.Еслитаместьпредупреждения/ошибки-значит,где-тоHMRнесработал.Достаточнообновитьбраузер.Все-такиспоследнегоразамысоздалидостаточнофайлов,чтобыреакт/вебпакрастерялдолюсвоеймагииипопросилбынасобновитьстраницу.

Всеок,ноничегоинтересного.Вследующейглаведобавимвыводимениюзера.

СозданиеReducer

34

Page 35: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

Связываниеданныхизstoreскомпонентамиприложения

Вразделе"Точкавхода"шларечьонекойфункцииconnect,котораяпоможетнамполучитьвкачествеpropsдлякомпонента<App/>данныеизstore.Добавимее:

src/containers/App.js

importReact,{Component}from'react'

import{connect}from'react-redux'

classAppextendsComponent{

render(){

return<div>ПриветизApp,{this.props.user}!</div>

}

}

functionmapStateToProps(state){

return{

user:state.user

}

}

exportdefaultconnect(mapStateToProps)(App)

Назначениефункцииconnectвытекаетизназвания:подключиReactкомпоненткReduxstore.

Результатработыфункцииconnect-новыйприсоединенныйкомпонент,которыйоборачиваетпереданныйкомпонент.

Унасбылкомпонент<App/>,анавыходеполучился<Connected(App)>.Вэтомнетрудноубедиться,есливзглянутьвreactdevtools.

Присоединениеданных(connect)

35

Page 36: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

Взглянитенаправуючастьскриншота,ивыувидите,чтовсвойствах(props)нашегокомпонента<App/>теперьестьметодreduxstore-dispatch,иобъект(внашемслучае,покачтострока)user.Этотакжерезультатработыфункцииconnect.

Давайтеещепоиграемсяспростымпримером.Дляначалаизменимнаборданных:

src/reducers/index.js

constinitialState={

name:'Василий',

surname:'Реактов',

age:27

}

exportdefaultfunctionuserstate(state=initialState){

returnstate

}

затемподкрутимкомпонент:

src/containers/App.js

Присоединениеданных(connect)

36

Page 37: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

importReact,{Component}from'react'

import{connect}from'react-redux'

classAppextendsComponent{

render(){

const{name,surname,age}=this.props.user

return<div>

<p>ПриветизApp,{name}{surname}!</p>

<p>Тебеуже{age}?</p>

</div>

}

}

functionmapStateToProps(state){

return{

user:state

}

}

exportdefaultconnect(mapStateToProps)(App)

Всеработаетровнотак,какмыуказали:вобъектuser"подключилось"всесостояниенашегоприложенияstate,котороесейчасоченьпростоеиописановsrc/reducer/index.js.

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

Исходныйкоднатекущиймомент.

P.S.есливыпереживаете,чтоHMRвданныймоментнеработает,обратитевниманиенаданныйкомментарий(этонеобязательно,атолькодлятех,ктостолкнулсяспроблемой).Полезныессылки:

connect(офф.документация)

Присоединениеданных(connect)

37

Page 38: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

КомбинированиередьюсеровЗачем?Когданашеприложениеразрастается,хочетсяещебольшемодульности,чтобыкаждыйкусочеккодаотвечалзаконкретнуючасть.Такжеисредьюсерами,мыможемразбитьнашглавныйредьюсернанесколькоболеемелких,испомощьюcombineReducersизпакетаreduxсобратьихвоедино.Причем,абсолютноникакоймагии,combineReducersпростовозвращает"составной"редьюсер.

Длянашегоприложения,можновыделитьследующиеreducer'ы(согласносхеме):

userpage

Создадимих:

src/reducers/user.js

constinitialState={

name:'Аноним'

}

exportdefaultfunctionuser(state=initialState){

returnstate

}

src/reducers/page.js

constinitialState={

year:2016,

photos:[]

}

exportdefaultfunctionpage(state=initialState){

returnstate

}

Обновимточкувходадляредьюсеров:

src/reducers/index.js

Комбинированиередьюсеров

38

Page 39: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

import{combineReducers}from'redux'

importpagefrom'./page'

importuserfrom'./user'

exportdefaultcombineReducers({

page,

user

})

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

{

user:{

name:'Аноним'

}

page:{

year:2016,

photos:[]

}

}

Темнеменее,вбраузереунаснерабочееприложение.Вчемжепроблема?

ОтветкроетсявработефункцииconnectивфункцииmapStateToPropsизнашегофайлаApp.js.Сейчасунастамнаписаноследующее:

functionmapStateToProps(state){

return{

user:state

}

}

Чтоможноперевеститак:возьмиполностью"стейт"приложенияиприсоединиеговпеременнуюuser,дабыонабыладоступнаизкомпонетаApp.jsкакthis.props.user

Комбинированиередьюсеров

39

Page 40: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

Идействительно,есливзглянутьвdevtools,всетакиесть:

Здесь,япредложупростуюзадачкунапониманиепроисходящего.ИзменитекомпонентApp.jsифункциюmapStateToPropsтак,чтобыполучитьследующуюкартину:

Ответ:

src/containers/App.js

Комбинированиередьюсеров

40

Page 41: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

importReact,{Component}from'react'

import{connect}from'react-redux'

classAppextendsComponent{

render(){

const{name}=this.props.user//(1)

const{year,photos}=this.props.page//(2)

return<div>

<p>Привет,{name}!</p>

<p>Утебя{photos.length}фотоза{year}год</p>

</div>

}

}

functionmapStateToProps(state){

return{

user:state.user,//(1)

page:state.page//(2)

}

}

exportdefaultconnect(mapStateToProps)(App)

Сносками(1)и(2)япометилсвязь.

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

Второйраззаглаву,возникаетвопрос-зачем?Ответпрежний:модульность,меньшийобъемкодавкаждомфайлеилучшаячитаемость.Аглавное,мытакдобьемсяменьшегоколичестваненужныхобновлений,представьте:пользователькликнулнакнопку"2015",иобновилсяблокpage,приэтомблокuserосталсябынетронутымвообще,еслибыонявлялсяотдельнымкомпонентом.

Намничегонемешаетисправитьэто.Продолжимвследующейглаве.

Исходныйкоднатекущиймомент.

Полезныессылки:

combineReducers(офф.документация)

Комбинированиередьюсеров

41

Page 42: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

КонтейнерыикомпонентыПреждечеммыразобьемApp.jsнакомпонентыUser.jsиPage.jsхотелосьбыотметитьпроразделениена"компоненты"и"контейнеры",иначеназываемые:"глупые"и"умные"компоненты,"Presentational"и"Container"ибытьможеткак-тоеще.

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

Компонент(глупый) Контейнер(умный)

Цель Какэтовыглядит(разметка,стили)

Какэтоработает(получениеданных,обновлениесостояния)

ОсведомленоRedux Нет Да

Длясчитывания

данных

Читаетданныеизprops ПодписаннаReduxstate(состояние)

Дляизменения

данных

Вызываетcallbackизprops

Отправляет(dispatch)Reduxдействие(actions)

Пишутся Вручную Обычно,генерируютсяRedux

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

Создадимкомпоненты.

src/components/User.js

Контейнерыикомпоненты

42

Page 43: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

importReact,{PropTypes,Component}from'react'

exportdefaultclassUserextendsComponent{

render(){

const{name}=this.props

return<div>

<p>Привет,{name}!</p>

</div>

}

}

User.propTypes={

name:PropTypes.string.isRequired

}

src/components/Page.js

importReact,{PropTypes,Component}from'react'

exportdefaultclassPageextendsComponent{

render(){

const{year,photos}=this.props

return<div>

<p>Утебя{photos.length}фотоза{year}год</p>

</div>

}

}

Page.propTypes={

year:PropTypes.number.isRequired,

photos:PropTypes.array.isRequired

}

НашфайлApp.jsужепрактическииестьcontainer,ондажележитвсоответствующейпапке.Изменим-с...

src/containers/App.js

Контейнерыикомпоненты

43

Page 44: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

importReact,{Component}from'react'

import{connect}from'react-redux'

importUserfrom'../components/User'

importPagefrom'../components/Page'

classAppextendsComponent{

render(){

const{user,page}=this.props

return<div>

<Username={user.name}/>

<Pagephotos={page.photos}year={page.year}/>

</div>

}

}

functionmapStateToProps(state){

return{

user:state.user,

page:state.page

}

}

exportdefaultconnect(mapStateToProps)(App)

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

Контейнерыикомпоненты

44

Page 45: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

СозданиеactionsНаконец-томыподходимквопросувзаимодействияспользователемприложения.Практическилюбоедействиепользователявинтерфейсе=отправкадействия(dispatchactions)

Покликунакнопкугода,нашеприложение:

устанавливаетзаголовокзагружаетфотоэтогогода

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

1. Приложениеполучилоизначальноесостояние(initialstate)2. Пользовательнажавкнопку,отправилдействие(dispatchaction)3. Соответсвующийредьюсеробновилчастьприложения,всогласиистем,что

узналотдействия.4. Приложениеизменилосьитеперьотражаетновоесостояние.5. ...(всеповторяетсяпокругу,спункта2)

Этоиестьоднонаправленныйпотокданных.

Создадимpageaction:

src/actions/PageActions.js

exportfunctionsetYear(year){

return{

type:'SET_YEAR',

payload:year

}

}

Напоминаю,чтополяtypeиpayload-всеголишь"негласное"соглашение.Немногообэтом,можнопочитатьнаанглийскомтут.

Созданиеactions

45

Page 46: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

Поправимредьюсерpage:

src/reducers/page.js

constinitialState={

year:2016,

photos:[]

}

exportdefaultfunctionpage(state=initialState,action){

switch(action.type){

case'SET_YEAR':

return{...state,year:action.payload}

default:

returnstate;

}

}

Обратитевнимание,варгументахуфункцииpageуказанвторойаргумент-action.Этостандартныеаргументыreduxreducer'а.Благодаряэтому,мыможемлегкообрабатыватьразличныедействияпоихтипу,попадаявнужнуюсекциюcaseоператораswitch.

Такжеобратитевнимание,чтомынеизменилиобъектstate,авернулиновыйсполемyearравнымaction.payload(азначитгодом,выбраннымпользователем).

Добавляемвызовactionsизкомпонентов

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

Согласнотаблицеизпрошлогораздела:дляизмененияданных,нашкомпонентPage.js,долженвызыватьcallbackизthis.props,анашконтейнер*App.js-отправлятьдействие(dispatchaction).

*яговорю,контейнер,хотяправильнееназыватьконтейнером<Connect(App)/>,нотаккаконгенерируетсяфункциейconnectнаосновеApp.js,считаюэтодопустимым.

Издокументациифункцииconnect,намтакжестановитсяясно,чтоспомощьюэтойфункциимыможемнетолькоподписатьсянаобновленияданных,нои"прокинуть"нашиactionsвконтейнер.

Созданиеactions

46

Page 47: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

connect,первымаргументомпринимает"маппинг"(соответствие)stateкprops,авторыммаппингdispatchкprops.Какбыдикоэтонезвучало,напрактикеэтозначит,чтонамдостаточнопередатьвторойаргумент.

ИсправимApp.js

src/containers/App.js

importReact,{Component}from'react'

import{bindActionCreators}from'redux'

import{connect}from'react-redux'

importUserfrom'../components/User'

importPagefrom'../components/Page'

import*aspageActionsfrom'../actions/PageActions'

classAppextendsComponent{

render(){

const{user,page}=this.props

const{setYear}=this.props.pageActions

return<div>

<Username={user.name}/>

<Pagephotos={page.photos}year={page.year}setYear={setYear}/>

</div>

}

}

functionmapStateToProps(state){

return{

user:state.user,

page:state.page

}

}

functionmapDispatchToProps(dispatch){

return{

pageActions:bindActionCreators(pageActions,dispatch)

}

}

exportdefaultconnect(mapStateToProps,mapDispatchToProps)(App)

НачнемсразбораmapDispatchToProps.Внутрифункциимыиспользоваливспомогательнуюфункциюизredux-bindActionCreators(офф.документация,котораяпозволилавызыватьsetYear,есливыразитьсяпростоснекоторымидопущениямикак:

Созданиеactions

47

Page 48: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

store.dispatch({

type:'SET_YEAR'

payload:2016

})

Темсамымнеобходимоеизменениепрослушиваетсявreduxstore,ивнашемредьюсереPageсоответственно.

Следовательно,послевыполненияconnect(mapStateToProps,mapDispatchToProps)(App),мыполучиливApp.jsновыесвойства(props),чтонагляднодемонстрируетвкладка"React"вchromedevtools.

ДобавивsetYearвсвойстваPage.js,несоставиттрудаиспользоватьнеобходимыйactionизкомпонента,которыйпопрежнемузнатьничегонезнаетоredux.

UPDATE[18.03.16]:свойствоinnerTextприведенноевкодениже-нестандартное,поэтомусниммогутвозникнутьпроблемывнекоторыхбраузерах.Вместонего,выможетеиспользовать-textContent.

src/components/Page.js

Созданиеactions

48

Page 49: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

importReact,{PropTypes,Component}from'react'

exportdefaultclassPageextendsComponent{

onYearBtnClick(e){

this.props.setYear(+e.target.innerText)

}

render(){

const{year,photos}=this.props

return<div>

<p>

<buttononClick={::this.onYearBtnClick}>2016</button>

<buttononClick={::this.onYearBtnClick}>2015</button>

<buttononClick={::this.onYearBtnClick}>2014</button>

</p>

<h3>{year}год</h3>

<p>Утебя{photos.length}фото.</p>

</div>

}

}

Page.propTypes={

year:PropTypes.number.isRequired,

photos:PropTypes.array.isRequired,

setYear:PropTypes.func.isRequired

}

Собственно,кодкомпонентаPageпопрежнемуоченьпростой.Строка::this.onYearBtnClick===this.onYearBtnClick.bind(this),инужнатаккакReactсверсии0.14.xнепривязываетthisккомпоненту.

Использованиедвойногодвоеточия-этовозможностьES7(experimental),котораядоступнавbabelснастройкойstage=0(длятехктописалкод,начинаясраздела"Подготовка"-всеуженастроено,смотрифайл.babelrc)

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

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

Созданиеactions

49

Page 50: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

Искодныйкод

Созданиеactions

50

Page 51: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

КонстантыЕсливынестивсеpageactionsвотдельныйфайлсконстантами,товбудущемнамудобнеебудетписатьтесты/работатьвкоманде/поддерживатькод.Ктомуже,такимобразоммынебудемотходитьот"соглашений"принятыхвразработкеFlux/Reduxприложений.

src/constants/Page.js

exportconstSET_YEAR='SET_YEAR'

ПодключимконстантувредьюсерPageивPageActions

src/reducers/page.js

import{SET_YEAR}from'../constants/Page'

constinitialState={

year:2016,

photos:[]

}

exportdefaultfunctionpage(state=initialState,action){

switch(action.type){

caseSET_YEAR://незабудьтеобновитьстрокунаконстанту

return{...state,year:action.payload}

default:

returnstate;

}

}

src/actions/PageActions.js

Константы

51

Page 52: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

import{SET_YEAR}from'../constants/Page'

exportfunctionsetYear(year){

return{

type:SET_YEAR,//аналогично,теперьиспользуемконстанту

payload:year

}

}

Вдальнейшеммыещедобавимконстант,нетолькодлякомпонента<Page/>,ноидлякомпонента<User/>,которыемытакжебудемобъявлятьвотдельномфайле.

Константы

52

Page 53: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

НаводимпорядокДаннаяглаваявляетсясвоегорода"перекуром".Оназатрагиваетвопросыстилейиверсткиприложения.

Autoprefixerистили

Добавимвwebpackвозможностьобрабатыватьстили,заодносразунакинувнанихвозможностиautoprefixer'а.

npminstallstyle-loadercss-loaderpostcss-loaderautoprefixerprecss--save-dev

P.S.длятехктопользуетсяавтоматическимспособомдобавлениязависимостей:таккакмыдобавляемзависимостивwebpack.config-npm-insatll-pluginнеможетподтянутьихавтоматически.

webpack.config.js

varpath=require('path')

varwebpack=require('webpack')

varNpmInstallPlugin=require('npm-install-webpack-plugin')

varautoprefixer=require('autoprefixer');

varprecss=require('precss');

module.exports={

devtool:'cheap-module-eval-source-map',

entry:[

'webpack-hot-middleware/client',

'babel-polyfill',

'./src/index'

],

output:{

path:path.join(__dirname,'dist'),

filename:'bundle.js',

publicPath:'/static/'

},

plugins:[

newwebpack.optimize.OccurenceOrderPlugin(),

newwebpack.HotModuleReplacementPlugin(),

newNpmInstallPlugin()

],

module:{

preLoaders:[

Наводимпорядок

53

Page 54: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

{

test:/\.js$/,

loaders:['eslint'],

include:[

path.resolve(__dirname,"src"),

],

}

],

loaders:[

{

loaders:['react-hot','babel-loader'],

include:[

path.resolve(__dirname,"src"),

],

test:/\.js$/,

plugins:['transform-runtime'],

},

{

test:/\.css$/,

loader:"style-loader!css-loader!postcss-loader"

}

]

},

postcss:function(){

return[autoprefixer,precss];

}

}

Незабудьтеподключитьглавныйфайлстилейдлявсегоприложения:

src/index.js

importReactfrom'react'

import{render}from'react-dom'

import{Provider}from'react-redux'

importAppfrom'./containers/App'

import'./styles/app.css'//<--импортстилей

importconfigureStorefrom'./store/configureStore'

conststore=configureStore()

render(

<Providerstore={store}>

<divclassName='app'>{/*обернуливсев.app*/}

<App/>

</div>

</Provider>,

document.getElementById('root')

)

Наводимпорядок

54

Page 55: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

Возможно(есливыкопируетекод,либотожелюбитеодинарныекавычки),wepbackвыдастошибкуотESLint.Дляиспользованияодинарнойкавычки,исправьтеправило"jsx-quotes":[1,"prefer-single"]вфайлике.eslintrc

Верстка

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

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

Наводимпорядок

55

Page 56: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

Middleware(Усилители).ЛоггерПреждечеммысможемсоздаватьасинхронныедействия,поговоримобусилителяхинапишем,обещанныйранееусилитель-логгер.

Представляйтеусилитель,какнечтостороннее,добавляющеефункционалдлянашегоstore.

Усилители,этоmiddleware.Сутьmiddlewareфункций,взятьвходныеданные,добавитьчто-тоипередатьдальше.

Например:естьконвейер,покоторомудвижетсяпальто.НаконвейереработаютЗинаиЛюдмила.Зинапришиваетпуговку,Людмилаприкладываетбирку.Внезапно,появляетсяmiddlewareЛена,встаетмеждуЗинойиЛюдмилойикраситпуговкувхипстерскиймодныйцвет.ТаккакЛенапослепокраскинеуноситпальтоссобой,апередаетдальше,тоЛюдмилакакнивчемнебывалоприделываетбиркуипальтоготово.Толькотеперьонохипстерское.Усиленное.

Длялучшегопонимания,предлагаюнаписатьбесполезныйусилитель,выдающийconsole.log('ping'),накаждоедействие.Приэтом,мыбудемиспользоватьпредложенныйreduxметоддобавленияусилитейспомощьюapplyMiddleware.

Обновимфайлконфигурацииstore:

store/configureStore.js

Middleware(усилители).Логгер

56

Page 57: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

import{createStore,applyMiddleware}from'redux'

importrootReducerfrom'../reducers'

import{ping}from'./enhancers/ping'//<!--подключаемнашenhancer

exportdefaultfunctionconfigureStore(initialState){

conststore=createStore(

rootReducer,

initialState,

applyMiddleware(ping))//<!--добавляемеговцепочкуmiddleware'ов

if(module.hot){

module.hot.accept('../reducers',()=>{

constnextRootReducer=require('../reducers')

store.replaceReducer(nextRootReducer)

})

}

returnstore

}

Напишемусилитель:

store/enhancers/ping.js

/*eslint-disable*/

exportconstping=store=>next=>action=>{

console.log('ping')

returnnext(action)

}

/*eslint-enable*/

Боюсь,здесьнеобойтисьбезES5версии:

varping=functionping(store){

returnfunction(next){

returnfunction(action){

console.log('ping');

returnnext(action);

};

};

};

Поехали:

eslint-disable-простовыключаетпроверкуэтогоблока"линтером".ping-этофункция,котораявозвращаетфункцию.Middleware-этовсегдафункция,которыеобычновозвращаютфункцию,еслитолькоцельюmiddlewareнеявляется

Middleware(усилители).Логгер

57

Page 58: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

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

store-redux-storeнашегоприложения;next-функция-обертка,котораяпозволяетпродолжитьвыполнениецепочки;action-действие,котороебыловызвано(каквыпомните,вызванныедействия-этоstore.dispatch)

Сейчас,прикликенакнопки,унасвконсолипоявляетсястрокаping.Давайтеизменимее,написавпростейшийлоггер:store/enhancers/ping.js

/*eslint-disable*/

exportconstping=store=>next=>action=>{

console.log(`Типсобытия:${action.type},дополнительныеданныесобытия:${action.pa

yload}`)

returnnext(action)

}

/*eslint-enable*/

Яиспользовалновыйстроковыйсинтаксис.Впрошлом,нашconsole.logвыгляделбытак:

console.log('Типсобытия:'+action.type+',дополнительныеданныесобытия:'+acti

on.payload)

Middleware(усилители).Логгер

58

Page 59: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

Покликайтенакнопки,результатдолженбытьследующим:

Redux-logger

Отбросимнашвелосипедипоставимпопулярныйлоггер.

npmi--saveredux-logger

Удалитепапкуenchancers,иизменитеconfigureStore.

src/store/configureStore.js

Middleware(усилители).Логгер

59

Page 60: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

import{createStore,applyMiddleware}from'redux'

importrootReducerfrom'../reducers'

importcreateLoggerfrom'redux-logger'

exportdefaultfunctionconfigureStore(initialState){

constlogger=createLogger()

conststore=createStore(

rootReducer,

initialState,

applyMiddleware(logger))

if(module.hot){

module.hot.accept('../reducers',()=>{

constnextRootReducer=require('../reducers')

store.replaceReducer(nextRootReducer)

})

}

returnstore

}

Можетепроверить-логгердостаточноинформативныйиудобенвиспользовании.

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

Однимизпопулярнейшихусилителей,являетсяredux-thunk,которыймыкакразбудемиспользоватьдлясозданияасинхронныхдействий.

Исходныйкоднатекущиймомент.

Middleware(усилители).Логгер

60

Page 61: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

АсинхронныеactionsДавайтепредставимсинхронноедействие:

1. Пользователькликнулнакнопку2. dispatchaction{type:ТИП_ДЕЙСТВИЯ,payload:доп.данные}3. интерфейсобновился

Давайтепредставимасинхронноедействие:

1. Пользователькликнулнакнопку2. dispatchaction{type:ТИП_ДЕЙСТВИЯ_ЗАПРОС}3. запросвыполнилсяуспешно

i. dispatchaction{type:ТИП_ДЕЙСТВИЯ_УСПЕШНО,payload:доп.данные}4. запросвыполнилсянеудачно

i. dispatchaction{type:ТИП_ДЕЙСТВИЯ_НЕУДАЧНО,error:true,payload:доп.данныеошибки}

Благодарятакойсхеме,вreducer'eмысможемреализоватьподобное:

switch(тип_действия)

caseТИП_ДЕЙСТВИЯ_ЗАПРОС:

покажиpreloader

caseТИП_ДЕЙСТВИЯ_УСПЕШНО:

скройpreloader,покажиданные

caseТИП_ДЕЙСТВИЯ_НЕУДАЧНО:

скройpreloader,покажиошибку

Какнамизвестно,действие-этопростойобъект,которыйвозвращаетсяфункциейегосоздающей(actioncreator).

Убедимсявэтом:

src/actions/PageActions.js

import{SET_YEAR}from'../constants/Page'

exportfunctionsetYear(year){

return{

type:SET_YEAR,

payload:year

}

}

Асинхронныеactions

61

Page 62: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

Былобынеплохоиметьвозможностьвозвращатьнепростойобъект,афункцию,внутрикоторойиметьдоступкметодуdispatch,ивызыватьегоснеобходимымтипомдействия.Псевдокод,могбывыглядетьтак:

exportfunctiongetPhotos(year){

return(dispatch)=>{

dispatch({

type:GET_PHOTOS_REQUEST

})

$.ajax(url)

.success(

dispatch({

type:GET_PHOTOS_SUCCESS,

payload:response.photos

})

)

.error(

dispatch({

type:GET_PHOTOS_FAILURE,

payload:response.error,

error:true

})

)

}

}

Новотнезадача,actions-этопростойобъект,иеслиactioncreatorвозвращаетнепростойобъект,афункцию,тоэтокак-то...Подождите!Ведьэтоименното,чтонамнужно:Еслиactioncreatorвозвращаетнепростойобъект,афункцию-выполниее,иначееслиэтопростойобъект...тадам,передайдальше.Болеетого,благодаряapplyMiddlewareунаскакразестьдоступныйметодdispatch!ИещебонусомgetState.

Отлично,мытолькочтопоняли,чтонамнуженещеодинусилитель.Такойусилительуженаписан,причемкодегоневероятнопрост,ядажеприведуегоздесь:

усилитель:redux-thunk

functionthunkMiddleware({dispatch,getState}){

returnnext=>action=>

typeofaction==='function'?

action(dispatch,getState):

next(action);

}

module.exports=thunkMiddleware

Асинхронныеactions

62

Page 63: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

Намостаетсялишьдобавитьзависимостьвнашпроект,иубедиться,чтоунасreduxверсии,нениже3.1.0

npmupdateredux--save

npminstallredux-thunk--save

Дляпрактики,предлагаюнаписатьследующее:

покликунакнопкусномеромгодаменяетсягодвзаголовкениже(гдедолжныбытьфото),появляетсятекст"Загрузка..."

послеудачнойзагрузки*убратьтекст"Загрузка..."отобразитьстроку"УтебяХХфото"(зависит,отдлинымассива,переданноговaction.payload)

*вместореальногометодазагрузки,использоватьsetTimeout,которыйявляетсяудобнымдлятренировокисполненияасинхронныхзапросов.

Выможетепопробоватьвыполнитьэтозаданиесами,апотомсравнитьегосрешениемниже.

Дляотображения/скрытияфразы"Загрузка...",используйтевreducer'еещеодносвойствоусостояния.Например,fetching:

constinitialState={

year:2016,

photos:[],

fetching:false

}

Решениениже.

Дляначалаизменимнаборконстант:

src/constants/Page.js

exportconstGET_PHOTOS_REQUEST='GET_PHOTOS_REQUEST'

exportconstGET_PHOTOS_SUCCESS='GET_PHOTOS_SUCCESS'

Далеедобавимновыйусилитель:src/store/configureStore.js

Асинхронныеactions

63

Page 64: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

import{createStore,applyMiddleware}from'redux'

importrootReducerfrom'../reducers'

importcreateLoggerfrom'redux-logger'

importthunkfrom'redux-thunk'//<--добавилиredux-thunk

exportdefaultfunctionconfigureStore(initialState){

constlogger=createLogger()

conststore=createStore(

rootReducer,

initialState,

applyMiddleware(thunk,logger))//<--добавилиеговцепочкупередlogger'ом

if(module.hot){

module.hot.accept('../reducers',()=>{

constnextRootReducer=require('../reducers')

store.replaceReducer(nextRootReducer)

})

}

returnstore

}

Изменимactioncreator:src/actions/PageActions.js

import{

GET_PHOTOS_REQUEST,

GET_PHOTOS_SUCCESS

}from'../constants/Page'

exportfunctiongetPhotos(year){

return(dispatch)=>{

dispatch({

type:GET_PHOTOS_REQUEST,

payload:year

})

setTimeout(()=>{

dispatch({

type:GET_PHOTOS_SUCCESS,

payload:[1,2,3,4,5]

})

},1000)

}

}

Изменимreducer:src/reducers/page.js

Асинхронныеactions

64

Page 65: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

import{

GET_PHOTOS_REQUEST,

GET_PHOTOS_SUCCESS

}from'../constants/Page'

constinitialState={

year:2016,

photos:[],

fetching:false

}

exportdefaultfunctionpage(state=initialState,action){

switch(action.type){

caseGET_PHOTOS_REQUEST:

return{...state,year:action.payload,fetching:true}

caseGET_PHOTOS_SUCCESS:

return{...state,photos:action.payload,fetching:false}

default:

returnstate;

}

}

Унасготовалогикадляобновлениясостояния(иинтерфейса,разумеется).Осталосьпоправитьотображение.

Таккакмыпереписалиипереименовалифункцию(setYear->getPhotos):

src/containers/App.js

...

const{getPhotos}=this.props.pageActions

return<divclassName='row'>

<Pagephotos={page.photos}year={page.year}getPhotos={getPhotos}fetching={page.

fetching}/>

...

Причем,вmapDispatchToProps-намничегоменятьненужно,таккакмыпопрежнемуприсоединяемвсеpageActionsвpropsконтейнера<App/>

Обновимсоответствующийкомпонент:src/components/Page.js

Асинхронныеactions

65

Page 66: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

importReact,{PropTypes,Component}from'react'

exportdefaultclassPageextendsComponent{

onYearBtnClick(e){

this.props.getPhotos(+e.target.innerText)

}

render(){

const{year,photos,fetching}=this.props

return<divclassName='ibpage'>

<p>

<buttonclassName='btn'onClick={::this.onYearBtnClick}>2016</button>{''}

<buttonclassName='btn'onClick={::this.onYearBtnClick}>2015</button>{''}

<buttonclassName='btn'onClick={::this.onYearBtnClick}>2014</button>

</p>

<h3>{year}год</h3>

{

fetching?

<p>Загрузка...</p>

:

<p>Утебя{photos.length}фото.</p>

}

</div>

}

}

Page.propTypes={

year:PropTypes.number.isRequired,

photos:PropTypes.array.isRequired,

getPhotos:PropTypes.func.isRequired

}

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

Покамыписаликоддляасинхронногозапроса,мыНЕнарушилиглавныепринципыredux-приложения:

1. Мывсегдавозвращалиновоесостояние(новыйобъект,смотритеsrc/reducers/page.js)

2. Мыстрогоследовалиоднонаправленномупотокуданныхвприложении:юзеркликнул-возниклодействие-редьюсеризменил-компонентотобразил.

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

Асинхронныеactions

66

Page 67: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

потребуетсясоздатьвинтерфейсеVKприложение,ивыполнятьнашизапросысреальногосервера,таккакVK.APIнеработаетсlocalhost.

Обэтоммыипоговоримвследующейглаве.

Исходныйкоднаданныймомент.

Асинхронныеactions

67

Page 68: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

ВзаимодействуемсVKЧтобыработатьсVKAPIвамнеобходимобудетсоздатьприложениенасайтеvk.com,иуказатьвнастройкахURLсервера,скотороговыбудетевыполнятьзапросы.

Localhostнеподдерживается.

ИнтеграцияVKAPI

Необходимодобавитьскриптopenapiпереднашейсборкой-bundle.js,атакжевызватьVK.init

<!DOCTYPEhtml>

<html>

<head>

<title>Redux[RU]Tutorial</title>

</head>

<body>

<divid="root"class="container-fluid">

</div>

<scriptsrc="//vk.com/js/api/openapi.js"></script>

<scriptsrc="/static/bundle.js"></script>

<scriptlanguage="javascript">

VK.init({

apiId:5087365

});

</script>

</body>

</html>

Авторизация

СоздадимдействиядляUser.

src/actions/UserActions.js

ВзаимодействуемсVK

68

Page 69: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

import{

LOGIN_REQUEST,

LOGIN_SUCCES,

LOGIN_FAIL

}from'../constants/User'

exportfunctionhandleLogin(){

returnfunction(dispatch){

dispatch({

type:LOGIN_REQUEST

})

VK.Auth.login((r)=>{//eslint-disable-lineno-undef

if(r.session){

letusername=r.session.user.first_name;

dispatch({

type:LOGIN_SUCCES,

payload:username

})

}else{

dispatch({

type:LOGIN_FAIL,

error:true,

payload:newError('Ошибкаавторизации')

})

}

},4);//запросправнадоступкphoto

}

}

Проверьтесписокконстант:

exportconstLOGIN_REQUEST='LOGIN_REQUEST'

exportconstLOGIN_SUCCES='LOGIN_SUCCES'

exportconstLOGIN_FAIL='LOGIN_FAIL'

"Приконнектим"в<App/>UserActions,идобавимновыесвойствавкомпонент<User/>

src/containers/App.js

ВзаимодействуемсVK

69

Page 70: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

importReact,{Component}from'react'

import{bindActionCreators}from'redux'

import{connect}from'react-redux'

importUserfrom'../components/User'

importPagefrom'../components/Page'

import*aspageActionsfrom'../actions/PageActions'

import*asuserActionsfrom'../actions/UserActions'

classAppextendsComponent{

render(){

const{user,page}=this.props

const{getPhotos}=this.props.pageActions

const{handleLogin}=this.props.userActions

return<divclassName='row'>

<Pagephotos={page.photos}year={page.year}getPhotos={getPhotos}fetching={page.

fetching}/>

<Username={user.name}handleLogin={handleLogin}error={user.error}/>

</div>

}

}

functionmapStateToProps(state){

return{

user:state.user,

page:state.page

}

}

functionmapDispatchToProps(dispatch){

return{

pageActions:bindActionCreators(pageActions,dispatch),

userActions:bindActionCreators(userActions,dispatch)

}

}

exportdefaultconnect(mapStateToProps,mapDispatchToProps)(App)

Обновимreduceruser:

src/reducers/user.js

ВзаимодействуемсVK

70

Page 71: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

import{

LOGIN_SUCCES,

LOGIN_FAIL

}from'../constants/User'

constinitialState={

name:'',

error:''

}

exportdefaultfunctionuser(state=initialState,action){

switch(action.type){

caseLOGIN_SUCCES:

return{...state,name:action.payload,error:''}

caseLOGIN_FAIL:

return{...state,error:action.payload.message}

default:

returnstate

}

}

Ипокажемвсеэтовкомпоненте<User/>

src/components/User.js

ВзаимодействуемсVK

71

Page 72: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

importReact,{PropTypes,Component}from'react'

exportdefaultclassUserextendsComponent{

render(){

const{name,error}=this.props

lettemplate

if(name){

template=<p>Привет,{name}!</p>

}else{

template=<buttonclassName='btn'onClick={this.props.handleLogin}>Войти</button

>

}

return<divclassName='ibuser'>

{template}

{error?<pclassName='error'>{error}.<br/>Попробуйтеещераз.</p>:''}

</div>

}

}

User.propTypes={

name:PropTypes.string.isRequired,

handleLogin:PropTypes.func.isRequired,

error:PropTypes.string.isRequired

}

Сейчасесликликнутьна"войти"-всплыветVKокносподтверждениемправдоступа(первыйраз).Послеподтвержденияправ,вместокнопкивойтипоявляетсянадпись"Привет,ХХХ".Приперезагрузкесайтаиповторныхнажатияхна"войти"-VKокномгновеннозакрывается,акнопкавновьизменяетсяна"Привет,XXX".Неплохобыбылопроверять"статус",напримервcomponentWillMount,нооставлюэтона"домашку".

Каквсегда,доблестныйлоггерпишетвконсоли-чтопроисходит.

Загрузкафото

Намнужнопрактическиповторить,всечтонаписановыше,толькодляблокаPage.

Можетепопробоватьсами,используяметодphotos.getAllизVKAPI.

Дляначала,проверимсписокконстант:

ВзаимодействуемсVK

72

Page 73: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

src/constants/Page.js

exportconstGET_PHOTOS_REQUEST='GET_PHOTOS_REQUEST'

exportconstGET_PHOTOS_SUCCESS='GET_PHOTOS_SUCCESS'

exportconstGET_PHOTOS_FAIL='GET_PHOTOS_FAIL'

Напишемнемалокода,длязагрузкифото:

src/actions/PageActions.js

import{

GET_PHOTOS_REQUEST,

GET_PHOTOS_FAIL,

GET_PHOTOS_SUCCESS

}from'../constants/Page'

letphotosArr=[]

letcached=false

functionmakeYearPhotos(photos,selectedYear){

letcreatedYear,yearPhotos=[]

photos.forEach((item)=>{

createdYear=newDate(item.created*1000).getFullYear()

if(createdYear===selectedYear){

yearPhotos.push(item)

}

})

yearPhotos.sort((a,b)=>b.likes.count-a.likes.count);

returnyearPhotos

}

functiongetMorePhotos(offset,count,year,dispatch){

VK.Api.call('photos.getAll',{extended:1,count:count,offset:offset},(r)=>{//

eslint-disable-lineno-undef

try{

if(offset<=r.response[0]-count){

offset+=200;

photosArr=photosArr.concat(r.response)

getMorePhotos(offset,count,year,dispatch)

}else{

letphotos=makeYearPhotos(photosArr,year)

cached=true

dispatch({

type:GET_PHOTOS_SUCCESS,

payload:photos

})

}

}

ВзаимодействуемсVK

73

Page 74: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

catch(e){

dispatch({

type:GET_PHOTOS_FAIL,

error:true,

payload:newError(e)

})

}

})

}

exportfunctiongetPhotos(year){

return(dispatch)=>{

dispatch({

type:GET_PHOTOS_REQUEST,

payload:year

})

if(cached){

letphotos=makeYearPhotos(photosArr,year)

dispatch({

type:GET_PHOTOS_SUCCESS,

payload:photos

})

}else{

getMorePhotos(0,200,year,dispatch)

}

}

}

makeYearPhotosиgetMorePhotosможновынестивпапкуutils,каквспомогательныефункции.

Главноездесь,чтомыпопрежнемувызываемдействия(dispatchactions).Всетак,какбыловсамомначале,простодобавилосьнемногобольшелогикидляполученияфото.Алгоритмполучениявсехфото(даинеобходимостьполучениявсех)-оставляюбезкомментариев.Мнекажется,этоприемлемыйспособ.

Чтобыпотестироватьпоказошибок,достаточнопростоисправитьцифру200на2или20.VKслюбовьювамответит,чтовымягко-говоря,оченьнастойчивообращаетиськAPI;)

Исправивредьюсериотрисовкувкомпоненте,мызакончимначатое.

src/reducers/page.js

ВзаимодействуемсVK

74

Page 75: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

import{

GET_PHOTOS_REQUEST,

GET_PHOTOS_SUCCESS,

GET_PHOTOS_FAIL

}from'../constants/Page'

constinitialState={

year:2016,

photos:[],

fetching:false,

error:''

}

exportdefaultfunctionpage(state=initialState,action){

switch(action.type){

caseGET_PHOTOS_REQUEST:

return{...state,year:action.payload,fetching:true,error:''}

caseGET_PHOTOS_SUCCESS:

return{...state,photos:action.payload,fetching:false,error:''}

caseGET_PHOTOS_FAIL:

return{...state,error:action.payload.message,fetching:false}

default:

returnstate;

}

}

src/components/Page.js

ВзаимодействуемсVK

75

Page 76: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

importReact,{PropTypes,Component}from'react'

exportdefaultclassPageextendsComponent{

onYearBtnClick(e){

this.props.getPhotos(+e.target.innerText)

}

render(){

const{year,photos,fetching,error}=this.props

constyears=[2016,2015,2014,2013,2012,2011,2010]

return<divclassName='ibpage'>

<p>

{years.map((item,index)=><buttonclassName='btn'key={index}onClick={::th

is.onYearBtnClick}>{item}</button>)}

</p>

<h3>{year}год[{photos.length}]</h3>

{error?<pclassName='error'>Вовремязагрузкифотопроизошлаошибка</p>:''

}

{

fetching?

<p>Загрузка...</p>

:

photos.map((entry,index)=>

<divkey={index}className='photo'>

<p><imgsrc={entry.src}/></p>

<p>{entry.likes.count}❤</p></div>

)

}

</div>

}

}

Page.propTypes={

year:PropTypes.number.isRequired,

photos:PropTypes.array.isRequired,

getPhotos:PropTypes.func.isRequired,

error:PropTypes.string.isRequired

}

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

Исходныйкоднатекущиймомент.

P.S.cssтожебылслегкаподправлен.

ВзаимодействуемсVK

76

Page 77: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

ВзаимодействуемсVK

77

Page 78: Table of Contents · 1.1 1.2 1.2.1 1.2.2 1.2.3 1.2.4 1.2.5 1.2.6 1.3 1.3.1 1.3.2 1.3.3 1.3.4 1.3.5 1.3.6 1.3.7 1.3.8 1.3.9 1.3.10 1.3.11 1.3.12 1.3.13 1.4 Table of Contents

ЗаключениеСпасибо,чтовыпрочиталиданныйучебник,еслиувасосталисьвопросы,задавайтеихвтвиттере,илипишитенапочту[email protected]стемой"ReduxRU".

Планобновлений:

разделпоосновамreact.js->написантуториалтестированиеоптимизациясборки

Какговорится,staytuned!

Заключение

78