Отладка Vue composables
Повышаем удобство отладки и улучшаем возможности статического анализа Vue composables
Содержание
В коммерческой разработке сложность веб-приложений часто перерастает комфортный для разработчика уровень. Это значит, что такие веб-приложения неизбежно придется отлаживать в поисках источника бага или при выяснении связей компонентов веб-приложения друг с другом.
В этой статье я расскажу, как повысить удобство отладки веб-приложения на Vue и как улучшить возможности IDE по статическому анализу кода.
Деструктуризация возвращаемого значения composable
В приложениях на vue часто используются composable для переиспользования кусочков стейтфул-логики. Вот типичный пример composable:
export const useMyButton = () => {
const clickCounter = ref(0)
const doubledClickCount = computed(() => clickCounter.value * 2)
const click = () => {
clickCounter.value++
}
return { clickCounter, doubledClickCount, click }
}
И вот типичный пример его использования в компоненте:
<template>
<button @click="click"><slot></slot></button>
<div>clicks x 2: {{ doubledClickCount }}</div>
</template>
<script setup lang="ts">
import { useMyButton } from '@/composables/useMyButton.ts'
const { click, doubledClickCount } = useMyButton()
</script>
Деструктуризация возвращаемого значения composable-функции позволяет явно обозначить используемые части потребляемого API. Это плюс. Минус же такого подхода в том, что некоторые элементы внутреннего состояния composable не видны в Vue dev tools.

Vue dev tools не показывают никаких полей, возвращаемых из composable, кроме тех, на которые ссылается компонент в своей setup функции.
Попытка решить проблему: перестаем использовать деструктуризацию возвращаемого значения composable
Очевидный способ решить вышеописанную проблему — перестать деструктурировать возвращаемое значение composable-функции.
<template>
<button @click="myButton.click"><slot></slot></button>
<div>clicks x 2: {{ myButton.doubledClickCount }}</div>
</template>
<script setup lang="ts">
import { useMyButton } from '@/composables/useMyButton.ts'
const myButton = useMyButton()
</script>

Vue dev tools показывает все поля, возвращаемые composable.
Теперь Vue dev tools показывают все поля, возвращаемые из composable. Это позволяет нам отлаживать внутреннее состояние composable.
Но такой подход создает другую проблему. Теперь компонент явно не обозначает потребляемые поля composable, и отследить использование каждого поля в отдельности становится сложно. Все ссылки разбросаны по коду компонента, и IDE не помогает нам их отслеживать. Теперь при рефакторинге composable нам придется вручную обходить каждого потребителя, чтобы удостовериться, использует он конкретное поля composable или нет.
Проблема в том, что IDE (любая), используя средства статического анализа кода, не может отследить использование конкретного поля, созданного в области видимости composable-функции.
Решаем проблему, используя классы вместо функций
Но IDE хорошо справляется с отслеживанием ссылок на публичные поля классов. Почему бы нам не использовать классы вместо функций при написании composables? Вот так будет выглядеть вышеупомянутый composable, переписанный на класс:
export class UseMyButton {
clickCounter = ref(0)
doubledClickCount = computed(() => this.clickCounter.value * 2)
constructor() {
this.click = this.click.bind(this)
}
click() {
this.clickCounter.value++
}
}
Использование его в компоненте-потребителе будет выглядеть так:
<template>
<button class="my-button" @click="myButton.click"><slot></slot></button>
<div>clicks x 2: {{ myButton.doubledClickCount }}</div>
</template>
<script setup lang="ts">
import { UseMyButton } from '@/composables/useMyButton.ts'
const myButton = new UseMyButton()
</script>
Теперь IDE показывает все ссылки на каждое публичное поле composable-класса (в JetBrains IDE это работает из коробки, а в VSCode нужно включить Code Lens references). Поэтому мы можем избегать деструктуризации composable в компоненте, не теряя возможности отследить потребителей.
Проблема решена
Итак, избегая деструктуризации возвращаемого значения composable, мы получаем возможность отладки полного внутреннего состояния composable через Vue dev tools. Используя классы вместо функций для создания composables, мы получаем возможность отслеживать потребителей каждого поля composable, что повышает ясность связей в коде и упрощает рефакторинг composables.
Возможности для дополнительных улучшений
Вероятно, не все поля composable стоит делать публичными. При написании composables
в виде функций проблема решается просто: не возвращаем ссылку — поле недоступно извне
(и для Vue dev tools они тоже недоступны).
В подходе с composable-классом можно использовать private поля класса (в TypeScript).
Такие поля являются приватными лишь на уровне проверки типов, но при этом
доступны во время выполнения, т.е. они доступны для Vue dev tools. Идеально.
Дополнительные детали
Реальные компоненты сложнее вышеописанных. В них есть вотчеры, хуки жизненного цикла, необходимость использования автоматических механизмов Vue для освобождения ресурсов. composable-классы подходят для такого использования. Нужно помнить, что все, что обычно вызывается в теле composable-функции, нужно поместить в тело конструктора класса в composable-классе.
export class UseMyButton {
private readonly clickCounter = ref(0)
doubledClickCount = computed(() => this.clickCounter.value * 2)
constructor() {
this.click = this.click.bind(this)
onMounted(() => {
console.log('mounted')
})
}
click() {
this.clickCounter.value++
}
}
Итог
composable-классы, по-видимому, неплохо подходят для применения в коммерческих приложениях. При первом подходе я не обнаружил существенных недостатков такого подхода, кроме его необычности. Я не применял этот подход в реальных приложениях, поэтому не гарантирую его жизнеспособность. Но выглядит он достаточно перспективно. А что думаете вы?