Vue 3: CompositionAPI + Typescript эксперименты

от автора

В прошлой статье меня упрекнули, что я при живом Vue 3 пишу про «устаревший» Vue 2. Отговорившись тем, что Vue 3 еще не production-ready, я понемногу начал его смотреть и изучать. И поскольку я заядлый любитель типизации и различных фичей с сахарком, то рассматривать Vue 3 с его новеньким CompositionAPI в статье именно с этой точки зрения. А заодно поэкспериментируем и попробуем написать свой типизированный store, организовать компоненты в стиле <script setup> и подружить его с typescript и eslint, а также напишем небольшой компонент на TSX в качестве еще одного эксперимента. Весь код приложения.

Ремарка

Автор пишет всего второй пост, фидбек в комментарии или ЛС — приветствуется! Первый пост про Vue 2 и его типизацию на Typescript можете прочитать здесь, в большие проекты это все еще актуально.

Что глобально нового во Vue 3?

В общем и целом команда разработки фреймворка в этом релизе постаралась максимально упростить работу с реактивностью, улучшить возможности декомпозиции логики на более мелкие части и в дополнение к этому дала буст к производительности (но тестировать это я буду в следующий раз).

Как Vue 3 поможет типизации?

CompositionAPI позволит уйти от привычного всем Vuex, который очень сложно нормально типизировать (в другой статье я попытался максимально легко и полноценно это сделать — и все равно остались серьезные проблемы). Большая часть трудностей касается логики actions Vuex — в разных модулях могут быть экшены с одинаковым названием, поэтому Vuex не может гарантировать тип возвращаемых значений из них. Основные библиотеки для типизации Vuex давно перешли на модульность — импортируете себе конкретный модуль и напрямую через его инстанс совершаете все манипуляции, без навешивания чего-либо на Vue прототип. Поэтому в этой статье попробуем написать замену Vuex.

Также во vue 3 можно уйти от class-components, которые сильно улучшали типизацию во vue 2, а все писать прямо в <script lang="ts" setup> и полностью типизировать состояние, методы, хуки и тп, новые возможности setup позволят типизировать сигнатуры Emit ивентов (правда, только со стороны «детей»).

Какие проблемы остались?

Типизация в темплейтах — головная боль Vue, которой нет в не любимом мной реакте. Эта типизация касается как различных циклов и слотов, так и props, eventHandlers на кастомные компоненты, их типизация доступна только внутри компонентов-детей. Попробуем это решить с помощью TSX и новым setup’ом.

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

Я использовал vite для создания проекта, дополнительные настройки — typescript (vite template vue-ts), в дополнение установил eslint с рекомендованными Vue пресетами, со следующим отключенный правилом, позже объясню его значимость:

'@typescript-eslint/no-unused-vars': 'off', // uses in new <script setup>

Вы можете использовать Vue 3 откуда угодно, важно лишь его наличие и typescript.

Хранилище данных (store)

Я давно хотел это сделать вместо Vuex встроить кастомное хранилище на новых Ref и Reactive апи. В ходе раздумий принял решение архитектурно сохранить вид «как Vuex», что означает соблюдение следующих принципов:

  1. Модульность (куда же без нее)

  2. Разбиение на те же составляющие для работы с данными

Каждый слой будет отвечать за те же действия, что и аналогичный в Vuex:

  1. State — класс с статичными полями, каждое обернуто в соответствии с типом в Ref (для примитивов) или Reactive (для объектов, массивов и т.п.)

  2. Mutations — класс, который напрямую взаимодействует со State, состояние меняется только с помощью мутаций, они также не доступны в открытом апи модуля (так как все манипуляции со стором должны идти через actions)

  3. Actions — бизнес логика модулей, под собой вызывает мутации

  4. Getters — получение данных из состояния с необходимыми изменениями и кэшированием (например — отфильтрованный массив элементов, но если нужен просто сам массив целиком — читайте его сразу из стейта)

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

Код хранилища данных (store)
/**  * @file src/store/index.ts  * @author Artem Shuvaev  * @version 1.0.0  * @fileoverview Entry point of store  */  import habrModule from './modules/habrModule'  export { habrModule } 
/**  * @file src/store/modules/habrModule/index.ts  * @author Artem Shuvaev  * @version 1.0.0  * @fileoverview Entry of store module  */  import state from './state' import getters from './getters' import actions from './actions'  export default {     state,     getters,     actions }
/**  * @file src/store/modules/habrModule/state.ts  * @author Artem Shuvaev  * @version 1.0.0  * @fileoverview State for store example with new Ref and Reactive Vue3  */  import { ref, reactive } from 'vue'  export default class State {   /**    * Example string ref (reactive)    */   static strExample = ref('string example')    /**    * Example number ref (reactive)    */   static numberExample = ref(0)    /**    * Example string reactive    */   static objExample = reactive({     property: 'property string',   }) } 
/**  * @file src/store/modules/habrModule/mutations.ts  * @author Artem Shuvaev  * @version 1.0.0  * @fileoverview Mutations for store example with new Ref and Reactive Vue3  */  import State from './state'  export default class Mutations {   /**    * Set to state new string    * @param value     */   static setString (value: string): void {     State.strExample.value = value   }    /**    * Increate the number in state    */   static increaseNumber (): void {     State.numberExample.value++   } } 
/**  * @file src/store/modules/habrModule/getters.ts  * @author Artem Shuvaev  * @version 1.0.0  * @fileoverview Getters for store example with new Ref and Reactive Vue3  */  import type { Ref } from 'vue' import State from './state'  export default class Getters {   /**    * Test getter with cache    * @deprecated must use readonly and state proxy if not need any business logic    */   static get strExample(): Ref<string> {     return State.strExample   } } 

Компонент в новом синтаксисе setup

Главной «фичой» которую я хотел посмотреть и потрогать во Vue 3 является новый способ написания компонентов, и если вы подумали о новом defineComponent — то вы немного ошиблись. В 3 мажорной версии, как известно, введен новый способ описания логики — внутри специальной функции setup. Что-то типо такого.

<script> export default {   props: {     title: String   },   setup(props) {     console.log(props.title)   } } </script>

Но недавно я открыл для себя возможность другого, более «сахарного» приема для этой функции:

<script setup> // all from SETUP here </script>

Что это дает? Упрощение и этим все сказано! В сочетании с новым Composition API это позволит дробить компоненты до условной бесконечности, ну и писать немножко проще. Для экспериментов над Vue 3 я выбрал его как основной стиль написания компонентов, но в дополнение ниже я приведу пример компонента на TSX.

Что из минусов? К сожалению, редакторы кода (а заодно и eslint) не видят использования переменных из этого скрипта в темплейте — это может привести к действительно не используемым переменным. Также нужно быть осторожными с формированием таких компонентов. Vue не декларирует как вы должны их писать, поэтому важно договориться в команде/проекте об особенностях данного стиля написания. А еще это экспериментальная фича, и надо помнить об этом (как и сказано выше — рубрика — эксперименты!)

Ниже можете увидеть мои мысли в реализации примера, но я сделал это из собственной головы, если вы придумаете другую реализацию (например, некоторые объявляют состояние компонента единым объектом, как во Vue 2 и оборачивает его в Reactive — это тоже жизнеспособный подход, возможно даже лучше моего), не поленитесь рассказать о ней в комментариях, пожалуйста!

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

Код — App.vue
<template>   <div>     <!-- Main body -->     <div class="main">       <h5>App.vue</h5>        String from store: {{ readonlyStringFromStore }}        <div>         <button @click="increaseCounter">Counter: {{ counter }}</button>       </div>     </div>     <!-- /Main body -->      <TheHelloWorldComponent :counter="counter" @customEvent="increaseCounter" />      <TheTestComponent :counter="counter" @click="increaseCounter" />   </div> </template>  <script lang="ts" setup> /**  * @file src/App.vue  * @author Artem Shuvaev  * @version 1.0.0  * @fileoverview Entry Vue example file for it's project  */  import { ref, onMounted, readonly, defineAsyncComponent } from 'vue' import { habrModule } from './store'  /* Components */  // Vue + new setup script example component const TheHelloWorldComponent = defineAsyncComponent(   () => import('./components/TheHelloWorldComponent.vue') )  // Vue + new setup and TSX render example const TheTestComponent = defineAsyncComponent(   () => import('./components/TheTestComponent/index') )  /* Data */  // must be readonly cause of state proxying const readonlyStringFromStore = readonly(habrModule.state.strExample)  // simple reactive variable const counter = ref(0)  /* Hooks */ onMounted(() => {   habrModule.actions.setString('changed from App.vue string') })  /* Methods */  /**  * Increase local reactive counter  */ const increaseCounter = () => {   counter.value++ } </script>¬  <style lang="css"> @import 'styles.css'; </style>  <style lang="css" scoped> .main {   background-color: aquamarine;   padding: 5vh; } </style> 

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

Взаимодействие детей с родителями

Выше приведен пример компонента-родителя, внутри которого есть пара детей, для примера (поскольку в интернете еще не так много документации об этой особенности) я приведу полноценный компонент с использованием props, имитирующий события в родителя и взаимодействующий с хранилищем данных.

Из новых особенностей тут — можно типизировать emit полноценно, вместе с полезной нагрузкой, лучше чем при использовании class-components во Vue 2.

Неверный тип payload, это ли не чудо типизации?
Неверный тип payload, это ли не чудо типизации?
Код TheHelloWorldComponent
<template>   <div class="hello">     <h5>TheHelloWorldComponent</h5>      Now counter is {{ props.counter }}     <div>       <button @click="emitCustomEvent">         Emit custom event (increase counter)       </button>       <button @click="changeString">Change string in store</button>     </div>   </div> </template>  <script lang="ts" setup> /**  * @file /src/components/TheHelloWorldComponent.vue  * @fileoverview Child component example with using new setup, emits, store etc  * @author Artem Shuvaev  * @version 1.0.0  */  import { defineEmit, defineProps } from 'vue' import { habrModule } from '../store'  /* Props */ const props = defineProps({   counter: {     type: Number,     required: true,   }, })  /* Emits */  // Define emits signatures,  // I don't know but signatures works only with null as output type (instead VOID or UNDEFINED) const emit = defineEmit({   customEvent: null,   customEvent2: (payload: { test: string }) => null, })  /* Methods */  /**  * Call dispatch of store module  */ const changeString = () =>   habrModule.actions.setString('changed from HelloWorld.vue')  /**  * @emits customEvent  */ const emitCustomEvent = () => emit('customEvent')  // it's typed payload! const emitCustomEvent2 = () =>   emit('customEvent2', {     test: 'string',   }) </script>  <style lang="css" scoped> .hello {   background-color: lightsteelblue;   padding: 5vh;   margin-top: 5vh; } </style>

Закуска — TSX компонент

Да, я люблю типизацию, а что мешает полноценной типизации во Vue? Правильно — template, это красивая и очень удобная нотация написания компонентов as html, но я хочу туда подсветку, ошибки не в консоли браузера, а в редакторе кода. Есть ли решение? Да, пойти к React и попросить оттуда tsx. Работает ли это во Vue 3? Да, и еще лучше чем во втором! Работает ли это с сахарным сетапом, как выше? Нет, но возможно я неправильно его готовил, если подскажите в комментариях — буду очень благодарен.

Из воды выше — писать необходимо «как обычно», через defineComponent, но в setup нужно возвращать не переменные, которые будут использоваться в темплейте, а сам темплейт в стиле tsx. И вот что у меня получилось.

/**  * @file /src/components/TheTestComponents/index.tsx  * @fileoverview Test component with using TSX rendering in setup function  * @author Artem Shuvaev  * @version 1.0.0  */  import { defineComponent } from 'vue' import styles from './styles.module.css'  export default defineComponent({   name: 'TheTestComponent',   props: {     counter: {       type: Number,       default: 0,     },   },   emits: {     click: null,   },   setup(props, { emit }) {     /** Methods */      /**      * Simple click handler      * @emits click      */     const onClickHandler = () => emit('click')      /** Rendering */     return () => (       <div class={styles.test}>         <h5>TheTestComponent</h5>         <div>Now counter is: {props.counter}</div>         <button onClick={onClickHandler}>Emit click (increase counter)</button>       </div>     )   }, }) 

Это уже стабильно и полностью поддерживается => практически готово к продакшену (в ногу с самим Vue 3). Удобно ли это? С точки зрения типизации — абсолютный рай по сравнению с template. По-vue’шному ли это? Не особенно, но чего не сделаешь ради 100% автоподсказок в коде.

Заключение

Я перечитал весь текст выше много раз, но морали в нем нет и не будет. Я изучал и экспериментировал над новой версией своего любимого фреймворка и буду продолжать это интересное занятие, чего и вам советую. Но надеюсь, что мои эксперименты кому-то помогут! Код приложения.

Нравится ли мне Vue 3? Конечно, это ускорение, новые возможности реактивности и многое другое. Что вызывает сомнения — много вариаций написания приложений, как будто это React, а ведь именно от этой парадигмы «каждое приложение — уникально» Vue и пытался отойти при создании, рекомендуя использовать только экосистемные библиотеки и реализации (роутер, стор и тому подобное). Это своего рода строгий Golang во фронтенде, его код, написанной в далекой и жаркой Индии в соответствии со стайл-гайдами и линтерами будет понятен каждому стартаперу в Долине, а экосистема имеет в себе все необходимое и одинакова для всех, не нужно выбирать — просто пишите код! И я боюсь, что с новым релизом мы отдалимся от этого. Плохо ли это — покажет время и путь развития, который предложат нам создатели фреймворка.

А вам нравится Vue 3? Планируете скорый переход на него в проде, или еще годик пусть поварится в домашнем проекте?

P.S.

Если что — извиняюсь за код в статье, я не придумал, как его оформить красивее, а эти различия в отображениях синтаксиса меня добили, а заодно за материал и его подачу, получилось сумбурно, но я надеюсь интересующимся будет полезно.
Объясните мне, почему код на хабре имеет разную подсветку при написании, в черновиках и после публикации? Это фишка нового редактора или я что-то не понимаю? Причем кардинальные различия. Я понимаю, что я скармливаю не JS, а полноценный Vue файл, но различия в парсинге ключевых слов и комментариев, в редакторе новый и модный, а в самих публикациях старый и глупенький парсер?

Скрины
Черновик публикации
Черновик публикации
Редактор статьи
Редактор статьи

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


Комментарии

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

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