На полпути к Vue 3
Николай Пугачев
Frontend developer at
@jspoetry
1. Новые фичи во Vue 3
Структура доклада
2. Паттерны переиспользования функциональности во Vue 2
4. Миграция на Composition API (Vue 3)
5. Миграция на @vue/composition-api (Vue 2)
3. Можно ли мигрировать на Vue 3
Vue 3
Vue 3
- Teleport
Teleport
<template>
<button @click="isVisible = true">
Open modal
</button>
<teleport to="body">
<Modal v-if="isVisible" @close="isVisible = false">
</teleport>
</template>
Vue 3
- Teleport
- Fragments
Fragments
<template>
<div>
<label>{{ label }}</label>
<input v-model="value" type="text">
</div>
</template>
<template>
<label>{{ label }}</label>
<input v-model="value" type="text">
</template>
Vue 3
- Teleport
- Fragments
- State-driven CSS Variables
State-driven CSS Variables
<template>
<button class="button" :style="buttonStyles">
<slot></slot>
</button>
</template>
<script>
export default {
props: ['disabled'],
computed: {
buttonStyles() {
return {
'--button-background': this.backgroundColor
}
},
backgroundColor() {
return this.disabled ? 'gray' : 'violet'
},
}
}
</script>
<style>
.button {
background-color: var(--button-background);
}
</style>
<template>
<button class="button">
<slot></slot>
</button>
</template>
<script>
export default {
props: ['disabled'],
data() {
return {
colorMap: {
error: 'red',
warning: 'yellow'
}
}
},
computed: {
backgroundColor() {
return this.disabled ? 'gray' : 'violet'
},
}
}
</script>
<style>
.button {
background-color: v-bind(backgroundColor);
color: v-bind('colorMap.error')
}
</style>
Vue 3
- Teleport
- Fragments
- State-driven CSS Variables
- Поддержка Typescript
Поддержка Typescript
<script lang="ts">
import {
Component,
Vue,
Prop,
Watch
} from 'vue-property-decorator';
@Component()
export default class MyComponent extends Vue {
@Prop() name!: string;
description = 'Some description';
@Watch('name')
onChangeName(name: string) {
this.description = `Some description about ${name}`
}
onClick(event: MouseEvent): void {
this.$emit('update-description', this.description)
}
}
</script>
<script lang="ts">
import { defineComponent } from 'vue'
export default defineComponent({
props: {
name: {
type: String,
default: ''
},
},
data() {
return {
description: 'Some description',
}
},
watch: {
name: function(name: string) {
this.description = `Some description about ${name}`
}
},
methods: {
onClick(event: MouseEvent): void {
this.$emit('update-description', this.description)
}
}
})
</script>
Vue 3
- Teleport
- Fragments
- State-driven CSS Variables
- Поддержка Typescript
- Composition API
Недостатки паттернов переиспользования функциональности во Vue 2
Недостатки паттернов переиспользования функциональности во Vue 2
Mixin
HOC
Renderless component
Mixin
<template>
<div>{{ foo }}</div>
</template>
<script>
import FooMixin from '@/mixins/Foo'
export default {
name: 'SomeComponent',
mixins: [FooMixin]
}
</script>
export default {
data() {
return {
foo: 'bar'
}
}
}
Mixin
<template>
<div>{{ foo }}</div>
</template>
<script>
import UsefulMixin from '@/mixins/Useful'
export default {
name: 'SomeComponent',
mixins: [UsefulMixin],
methods: {
update() {
// ...
}
}
}
</script>
export default {
methods: {
update() {
//...
}
}
}
Mixin
<template>
<div>
<h1>{{ title }}</h1>
<div v-for="item of filteredItems" @click="onClick">
{{ item }}
</div>
</div>
</template>
<script>
import HeyMixin from '@/mixins/Hey'
import HelloMixin from '@/mixins/Hello'
import HiMixin from '@/mixins/Hi'
import GoodDayMixin from '@/mixins/GoodDay'
import GoodNightMixin from '@/mixins/GoodNight'
export default {
name: 'FancyList',
mixins: [HeyMixin, HelloMixin, HiMixin, GoodDayMixin, GoodNightMixin],
mounted() {
this.sayGreetings()
}
}
</script>
?
??
???
????
Недостатки паттернов переиспользования функциональности во Vue 2
Mixin
HOC
Renderless component
- Конфликты неймспейса
- Неочевидно, что внедряется в компонент
High Order Component (HOC)
export default function withUsers(Inner: Component) {
return {
props: {
...Inner.props, // spread prop declarations
},
data() {
return {
users: [],
};
},
async mounted() {
const response = await fetch(
"https://jsonplaceholder.typicode.com/users"
);
const users = await response.json();
this.users = users;
},
render(h: CreateElement) {
return h(Inner, {
props: {
...this.$props, // pass all props
items: this.users,
},
on: {
...this.$listeners, // pass all listeners
},
});
},
};
}
<template>
<ul>
<li v-for="item of items" :key="item.id">
{{ item.name }}
</li>
</ul>
</template>
<script>
export default {
name: 'FancyList',
props: {
items: {
type: Array,
default: () => []
}
}
}
</script>
High Order Component (HOC)
<template>
<h1>
Users
</h1>
<UserList />
</template>
<script>
import FancyList from '@/components/FancyList.vue'
import withUsers from '@/components/withUsers'
const FancyListWithUsers = withUsers(FancyList)
export default {
name: 'UsersPage',
components: {
UserList: FancyListWithUsers
}
}
</script>
High Order Component (HOC)
const FullStackComponent = withFeatureA(withFeatureB(withFeatureC(withFeatureD(MyComponent))))
Недостатки паттернов переиспользования функциональности во Vue 2
Mixin
HOC
- Неочевидно, что попадает в props
- Инициализация инстанса
Renderless component
- Конфликты неймспейса
- Неочевидно, что внедряется в компонент
Renderless Component
<template>
<EditorMenuBar v-slot="{ commands }">
<button @click="commands.bold">
Bold
</button>
<button @click="commands.italic">
Italic
</button>
<button @click="commands.underline">
Underline
</button>
</EditorMenuBar>
</template>
tiptap — WYSIWYG-редактор
<template>
<slot v-bind="{ bold, italic, stroke }" />
</template>
Renderless Component
<template>
<FeatureA v-slot="{ a }">
<FeatureB v-slot="{ b: foo }">
<FeatuerC v-slot="{ c }">
<FeatureD v-slot="{ d }">
<MyComponent
:a="a"
:b="foo"
:c="c"
:d="d"
/>
</FeatureD>
</FeatuerC>
</FeatureB>
</FeatureA>
</template>
Недостатки паттернов переиспользования функциональности во Vue 2
Mixin
HOC
- Неочевидно, что попадает в props
- Инициализация инстанса
Renderless component
- Увеличивается темплейт
- Инициализация инстанса
- Конфликты неймспейса
- Неочевидно, что внедряется в компонент
Главный недостаток паттернов — масштабирование
Composition API
Options API vs Composition API
data
computed
watch
methods
life-cycle hooks
setup
Composition API
<script>
import { defineComponent } from 'vue'
export default defineComponent({
setup() {
const { featureA } = useFeatureA()
const { featureB } = useFeatureB()
const { featureC } = useFeatureC()
const { featureD } = useFeatureD()
return {
featureA,
featureB,
featureC,
featureD
}
}
})
function useFeatureA() {}
function useFeatureB() {}
function useFeatureC() {}
function useFeatureD() {}
</script>
Уже можно мигрировать на Vue 3?
Уже можно мигрировать на Vue 3?
Нельзя, если есть:
1. Зависимости, которые не мигрировали на Vue 3
2. В поддержке IE11
@vue/composition-api
Зачем использовать @vue/compisition-api?
Миграция проекта на Composition API
Миграция проекта на Composition API
1. createApp
3. emits
2. defineComponent
4. setup
5. reactive
6. ref
8. unwrap ref'ов
7. ref или reactive
9. toRefs
10. template ref
11. readonly
12. shallowReadonly
createApp
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
createApp(App).use(store).use(router).mount("#app");
createApp вместо new Vue()
У каждого приложения свой контекст
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import App2 from "./App2.vue";
import router2 from "./router2";
import store2 from "./store2";
createApp(App).use(store).use(router).mount("#app");
createApp(App2).use(store2).use(router2).mount('#app2')
defineComponent
export default defineComponent({
name: 'MyComponent',
components: {},
props: {},
data() { return {} },
methods: {}
})
- Возвращает объект как есть
- Предоставляет типы
emits
export default defineComponent({
emits: ['update', 'change'],
setup(props, { emit }) {
emit('wrong-event') // warn
}
})
- Ошибка в Typescript и warn в консоли при эмите события, которое не было указано
- В объектном синтаксисе можно указать валидатор
export default defineComponent({
emits: {
// validate event payload
update: (data) => Boolean(data.username),
// don't validate
change: null
},
})
setup
beforeCreated
setup
created
setup
<template>
<button :type="type" :disabled="isDisabled" @click="onClick">
{{ foo }}
</button>
</template>
<script>
import { defineComponent } from 'vue'
export default defineComponent({
props: ['foo', 'type'],
setup(props, context) {
const foobar = props.foo + 'bar'
const { attrs, slots, emit } = context
const isDisabled = false
const onClick = () => {
emit('click')
}
return {
foo: foobar,
isDisabled,
onClick
}
}
})
</script>
Система реактивности
- Proxy + WeakMap
- Опциональная
- Решены проблемы реактивности Vue 2
reactive
import { defineComponent, reactive, isProxy } from 'vue'
export default defineComponent({
setup() {
const state = reactive({
foo: 1,
bar: 'hey'
})
console.log(isProxy(state)) // true
}
})
ref
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const count = ref(0)
console.log(count.value) // 0
const increaseCount = () => {
count.value += 1
}
const items = ref({}) // reactive({})
}
})
reactive vs ref для объектов
import { defineComponent, reactive } from 'vue'
export default defineComponent({
setup() {
const objectOne = { name: 'David' }
const objectTwo = reactive({
name: 'Jhon'
})
objectOne.name // reactive or plain?
objectTwo.name // reactive or plain?
}
})
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const objectOne = { name: 'David' }
const objectTwo = ref({
name: 'Jhon'
})
objectOne.name // plain
objectTwo.value.name // reactive
}
})
Явность
reactive vs ref для объектов
import { defineComponent, reactive, ref } from 'vue'
export default defineComponent({
setup() {
const state = reactive({ category: 'T-Shirt' })
const productCount = ref(50)
state // .value?
productCount // .value?
}
})
import { defineComponent, ref, computed } from 'vue'
export default defineComponent({
setup() {
const productsState = ref({
category: 'T-Shirt',
products: []
})
const isEmpty = computed(() =>
!Boolean(ref.value.products.length)
)
productState // .value
isEmpty // .value
}
})
Консистентность
unwrap
<template>
<div>{{ objectWithNestedRefs.value.nested.value.deepNested.value.myDeepProp.value }}</div>
</template>
<script>
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const objectWithNestedRefs = ref({
nested: ref({
deepNested: ref({
myDeepProp: ref('deep inside')
})
})
})
return {
objectWithNestedRefs
}
}
})
</script>
unwrap
<template>
<div>{{ objectWithNestedRefs.nested.deepNested.myDeepProp }}</div>
</template>
<script>
import { defineComponent, ref } from 'vue'
export default defineComponent({
setup() {
const objectWithNestedRefs = ref({
nested: ref({
deepNested: ref({
myDeepProp: ref('deep inside')
})
})
})
return {
objectWithNestedRefs
}
}
})
</script>
unwrap
<template>
<div>{{ objectWithNestedRefs.nested.deepNested.myDeepProp }}</div>
</template>
<script>
import { defineComponent, ref, reactive } from 'vue'
export default defineComponent({
setup() {
const objectWithNestedRefs = reactive({
nested: ref({
deepNested: ref({
myDeepProp: ref('deep inside')
})
})
})
objectWithNestedRefs.nested.deepNested.myDeepProp // deep inside
return {
objectWithNestedRefs
}
}
})
</script>
unwrap
<template>
<div>{{ arrayWithRefs[0].value }}</div>
<div>{{ arrayWithObjectRefs[0] }}</div>
</template>
<script>
import { defineComponent, ref, reactive } from 'vue'
export default defineComponent({
setup() {
const arrayWithRefs = reactive([ref(0)])
arrayWithRefs[0].value // 0
const arrayWithObjectRefs = reactive([{id: ref(0)}])
arrayWithObjectRefs[0] // 0
return {
arrayWithRefs,
arrayWithObjectRefs
}
}
})
</script>
toRefs
import { defineComponent, reactive, computed } from 'vue'
export default defineComponent({
props: ['foo'],
setup(props) {
const state = reactive({ name: 'Peter' })
const { name } = state // not reactive
const { foo } = props // not reactive
const hasFoo = computed(() => Boolean(foo)) // wouldn't update
setTimeout(() => {
name = 'John' // wouldn't update
}, 2000)
return {
name,
hasFoo
}
}
})
toRefs
import { defineComponent, reactive, computed, toRefs } from 'vue'
export default defineComponent({
props: ['foo'],
setup(props) {
const state = reactive({ name: 'Peter' })
const { name } = toRefs(state)
const { foo } = toRefs(props)
const hasFoo = computed(() => Boolean(foo.value))
setTimeout(() => {
name.value = 'John'
}, 2000)
return {
name,
hasFoo
}
}
})
template ref
<template>
<div ref="container">
<!-- elements -->
</div>
</template>
<script>
import { defineComponent, ref, onMounted } from 'vue'
export default defineComponent({
setup() {
const container = ref(null)
onMounted(() => {
console.log(container.value) // HTMLDivElement
})
return {
container
}
}
})
</script>
readonly
import { defineComponent, readonly, isProxy } from 'vue'
export default defineComponent({
setup() {
const state = {
status: 'started',
user: {
name: 'Jhon',
lastname: 'Doe'
}
}
const immutableObject = readonly(state)
console.log(isProxy(immutableObject)) // true
immutableObject.status = 'failed' // error
immutableObject.user.name = 'Peter' // error
}
})
readonly
import { defineComponent, readonly, isProxy, reactive } from 'vue'
import useOnline from '@/composable/useOnline'
export default defineComponent({
setup() {
const state = reactive({
status: 'started',
user: {
name: 'Jhon',
lastname: 'Doe'
}
})
const immutableObject = readonly(state)
console.log(isProxy(immutableObject)) // true
immutableObject.status = 'failed' // error
immutableObject.user.name = 'Peter' // error
}
})
readonly
import { defineComponent } from 'vue'
import useOnline from '@/composable/useOnline'
export default defineComponent({
setup() {
const isOnline = useOnline()
isOnline.value = false // error
}
})
import {
defineComponent,
readonly,
onMounted,
onBeforeUnmount
} from 'vue'
export default function useOnline() {
const isOnline = ref(true)
const onOffline = () => isOnline.value = false
const onOnline = () => isOnline.value = true
onMounted(() => {
window.addEventListener('offline', onOffline)
window.addEventListener('online', onOnline)
})
onBeforeUnmount(() => {
window.removeEventListener('offline', onOffline)
window.removeEventListener('online', onOnline)
})
return readonly(isOnline)
}
shallowReadonly
import { defineComponent, shallowReadonly, isProxy } from 'vue'
import useOnline from '@/composable/useOnline'
export default defineComponent({
setup() {
const state = {
status: 'started',
user: {
name: 'Jhon',
lastname: 'Doe'
}
}
const immutableObject = shallowReadonly(state)
console.log(isProxy(immutableObject)) // true
immutableObject.status = 'failed' // error
immutableObject.user.name = 'Peter' // ok
}
})
Глобальные свойства в Composition API
import router from '@/router'
export default function useRouter() {
return router
}
getCurrentInstance
getCurrentInstance
type getCurrentInstance = () => ComponentInternalInstance | null;
import {
defineComponent,
getCurrentInstance,
onMounted
} from 'vue'
export default defineComponent({
setup() {
console.log(getCurrentInstance()) // ComponentInternalInstance
onMounted(() => {
console.log(getCurrentInstance()) // ComponentInternalInstance
})
const onClick = () => {
console.log(getCurrentInstance()) // null
}
}
})
getCurrentInstance
type getCurrentInstance = () => ComponentInternalInstance | null;
import {
defineComponent,
getCurrentInstance,
onMounted
} from 'vue'
export default defineComponent({
setup() {
const instance = getCurrentInstance() // ComponentInternalInstance
onMounted(() => {
console.log(getCurrentInstance()) // ComponentInternalInstance
})
const onClick = () => {
console.log(instance) // ComponentInternalInstance
}
}
})
getCurrentInstance
type getCurrentInstance = () => ComponentInternalInstance | null;
interface ComponentInternalInstance {
uid: number;
type: ConcreteComponent;
parent: ComponentInternalInstance | null;
root: ComponentInternalInstance;
appContext: AppContext;
vnode: VNode;
subTree: VNode;
update: ReactiveEffect;
proxy: ComponentPublicInstance | null;
exposed: Record<string, any> | null;
data: Data;
props: Data;
attrs: Data;
slots: InternalSlots;
refs: Data;
emit: EmitFn;
isMounted: boolean;
isUnmounted: boolean;
isDeactivated: boolean;
}
Глобальные свойства в Composition API
import { getCurrentInstance } from 'vue'
export default function useRouter() {
return getCurrentInstance().appContext.config.globalProperties.$router
}
Миграция проекта на @vue/composition-api
createApp
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
createApp(App).use(store).use(router).mount("#app");
createApp вместо new Vue()
У каждого приложения свой контекст
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import App2 from "./App2.vue";
import router2 from "./router2";
import store2 from "./store2";
createApp(App).use(store).use(router).mount("#app");
createApp(App2).use(store2).use(router2).mount('#app2')
createApp
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
createApp(App).use(store).use(router).mount("#app");
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import App2 from "./App2.vue";
import router2 from "./router2";
import store2 from "./store2";
createApp(App).use(store).use(router).mount("#app");
createApp(App2).use(store2).use(router2).mount('#app2')
createApp вместо new Vue()
Во Vue 2 только глобальный контекст
createApp
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
createApp(App).use(store).use(router).mount("#app");
import { createApp } from "vue";
import App from "./App.vue";
import router from "./router";
import store from "./store";
import App2 from "./App2.vue";
import router2 from "./router2";
import store2 from "./store2";
createApp(App).use(store).use(router).mount("#app");
createApp(App2).use(store2).use(router2).mount('#app2')
createApp === new Vue()
Во Vue 2 только глобальный контекст
createApp
createApp === new Vue()
import Vue from 'vue'
import CompositionAPI, { createApp } from "@vue/composition-api";
import Vuex from 'vuex';
import VueRouter from 'vue-router'
import App from "./App.vue";
import router from "./router";
import store from "./store";
Vue.use(CompositionAPI)
const app = createApp({
store,
router,
render: (h) => h(App),
})
app.use(VueRouter).use(Vuex)
app.mount("#app");
emits
setup
import { defineComponent } from 'vue'
export default defineComponent({
setup(props, context) {
const { attrs, slots, emit } = context
}
})
setup
import { defineComponent } from 'vue'
export default defineComponent({
setup(props, context) {
const { parent, root, listeners, refs, ...vue3Context } = context
}
})
Система реактивности
- Object.defineProperty + WeakMap
- Те же проблемы реактивности Vue 2
- Опциональная
set и del
import { defineComponent, reactive, set, del } from '@vue/composition-api'
export default defineComponent({
setup() {
const reactiveObj = reactive({
name: 'ObiWan',
type: 'jedi'
})
set(reactiveObj, 'lasersaber', 'blue') // this.$set
del(reactiveObj, 'type') // this.$delete
}
})
reactive
import { defineComponent, reactive } from '@vue/composition-api'
export default defineComponent({
setup() {
const state = {
name: 'Dooku',
}
const reactiveState = reactive(state) // Object.defineProperty
console.log(state === reactiveState) // true
// isProxy(reactiveState) -> false
}
})
unwrap
import { defineComponent, reactive, ref } from '@vue/composition-api'
export default defineComponent({
setup() {
const reactiveObject = reactive({
clones: [{ weapon: ref('blaster') }]
})
reactiveObject.clones[0].weapon.value // wound't unwrap
}
})
unwrap
import { defineComponent, reactive, ref } from '@vue/composition-api'
export default defineComponent({
setup() {
const reactiveObject = reactive({
clones: [reactive({ weapon: ref('blaster') })]
})
reactiveObject.clones[0].weapon
}
})
toRefs
import { defineComponent, toRefs, isRef } from '@vue/composition-api'
export default defineComponent({
props: {
jedi: {
type: Object,
default: () => ({ laserSaber: 'blue' })
}
},
setup(props) {
const { laserSaber } = toRefs(props.jedi) // warn, not reactive
}
})
toRefs
<template>
<Spaceship :jedi="{ laserSaber: 'green' }" />
</template>
<template>
<Spaceship :jedi="staticJedi" />
</template>
<script>
import { defineComponent } from '@vue/compositio-api'
defineComponent({
setup() {
const staticJedi = { laserSaber: 'green' }
return {
staticJedi
}
}
})
</script>
toRefs
<template>
<Spaceship :jedi="jedi" />
</template>
<script>
export default {
data() {
return {
jedi: { laserSaber: 'green' }
}
}
}
</script>
<template>
<Spaceship :jedi="jedi" />
</template>
<script>
import {
defineComponent,
reactive
} from '@vue/compositio-api'
export default defineComponent({
setup() {
const jedi = reactive({ laserSaber: 'green' })
return {
jedi
}
}
})
</script>
template ref
<template>
<div
v-for="(item, i) in list"
:ref="el => { if (el) divs[i] = el }"
>
{{ item }}
</div>
</template>
<script>
export default {
setup() {
const list = reactive([1, 2, 3])
const divs = ref([])
// make sure to reset the refs before each update
onBeforeUpdate(() => {
divs.value = []
})
return {
list,
divs
}
}
}
</script>
readonly
import { readonly } from '@vue/composition-api'
export default {
setup() {
const plainObject = {
status: 'waiting'
}
// Readonly<Record<string, string>>
const readonlyObject = readonly(plainObject)
readonlyObject.status = 'failed' // allowed
}
}
shallowReadonly
import { defineComponent, shallowReadonly, set } from '@vue/composition-api'
export default defineComponent({
setup() {
const sourceObject = reactive({
side: 'dark',
skill: 'lightning strike',
visitedPlanets: {
Tatooine: true,
Naboo: false
}
})
// { get dark() {}, get skill() {}, get visitedPlanets() {} }
const shallowReadonlyObject = shallowReadonly(sourceObject)
shallowReadonlyObject.side = 'light' // warn
shallowReadonlyObject.visitedPlanets.Naboo = true
set(sourceObject, 'level', 5) // wouldn't update shallowReadonlyObject
shallowReadonlyObject.level // undefined
shallowReadonlyObject.illegalProp = 'absolutely illegal' // ok
}
})
Глобальные свойства в
@vue/composition-api
import { getCurrentInstance } from '@vue/composition-api'
export default function useRouter() {
return getCurrentInstance().appContext.config.globalProperties.$router
}
getCurrentInstance
interface ComponentInternalInstance {
uid: number;
type: ConcreteComponent;
parent: ComponentInternalInstance | null;
root: ComponentInternalInstance;
appContext: AppContext;
vnode: VNode;
subTree: VNode;
update: ReactiveEffect;
proxy: ComponentPublicInstance | null;
exposed: Record<string, any> | null;
data: Data;
props: Data;
attrs: Data;
slots: InternalSlots;
refs: Data;
emit: EmitFn;
isMounted: boolean;
isUnmounted: boolean;
isDeactivated: boolean;
}
Глобальные свойства в
@vue/composition-api
import { getCurrentInstance } from '@vue/composition-api'
export default function useRouter() {
return getCurrentInstance().proxy.$router
}
import { getCurrentInstance, computed } from '@vue/composition-api'
export default function useRoute() {
return computed(() => getCurrentInstance().proxy.$route)
}
Глобальные свойства в
@vue/composition-api
import { defineComponent } from '@vue/composition-api'
import useRouter from '@/composable/useRouter'
export default defineComponent({
setup() {
const router = useRouter()
router.push('/')
}
})
Глобальные свойства в
@vue/composition-api
import { defineComponent } from '@vue/composition-api'
import { useRouter } from 'vue-router'
export default defineComponent({
setup() {
const router = useRouter()
router.push('/')
}
})
Ссылки
Николай Пугачев
Frontend developer at
Спасибо за внимание
@jspoetry
На полпути к Vue 3
By Nikolai Pugachev
На полпути к Vue 3
- 261