Цей шаблон був створений для внутрішніх потреб розробки, щоб мати можливість тестувати чат у локальному середовищі, а під час роботи над ним я зробив кілька нотаток про Vue (я вже мав досвід роботи з ним, але без використання хуків). Тож це просто мої нотатки в Obsidian, сподіваюся, вони будуть корисні 🙂
Повний посібник із освоєння Composition API Vue, реактивних патернів і інтеграції Pinia store. Ідеально підходить для розробників, які хочуть підняти свої навички в Vue.js на новий рівень.
## Зміст
- Ref і реактивні посилання
- Watch і реактивність
- Composables
- Реактивність Vue API (reactive vs ref)
- Інтеграція Pinia Store
- Практичні приклади
- Кращі практики
- Типові проблеми
- Розширені патерни
- Оптимізація продуктивності
## Ref і реактивні посилання
Розуміння основ системи реактивності Vue є ключовим для створення надійних додатків. У цьому розділі розглядається, як управляти реактивним станом за допомогою refs і реактивних посилань, з практичними прикладами та інтеграцією TypeScript.
Що таке Ref?
ref — це спосіб Vue зробити примітивні значення реактивними. Він обгортає значення в реактивний об'єкт з властивістю .value.
import { ref } from ‘vue’
// У Pinia Store
export const useMyStore = defineStore(‘my-store’, () => {
// Створює реактивне посилання
const count = ref(0)
// Для доступу або зміни:
function increment() {
count.value++ // Необхідно використовувати .value для refs
}
return {
count, // Коли передається в компоненти, вони можуть використовувати його без .value
increment
}
})
Типи Ref в Stores
// Простий ref
const isLoading = ref(false)
// Масив ref
const messages = ref([])
// Складний об'єкт ref
const currentUser = ref(null)
// Ref з undefined
const selectedId = ref(undefined)
## Watch і реактивність
Освоєння можливостей Vue для спостереження дозволяє ефективно реагувати на зміни стану і створювати динамічні, адаптивні додатки. Дізнайтесь, як ефективно використовувати watchers і працювати з складними реактивними патернами.
Основне використання Watch
import { watch, ref } from ‘vue’
export const useMyStore = defineStore(‘my-store’, () => {
const messages = ref([])
// Простий watch
watch(messages, (newMessages, oldMessages) => {
console.log(‘Повідомлення змінено:’, newMessages)
})
})
Опції Watch
// Негайне виконання
watch(messages, (newMessages) => {
// Це виконується негайно та при змінах
}, { immediate: true })
// Глибоке спостереження
watch(messages, (newMessages) => {
// Виявляє глибокі зміни об'єкта
}, { deep: true })
// Багато джерел
watch(
[messages, selectedId],
([newMessages, newId], [oldMessages, oldId]) => {
// Тригерить при зміні будь-якого з них
}
)
## Composables
Composables — це основа для створення повторно використовуваної логіки у Vue 3. Цей розділ покаже вам, як створювати потужну, багаторазову функціональність, яку можна поділити по всьому додатку, при цьому зберігаючи код чистим і організованим.
Створення кастомних Composables
Composables — це повторно використовувані функції з реактивним станом, які слідують конвенції з префіксом use
.
// useCounter.ts
import { ref, computed } from ‘vue’
export function useCounter(initialValue = 0) {
const count = ref(initialValue)
const doubleCount = computed(() => count.value * 2)
function increment() {
count.value++
}
function decrement() {
count.value —
}
return {
count,
doubleCount,
increment,
decrement
}
}
Інтеграція в життєвий цикл Composable
// useMousePosition.ts
import { ref, onMounted, onUnmounted } from ‘vue’
export function useMousePosition() {
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 }
}
Асинхронні Composables
```ts
// useAsyncData.ts
import { ref, watchEffect } from ‘vue’
export function useAsyncData(asyncGetter: () => Promise) {
const data = ref(null)
const error = ref(null)
const isLoading = ref(false)
async function fetch() {
isLoading.value = true
error.value = null
try {
data.value = await asyncGetter()
} catch (e) {
error.value = e as Error
} finally {
isLoading.value = false
}
}
watchEffect(() => {
fetch()
})
return {
data,
error,
isLoading,
refresh: fetch
}
}
```
Залежності Composables
// useUserProfile.ts
import { computed } from ‘vue’
import { useAuth } from ‘./useAuth’
import { useApi } from ‘./useApi’
export function useUserProfile() {
const { user } = useAuth()
const { get } = useApi()
const userProfile = computed(() => {
if (!user.value) return null
return get(`/users/${user.value.id}/profile`)
})
return {
userProfile
}
}
Повторно використовувана валідація форм
```ts
// useFormValidation.ts
import { ref, computed } from ‘vue’
export function useFormValidation>(initialState: T) {
const formData = ref(initialState)
const errors = ref>>({})
const isValid = computed(() => Object.keys(errors.value).length === 0)
function validate(rules: Record string | null>) {
errors.value = {}
Object.entries(rules).forEach(([field, validator]) => {
const error = validator(formData.value[field])
if (error) {
errors.value[field as keyof T] = error
}
})
return isValid.value
}
return {
formData,
errors,
isValid,
validate
}
}
```
Використання Composables в компонентах
## Реактивність Vue API
Глибоке занурення в потужну систему реактивності Vue.
Learn how to leverage reactive objects, refs, and computed properties to build dynamic and efficient applications.
Використання реактивних об'єктів
Метод reactive
створює реактивний проксі для об'єкта, роблячи всі його властивості глибоко реактивними.
// Основний реактивний об'єкт
import { reactive } from ‘vue’
interface User {
name: string
age: number
settings: {
theme: string
notifications: boolean
}
}
const user = reactive({
name: ‘John’,
age: 30,
settings: {
theme: ‘dark’,
notifications: true
}
})
// Прямий доступ до властивості (без .value)
console.log(user.name)
user.age = 31
Reactivity vs Ref
```ts
// Порівняння використання reactive та ref
import { reactive, ref } from ‘vue’
// Використання ref
const count = ref(0)
const user = ref({
name: ‘John’,
age: 30
})
// Потрібно .value для ref
count.value++
user.value.age++
// Використання reactive
const state = reactive({
count: 0,
user: {
name: ‘John’,
age: 30
}
})
// Прямий доступ до властивостей
state.count++
state.user.age++
```
Обмеження та робота з типами
// ❌ Обмеження reactive
import { reactive } from ‘vue’
// Не деструктуруйте реактивні об'єкти
const state = reactive({ count: 0 })
const { count } = state // Втрачається реактивність!
// ✅ Замість цього використовуйте computed або методи
import { reactive, computed } from ‘vue’
const state = reactive({ count: 0 })
const doubleCount = computed(() => state.count * 2)
// Або зберігайте посилання на вкладені об'єкти
const nested = reactive({
user: {
profile: {
name: ‘John’
}
}
})
// Це зберігає реактивність
const profile = nested.user.profile
Реактивні масиви та колекції
```ts
import { reactive } from ‘vue’
interface TodoItem {
id: number
text: string
completed: boolean
}
const todos = reactive([])
// Методи зберігають реактивність
function addTodo(text: string) {
todos.push({
id: Date.now(),
text,
completed: false
})
}
// Робота з реактивними колекціями
const collection = reactive(new Map())
collection.set(‘key’, 1)
```
Комбінування з Composables
```ts
// useTaskManager.ts
import { reactive, computed } from ‘vue’
interface Task {
id: number
title: string
completed: boolean
}
export function useTaskManager() {
const state = reactive({
tasks: [] as Task[],
filter: ‘all’ as ‘all’ | ‘active’ | ‘completed’
})
const filteredTasks = computed(() => {
switch (state.filter) {
case ‘active’:
return state.tasks.filter(task => !task.completed)
case ‘completed’:
return state.tasks.filter(task => task.completed)
default:
return state.tasks
}
})
function addTask(title: string) {
state.tasks.push({
id: Date.now(),
title,
completed: false
})
}
function toggleTask(id: number) {
const task = state.tasks.find(t => t.id === id)
if (task) {
task.completed = !task.completed
}
}
return {
state,
filteredTasks,
addTask,
toggleTask
}
}
```
Кращі практики роботи з реактивністю
```ts
// ✅ Кращі практики
import { reactive, toRefs } from ‘vue’
// 1. Використовуйте інтерфейси для безпеки типів
interface State {
loading: boolean
error: Error | null
data: string[]
}
// 2. Ініціалізуйте всі властивості
const state = reactive({
loading: false,
error: null,
data: []
})
// 3. Використовуйте toRefs, коли потрібно деструктурувати
function useFeature() {
const state = reactive({
foo: 1,
bar: 2
})
// Зробіть це безпечним для деструктурування
return toRefs(state)
}
// 4.
Avoid nested reactivity when possible
// ❌ Погано
const nested = reactive({
user: reactive({
profile: reactive({
name: ‘John’
})
})
})
// ✅ Добре
const state = reactive({
user: {
profile: {
name: ‘John’
}
}
})
**Інтеграція з TypeScript**
```ts
// Розширене використання TypeScript з reactive
import { reactive } from ‘vue’
// Визначення складних типів
interface User {
id: number
name: string
preferences: {
theme: ‘light’ | ‘dark’
notifications: boolean
}
}
interface AppState {
currentUser: User | null
isAuthenticated: boolean
settings: Map
}
// Створення реактивного стану з типами
const state = reactive({
currentUser: null,
isAuthenticated: false,
settings: new Map()
})
// Типізовані методи
function updateUser(user: Partial<User>) {
if (state.currentUser) {
Object.assign(state.currentUser, user)
}
}
// Тільки для читання реактивний стан
import { readonly } from ‘vue’
const readonlyState = readonly(state)
## Інтеграція з Pinia Store
Дізнайтеся, як ефективно інтегрувати Pinia stores з Composition API Vue. Ознайомтесь з найкращими практиками управління станом та як структурувати свої stores для масштабованості.
Структура Store з Refs
```ts
export const useMyStore = defineStore(‘my-store’, () => {
// Стан
const items = ref([])
const isLoading = ref(false)
const error = ref(null)
// Обчислене
const itemCount = computed(() => items.value.length)
// Дії
const fetchItems = async () => {
isLoading.value = true
try {
items.value = await api.getItems()
} catch (e) {
error.value = e as Error
} finally {
isLoading.value = false
}
}
return {
items,
isLoading,
error,
itemCount,
fetchItems
}
})
```
Комбінування Stores
```ts
export const useMainStore = defineStore(‘main-store’, () => {
// Використання іншого store
const otherStore = useOtherStore()
// Слідкування за станом іншого store
watch(
() => otherStore.someState,
(newValue) => {
// Реакція на зміни в іншому store
}
)
})
```
## Практичні приклади
Приклади з реального життя, які демонструють, як реалізувати поширені функції та патерни у Vue-додатках. Ці приклади покажуть вам, як застосувати теорію на практиці.
Реалізація автоновлення
```ts
export const useChatStore = defineStore(‘chat-store’, () => {
const messages = ref([])
const refreshInterval = ref(null)
const isRefreshing = ref(false)
// Спостереження за станом автоновлення
watch(isRefreshing, (shouldRefresh) => {
if (shouldRefresh) {
startAutoRefresh()
} else {
stopAutoRefresh()
}
})
const startAutoRefresh = () => {
refreshInterval.value = window.setInterval(() => {
fetchNewMessages()
}, 5000)
}
const stopAutoRefresh = () => {
if (refreshInterval.value) {
clearInterval(refreshInterval.value)
refreshInterval.value = null
}
}
return {
messages,
isRefreshing,
startAutoRefresh,
stopAutoRefresh
}
})
```
Управління станом завантаження
export const useDataStore = defineStore(‘data-store’, () => {
const data = ref([])
const isLoading = ref(false)
const error = ref(null)
// Спостереження за станом завантаження для побічних ефектів
watch(isLoading, (loading) => {
if (loading) {
// Показати індикатор завантаження
} else {
// Сховати індикатор завантаження
}
})
// Спостереження за помилками
watch(error, (newError) => {
if (newError) {
// Обробка помилки (показ повідомлення, тощо)
}
})
})
## Кращі практики
Уникайте поширених помилок і головного болю при налагодженні, ознайомившись з цими часто зустрічаються проблемами та їх рішеннями.
1. Ініціалізація Ref
// ❌ Погано
const data = ref() // Тип ‘any’
// ✅ Добре
const data = ref([]) // Явно вказаний тип
2.
Watch Cleanup
// ❌ Погано — Без очищення
watch(source, () => {
const timer = setInterval(() => {}, 1000)
})
// ✅ Добре — З очищенням
watch(source, () => {
const timer = setInterval(() => {}, 1000)
return () => clearInterval(timer) // Функція очищення
})
3. Computed vs Watch
// ❌ Погано — Використання watch для похідного стану
watch(items, (newItems) => {
itemCount.value = newItems.length
})
// ✅ Добре — Використання computed для похідного стану
const itemCount = computed(() => items.value.length)
4. Організація Store
// ✅ Добра організація store
export const useStore = defineStore(‘store’, () => {
// Стан refs
const data = ref([])
const isLoading = ref(false)
// Обчислені властивості
const isEmpty = computed(() => data.value.length === 0)
// Спостерігачі
watch(data, () => {
// Обробка змін даних
})
// Дії
const fetchData = async () => {
// Реалізація
}
// Публічний інтерфейс
return {
data,
isLoading,
isEmpty,
fetchData
}
})
## Загальні проблеми
- Забування
.value
// ❌ Погано
const count = ref(0)
count++ // Не працюватиме
// ✅ Добре
count.value++
- Час спостереження (Watch Timing)
// ❌ Погано — Може пропустити початковий стан
watch(source, () => {})
// ✅ Добре — Захоплює початковий стан
watch(source, () => {}, { immediate: true })
- Витоки пам'яті (Memory Leaks)
// ❌ Погано — Без очищення
const store = useStore()
setInterval(() => {
store.refresh()
}, 1000)
// ✅ Добре — З очищенням
const intervalId = setInterval(() => {
store.refresh()
}, 1000)
onBeforeUnmount(() => clearInterval(intervalId))
## Складні патерни
Підніміть свої навички Vue.js на новий рівень за допомогою складних патернів та технік для створення комплексних і масштабованих додатків.
Складна комунікація між компонентами
```ts
// Приклад складних патернів комунікації між компонентами
export function useComponentBridge() {
const events = ref(new Map())
function emit(event: string, data: any) {
if (events.value.has(event)) {
events.value.get(event).forEach((handler: Function) => handler(data))
}
}
function on(event: string, handler: Function) {
if (!events.value.has(event)) {
events.value.set(event, new Set())
}
events.value.get(event).add(handler)
return () => events.value.get(event).delete(handler)
}
return { emit, on }
}
```
Стратегії тестування
Дізнайтесь, як ефективно тестувати ваші компоненти Vue та stores, використовуючи сучасні практики та інструменти тестування.
Тестування Composables
```ts
import { renderComposable } from ‘@testing-library/vue-composables’
import { useCounter } from ‘./useCounter’
describe(‘useCounter’, () => {
test(‘should increment counter’, () => {
const { result } = renderComposable(() => useCounter())
expect(result.current.count.value).toBe(0)
result.current.increment()
expect(result.current.count.value).toBe(1)
})
})
```
## Оптимізація продуктивності
Техніки та стратегії для оптимізації ваших Vue-додатків для кращої продуктивності та користувацького досвіду.
Оптимізація обчислених властивостей (Computed Property Optimization)
```ts
// Оптимізація обчислених властивостей для кращої продуктивності
const expensiveComputation = computed(() => {
return memoize(() => {
// Дорога обчислювальна операція
return result
})
})
Перекладено з: Supercharged Vue 3.0 Reactivity + Pinia Stores