scripts / store / configureStore.js (✓ хорошо)
import { createStore } from 'redux'
import reducers from '../reducers'
export default function configureStore () {
return createStore(reducers)
}
Я сохранил модуль, но вместо этого экспортировал функцию с именем configureStore
Обратите внимание, что это только основная концепция; Я также использую расширение Redux DevTools и загружаю постоянное состояние через localStorage
scripts / store / connect.js (✓ хорошо)
export function getItemList (store) {
return store.getState().items.all
}
Вспомогательные функции connect
Сначала я не решался использовать это решение, потому что подумал: «Какой смысл тогда вспомогательная функция?» . Теперь я думаю, что они хороши и достаточно высокого уровня, что делает все более читабельным.
скрипты / index.js
import configureStore from './store'
import { PageControls, TetrisGame } from './components'
const store = configureStore()
const pageControls = new PageControls(store)
const tetrisGame = new TetrisGame(store)
Это точка входа в приложение. store
PageControls
TetrisGame
Прежде чем переместить магазин сюда, он выглядел в основном так же, но без передачи магазина всем модулям по отдельности. Как упоминалось ранее, компоненты имели доступ к хранилищу через мой неудачный подход к connect
Компоненты
Я решил работать с двумя видами компонентов: презентационные и контейнерные .
Презентационные компоненты не делают ничего кроме чистой обработки DOM; они не знают о магазине. С другой стороны, компоненты контейнера могут отправлять действия или подписываться на изменения.
Дан Абрамов написал отличную статью об этом для компонентов React, но методология может быть применена и к любой другой архитектуре компонентов.
Для меня есть исключения, хотя. Иногда компонент действительно минимален и делает только одно. Я не хотел разбивать их на один из вышеупомянутых шаблонов, поэтому я решил смешать их. Если компонент растет и приобретает больше логики, я его отделю.
скрипты / компоненты / pageControls.js
import { $$ } from '../utils'
import { startGame, endGame, addScore, openSettings } from '../actions'
export default class PageControls {
constructor ({ selector, store } = {}) {
this.$buttons = [...$$('button, [role=button]')]
this.store = store
}
onClick ({ target }) {
switch (target.getAttribute('data-action')) {
case 'endGame':
this.store.dispatch(endGame())
this.store.dispatch(addScore())
break
case 'startGame':
this.store.dispatch(startGame())
break
case 'openSettings':
this.store.dispatch(openSettings())
break
default:
break
}
target.blur()
}
addEvents () {
this.$buttons.forEach(
$btn => $btn.addEventListener('click', this.onClick.bind(this))
)
}
}
Приведенный выше пример является одним из этих компонентов. Он имеет список элементов (в данном случае все элементы с атрибутом data-action
Ничего больше. Другие модули могут затем прослушивать изменения в хранилище и обновлять себя соответственно. Как уже упоминалось, если компонент также сделал обновления DOM, я бы отделил его.
Теперь позвольте мне показать вам четкое разделение компонентов обоих типов.
Обновление DOM
Один из самых больших вопросов, которые у меня возникли при запуске проекта, — как на самом деле обновить DOM. React использует быстрое представление DOM в памяти, называемое Virtual DOM, чтобы свести обновления DOM к минимуму.
Я на самом деле думал сделать то же самое, и я вполне мог бы перейти на Virtual DOM , если мое приложение будет расти больше и тяжелее DOM, но сейчас я делаю классические манипуляции с DOM, и это прекрасно работает с Redux.
Основной поток выглядит следующим образом:
- Новый экземпляр компонента контейнера инициализируется и передается в
store
- Компонент подписывается на изменения в магазине
- И использует другой компонент представления для отображения обновлений в DOM
Примечание: я фанат префикса $
Как вы уже догадались, он берется из $
Следовательно, имена файлов чисто презентационных компонентов начинаются со знака доллара.
скрипты / index.js
import configureStore from './store'
import { ScoreObserver } from './components'
const store = configureStore()
const scoreObserver = new ScoreObserver(store)
scoreObserver.init()
Здесь нет ничего необычного. Контейнерный компонент ScoreObserver
Что это на самом деле делает? Он обновляет все элементы представления, связанные с оценкой: список рекордов и, во время игры, информацию о текущем счете.
скрипты / компоненты / scoreObserver / index.js
import { isRunning, getScoreList, getCurrentScore } from '../../store'
import ScoreBoard from './$board'
import ScoreLabel from './$label'
export default class ScoreObserver {
constructor (store) {
this.store = store
this.$board = new ScoreBoard()
this.$label = new ScoreLabel()
}
updateScore () {
if (!isRunning(this.store)) {
return
}
this.$label.updateLabel(getCurrentScore(this.store))
}
updateScoreBoard () {
this.$board.updateBoard(getScoreList(this.store))
}
init () {
this.store.subscribe(this.updateScore.bind(this))
}
}
Имейте в виду, что это простой компонент; другие компоненты могут иметь более сложную логику и заботиться о вещах. Что здесь происходит? Компонент ScoreObserver
store
Метод init
$label
Метод updateScoreBoard
Не имеет смысла обновлять список каждый раз, когда происходит изменение, так как представление все равно не активно. Существует также компонент маршрутизации, который обновляет или деактивирует различные компоненты при каждом изменении представления. Его API выглядит примерно так:
route.onRouteChange((leave, enter) => {
if (enter === 'scoreboard') {
scoreObserver.updateScoreBoard()
}
})
Примечание: $
$$
document.querySelector
скрипты / компоненты / scoreObserver / $ board.js
import { $ } from '../../utils'
export default class ScoreBoard {
constructor () {
this.$board = $('.tetrys-scoreboard')
}
emptyBoard () {
this.$board.innerHTML = ''
}
createListItem (txt) {
const $li = document.createElement('li')
const $span = document.createElement('span')
$span.appendChild(document.createTextNode(txt))
$li.appendChild($span)
return $li
}
updateBoard (list = []) {
const fragment = document.createDocumentFragment()
list.forEach((score) => fragment.appendChild(this.createListItem(score)))
this.emptyBoard()
this.$board.appendChild(fragment)
}
}
Опять же, базовый пример и базовый компонент. Метод updateBoard()
скрипты / компоненты / scoreObserver / $ label.js
import { $ } from '../../utils'
export default class ScoreLabel {
constructor () {
this.$label = $('.game-current-score')
this.$labelCount = this.$label.querySelector('span')
this.initScore = 0
}
updateLabel (score = this.initScore) {
this.$labelCount.innerText = score
}
}
Этот компонент выполняет почти то же самое, что и ScoreBoard
Другие ошибки и советы
Другим важным моментом является реализация магазина, основанного на сценариях использования. На мой взгляд, важно хранить только то, что необходимо для приложения. В самом начале я хранил почти все: текущий активный вид, настройки игры, результаты, эффекты при наведении, схему дыхания пользователя и так далее.
Хотя это может быть актуально для одного приложения, это не для другого. Может быть хорошо сохранить текущее представление и продолжить работу в той же позиции при перезагрузке, но в моем случае это воспринималось как плохой пользовательский опыт и скорее раздражающий, чем полезный. Вы бы не хотели хранить переключатели меню или модальных, не так ли? Почему пользователь должен вернуться в это конкретное состояние? Это может иметь смысл в большем веб-приложении. Но в моей маленькой игре, ориентированной на мобильные устройства, довольно неприятно возвращаться к экрану настроек только потому, что я остановился там.
Вывод
Я работал над проектами Redux с React и без него, и мой главный вывод заключается в том, что огромные различия в дизайне приложений не нужны. Большинство методологий, используемых в React, можно адаптировать к любой другой настройке обработки представления. Мне потребовалось некоторое время, чтобы осознать это, поскольку я начал думать, что должен действовать по-другому , но в конце концов я решил, что в этом нет необходимости.
Однако отличается от того, как вы инициализируете свои модули, свой магазин и насколько осведомлен компонент о состоянии приложения в целом. Концепции остаются прежними, но реализация и объем кода соответствуют именно вашим потребностям.
Redux — отличный инструмент, который помогает структурировать ваше приложение более продуманно. При использовании в одиночку, без каких-либо библиотек представлений, сначала это может быть довольно сложно, но как только вы преодолеете это первоначальное замешательство, ничто не сможет вас остановить.
Что вы думаете о моем подходе? Вы использовали Redux один с другой настройкой обработки представления? Я хотел бы получить ваши отзывы и обсудить это в комментариях.
Если вам нужна дополнительная информация о Redux, ознакомьтесь с нашим мини-курсом « Перезапись и тестирование Redux для решения проблем проектирования» . В этом курсе вы создадите приложение Redux, которое получает твиты, организованные по темам, через соединение с веб-сокетом. Чтобы получить представление о том, что в магазине, ознакомьтесь с бесплатным уроком ниже.