Vue 3.5: улучшения реактивности и Composition API
Vue 3.5 — значительное обновление с улучшениями в реактивности и Composition API. Эта версия приносит оптимизации производительности, улучшенную поддержку TypeScript и новые возможности для организации кода.
Установка
Vue 3.5 устанавливается через npm или создаётся новый проект через CLI.
npm create vue@latest my-vue-app
# или
npm install vue@3.5
Команда create vue создаст проект с настроенным Vite и TypeScript.
Composition API
Composition API — это подход к организации кода, который позволяет группировать логику по функциональности, а не по опциям.
setup script
Синтаксис <script setup> — это компиляторная макросъёмка, которая упрощает использование Composition API.
<script setup lang="ts">
import { ref, computed, watch, onMounted } from 'vue'
const count = ref(0)
const doubled = computed(() => count.value * 2)
function increment() {
count.value++
}
watch(count, (newVal, oldVal) => {
console.log(`Changed from ${oldVal} to ${newVal}`)
})
onMounted(() => {
console.log('Component mounted')
})
</script>
<template>
<button @click="increment">
Count: {{ count }}, Doubled: {{ doubled }}
</button>
</template>
Реактивные объекты
reactive создаёт прокси-объект, который отслеживает изменения вложенных свойств. Для доступа к свойствам в template не нужно .value.
<script setup lang="ts">
import { reactive, toRefs } from 'vue'
const state = reactive({
user: {
name: 'John',
email: 'john@example.com'
},
loading: false
})
const { user, loading } = toRefs(state)
function updateName(name: string) {
state.user.name = name
}
</script>
<template>
<div>
<p>{{ user.name }}</p>
<p>{{ loading }}</p>
</div>
</template>
toRefs преобразует свойства reactive объекта в ref, сохраняя реактивность при деструктуризации.
provide/inject
provide/inject — механизм для передачи данных через дерево компонентов без пропсов. Полезно для глобальных зависимостей.
<!-- Parent.vue -->
<script setup lang="ts">
import { provide, ref } from 'vue'
const count = ref(0)
provide('count', count)
provide('updateCount', (delta: number) => {
count.value += delta
})
</script>
<!-- Child.vue -->
<script setup lang="ts">
import { inject } from 'vue'
const count = inject('count')
const updateCount = inject('updateCount') as (delta: number) => void
</script>
Pinia
Store definition
// stores/user.ts
import { defineStore } from 'pinia'
import { ref, computed } from 'vue'
export const useUserStore = defineStore('user', () => {
const user = ref<{ name: string; email: string } | null>(null)
const isLoggedIn = computed(() => user.value !== null)
async function login(email: string, password: string) {
const response = await fetch('/api/login', {
method: 'POST',
body: JSON.stringify({ email, password })
})
user.value = await response.json()
}
function logout() {
user.value = null
}
return { user, isLoggedIn, login, logout }
})
Использование в компоненте
<script setup lang="ts">
import { useUserStore } from '@/stores/user'
const userStore = useUserStore()
</script>
<template>
<div v-if="userStore.isLoggedIn">
Welcome, {{ userStore.user?.name }}
</div>
<div v-else>
Please login
</div>
</template>
Teleport
<template>
<button @click="showModal = true">Open Modal</button>
<Teleport to="body">
<div v-if="showModal" class="modal-overlay" @click="showModal = false">
<div class="modal" @click.stop>
<h2>Modal Title</h2>
<button @click="showModal = false">Close</button>
</div>
</div>
</Teleport>
</template>
Suspense
<template>
<Suspense>
<template #default>
<AsyncComponent />
</template>
<template #fallback>
<LoadingSpinner />
</template>
</Suspense>
</template>
Custom Directives
<script setup lang="ts">
const vFocus = {
mounted: (el) => el.focus()
}
</script>
<template>
<input v-focus />
</template>
Router
// router/index.ts
import { createRouter, createWebHistory } from 'vue-router'
const router = createRouter({
history: createWebHistory(),
routes: [
{
path: '/',
name: 'home',
component: () => import('@/views/Home.vue')
},
{
path: '/about',
name: 'about',
component: () => import('@/views/About.vue')
},
{
path: '/user/:id',
name: 'user',
component: () => import('@/views/User.vue'),
props: true
}
]
})
export default router
Navigation guards
router.beforeEach((to, from, next) => {
const isAuthenticated = // check auth
if (to.meta.requiresAuth && !isAuthenticated) {
next('/login')
} else {
next()
}
})
TypeScript Support
<script setup lang="ts">
interface Props {
title: string
count?: number
}
const props = withDefaults(defineProps<Props>(), {
count: 0
})
const emit = defineEmits<{
(e: 'update', value: number): void
(e: 'delete'): void
}>()
function handleClick() {
emit('update', props.count + 1)
}
</script>
Composables
// composables/useMouse.ts
import { ref, onMounted, onUnmounted } from 'vue'
export function useMouse() {
const x = ref(0)
const y = ref(0)
function update(event: MouseEvent) {
x.value = event.pageX
y.value = event.pageY
}
onMounted(() => window.addEventListener('mousemove', update))
onUnmounted(() => window.removeEventListener('mousemove', update))
return { x, y }
}
<script setup lang="ts">
import { useMouse } from '@/composables/useMouse'
const { x, y } = useMouse()
</script>
<template>
Mouse: {{ x }}, {{ y }}
</template>
Заключение
Vue 3.5 предоставляет мощный Composition API для создания реактивных приложений с отличной TypeScript поддержкой.