Галоўная / Артыкулы

Vue 3 у практыцы: композаблі, типаваныя кантракты і прапорцыйны стан

Як API складання Vue 3, атрыбуты з типамі і функцыя emits, Pinia oraz постауповая адаптация дазволяюць додатку ставаць настолькі ж складным, насколькі яго трэба, і калі Vue ўсё-такі не являецца правым выборам.

Выдавец: REACTAPP.TOP12 хв чытання2722 слоў

Крыніцы: Vue.js Is the Frontend Framework More Teams Should Be Using

Калі команда выбирае React, ніхто не спытае працэграматы; калі ж каманда выбирае Vue, хтось практычна завжды яе спытае. Такая рэакція больш сказываеся пра прычыны, звычкі, чым пра саму фрэймворк-систему, адказваючы на чыё Vue 3 — гэта зроўнасцевая апцыя з офіцыйным механізамам навігацыі і керування станом, падтрымкай TypeScript першага класу і можлівасцю стварэння цэлага стака за дапамою Nuxt. У гэтым артыкуле рассматрываецца Vue 3 на прыкладзе рэальнага коду: як чытаюцца стан і вырахоўваныя значэння, як компонаблі пакетуюць поведзенне, як типаваныя пропсы і эміты выклікаюць адпаведныя контракты компонентаў, як ужываецца Pinia, і дзе Vue проста не ўсунецца як правильны выбар.

Паўтараючыся тэма — прапорцыйнасць. Vue дае достатню структуру для серйозных прыкладоў выкарыстоўвання, не ператвараючы кожную маленькую функцыя ў архітэктурнае рашэнне, і гэюя якосць лёгка ацэніць на рэальным компоненте, а не праз пораўнанне функцый.

Рэалістычны першы компонент: фільтраванне пошуку

Додаткі парадыграм «То-ду» мало ўсьміхаюць пра фреймворк. Чыргавае более рэалістычнае тэставанне — цэх пошуку продуктаў: поле для введення, спіс продуктав і тыя элементы, якія падходзяць да запиту.

Логіка, паказаная нижэй, выкорыстоўвае два прымітывы. ref стварае рэактыўны стан — у гэтым случыку текст запиту і масів продуктав. computed вырахоўвае значэнне на адной з гэтых баз і кешуе яго, пакуль не змяніцца адна з його залежнасцей. Заўважыце, што фільтр адразу ж выкарыстоўвае функцыю trim і прыводзіць запит да малага роўнаго, вяртае цэлы спіс, калі запит пусты, і толькі пасля чаго выкарыстоўвае фільтрацыю.

import { computed, ref } from 'vue'

interface Product {
  id: number
  name: string
  price: number
}

const search = ref('')
const products = ref<Product[]>([
  { id: 1, name: 'Mechanical Keyboard', price: 120 },
  { id: 2, name: '4K Monitor', price: 450 },
  { id: 3, name: 'Wireless Mouse', price: 60 }
])

const filteredProducts = computed(() => {
  const query = search.value.toLowerCase().trim()

  if (!query) {
    return products.value
  }

  return products.value.filter(product =>
    product.name.toLowerCase().includes(query)
  )
})

Шаблон безпасебна прыўязуецца да гэтых значэнняў. v-model падтрымлівае сінхронізацыю межу полем введення і змянай search у обох напрамках, v-for атрыбутавае кожны элемент спісу, які праявіўся пасля фільтрацыі, а :key дае Vue стабільную ідэнтыфікацыю кожнай строкі, ўжо тады як він можа эфектыва апдэйтаваць спіс.

<input
  v-model="search"
  type="search"
  placeholder="Search products..."
/>

<ul>
  <li
    v-for="product in filteredProducts"
    :key="product.id"
  >
    {{ product.name }} - ${{ product.price }}
  </li>
</ul>

Няма большой канцэраматы. Стан тэксту ўвіднага видны, вырашаная значэнне таксама ўвіднага видны, а маркап, який іх выкарыстоўвае, таксама ўвіднага видны. Можна чытаць яго зверху вярх без падготовкі, не вучычыся спецыфічным для фрэймворку механізамам. Гэта можа здавацца незначным прынтам, але калі кодбаза расте, швальнасць разумення незнайомага файла становіцца адной з найважлівейшых рэчаў, якія вплываюць фрэймворк.

Одна деталь, яку варта знать: у шаблоне рэферэнсы автаматычна адкрываюцца, таму пішаце search, а не search.value. У кодзе скрыптавага типу все ж выкарыстоўваеце .value, таму функцыя, якая вырахоўвае значэння, чытае search.value і products.value.

The Composition API: паўтарна выкарыстоўванне поведзення, а не компонентав

Самая значныцае змянена ў старым Vue і Vue 3 — это Composition API. Options API групаваў код па катэгоріях фрэймворку (даныя, методы, вычысленыя значэння, стежыкі), чыягоўнае розсіявала адну функцыю па кальколькі разлічных частак. Composition API дазволяе групаваць код па тым, калі саме выконваеяся функцыя.

Возьмімо стораніцу продукту, якой трэба запрашваць даныя і стежыць за станамі завантажэння і паказкі адказаў. Гэтая логіка можа быць перакладзеная ў компонент, тое есть простую функцыю, назва якой звычайна пачынаецца з use, і якая вяртае рэактыўны стан асабліва функцыі, якія яго зменяюць:

import { ref } from 'vue'

export interface Product {
  id: number
  name: string
  price: number
}
export function useProducts() {
  const products = ref<Product[]>([])
  const loading = ref(false)
  const error = ref<string | null>(null)
  async function loadProducts(): Promise<void> {
    loading.value = true
    error.value = null
    try {
      const response = await fetch('/api/products')
      if (!response.ok) {
        throw new Error(`Request failed with ${response.status}`)
      }
      products.value = (await response.json()) as Product[]
    } catch (err) {
      error.value =
        err instanceof Error
          ? err.message
          : 'Failed to load products.'
    } finally {
      loading.value = false
    }
  }
  return {
    products,
    loading,
    error,
    loadProducts
  }
}

Калькольнае рашэнне дзеяе ў калькольных частках. Значэнне loading скасоўваецца ў блоку finally, таму яно ачышваецца незалежна ад таго, чы спрачаў успяў чы не. Значэнне error ачышваецца пры кожной спробе, ўбліжчаючы тады, каб старыя паведамленні не застаўаліся. Адпаведна, адказ з кодам, не ў межах 2xx, ператвараецца на асключэнне, адтуды што fetch адхоўляе толькі у разе проблем з сетчу, а не па статусам HTTP-адпаведзей. Блок catch спачатку фільтруе значэнне err за дапамою instanceof Error, перш чым чытаць message; гэта неабходна, таму што у TypeScript типы пры ловлі значэнняў выкарыстоўвалі тип unknown.

Важным аспектам ёсць тое, што пераўтвараецца на спільнае. Вы не дзеліцеся компанентам з його маркапом; вы дзеліцеся яго функцыянамі. Компаненты зосераджваюцца на тым, што бачыць корыстнік, а компонуемы элементы — на тым, як працюе адпаведная функцыя, і ніхто з іх не патрабуе стаць глобальной абстракцыяй лишчынкай таго, каб два экраны моглі спільна выкарыстоўваць якуюсь логіку. Адна застерэжненне: кожны вызов useProducts() стварае свой сабэйнстанцыйны стан. Зазвычай гэта самае тое, чаго вы хочаце, але якщо два компаненты павінны бачыць той самы список продуктав, вам патрэбны спільныя станы, і тут на сцену выходзіт спецыяльныя хранілнікі.

Надаўчы кожнаму аспекту свойяе месца

Фрэймворк можа быць моцным ў тым сэнсе, што ён праставляе множнацьце можлівасцей, або ў тым сэнсе, што зменшае разлік межу тым, калі вы меркавалі, і тым, што насправды пішлі. Vue склоняецца да другага варыянта.

Разглянема такую патэнцію, яка здаецца простай: калі змінюецца фільтр, яго трэба адражнаць у URL, падзецца перзапытка данных, сохраняецца структура сторнікаў, а таксама паказваецца індыкатор загрузкі. Існуе калькі элементаў, і ў логічной архітэктуре Vue кожны з іх мае чыста вялікіяўца:

  • компанента атрыбуець інтерфейс
  • компонабель рэалізуея прыемство функцый
  • рутэр керуе станам URL
  • кліент API адпавядае за комунікацыю з бэкендам
  • хранальнік зберагае данныя толькі тады, калі іх сапраўды трэба раздаваць у всій аплікацыі

Няктары адзін элемент не патрабуе ведаць пра все. Такое раздзеленне не ўнікальнае для Vue — яго можна рэалізаваць у будзь-якай сучасной фрэймворку. Асабліва прыгляднае тое, як прыродным чынам прыемства Vue спрыяюць такаму падходу.

Компаненты з аднаго файла: адна пазіцыя для адной ейканай еўранты

Компаненты Vue з ядрам у аднам файле спаўнаюць шаблон, скрыпты і стылі ў файле .vue. Критыкі інодзе спачатку вважаюць гэта стылістычным эксцентрызмам, але сутнасць не ў тым, «чыба не пакласты все ў адны файл». Сутнасць у тым, што адна елемента інтэрфейсу павінна знаходзіцца ў адным месцы. Тут важлівы сама модэль компаненту, а не формат файлу.

Працэс керавання простым переключальнікам адкрыція/закрыцця выражаецца за дапамогою аднаго булевага значэння і функцыі, якая яго змінюе:

import { ref } from 'vue'

const isOpen = ref(false)
function toggle(): void {
  isOpen.value = !isOpen.value
}

Маркап таксама чытаемы без проблем. @click під’єднуе кнопку да функцыі toggle, а v-if дадае або адключае панель залежна ад значэння isOpen:

<button type="button" @click="toggle">
  Account Settings
</button>

<div v-if="isOpen">
  Manage your account preferences.
</div>

Адзёрнае адстань межа станам і UI ў мінімуме, і такая ж застаёцца. Калі чыраз калькі месцаў праз вярнуцца да данай функцыі, каб яе дыбагаваць, зв’язак межа флажкам і тым, што праказваецца на экране, яшчэ застаёцца зрозумелым. З’явіце, што v-if пачынае ўсунуць элемент з DOM цэлкам; якщо панель часта переключаецца або містіць ресурсы з вялікіми витратамі, v-show, який толькі хавае його за дапамою CSS, можа стаць кращым выборам.

Хорашая архітектура компонентаў не стосуецца мінімізацыі колькасці файлоў. Яна стосуецца таго, каб скарыстыя рэшткі было лёгка знайсці.

TypeScript, дзе знаходзяцца кантракты

Фронтэнд-зявіце наполненая угодамі: параметрамі, які прыймае компанент, здарэннямі, якія ён выпускае, значэннямі, якія вяртае складны элемент, моделямі, якія стварае кліент API, а таксама станамі і дзеяннямі, якія адкрывае хранальнік. Калі гэтыя угоды ўскрытыя, рефактораванне большога кодавая базы значыць паслуговацца тым, што людзі запамятаюць падазры, а не чытаюць іх.

Vue 3 дазволяе гэтыя угоды знаходзіцца проста падле логіки компанента. Унутрь <script setup> макросы defineProps і defineEmits являюцься макросамі кампайлера, якія прыймаюць толькі заявы пра типы:

interface User {
  id: number
  name: string
  email: string
}

const props = defineProps<{
  user: User
}>()
const emit = defineEmits<{
  updated: [user: User]
}>()
function save(): void {
  emit('updated', props.user)
}

Тып пропаў кажа, што компанента выклікаецца з user тыпу User. Тып эмітавання выкарыстоўвае падпис у стылі туплы, што значыць, ў зместе updated є роўна аднаго аргумента — User. Выклік emit з некоректным іменем зместу або пакетам дадзеных ўсёлякі тыповы бяда. Кожны, хто ачынае файл, можа за мінуту пазначыць публічны інтерфейс компаненты.

Гэта та частка TypeScript, якая найбольш дапамагае у велікіх фронтэндах. Автодаполненне і падказкі ведчыка ўжо прыемныя, але самэкспліцытныя контракты робяць змены болей безпечнымі для аналізу і рефакторынг менш рызыкавым.

Зберагаюце стан як можна локальней

Надзеяны спосаб зробіць фронтэнд складным — це размесціваць усё ў глобальным стаце: хранільніку для модалаго вікна, хранільніку для поля формы, хранільніку для выбранай строчкі таблыцы, хранільніку для тымчасовага запиту на пошук. Чырз некалькі часоў палова прыемлівання жыве ў аднам вялікім шары стацу, дзе будь-які код можа зменіць усё.

Vue падтрымае болей мягкі спосаб развіцця:

  • зберагаць локальны стац у самым компоненте, які яго выкарыстоўвае
  • перакладаць повтарна выкарыстоўваныя функціі ў композаблі
  • размесціваць справжнія спільныя даны ў хранільніку

Для гэтаго апошнага крока Pinia ёсць ofіцыяльным хранальнікам Vue. Прыводзіма прыклад хранальніка у стыле options для корзіны пакупак. state вяртае пачатковыя данні, гэтар total вырахоўвае суму пакупак на аднойчынку з элементаў і ўпорам, а акцыі меняюць спіс. addItem збільшае упор, калі тавар уже ў корзіне, а інакш дадае копію элемента, ўжо каб хранальнік не дзеліўся рэферэнсам об’екта з тым, хто яго вызвае.

import { defineStore } from 'pinia'

export interface CartItem {
  id: number
  name: string
  price: number
  quantity: number
}
interface CartState {
  items: CartItem[]
}
export const useCartStore = defineStore('cart', {
  state: (): CartState => ({
    items: []
  }),
  getters: {
    total: (state): number =>
      state.items.reduce(
        (sum, item) => sum + item.price * item.quantity,
        0
      )
  },
  actions: {
    addItem(item: CartItem): void {
      const existing = this.items.find(
        current => current.id === item.id
      )
      if (existing) {
        existing.quantity += item.quantity
      } else {
        this.items.push({ ...item })
      }
    },
    removeItem(itemId: number): void {
      this.items = this.items.filter(
        item => item.id !== itemId
      )
    }
  }
})

Сам магазін не ўсё; цікава частка — гэта рашынак стварыць яго. Ввод тэксту практычна ніколі не выкарыстоўвае глобальны стан. А кошык, які чытаюць заголовак, сторунки продуктав і процес платежа, з галоўным правам выкарыстоўвае глобальны стан. Vue дае можлівасць прымкнуць такую рашынку, замест таго каб усе элементы стану размішваць у аднам контейнере. Pinia таксама падтрымляе синтаксі у стылі setup, якая выкарыстоўвае ref і computed безпосередна, што якімсь камандам падабаецца, бо гэта адпавядае способу напісання композаблей.

Шлях росту замест адразовага обявлення

Vue не вымагаец, каб вы з’явіліся всю архітэктуру в першы дзень. Вы пачынаеце з самага Vue, дадаўте Vue Router, калі патрэбна навігацыя, дадаўте Pinia, калі патрэбны данні для всей аплікацыі, а калі хочаце сервернай рэндарынацыі, серверных маршрутаў і болей шырокай настаўкі full-stack, пераходзіце на Nuxt. Малая аплікацыя можа застацца малай; велікая можа атрымаць структуру праз адпаведныя запатраненні.

Гэта ўсё важная для існуючых продуктов. Vue можа быць адключаны да часткі аплікацыі, якая рэндаруецца на сервере і створаная з Django, Laravel, Rails або простага PHP, без неабходнасці перзначэння всего фронтэнду. Можа, толькі панелі аналітыки патрабуець высокай інтеракtywnасці. Можа, адна экран з настройкамі перастала падходзіць серверным шаблонам, альбо рэдагувальнік патрабуець сложнай роботы з боку кліента. Vue можа кераваць той часткай і не втручацца ў всё іншае.

Не кожны фронтэнд павінен быть SPA

Экасістэма іноды спрытвае «фронтэнд-зялёжнасць» і «зялёжнасць з адной сторанай» як сінонімы. Але гэта не так. Для багато продуктов кращыя наследкі даёць варыянт з сторонамі, якія генеруюцца на серверы, і калькамі, якія былі рэштыраны з думкай пра інтэрактыўнасць, а Vue чыста падходзіць для такой модэлі.

Для команд, якія адтульнуюць зрэлыя зялёжнасці, гэта цалкам перакрашвае процес міграцыі. Пытанне перестае быць «як нам перапісаць продукт на Vue?» і становіцца «дзе Vue зможа скасаваць наяўныя прытваранні?». Другое пытанне ведае да меньшых, болей безпечных крокаў і ранейшага атрымання корысті.

Прыдатнасць выходзіць з архітэктуры, а не з критэрыяў

Vue працюе добра, але графікі з теставання ўстойчывасці — гэта слабыя падставы для выбору будзь-каго фреймворку. Продакшн-зялёныя рэдка калі працуюць повольна чераз тое, што ўсёў іх фреймворк прасідзеў у мікротэставанні на калькі мілісекунд. Яны працуюць повольна таму, што атрыбутуюць вялічзююцыя спісы, апрацоўваюць занадта вялікія пакеты, павтараюць сітевыя запиты, заставляюць занадто многа данных працаваць у памяці, або выканаюць дорогія задачы не там, дзе трэба.

Модель Vue спакойвае ухіліцца ад гэтых проблем:

  • компаненты можаць застацца маленькімі і сфармаванымі на адну мету
  • стан можаць застацца локальным, а не спрычынюваць апдэйты ў всім додатку
  • функцыі і маршруты можна завантажваць толькі тады, калі яны патрэбны
  • логіку, якая можа быть перызнаемай, можна ізаліраваць у композаблі
  • глобальны стан можна обмежыць там, што сапраўды яму належыць

Нічога з гэтага не робіць працэздатнасць автаматычной. Гэта спакойвае кращую працэздатнасць, якую легча аналізаваць, і гэта ценяецца больш, чым перамога ў тэставанні.

Зроўналелеглы экасістэм

Выбір Vue адначасова означаў прыйняць меншы экоспектр. Што стосуецца гэтага аргумента, ён зараз значна слабейшы. Існуе офіцыйны рутер, Pinia для керавання станам прыкладнення, розвіталіся інструменты для разработчыкаў, шмат бібліятэк компонентаў UI і Nuxt для команд, якія хочуць павноцэнную рамку прыкладнення.

Разам яны пакрываюць шырокі спектар: адзін інтэрактыўны віджет, большое SPA, сайт, які генеруецца на серверы, або продукт з павноцэнным фронт- і бэкендам. Вам не трэба знать, калі саме з гэтых варыянтаў вы будзеце ствараць прыкладнення, перш чым напісаць першую лінію коду, і гэя гнучкасць сама па сабе ёсць перавагай.

Менш відчутны аргумент: рашэнні прыймаюцца на адповіднасць патрэбам

Сярод усіх выгодаў, якія важка прамерыць, — гэта адчуття, яке даёць прыменне ўпрадовж дня. Вы ачынаеце компонент і розумееце яго. Калі патрэбна можлівасць павтаральнага выкарыстоўвання, вы пішаце компонент, які можна складваць. Стан належыць аднаму компоненту, таму ён застаецца там. Калі чыгунна патрэбна можлівасць аддачы іншым часткам, данні пераходзяць у хранілню. Для прыемкі маршрутаў патрэбен спецыяльны інструмент, таму вы дадаеце рутэр. Калі прыложэнне вырастае за межы кліентскага прыемкі, вы адмацаваеце Nuxt.

Кожны крок адпавядае рэальнай складнасці, якая існуе. Вы прыменяеце архітэктурныя рашэнні таму, што гэта патрабуе сам продукт, а не таму, што фрэймворк выказвае такія трэбаванні з самага пачатку. За весь час жыцця проекту гэтае разлікаванне стае ўсё бол значным.

Калі Vue — неправы выбар

Vue не ўсега яе правы выбар. Арганізацыя, якая вже мае большы код на React, сэрвіс сваёй бібліятэкі компонентав на React, глыбокі досвяд працы з React і практыку найму інжынераў, якіе ведаюць React, верагатна пры перайменаванні на іншую фрамворк паспячыць больш працы, чым заўсёды. Фрамворкі ніколі не выбіраюцца ізольавана; навыкі команды, існуючая інфраструктура, расходы на найм і перайменавання — усё гэта мае не меншое значэнне, чым сама модель программавання.

Vue таксама не захоўвае вас ад паганага інжынерскага падходу. У ёй таксама можна стварыць вялікія компоненты, перадмернае выкарыстоўванне глобальных сховішчаў, непатрэбныя шары абстракцыі і медленныя сторанкі, так жа як і ў будзь-ям другім фрамворку. Яна проста прысутніць зручныя стандарты, але не ўзаменяе сабою правільную дыягнозацыю.

Пропорцыя як прынцып дизайна

Тое, што выдзейнае Vue, — это яго правільны падход да співвяротнасоўваў. Невялікая проблема патрапляе да невялікага рашэння. Повтарна выкарыстоўваныя функцыі стаюць компонабельнымі элементамі. Спакушаны стан стае «складом» толькі тады, калі гэта неабходна. Навігацыя належыць рутэру; большыя прыкладненні можуць выкарыстоўваць Nuxt, а TypeScript апісвае межы без таго, каб перэймала контроль над кодам.

Нічога з гэтага не ўзрывнага, і самэ гэта і є сутнасць. Фреймворк не павінен занова ствараць прыемы разработкі фронтэнду кожныя калькі месцацоў; ён павінен спрабаваць зробіць існуючыя проблемы лёгкімі да рашэння.

Ключовыя выводы

  • ref і computed у Vue 3 робяць стан і вырахаваныя значэння явнымі, што дапамагае падтрымаць чытаемасць компонентоў у меру ўзроста ўжоў.
  • Компонабельныя элементы дзелюцца поведэнням, а не маркапом; кожны вызов стварае незалежны стан, таму трэба выкарыстоўваць «склад», калі даныя сапраўды павінны быць спакушаныя.
  • Функцыі defineProps і defineEmits, які выключаюць заданне значэння, робяць публічныя умовы компонента виднымі і праблемы з якімаў можна выявіць пад час компіляцыі.
  • Складзіце стан цярпеліва: спачатку на локальнам рэвэле, потым у складных структурах, а Pinia — толькі для справжная глобальных дадзеных.
  • Vue можна адпрацоўваць паштоўку за раз у адной інтэрактывной частцы ў вялікім дапрацаваным аплікацыі, што робяць яго варою з низкім рызыкам для готавых продуктов.
  • Экспертыза каманды, існуючы код і можлівасці прабывання новых спецыялістаў могу пераважыць эрганоміку будзь-каго фрэймворка, таму трэба ацэніць Vue ў гэтым контексте, а не на адной популярнасці чы стандартных тэстах.