Vue.js Composition API panaudojimas projektuose

Kodėl Composition API atsirado ir kam jo reikia

Kai Vue 3 buvo pristatytas su Composition API, daugelis kūrėjų žiūrėjo skeptiškai. Juk Options API veikė puikiai, kodas buvo aiškus, o naujokai galėjo greitai įsisavinti frameworką. Tačiau realybė tokia, kad didesniuose projektuose Options API pradėdavo rodyti savo trūkumus.

Pagrindinė problema – logikos išskaidymas. Kai komponentas auga, susijusi logika atsiduria skirtingose vietose: data(), methods, computed, watch. Norėdamas suprasti, kaip veikia viena funkcija, turi šokinėti per visą failą. O kai reikia tą logiką perkelti į kitą komponentą ar išskaidyti į mixins – prasideda tikras galvos skausmas.

Composition API šią problemą sprendžia leisdamas grupuoti logiką pagal funkcionalumą, o ne pagal opcijų tipus. Tai nereiškia, kad Options API yra blogas – jis puikiai tinka mažesniems projektams ir paprastesnėms situacijoms. Bet kai projektas auga, Composition API tampa tikru gelbėjimo ratu.

Pirmieji žingsniai su setup funkcija

Viskas prasideda nuo setup() funkcijos. Ji yra jūsų komponento įėjimo taškas, kur visa magija vyksta. Skirtingai nei Options API, čia turite vieną vietą, kur aprašote viską.

import { ref, computed, onMounted } from 'vue'

export default {
  setup() {
    const count = ref(0)
    const doubled = computed(() => count.value * 2)
    
    function increment() {
      count.value++
    }
    
    onMounted(() => {
      console.log('Komponentas užkrautas')
    })
    
    return {
      count,
      doubled,
      increment
    }
  }
}

Iš pirmo žvilgsnio gali atrodyti keista, kad reikia grąžinti viską, ką nori naudoti template’e. Bet būtent tai ir yra pranašumas – matai tiksliai, kas eksportuojama. Jokių paslėptų priklausomybių ar magijos su this.

Beje, ref ir .value pradžioje gali erzinti. Kodėl negalima tiesiog count++? Atsakymas techninis – JavaScript primityvai nėra reaktyvūs. ref sukuria objektą, kurį Vue gali stebėti. Template’uose .value nereikia, nes Vue automatiškai jį išpakuoja, bet script’e – būtina.

Kompozicijos funkcijos – tikroji jėga

Štai kur Composition API tikrai pradeda spindėti. Galite iškelti logiką į atskiras funkcijas, kurias galima pakartotinai naudoti bet kuriame komponente. Tradiciškai tokias funkcijas vadina composables.

Tarkime, kuriate aplikaciją, kur daug kur reikia sekti pelės poziciją:

// composables/useMouse.js
import { ref, onMounted, onUnmounted } from 'vue'

export function useMouse() {
  const x = ref(0)
  const y = ref(0)
  
  function update(event) {
    x.value = event.pageX
    y.value = event.pageY
  }
  
  onMounted(() => window.addEventListener('mousemove', update))
  onUnmounted(() => window.removeEventListener('mousemove', update))
  
  return { x, y }
}

Dabar bet kuriame komponente galite tiesiog:

import { useMouse } from '@/composables/useMouse'

export default {
  setup() {
    const { x, y } = useMouse()
    return { x, y }
  }
}

Palyginkite tai su mixins. Mixins turi vardų konfliktų problemą, nežinote, iš kur ateina kintamieji, ir negali lengvai naudoti kelių kartų. Composables šių problemų neturi – tai tiesiog funkcijos, kurias importuojate ir naudojate.

Reaktyvumas: ref vs reactive

Vienas dažniausių klausimų – kada naudoti ref, o kada reactive? Teoriškai reactive atrodo patogesnis, nes nereikia .value:

const state = reactive({
  count: 0,
  name: 'Jonas'
})

// Galima tiesiog
state.count++

Bet yra keletas spąstų. Pirma, reactive veikia tik su objektais. Antra, negalite pakeisti viso objekto:

let state = reactive({ count: 0 })
state = reactive({ count: 1 }) // Prarandate reaktyvumą!

Trečia, destruktūrizuojant prarandate reaktyvumą:

const { count } = reactive({ count: 0 })
count++ // Neveiks reaktyviai

Dėl šių priežasčių daugelis kūrėjų renkasi ref kaip numatytąjį pasirinkimą. Taip, .value gali erzinti, bet bent jau elgesys nuoseklus ir nėra netikėtų staigmenų. reactive naudoju tik tada, kai tikrai turiu didelį objektą su daug savybių, kurio neplanuoju destruktūrizuoti.

Yra ir toRefs funkcija, kuri leidžia saugiai destruktūrizuoti:

const state = reactive({ count: 0, name: 'Jonas' })
const { count, name } = toRefs(state)
// Dabar count ir name yra ref'ai ir išlaiko reaktyvumą

Lifecycle hooks naujoje tvarkoje

Lifecycle hooks’ai Composition API atrodo šiek tiek kitaip, bet principas tas pats. Vietoj opcijų, naudojate funkcijas:

import { onMounted, onUpdated, onUnmounted } from 'vue'

setup() {
  onMounted(() => {
    console.log('Komponentas sumountuotas')
  })
  
  onUpdated(() => {
    console.log('Komponentas atnaujintas')
  })
  
  onUnmounted(() => {
    console.log('Komponentas išmontuotas')
  })
}

Atkreipkite dėmesį – nėra onBeforeMount ar onCreated. Kodėl? Nes setup() funkcija pati vykdoma prieš komponentą mountinant. Viskas, ką rašote setup() viršuje, yra tarsi beforeCreate ir created fazėse.

Vienas didelis privalumas – galite naudoti tuos pačius hooks’us composables’uose. Tai neįmanoma su Options API. Pavyzdžiui, jūsų useMouse funkcija gali turėti savo onMounted ir onUnmounted, ir jie veiks teisingai, nesvarbu, kuriame komponente naudojate.

Computed ir watch – subtilybės

computed veikia panašiai kaip Options API, tik dabar tai funkcija:

const count = ref(0)
const doubled = computed(() => count.value * 2)

Svarbu suprasti, kad computed grąžina ref, todėl reikia doubled.value. Ir taip, jis yra lazy – perskaičiuojamas tik tada, kai kas nors jį naudoja.

Su watch situacija įdomesnė. Yra trys būdai jį naudoti:

// 1. Stebėti vieną ref
watch(count, (newValue, oldValue) => {
  console.log(`Pasikeitė iš ${oldValue} į ${newValue}`)
})

// 2. Stebėti kelis šaltinius
watch([count, name], ([newCount, newName], [oldCount, oldName]) => {
  console.log('Kažkas pasikeitė')
})

// 3. Stebėti reactive objekto savybę
const state = reactive({ count: 0 })
watch(() => state.count, (newValue) => {
  console.log('Count pasikeitė')
})

Trečias būdas dažnai užmirštamas, bet labai naudingas. Negalite tiesiog watch(state.count, ...), nes tai būtų primityvus skaičius. Reikia funkcijos, kuri grąžina tą savybę.

Yra ir watchEffect, kuris automatiškai stebi viską, kas naudojama funkcijoje:

watchEffect(() => {
  console.log(`Count yra ${count.value}`)
})

watchEffect naudoju retai, nes neaišku, ką tiksliai jis stebi. Bet kai reikia stebėti daug dalykų, jis gali sutaupyti kodo.

Script setup – sintaksės cukrus

Vue 3.2 pristatė <script setup>, kuris dar labiau supaprastina Composition API. Nereikia setup() funkcijos, nereikia nieko grąžinti:

<script setup>
import { ref, computed } from 'vue'

const count = ref(0)
const doubled = computed(() => count.value * 2)

function increment() {
  count.value++
}
</script>

<template>
  <div>
    <p>{{ count }} * 2 = {{ doubled }}</p>
    <button @click="increment">+1</button>
  </div>
</template>

Viskas, kas aprašyta <script setup>, automatiškai prieinama template’e. Importai, kintamieji, funkcijos – viskas. Tai ne tik trumpiau, bet ir našiau, nes Vue kompiliatorius gali geriau optimizuoti.

Komponentus taip pat galima tiesiog importuoti:

<script setup>
import MyButton from './MyButton.vue'
// Nereikia registruoti components objekte
</script>

<template>
  <MyButton />
</template>

Vienintelis minusas – šiek tiek sudėtingiau su props ir emits. Reikia naudoti defineProps ir defineEmits makro funkcijas:

<script setup>
const props = defineProps({
  title: String,
  count: Number
})

const emit = defineEmits(['update', 'delete'])

function handleClick() {
  emit('update', props.count + 1)
}
</script>

Šios funkcijos atrodo kaip importai, bet iš tikrųjų tai kompiliatoriaus makrosai. Jų nereikia importuoti, ir jos veikia tik <script setup> kontekste.

Ką daryti su esamu Options API kodu

Gera žinia – nereikia visko perrašyti iš karto. Vue 3 puikiai palaiko abu API. Galite turėti projektą, kur vieni komponentai naudoja Options API, kiti – Composition API. Jie netgi gali bendrauti be problemų.

Praktiškas požiūris būtų toks: naujus komponentus rašykite su Composition API, ypač jei jie sudėtingi ar turi daug logikos. Senus komponentus palieskite tik tada, kai reikia juos modifikuoti. Jei modifikacija nedidelė, gal net neverta konvertuoti. Bet jei pridėti naują feature’ą, galite apsvarstyti migraciją.

Kai migravau vieną projektą, pradėjau nuo composables. Išėmiau pasikartojančią logiką iš mixins į atskiras funkcijas. Paskui pamažu keitėme komponentus, pradedant nuo tų, kurie naudojo tuos composables. Procesas užtruko keletą mėnesių, bet nebuvo skausmingas.

Vienas patarimas – nemėginkite tiesiog „išversti” Options API į Composition API. Vietoj to, pagalvokite, kaip logiką galima geriau sugrupuoti. Galbūt keli computed properties ir metodai iš tikrųjų yra viena funkcija? Galbūt dalį logikos galima iškelti į composable? Tai proga ne tik pakeisti sintaksę, bet ir pagerinti architektūrą.

Realūs patarimai iš траншėjų

Po kelių metų darbo su Composition API, turiu keletą praktinių pastebėjimų, kurie gali sutaupyti laiko ir nervų.

Pirma, neperdirbkite su composables. Ne kiekviena funkcija turi būti composable. Jei logika naudojama tik viename komponente ir nėra sudėtinga, palikite ją komponente. Composables’ai turi prasmę, kai logika pakartotinai naudojama arba kai komponentas tampa per didelis.

Antra, vardai svarbu. Composables’us vadinkite use* formatu – useAuth, useFetch, useLocalStorage. Tai ne tik konvencija, bet ir padeda skaitant kodą suprasti, kad tai ne paprasta funkcija, o kažkas, kas turi reaktyvią būseną.

Trečia, būkite atsargūs su async funkcijomis setup(). Negalite padaryti setup async, nes Vue tikisi, kad ji grąžins objektą, o ne Promise. Jei reikia async duomenų, naudokite onMounted:

setup() {
  const data = ref(null)
  
  onMounted(async () => {
    data.value = await fetchData()
  })
  
  return { data }
}

Arba sukurkite composable, kuris tvarko async logiką:

function useFetch(url) {
  const data = ref(null)
  const error = ref(null)
  const loading = ref(false)
  
  async function fetch() {
    loading.value = true
    try {
      data.value = await (await fetch(url)).json()
    } catch (e) {
      error.value = e
    } finally {
      loading.value = false
    }
  }
  
  onMounted(fetch)
  
  return { data, error, loading, refetch: fetch }
}

Ketvirta, TypeScript su Composition API veikia puikiai, daug geriau nei su Options API. Jei dar nenaudojate TypeScript, Composition API gali būti geras pretekstas pradėti. Props tipai, computed tipai – viskas veikia intuityviai.

Penkta, naudokite Vue DevTools. Jie buvo atnaujinti palaikyti Composition API ir rodo, kokie ref’ai ir reactive objektai yra komponente. Tai labai padeda debuginant.

Kaip visa tai atrodo praktikoje

Geriausias būdas suprasti Composition API – pamatyti realų pavyzdį. Tarkime, kuriate todo aplikaciją (taip, dar vieną). Su Options API turėtumėte:

export default {
  data() {
    return {
      todos: [],
      filter: 'all',
      newTodo: ''
    }
  },
  computed: {
    filteredTodos() {
      if (this.filter === 'all') return this.todos
      if (this.filter === 'active') return this.todos.filter(t => !t.done)
      return this.todos.filter(t => t.done)
    },
    stats() {
      return {
        total: this.todos.length,
        active: this.todos.filter(t => !t.done).length,
        done: this.todos.filter(t => t.done).length
      }
    }
  },
  methods: {
    addTodo() {
      if (!this.newTodo) return
      this.todos.push({ text: this.newTodo, done: false })
      this.newTodo = ''
    },
    removeTodo(index) {
      this.todos.splice(index, 1)
    },
    toggleTodo(todo) {
      todo.done = !todo.done
    }
  },
  mounted() {
    const saved = localStorage.getItem('todos')
    if (saved) this.todos = JSON.parse(saved)
  },
  watch: {
    todos: {
      handler(todos) {
        localStorage.setItem('todos', JSON.stringify(todos))
      },
      deep: true
    }
  }
}

Su Composition API ir <script setup>:

<script setup>
import { ref, computed, watch } from 'vue'
import { useTodoStorage } from '@/composables/useTodoStorage'
import { useTodoFilters } from '@/composables/useTodoFilters'

const newTodo = ref('')
const { todos, saveTodos } = useTodoStorage()
const { filter, filteredTodos, stats } = useTodoFilters(todos)

function addTodo() {
  if (!newTodo.value) return
  todos.value.push({ text: newTodo.value, done: false })
  newTodo.value = ''
}

function removeTodo(index) {
  todos.value.splice(index, 1)
}

function toggleTodo(todo) {
  todo.done = !todo.done
}

watch(todos, saveTodos, { deep: true })
</script>

Kur useTodoStorage:

import { ref, onMounted } from 'vue'

export function useTodoStorage() {
  const todos = ref([])
  
  onMounted(() => {
    const saved = localStorage.getItem('todos')
    if (saved) todos.value = JSON.parse(saved)
  })
  
  function saveTodos() {
    localStorage.setItem('todos', JSON.stringify(todos.value))
  }
  
  return { todos, saveTodos }
}

Ir useTodoFilters:

import { ref, computed } from 'vue'

export function useTodoFilters(todos) {
  const filter = ref('all')
  
  const filteredTodos = computed(() => {
    if (filter.value === 'all') return todos.value
    if (filter.value === 'active') return todos.value.filter(t => !t.done)
    return todos.value.filter(t => t.done)
  })
  
  const stats = computed(() => ({
    total: todos.value.length,
    active: todos.value.filter(t => !t.done).length,
    done: todos.value.filter(t => t.done).length
  }))
  
  return { filter, filteredTodos, stats }
}

Matote skirtumą? Komponente liko tik specifinė to komponento logika. Storage ir filtravimas iškelti į pakartotinai naudojamus composables. Jei dabar reikės panašios funkcionalybės kitame komponente, galite tiesiog importuoti tuos composables.

Žinoma, šis pavyzdys supaprastintas. Realiame projekte turbūt naudotumėte Pinia ar Vuex būsenai, API kvietimus, validaciją ir t.t. Bet principas tas pats – logika sugrupuota pagal funkcionalumą, o ne pagal opcijų tipus.

Composition API nėra sidabrinis kulka, ir jis tikrai turi mokymosi kreivę. Bet kai jį įsisavinsite, grįžti atgal bus sunku. Galimybė organizuoti kodą taip, kaip jums patinka, pakartotinai naudoti logiką be mixins košmaro, ir turėti geresnį TypeScript palaikymą – tai verta investuoto laiko. Pradėkite nuo mažų dalykų, eksperimentuokite su composables, ir pamažu viskas sustos į savo vietas.

Parašykite komentarą

El. pašto adresas nebus skelbiamas. Būtini laukeliai pažymėti *