ОБНОВЛЕНИЕ 2018: Вышло второе издание (современный код и версии пакетов, данное издание УСТАРЕЛО)
На канале так же проводятся бесплатные вебинары, публикуются переводы и авторские материалы, присоединяйтесь !
Взаимодействуем с VK
Чтобы работать с VK API вам необходимо будет создать приложение на сайте vk.com , и указать в настройках URL сервера, с которого вы будете выполнять запросы.
Localhost не поддерживается.
Интеграция VK API
Необходимо добавить скрипт openapi перед нашей сборкой - bundle.js , а так же вызвать VK.init
Copy <!DOCTYPE html>
<html>
<head>
<title>Redux [RU]Tutorial</title>
</head>
<body>
<div id="root" class="container-fluid">
</div>
<script src="//vk.com/js/api/openapi.js"></script>
<script src="/static/bundle.js"></script>
<script language="javascript">
VK.init({
apiId: 5087365
});
</script>
</body>
</html>
Авторизация
Создадим действия для User.
src/actions/UserActions.js
Copy import {
LOGIN_REQUEST ,
LOGIN_SUCCES ,
LOGIN_FAIL
} from '../constants/User'
export function handleLogin () {
return function (dispatch) {
dispatch ({
type : LOGIN_REQUEST
})
VK . Auth .login ((r) => { // eslint-disable-line no-undef
if ( r .session) {
let username = r . session . user .first_name;
dispatch ({
type : LOGIN_SUCCES ,
payload : username
})
} else {
dispatch ({
type : LOGIN_FAIL ,
error : true ,
payload : new Error ( 'Ошибка авторизации' )
})
}
} , 4 ); // запрос прав на доступ к photo
}
}
Проверьте список констант:
Copy export const LOGIN_REQUEST = 'LOGIN_REQUEST'
export const LOGIN_SUCCES = 'LOGIN_SUCCES'
export const LOGIN_FAIL = 'LOGIN_FAIL'
"Приконнектим" в <App />
UserActions, и добавим новые свойства в компонент <User />
src/containers/App.js
Copy import React , { Component } from 'react'
import { bindActionCreators } from 'redux'
import { connect } from 'react-redux'
import User from '../components/User'
import Page from '../components/Page'
import * as pageActions from '../actions/PageActions'
import * as userActions from '../actions/UserActions'
class App extends Component {
render () {
const { user , page } = this .props
const { getPhotos } = this . props .pageActions
const { handleLogin } = this . props .userActions
return < div className = 'row' >
< Page photos = { page .photos} year = { page .year} getPhotos = {getPhotos} fetching = { page .fetching} />
< User name = { user .name} handleLogin = {handleLogin} error = { user .error} />
</ div >
}
}
function mapStateToProps (state) {
return {
user : state .user ,
page : state .page
}
}
function mapDispatchToProps (dispatch) {
return {
pageActions : bindActionCreators (pageActions , dispatch) ,
userActions : bindActionCreators (userActions , dispatch)
}
}
export default connect (mapStateToProps , mapDispatchToProps)(App)
Обновим reducer user:
src/reducers/user.js
Copy import {
LOGIN_SUCCES ,
LOGIN_FAIL
} from '../constants/User'
const initialState = {
name : '' ,
error : ''
}
export default function user (state = initialState , action) {
switch ( action .type) {
case LOGIN_SUCCES :
return { ... state , name : action .payload , error : '' }
case LOGIN_FAIL :
return { ... state , error : action . payload .message }
default :
return state
}
}
И покажем все это в компоненте <User />
src/components/User.js
Copy import React , { PropTypes , Component } from 'react'
export default class User extends Component {
render () {
const { name , error } = this .props
let template
if (name) {
template = < p >Привет, {name}!</ p >
} else {
template = < button className = 'btn' onClick = { this . props .handleLogin}>Войти</ button >
}
return < div className = 'ib user' >
{template}
{error ? < p className = '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
из VK API.
Для начала, проверим список констант:
src/constants/Page.js
Copy export const GET_PHOTOS_REQUEST = 'GET_PHOTOS_REQUEST'
export const GET_PHOTOS_SUCCESS = 'GET_PHOTOS_SUCCESS'
export const GET_PHOTOS_FAIL = 'GET_PHOTOS_FAIL'
Напишем немало кода, для загрузки фото:
src/actions/PageActions.js
Copy import {
GET_PHOTOS_REQUEST ,
GET_PHOTOS_FAIL ,
GET_PHOTOS_SUCCESS
} from '../constants/Page'
let photosArr = []
let cached = false
function makeYearPhotos (photos , selectedYear) {
let createdYear , yearPhotos = []
photos .forEach ((item) => {
createdYear = new Date ( item .created * 1000 ) .getFullYear ()
if (createdYear === selectedYear ) {
yearPhotos .push (item)
}
})
yearPhotos .sort ((a , b) => b . likes .count - a . likes .count);
return yearPhotos
}
function getMorePhotos (offset , count , year , dispatch) {
VK . Api .call ( 'photos.getAll' , {extended : 1 , count : count , offset : offset} , (r) => { // eslint-disable-line no-undef
try {
if (offset <= r .response[ 0 ] - count) {
offset += 200 ;
photosArr = photosArr .concat ( r .response)
getMorePhotos (offset , count , year , dispatch)
} else {
let photos = makeYearPhotos (photosArr , year)
cached = true
dispatch ({
type : GET_PHOTOS_SUCCESS ,
payload : photos
})
}
}
catch (e) {
dispatch ({
type : GET_PHOTOS_FAIL ,
error : true ,
payload : new Error (e)
})
}
})
}
export function getPhotos (year) {
return (dispatch) => {
dispatch ({
type : GET_PHOTOS_REQUEST ,
payload : year
})
if (cached) {
let photos = makeYearPhotos (photosArr , year)
dispatch ({
type : GET_PHOTOS_SUCCESS ,
payload : photos
})
} else {
getMorePhotos ( 0 , 200 , year , dispatch)
}
}
}
makeYearPhotos
и getMorePhotos
можно вынести в папку utils, как вспомогательные функции.
Главное здесь, что мы по прежнему вызываем действия (dispatch actions ). Все так, как было в самом начале, просто добавилось немного больше логики для получения фото. Алгоритм получения всех фото (да и необходимость получения всех) - оставляю без комментариев. Мне кажется, это приемлемый способ.
Чтобы потестировать показ ошибок, достаточно просто исправить цифру 200 на 2 или 20. VK с любовью вам ответит, что вы мягко-говоря, очень настойчиво обращаетись к API ;)
Исправив редьюсер и отрисовку в компоненте, мы закончим начатое.
src/reducers/page.js
Copy import {
GET_PHOTOS_REQUEST ,
GET_PHOTOS_SUCCESS ,
GET_PHOTOS_FAIL
} from '../constants/Page'
const initialState = {
year : 2016 ,
photos : [] ,
fetching : false ,
error : ''
}
export default function page (state = initialState , action) {
switch ( action .type) {
case GET_PHOTOS_REQUEST :
return { ... state , year : action .payload , fetching : true , error : '' }
case GET_PHOTOS_SUCCESS :
return { ... state , photos : action .payload , fetching : false , error : '' }
case GET_PHOTOS_FAIL :
return { ... state , error : action . payload .message , fetching : false }
default :
return state;
}
}
src/components/Page.js
Copy import React , { PropTypes , Component } from 'react'
export default class Page extends Component {
onYearBtnClick (e) {
this . props .getPhotos ( + e . target .innerText)
}
render () {
const { year , photos , fetching , error } = this .props
const years = [ 2016 , 2015 , 2014 , 2013 , 2012 , 2011 , 2010 ]
return < div className = 'ib page' >
< p >
{ years .map ((item , index) => < button className = 'btn' key = {index} onClick = {:: this .onYearBtnClick}>{item}</ button > )}
</ p >
< h3 >{year} год [{ photos . length }]</ h3 >
{ error ? < p className = 'error' > Во время загрузки фото произошла ошибка</ p > : '' }
{
fetching ?
< p >Загрузка...</ p >
:
photos .map ((entry , index) =>
< div key = {index} className = 'photo' >
< p >< img src = { 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 тоже был слегка подправлен.