В статье хочу поделиться опытом переписывания существующих классовых компонентов 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-apiimport { ref, defineComponent } from '@nuxtjs/composition-api' interface IStateObject { name: string, value: number } export default defineComponent({ name: 'ExampleClass', setup() { const objectvalue = reactive<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их нужно превратить в стейт компонента. Для этого используется методtoRefsimport { 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- пропсы типизируются с помощьюPropTypeComputed 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- вычисляемые свойства создаются с помощью методаcomputedimport { 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-apiimport { 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-apiimport { 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-apiimport { 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-apiimport { 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-apiimport { 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-apiimport { ref, onMounted, onBeforeUnmount, defineComponent } from '@nuxtjs/composition-api' export default defineComponent({ name: 'ExampleClass', setup() { let timeoutId = null const isVisible = ref(false) onMounted(() => { timeoutId = setTimeout(() => { isVisible.value = true }, 300) }) onBeforeUnmount(() => { clearTimeout(timeoutId) }) } })Решение не такое изящное, так как приходится выносить локальную переменную в общую кучу.
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,immediateEmit событий
* Эмитируем событие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- для доступа к значением контекст необходимо использовать методuseContextimport { 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, то есть часть состояния компонента. Чтобы свойство компонента связалось с компонентом необходимо ее вернуть изsetupimport { 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лучше хранить в самом компонентеchildComponentExampleClass
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- Тут есть несколько вариантов как обратиться к текущему элементу. Рассмотрим вариант сrefimport { 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- будем использовать методgetCurrentInstanceimport { 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 +getCurrentInstanceimport { 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 описывается в декораторе@ComponentExampleProvideClass
@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/injectExampleProvideClass
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изuseContextimport { 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описать название фичи дляNuxtAppOptionsimport { 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
