Быстрый старт
Для быстрого старта сделаем простую задачу: счетчик, который будет увеличиваться и уменьшаться при клике на кнопках increment + и decrement -. Изменение счетчика будет асинхронными действиями. Сделаем их долгими по 2 секунды. Во время выполнения действия, кнопки будут блокироваться. Дополнительно сделаем сброс счетчика в 0 по клику на кнопке reset.
Этого достаточно для демонстрации возможностей и преимущества перед другими библиотеками.
1. Определяем класс модели
Counter - стандартный класс унаследованный от ProtoModel, будет содержать свойство _count и методы increment, decrement и reset. Пометим их декоратором @action, чтобы они стали асинхронными действиями, и мы могли работать с ними как с реактивными объектами.
Наследование от ProtoModel обязательно, он содержит служебные методы. Подробнее здесь.
Зависимость от API сервиса будет передаваться в конструктор. Определяем для него интерфейс.
// getting-started/counter.ts
import { ProtoModel, action } from '@vue-modeler/model'
interface ApiService {
inc: (currentCount: number) => Promise<number>
dec: (currentCount: number) => Promise<number>
}
export class Counter extends ProtoModel {
protected _count = 0
constructor(
private apiService: ApiService,
) {
super()
}
get count(): number {
return this._count
}
@action async increment(): Promise<void> {
this._count = await this.apiService.inc(this._count)
}
@action async decrement(): Promise<void> {
this._count = await this.apiService.dec(this._count)
}
@action async reset(): Promise<void> {
this._count = 0
}
}2. Делаем заглушку API сервиса
Сделаем здесь просто заглушку для имитации медленных асинхронных запросов. Методы АПИ сервиса возвращают ошибки случайным образом. Это нужно, чтобы показать работу с ошибками в компоненте.
Ошибки можно обрабатывать и в действиях модели, если это нужно по бизнес логике. Но сейчас нам не нужно, поэтому ошибки не перехватываем.
// getting-started/api-service.ts
const timeout = 2000
export const apiService = {
inc: async (currentCount: number): Promise<number> => {
return new Promise((resolve, reject) => {
if (Math.random() < 0.5) {
reject(new Error('increment failed'))
return
}
setTimeout(
() => resolve(currentCount + 1),
timeout,
)
})
},
dec: async (currentCount: number): Promise<number> => {
return new Promise((resolve, reject) => {
if (Math.random() > 0.5) {
reject(new Error('decrement failed'))
return
}
setTimeout(
() => resolve(currentCount - 1),
timeout,
)
})
}
}3. Создаем модель и регистрируем в контейнере зависимостей
У каждого класса модели есть статический метод model, он создает модель.
Чтобы использовать модель в компоненте, нужно зарегистрировать её в контейнере зависимостей. Функция provider создает провайдер модели.
Провайдер модели это функция, которая создает модель при первом вызове, а при последующих возвращает готовый экземпляр. Подробнее здесь.
// getting-started/dc.ts
import { provider } from '@vue-modeler/dc'
import { Counter } from './counter'
import { apiService } from './api-service'
export const useCounter = provider(() => Counter.model(apiService))4. Используем модель в Vue компоненте
Получаем модель из контейнера привычным способом, через const counter = useCounter. Это похоже на обычный composable.
В компоненте максимально простой код, который отвечает только за отображение. Вся бизнес логика в модели. Состояния и ошибки действий получаем из самих действий.
// getting-started/app-counter.vue
<script setup lang="ts">
import { useCounter } from './dc'
const counter = useCounter()
const isBusy = computed(() =>
counter.increment.isPending
|| counter.decrement.isPending
|| counter.reset.isPending
)
const error = computed(() =>
counter.increment.error?.cause
|| counter.decrement.error?.cause
|| counter.reset.error?.cause
)
</script>
<template>
<div>
<div v-if="error">
{{ error }}
</div>
<div v-else>
Current value: {{ counter.count }}
</div>
<button
:disabled="isBusy"
@click="counter.increment.exec()"
>
{{ counter.increment.isPending ? 'increment ...' : 'increment +' }}
</button>
<button
:disabled="isBusy"
@click="counter.decrement.exec()"
>
{{ counter.decrement.isPending ? 'decrement ...' : 'decrement -' }}
</button>
<button
:disabled="isBusy"
@click="counter.reset.exec()"
>
{{ counter.reset.isPending ? '...' : 'reset' }}
</button>
</div>
</template>