Основы Vuex: Учебник и объяснение

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

В этой статье по-прежнему очень подробно рассматривается почему vuex важен, как он работает и как он может сделать ваши приложения лучше и проще в обслуживании.

Vuex — это разрабатываемая и прототипная библиотека создателя Vue.js, которая поможет вам создавать более крупные приложения более удобным для сопровождения способом, следуя принципам, аналогичным тем, которые стали популярными в библиотеке Flux Facebook (и последующих итерациях сообщества, таких как redux).

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

Что мы строим?

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

Допустим, в приложении есть два компонента:

  1. Кнопка (которая является источником события)
  2. Счетчик (который должен отражать обновления на основе исходного события)

Два компонента не знают друг о друге и не могут взаимодействовать друг с другом. Это очень распространенный шаблон даже в самых маленьких веб-приложениях. В больших приложениях десятки компонентов взаимодействуют друг с другом и должны контролировать друг друга. Не верите мне? Вот некоторые взаимодействия даже для простого списка задач:

Цели этой статьи

Мы рассмотрим четыре способа решения одной и той же проблемы:

  1. Использование широковещательных событий для связи между компонентами
  2. Использование объекта общего состояния
  3. Использование вьюекса

Надеюсь, после прочтения этой статьи вы сможете понять:

  1. Очень простой рабочий процесс использования vuex в вашем проекте.
  2. Какие проблемы он решает.
  3. Почему это может быть лучший подход, чем другие методы, несмотря на то, что он немного более подробный и строгий.

Создание отправной точки

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

$ npm install -g vue-cli $ vue init webpack vuex-tutorial $ cd vuex-tutorial $ npm install $ npm install --save vuex $ npm run dev

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

Сначала мы создаем компонент IncrementButton в src/components/IncrementButton.vue:

<template> <button @click.prevent="activate">+1</button> </template> <script> export default { methods: { activate () { console.log('+1 Pressed') } } } </script> <style> </style>

Затем мы создаем компонент CounterDisplay для фактического отображения счетчика. Давайте создадим новый базовый компонент vue в src/components/CounterDisplay.vue.

<template> Count is {{ count }} </template> <script> export default { data () { return { count: 0 } } } </script> <style> </style>

Замените App.vue этим файлом:

<template> <div id="app"> <h3>Increment:</h3> <increment></increment> <h3>Counter:</h3> <counter></counter> </div> </template> <script> import Counter from './components/CounterDisplay.vue' import Increment from './components/IncrementButton.vue' export default { components: { Counter, Increment } } </script> <style> </style>

Теперь, если вы снова запустите npm run dev и откроете страницу в своем браузере, вы должны увидеть кнопку и счетчик. Нажатие на кнопку показывает сообщение в консоли, больше ничего. Итак, теперь, когда у нас есть отправная точка, давайте продолжим.

Решение 1. Трансляции событий

Давайте внесем изменения в скрипты в компонентах. Во-первых, в IncrementButton.vue мы используем $dispatch для отправки сообщения родителю о том, что кнопка нажата.

export default { methods: { activate () { // Send an event upwards to be picked up by App this.$dispatch('button-pressed') } } }

В App.vue мы слушаем событие от дочернего элемента и повторно транслируем новое событие всем дочерним элементам для увеличения.

export default { components: { Counter, Increment }, events: { 'button-pressed': function () { // Send a message to all children this.$broadcast('increment') } } }

В CounterDisplay.vue мы слушаем событие increemnt и увеличиваем значение в состоянии.

export default { data () { return { count: 0 } }, events: { increment () { this.count ++ } } }

Некоторые недостатки этого подхода:

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

  1. Для каждого действия родительские компоненты должны связывать и «отправлять» события нужным компонентам.
  2. Трудно понять, откуда могут браться события для более крупного приложения.
  3. Нет четкого места для «бизнес-логики». this.count++ находится в CounterDisplay, но бизнес-логика может быть где угодно, что может сделать его непригодным для сопровождения.

Приведу пример того, как такой подход может привести к багу:

  1. Вы нанимаете двух стажеров: Алису и Боба. Вы говорите Алисе, что вам нужен еще один счетчик в другом компоненте. Вы говорите Бобу написать кнопку сброса.
  2. Алиса пишет новый компонент FormattedCounterDisplay, который подписан на инкремент и увеличивает свое собственное состояние. Алиса счастлива, совершает и подталкивает.
  3. Боб пишет новый компонент «Сброс», который отправляет событие сброса в приложение, которое повторно отправляет его. Он реализует сброс на CounterDisplay, чтобы установить счетчик на 0, но он не знает, что компонент Алисы также подписан на него.
  4. Ваш пользователь нажимает «+1» и видит, что приложение работает нормально. Но когда пользователь нажимает «сброс», сбрасывается только один счетчик.

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

Вернемся к тому, что мы сделали в Решении 1. Создадим новый файл src/store.js.

export default { state: { counter: 0 } }

Давайте сначала изменим CounterDisplay.vue:

<template> Count is {{ sharedState.counter }} </template> <script> import store from '../store' export default { data () { return { sharedState: store.state } } } </script>

Здесь мы делаем довольно много интересных вещей:

  1. Мы получаем объект хранилища, который является всего лишь одним постоянным объектом. Но это определяется в другом файле.
  2. В наших локальных данных мы создаем новый параметр с именем sharedState, сопоставленный с store.state.
  3. Поскольку это часть данных, vue делает store.state реактивным, что означает, что vue будет автоматически обновлять sharedState всякий раз, когда что-либо в store.state изменяется.

Пока все равно не получится. Но теперь мы можем изменить IncrementButton.vue

import store from '../store' export default { data () { return { sharedState: store.state } }, methods: { activate () { this.sharedState.counter += 1 } } }
  1. Здесь мы импортируем хранилище и подписываемся на реактивное состояние, как и в предыдущем примере.
  2. Когда вызывается функция активации, она переходит к общему состоянию, которое по-прежнему ссылается на store.state и увеличивает значение счетчика.
  3. Все компоненты и вычисляемые свойства, подписанные на счетчик, теперь будут обновлены.

Чем это лучше решения 1

Вернемся к проблеме двух стажеров — Алисы и Боба.

  1. Алиса пишет FormattedComponentDisplay, чтобы подписаться на счетчик общего состояния, который всегда будет показывать последнее значение счетчика.
  2. Компонент ResetButton Боба устанавливает счетчик общего состояния в 0. Это повлияет на CounterDisplay и FormattedCounterDisplay, которые написала Алиса.
  3. Пользователь замечает, что кнопка сброса работает должным образом

Как это еще не достаточно хорошо.

  1. В течение периода своего сотрудничества Алиса и Боб пишут несколько дисплеев счетчиков, кнопок сброса и увеличения в разных форматах, которые обновляют один и тот же общий счетчик. Жизнь хороша.
  2. Как только они вернутся в колледж, вы должны поддерживать их код.
  3. Кэрол — приходит новый менеджер и говорит: «Я никогда не хочу, чтобы счетчик поднимался выше 100».

Чем вы сейчас занимаетесь?

  1. Вы идете по всем десяткам компонентов и выясняете, где они обновляют счетчик? Это расстраивает.
  2. Вы идете на дисплеи и добавляете фильтр/форматтер там? Это тоже расстраивает.

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

Немного лучший подход

Теперь, когда вы реорганизовали весь исходный код, вы переписываете свой store.js следующим образом:

var store = { state: { counter: 0 }, increment: function () { if (store.state.counter < 100) { store.state.counter += 1; } }, reset: function () { store.state.counter = 0; } } export default store

Это делает код намного чище, поскольку вы явно вызываете приращение, и вся ваша бизнес-логика находится в хранилище. Однако новый стажер, не знающий теории, стоящей за всем этим, считает, что проще просто написать в store.state.counter из какой-то другой части приложения, и его становится сложно отлаживать.

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

Решение 3: Vuex

Давайте вернем все изменения, которые мы сделали в Решении 2. В принципе, vuex работает примерно так же, как в Решении 2. Вот немного пугающая диаграмма:

Давайте сначала снова создадим src/store.js, но на этот раз у нас будет такой код:

import Vuex from 'vuex' import Vue from 'vue' Vue.use(Vuex) var store = new Vuex.Store({ state: { counter: 0 }, mutations: { INCREMENT (state) { state.counter ++ } } }) export default store

Итак, давайте посмотрим, что происходит в этом коде:

  1. Мы получаем модуль Vuex и инструктируем Vue использовать его для активации плагина.
  2. Наш магазин больше не является простым объектом json, а является экземпляром Vuex.Store.
  3. Мы снова создаем счетчик в состоянии, установленном в 0
  4. У нас есть новый объект мутаций, который имеет INCREMENT, принимает состояние ввода и изменяет это состояние.

Вот несколько интересных вещей в этом коде:

  1. Все, что требует ('../store.js') или импортирует хранилище из ../store.js, будет использовать один и тот же экземпляр хранилища.
  2. Мы никогда не редактируем store.state.counter, но получаем копию состояния, которую должны обновить и изменить. Это будет важно позже.

Теперь, когда мы рассмотрели магазин, давайте перейдем к IncrementButton.vue.

import store from '../store' export default { methods: { activate () { store.dispatch('INCREMENT') } } }

Этот компонент даже не имеет никаких данных. Но при клике мы вызываем store.dispatch(‘INCREMENT’) . Мы скоро вернемся к этому.

Теперь обновите CounterDisplay.vue.

<template> Count is {{ counter }} </template> <script> import store from '../store' export default { computed: { counter () { return store.state.counter } } } </script>

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

Vue достаточно умен, чтобы понять, что вычисляемое свойство счетчика зависит от store.state.counter, поэтому всякий раз, когда хранилище обновляется, оно будет обновлять все связанные элементы. И это все!

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

  1. Обработчик событий Vue вызывает активацию. Эта написанная нами функция вызывает store.dispatch(‘INCREMENT’).
  2. Здесь INCREMENT — это имя действия. Он представляет собой идентификатор «Это тип изменения, которое следует внести в состояние». Мы также можем передать дополнительные параметры функции отправки, которая содержит дополнительные параметры для действия.
  3. Vue выясняет, какой мутатор вызвать для отправки. Сейчас у нас есть только один, но мы можем сделать его более сложным и настроить для более крупных приложений.
  4. Мутатор получает копию состояния и обновляет ее. Vue хранит старую копию состояния, которую позже можно использовать для расширенных функций.
  5. Когда состояние обновляется, vue автоматически обновляет все компоненты, зависящие от этого аспекта состояния.
  6. Это делает ваш код более тестируемым, если вам нравятся подобные вещи.

Вот почему это НАМНОГО ЛУЧШЕ, чем решение 2:

  1. Если во время разработки сохраняется копия всех состояний, разработчики vue потенциально могут создать то, что известно как «отладчик путешествий во времени». Помимо того, что оно звучит от одного из самых крутых имен супергероев, оно позволит вам «отменять» действия в вашем приложении, изменять логику и развиваться намного быстрее.
  2. Вы можете создать промежуточное ПО для работы при изменении состояния. Например, вы можете создать регистратор, который регистрирует все действия, выполняемые пользователем. Если они обнаружат ошибку, вы можете получить этот журнал, воспроизвести все эти действия и точно воспроизвести их ошибку.
  3. Вынуждая вас иметь все действия в одном месте, он становится хорошим справочником, который может использовать любой в вашей команде, обо всех способах изменения состояния вашего приложения.

Еще предстоит пройти долгий путь.

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

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

Эпилог: работа с кодексом стажера

Итак, вы перенесли свое приложение на vue.js, а ваш стажер до сих пор выясняет, что он может писать в store.state.counter как ярлык в своих компонентах. И у тебя это было, это последняя капля. Вы идете вперед и добавляете одну строку в свой store.js

var store = new Vuex.Store({ state: { counter: 0 }, mutations: { INCREMENT (state) { state.counter ++ } }, strict: true // Vuex's patent pending anti-intern device })

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

Первоначально опубликовано на skyronic.com 3 января 2016 г.