В статье хочу поделиться опытом переписывания существующих классовых компонентов 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 = 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их нужно превратить в стейт компонента. Для этого используется метод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— пропсы типизируются с помощью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— вычисляемые свойства создаются с помощью метода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() { 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— для доступа к значением контекст необходимо использовать метод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будет браться из контекста приложенияМы не все и всегда записываем в контекст, что-то просто инджектится в приложение.
exampleFeature
import { 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
ссылка на оригинал статьи https://habr.com/ru/post/694960/
Добавить комментарий