Используем XSTATE для VueJS

от автора

Маленький пример применения библиотеки 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оберем все вместе:

Template

<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> 

JS

const { Machine, interpret } = 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'           }         }     } 	})    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. Так что если вас заинтересовала тема, вам будет чем заняться.

ссылка на оригинал статьи https://habr.com/ru/post/518920/


Комментарии

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *