Маленький пример применения библиотеки XState от David Khourshid для декларативного описания логики компонента VueJS 2. XState это очень развитая библиотека для создания и использования конечных автоматов на JS. Неплохое подспорье в трудном деле создания веб приложений.
Предистория
В моей прошлой статье кратко описано зачем нужны машины состояний (конечные автоматы) и приведена простенькая реализация для работы с Vue. В моем велосипеде были только состояния и декларация состояний выглядела так:
{ idle: ['waitingConfirmation'], waitingConfirmation: ['idle','waitingData'], waitingData: ['dataReady', 'dataProblem'], dataReady: [idle], dataProblem: ['idle']}
По сути это было перечисление состояний и для каждого описан массив возможных состояний, в которые может перейти система. Приложение просто говорит машине состояний хочу перейти в такое состояние, если это возможно машина переходит в нужное состояние.
Этот подход работает, но возникают неудобства. Например если кнопка в разном состоянии должна инициировать переход в разные состояния. Придется городить условия. Вместо декларативности получаем кашу.
Изучив теорию по роликам из Ютюба, стало понятно, что события нужны и важны. В голове родился такой вид декларации:
{ idle: { GET: 'waitingConfirmation', }, waitingConfirmation: { CANCEL: 'idle', CONFIRM: 'waitingData' }, waitingData: { SUCCESS: 'dataReady', FAILURE: 'dataProblem' }, dataReady: { REPEAT: 'idle' }, dataProblem: { REPEAT: 'idle' }}
А это уже очень напоминает то, как описывает состояния библиотека XState. Почитав внимательней доку, я решил убрать самодельный велосипед в сарай, и пересесть на фирменный.
VUE + XState
Установка очень простая, читайте доку, после установки включаем XState в компонент:
import {Machine, interpret} from xstate
Создаем машину на основе объекта-декларации:
const myMachine = Machine({ id: 'myMachineID', context: { /* some data */ }, initial: 'idle', states: { idle: { on: { GET: 'waitingConfirmation', } }, waitingConfirmation: { on: { CANCEL: 'idle', CONFIRM: 'waitingData' } }, waitingData: { on: { SUCCESS: 'dataReady', FAILURE: 'dataProblem' }, }, dataReady: { on: { REPEAT: 'idle' } }, dataProblem: { on: { REPEAT: 'idle' } } }})
Понятно, что есть состояния idle, waitingConfirmation' и есть события в верхнем регистре GET, CANCEL, CONFIRM .
Сама по себе машина не работает, из нее надо создать сервис с помощью функции interpret. Ссылку на этот сервис разместим в наш state, а заодно и ссылку на текущее состояние current:
data: { toggleService: interpret(myMachine), current: myMachine.initialState,}
Сервис надо стартануть start(), а также указать, что при переходах состояния мы обновляем значение current:
mounted() { this.toggleService .onTransition(state => { this.current = state }) .start(); }
В методы добавляем функцию send, ее и используем для управления машиной передачи ей событий:
methods: { send(event) { this.toggleService.send(event); }, }
Ну а дальше все просто. Передавать событие просто вызовом:
this.send(SUCCESS)
Узнать текущее состояние:
this.current.value
Проверить нахождение машины в определенном состоянии так:
this.current.matches(waitingData')
Cоберем все вместе:
<div id="app"> <h2>XState machine with Vue</h2> <div class="panel"> <div v-if="current.matches('idle')"> <button @click="send('GET')"> <span>Get data</span> </button> </div> <div v-if="current.matches('waitingConfirmation')"> <button @click="send('CANCEL')"> <span>Cancel</span> </button> <button @click="getData"> <span>Confirm get data</span> </button> </div> <div v-if="current.matches('waitingData')" class="blink_me"> loading ... </div> <div v-if="current.matches('dataReady')"> <div class='data-hoder'> {{ text }} </div> <div> <button @click="send('REPEAT')"> <span>Back</span> </button> </div> </div> <div v-if="current.matches('dataProblem')"> <div class='data-hoder'> Data error! </div> <div> <button @click="send('REPEAT')"> <span>Back</span> </button> </div> </div> </div> <div class="state"> Current state: <span class="state-value">{{ current.value }}</span> </div></div>
const { Machine, interpret } = XStateconst myMachine = Machine({ id: 'myMachineID', context: { /* some data */ }, initial: 'idle', states: { idle: { on: { GET: 'waitingConfirmation', } }, waitingConfirmation: { on: { CANCEL: 'idle', CONFIRM: 'waitingData' } }, waitingData: { on: { SUCCESS: 'dataReady', FAILURE: 'dataProblem' }, }, dataReady: { on: { REPEAT: 'idle' } }, dataProblem: { on: { REPEAT: 'idle' } } }})new Vue({ el: "#app", data: { text: '', toggleService: interpret(myMachine), current: myMachine.initialState, }, computed: { }, mounted() { this.toggleService .onTransition(state => { this.current = state }) .start(); }, methods: { send(event) { this.toggleService.send(event); }, getData() { this.send('CONFIRM') requestMock() .then((data) => { this.text = data.text this.send('SUCCESS') }) .catch(() => this.send('FAILURE')) }, }})function randomInteger(min, max) { let rand = min + Math.random() * (max + 1 - min) return Math.floor(rand);}function requestMock() { return new Promise((resolve, reject) => { const randomValue = randomInteger(1,2) if(randomValue === 2) { let data = { text: 'Data received!!!'} setTimeout(resolve, 3000, data) } else { setTimeout(reject, 3000) } })}
Ну и конечно все это можно потрогать на jsfiddle.net
Visualizer
XState предоставляет замечательный инструмент Visualizer . Можно посмотреть диаграмму именно вашей машины. И не только посмотреть но и пощелкать по событиям и осуществить переходы. Вот так выглядит наш пример:
Итог
XState отлично работает, вместе с VueJS. Это упрощает работу компонента, позволяет избавиться от лишнего кода. Главное декларация машины позволяет быстро понять логику. Данный пример простой, но я уже пробовал и на более сложном примере для рабочего проекта. Полет нормальный.
В данной статье я использовал только самый базовый функционал библиотеки, так как мне его пока хватает, но библиотека содержит еще массу интересный возможностей:
- Guarded transitions
- Actions (entry, exit, transition)
- Extended state (context)
- Orthogonal (parallel) states
- Hierarchical (nested) states
- History
А есть еще аналогичные библиотеки, например Robot. Вот сравнение Comparing state machines: XState vs. Robot. Так что если вас заинтересовала тема, вам будет чем заняться.