Прежде чем перейти к статье, хочу вам представить, экономическую онлайн игру Brave Knights, в которой вы можете играть и зарабатывать. Регистируйтесь, играйте и зарабатывайте!
В статье хочу поделиться опытом переписывания существующих классовых компонентов vue на новый синтаксис vue-composition-api
.
Немного о нашем стеке.
Наше приложение написано на nuxt2
+ vue-class-components
+ typescript
. Из-за стека переезд на новый nuxt затруднился тем, что прежде чем сменить версию nuxt со 2 на 3 нам нужно переписать все наши компоненты. Тут нас очень спасла библиотека vuejs/composition-api
и nuxtjs-composition-api
В статье разберем случаи от самых примитивных до менее примитивных.
Стоит сразу отметить, что в composition-api
вся магия происходит внутри метода setup
, который включает в себя 2 хука жизненного цикла vue компонента: beforeCreate
и created
Помимо основных примеров я покажу как будет работать типизация в тех или иных кейсах.
* Все названия переменных вымышлены и не используются на продуктиве)
Поехали!
State компонента
* В примерахlocalValue
будет являться часть component state
Вклассовых компонентах
стейт компонента представлен как свойства класса.@Component({}) export default class ExampleClass extends Vue { localValue: string = null }
nuxtjs/composition-api
- примеры кода буду показывать с использованием данной библиотеки. В базе она использует тот жеvuejs/composition-api
и добавляет ряд своих методов для интеграции с nuxt.import { ref, defineComponent } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleClass', setup() { const localValue = ref(null) return { localValue } } })
из setup возвращается объект с теми свойствами, которые далее нужны будут или в
template
или к ним будут обращаться из родительских компонентов.Типизация state
* Типизируем объектobjectvalue
Вклассовых компонентах
стейт компонента типизируется внутри класса.interface IStateObject { name: string, value: number } @Component({}) export default class ExampleClass extends Vue { objectvalue: IStateObject = { name: 'example', value: 2 } }
nuxtjs/composition-api
import { ref, defineComponent } from '@nuxtjs/composition-api' interface IStateObject { name: string, value: number } export default defineComponent({ name: 'ExampleClass', setup() { const objectvalue = ref<IStateObject>({ name: 'example', value: 2 }) return { objectvalue } } })
На примерах видно, что переменной состояния задается дефолтное значение
{ name: 'example', value: 2 }
Пропсы компонента
* В примерах пропсом будет являться значениеexampleProps
Вклассовых компонентах
пропсы передаются в декораторе @Component.@Component({ props: { exampleProps: { type: Number, default: 1 } } }) export default class ExampleClass extends Vue { readonly exampleProps: number localValue: string = nul }
nuxtjs/composition-api
- пропсы описываются так же как и во vue2. Чтобы иметь доступ к пропсам внутриsetup
их нужно превратить в стейт компонента. Для этого используется методtoRefs
import { ref, toRefs, defineComponent } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleClass', props: { exampleProps: { type: Number, default: 1 } }, setup(props) { const { exampleProps } = toRefs(props) const localValue = ref(null) return { localValue } } })
Возвращать пропсы из
setup
не нужно. Они и так будут доступны в template.Типизация пропсов
* Типизируем объектobjectProps
Вклассовых компонентов
типизация пропсов проиходит внутри класса.interface IObjectProps { name: string, value: number } @Component({ props: { objectProps: { type: Object, required: true } } }) export default class ExampleClass extends Vue { readonly objectProps: IObjectProps }
nuxtjs/composition-api
- пропсы типизируются с помощьюPropType
Computed properties или вычисляемые свойства
* В примерахisExamplePropsEqualsTwo
является вычисляемым свойствомВ
классовых компонентах
вычисляемые свойства обозначаются какget
метод@Component({ props: { exampleProps: { type: Number, default: 1 } } }) export default class ExampleClass extends Vue { readonly exampleProps: number get isExamplePropsEqualsTwo () { return this.exampleProps === 2 } }
nuxtjs/composition-api
- вычисляемые свойства создаются с помощью методаcomputed
import { toRefs, computed, defineComponent } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleClass', props: { exampleProps: { type: Number, default: 1 } }, setup(props) { const { exampleProps } = toRefs(props) const isExamplePropsEqualsTwo = computed(() => { return exampleProps.value === 2 }) return { isExamplePropsEqualsTwo } } })
Из-за особенности работы
ref
, чтобы получить значение переменной, нужно обратиться к ее свойствуvalue
Типизация computed properties
В целом в большинстве случаев указывать тип вычисляемую свойству нет необходимости, потому что он правильно определяется, но бывают случаи когда его нужно указать явно.
* Типизируем вычисляемое свойствоisExamplePropsEqualsTwo
Классовые компоненты
@Component({ props: { exampleProps: { type: Number, default: 1 } } }) export default class ExampleClass extends Vue { readonly exampleProps: number get isExamplePropsEqualsTwo (): number { return this.exampleProps === 2 } }
nuxtjs/composition-api
import { toRefs, computed, defineComponent } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleClass', props: { exampleProps: { type: Number, default: 1 } }, setup(props) { const { exampleProps } = toRefs(props) const isExamplePropsEqualsTwo = computed<number>(() => { return exampleProps.value === 2 }) return { isExamplePropsEqualsTwo } } })
Сеттер для вычисляемого свойства
* В примерахinnerValue
является вычисляемым свойством
Вклассовых компонентах
сеттер для вычисляемого свойства, как можно догадаться, назначается с использованиемset
@Component({ props: { value: { type: String, default: null } } }) export default class ExampleClass extends Vue { readonly value: string get innerValue (): string { return value } set innerValue (value: number) { this.$emit('input', value) } }
nuxtjs/composition-api
import { toRefs, computed, defineComponent } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleClass', props: { value: { type: String, default: null } }, setup(props, { emit }) { const { value } = toRefs(props) const innerValue = computed({ get: () => value.value, set: (value) => emit('input', value) }) return { innerValue } } })
Про
emit
пока не думаем. Его разберем далее по статьеМетоды
Тут все достаточно банально.
* В примерахsayHello
является методом.Классовые компоненты
- методы это методы класса.@Component({}) export default class ExampleClass extends Vue { sayHello () { console.log("hello world") } }
nuxtjs/composition-api
import { defineComponent } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleClass', setup() { const sayHello = () => { console.log("hello world") } return { sayHello } } })
Хуки жизненного цикла
Вcomposition-api
список хуков жизненного цикла обновился. ХуковbeforeCreated
иcreated
теперь нет, они имплементированы в setup
* Хукcreated
Классовые компоненты
@Component({}) export default class ExampleClass extends Vue { created () { console.log("created") } }
nuxtjs/composition-api
import { defineComponent } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleClass', setup() { console.log("created") } })
*mounted/onMounted
Классовые компоненты@Component({}) export default class ExampleClass extends Vue { mounted () { console.log("mounted") } }
nuxtjs/composition-api
import { onMounted, defineComponent } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleClass', setup() { onMounted(() => { console.log("mounted") }) } })
Тут может возникнуть недопоминае касательно того где же теперь делать асинхронные запросы. Во vue документации говорится о
Suspense
компонентах - это компоненты, которые имеютasync setup
Не будем останавливаться на этом моменте сейчас. Просто оставлю ссылку на соотвествующую документацию.Отписка от нативных событий window
Решила вынести эту тему отдельно, потому что классовых компонентах очень удобно реализована возможность добавления события в хукbeforeDestroy,
а в новом синтаксисе отписка от нативных событий начинает выглядеть совершенно иначе.Классовые компоненты
- на мовй взгляд очень элегантная реализация получается благодаряthis.$on
@Component({}) export default class ExampleClass extends Vue { isVisible = false mounted () { const timeoutId = setTimeout(() => { this.isVisible = true }, 300) this.$on('hook:beforeDestroy', () => { clearTimeout(timeoutId) }) } }
nuxtjs/composition-api
import { ref, onMounted, onBeforeUnmount, defineComponent } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleClass', setup() { const timeoutId = ref<ReturnType<typeof setTimeout> | null>(null) onMounted(() => { timeoutId.value = setTimeout(() => { this.isVisible = true }, 300) }) onBeforeUnmount(() => { clearTimeout(timeoutId.value) }) } })
Решение не такое изящное, так как приходится выносить локальную переменную в общую кучу.
Watch
* Отслеживаемое свойствоlocalValue
В
классовых компонентах
watch
назначается внутри декоратора@Component
@Component({ watch: { localValue (value: string) { console.log('localValue was updated', value) } } }) export default class ExampleClass extends Vue { localValue: string = null }
nuxtjs/composition-api
- отслеживаемые свойства назначаются с помощью методаwatch
. Причем на каждое свойство назначается отдельныйwatch
.import { ref, watch, defineComponent } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleClass', setup() { const localValue = ref(null) watch(localValue, (value: string) => { console.log('localValue was updated', value) }) return { localValue } } })
watch
также может принимать 3м параметром объект с настройками такими какdeep
,immediate
Emit событий
* Эмитируем событиеinput
Вклассовых компонентах
$emit доступен внутри комнтекста класса компонента@Component({}) export default class ExampleClass extends Vue { notifyOthers () { this.$emit('input', 'new Value') } }
nuxtjs/composition-api
-emit
является свойством объекта, который передается вторым параметром в setupimport { defineComponent } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleClass', setup(_props, { emit }) { const notifyOthers = () => { emit('input', 'new Value') } } })
Контекст
* Значением из контекстаstore
В
классовых компонентах
все что лежит в контексте доступно по ключевому словуthis
@Component({}) export default class ExampleClass extends Vue { get somethingFromStore () { return this.$store.state.app.value } }
nuxtjs/composition-api
- для доступа к значением контекст необходимо использовать методuseContext
import { computed, useContext, defineComponent } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleClass', setup() { const { store } = useContext() const somethingFromStore = computed(() => { return store.state.app.value }) return { somethingFromStore } } })
Ref - сохранение ссылки на html элемент/компонент
* В примерах сохраним ссылку наinput
иchildComponent
В
классовых компонентах
для обращения к ссылкам на переменные используется свойство$refs
, в котором указывается список всех элементов, к которой компонент будет обращаться@Component({}) export default class ExampleClass extends Vue { $refs: { input: HTMLInputElement, childComponent: SomeComponent } emptyInput () { this.$refs.input.value = null } callSomeChildMethod () { this.$refs.childComponent.exampleMethod() } }
nuxtjs/composition-api
- ссылка на элемент это тот жеref
, то есть часть состояния компонента. Чтобы свойство компонента связалось с компонентом необходимо ее вернуть изsetup
import { ref, defineComponent } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleClass', setup() { const input = ref<HTMLInputElement>(null) const childComponent = ref(null) const emptyInput = () => { input.value.value = null } const callSomeChildMethod = () => { childComponent.value.exampleMethod() } return { input, childComponent } } })
Тут отмечу, что конструкция
input.value.value
появилась из-за особенностиref
Типизация ref компонентов
* Типизируем свойство childComponent из примера 14Для
классовых компонентов
ничего не поменяется еслиchildComponent
остается классовым компонентом. Если жеchildComponent
уже переписан под новый синтаксис, то для него необходимо интерфейс, описывающий какие свойства и методы возвращаются изsetup
уchildComponent
* Интерфейс дляchildComponent
лучше хранить в самом компонентеchildComponent
ExampleClass
import { ISomeChildComponent } from './SomeComponent.vue' @Component({}) export default class ExampleClass extends Vue { $refs: { childComponent: ISomeChildComponent } callSomeChildMethod () { this.$refs.childComponent.exampleMethod() } }
SomeChildComponent
import { ref, defineComponent } from '@nuxtjs/composition-api' // наследуемся от Element так как в классовых компонентах ожидается, // что ref будет расширенной версией Element export interface ISomeChildComponent extends Element { someLocalValue: string, someLocalMethod: () => void } export default defineComponent({ name: 'SomeChildComponent', setup() { const someLocalValue = ref(null) const someLocalMethod = () => { console.log("hello") } return { someLocalValue, someLocalMethod } } })
nuxtjs/composition-api
- тут такая же история как и для классовых компонентов. Если компонент, на который есть ссылка является классовым, то ничего не меняем. Если же компонент уже переписан, то компонент необходимо описать в интерфейсе.Рекомендации по сохранению ссылки на самого себя
В некоторых случаях нам нужно обратиться к родительскому блоку компонента
В
классовых компонентах
ссылка на главный блок компонента хранится вthis.$el
@Component({}) export default class ExampleClass extends Vue { findChildElements () { // находим все span элементы внутри данного компонента console.log(this.$el.querySelector('span')) } }
nuxtjs/composition-api
- Тут есть несколько вариантов как обратиться к текущему элементу. Рассмотрим вариант сref
import { ref, defineComponent } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleClass', setup() { const root = ref(null) const findChildElements = () => { // находим все span элементы внутри данного компонента console.log(root.value.querySelector('span')) } return { // обязательно возвращаем root root, findChildElements } } }) <template> <div ref="root"> <span> 1 </span> <span> 2 </span> <span> 3 </span> </div> </template>
В пункте 17 будем рассматривать работу с
currentInstance
и это будет вторым способом обращения к блоку текущего элементаCurrentInstance - обратиться к контексту текущего компонента
Для
классовых компонентов
контекст всегда доступен по ключевому словуthis
, поэтому все что будет далее описано дляcomposition-api
можно смело получить черезthis
.nuxtjs/composition-api
- будем использовать методgetCurrentInstance
import { defineComponent, getCurrentInstance } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleClass', setup() { const instance = getCurrentInstance() const findChildElements = () => { // находим все span элементы внутри данного компонента console.log(instance.proxy.$el.querySelector('span')) } return { findChildElements } } })
Данный способ получения instance очень пригождается, когда есть необходимость обратиться к таким свойствам как например
$vnode
Задачка: Нужно вручную создать и сохранить инстанс компонента через код
Когда переписывала эту часть приложения пришлось пошевелить мозгами и порыть доки.
Такой функционал нам нужен был, чтобы для карты создавать попап и передавать код созданного попапа карте, чтобы уже карта установила его в необходимое ей место.Классовые компоненты
@Component({}) export default class ExampleClass extends Vue { createComponentFromCode (el) { // в el приходит ссылка на блок, куда будет смонтирован компонент const examplePopup = new ExamplePoopup({ parent: this }).$mount(el) } }
nuxtjs/composition-api
- тут мы прибегнем к некоторым хакам работы vue +getCurrentInstance
import { defineComponent, getCurrentInstance } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleClass', setup() { const instance = getCurrentInstance() const createComponentFromCode = (el) => { // в el приходит ссылка на блок, куда будет смонтирован компонент const Popup = Vue.extend(ExamplePoopup) const examplePopup = new ExamplePoopup({ parent: instance.proxy }).$mount(el) } } })
Inject/Provide
* В примерах будет передаваться и инджектиться обхектexampleInject
В
классовых компонентах
provide/inject описывается в декораторе@Component
ExampleProvideClass
@Component({ provide () { return { exampleInject: this.exampleInject } } }) export default class ExampleProvideClass extends Vue { exampleInject = { name: 'example', value: 'inject' } }
ExampleInjectClass
@Component({ inject: ['exampleInject'] }) export default class ExampleInjectClass extends Vue { // для таких случаев конечнолучше написать интерфейс exampleInject: { name: string, value: string } }
nuxtjs/composition-api
- будем использовать методыprovide/inject
ExampleProvideClass
import { ref, provide, defineComponent } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleProvideClass', setup() { const exampleInject = ref({ name: 'example', value: 'inject' }) provide('exampleInject', exampleInject.value) } })
ExampleInjectClass
import { inject, defineComponent } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleInjectClass', setup() { // вторым параметром можно указать значение по умолчанию // Для типизации лучше написать интерфейс const exampleInject = inject<{ name: string, value: string }>('exampleInject', null) } })
Что-то из приложения
* В примерах значениеexampleFeature
будет браться из контекста приложенияМы не все и всегда записываем в контекст, что-то просто инджектится в приложение.
exampleFeatureimport { Plugin } from '@nuxt/types' const plugin: Plugin = (_, inject) => { const feature = { someMethod: () => { console.log("Very cool feature")} } inject('exampleFeature', feature) }
В
классовых компонентах
нет разделения контекстов. Все что было вписано в приложение будет доступно по ключевому словуthis
@Component({}) export default class ExampleClass extends Vue { getSomethingFromApp () { console.log(this.$exampleFeature.someMethod()) } }
nuxtjs/composition-api
- контекст приложения изначально недоступен в компоненте. Для получения контекста приложения необходимо использовать свойствоapp
изuseContext
import { useContext, defineComponent } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleClass', setup() { const { app } = useContext() // через деструктуризацию забираем необходимое нам значение из app const { $exampleFeature } = app const getSomethingFromApp = () => { console.log($exampleFeature.someMethod()) } } })
Типизация чего-то из приложения
Чтобы оповестить
typescript
о том, что в объекте app появилась новая фича, необходимо черезdeclare
описать название фичи дляNuxtAppOptions
import { Plugin } from '@nuxt/types' const plugin: Plugin = (_, inject) => { const feature = { someMethod: () => { console.log("Very cool feature")} } inject('exampleFeature', feature) } declare module '@nuxt/types' { interface NuxtAppOptions { // *** тут лучше написать интерфейс для фичи $exampleFeature: { someMethod: () => void } } }
Спасибо, что дочитали статью доконца. Надеюсь она поможет вам без проблем зарефакторить свою приложение на новый vue синтаксис.
Делитесь своими находнами вовремя рефакторинга в комментариях)
Источники:
Vue - https://vuejs.org/
vuejs/composition-api - https://github.com/vuejs/composition-api
nuxtjs/compiosition-api - https://github.com/nuxt-community/composition-api