React, Vue и Svelte: Бой чемпионов. 🏆️

Angular, Svelte, Solid, React, Vue — существует огромное количество фреймворков. И вы наверняка сталкивались с «дилеммой фреймворка» в какой-то момент своей карьеры.

Некоторые из нас выбирают React, самый популярный фреймворк. И это вполне объяснимо, потому что его известность также подразумевает, что это самый востребованный навык на рынке труда.

Далее следует Vue и его вторая позиция в спектре популярности. Кто-то предпочитает Vue за то, что он такой легкий и при этом масштабируемый, а кому-то нравится его простота и схожесть с другими фреймворками.

Давайте сделаем прыжок веры и изменим критерии выбора с популярности на симпатию.

Svelte — самый любимый фреймворк, и не зря. В Svelte большое внимание уделяется счастью разработчиков, потому что его так легко освоить, а компилятор более эффективен, чем у аналогов.

Мне всегда было интересно, насколько по-разному можно построить одно и то же приложение с одними и теми же функциями, но на трех разных фреймворках. Для каждого из них я хочу ответить на следующие вопросы:

  • С каким количеством ошибок я столкнусь?
  • Сколько кода нужно написать?
  • Сколько времени займет сборка?

Мы рассмотрим каждый из вышеупомянутых фреймворков в отдельном материале в рамках серии из четырех статей.

Мы создадим сайт для публичного обмена короткими сообщениями под названием «ithink». Это как Twitter, только без аккаунтов и без возможности удалить то, что вы публикуете.

Но сначала нам нужен API.

Если вы поиграете с CodePen выше, то быстро поймете, что база данных не фальшивая.

На самом деле у нас есть внутренняя сторона нашего приложения: она управляет хранением и получением простых текстовых данных. Она также построена на службе хранения объектов S3 от Amazon.

Обычно разработчик тратит часы и часы на настройку такой базы данных, и это тоже не бесплатно.

К счастью для нас, при использовании Cyclic мы получаем бесплатный экземпляр хранилища AWS S3. Никакой кредитной карты не требуется.

Vue, легкий фреймворк

Давайте начнем с изучения чудес Vue, фреймворка, который претендует на звание одного из самых производительных фронтенд-фреймворков.

Создание нового проекта

Не секрет, что создание проекта — один из самых важных этапов его жизненного цикла. Это самый первый шаг на пути превращения идеи в реальное, рабочее приложение, работающее на современном компьютере.

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

К счастью, Vue делает всю работу за нас, когда мы выполняем следующую команду:

npm init vue@latest
Войти в полноэкранный режим Выйти из полноэкранного режима

Он задаст вам кучу вопросов, чтобы вы могли создать оптимальную настройку, которая лучше всего подходит для вашего проекта. Итак, пройдите вперед и примите/отклоните следующие вопросы:

  • Название проекта: ithink
  • TypeScript? Нет
  • Поддержка JSX? Нет
  • Vue Router? Нет
  • Pinia? Да
  • Vitest? Нет
  • Кипарис? Нет
  • ESLint? Нет
  • Красивее? Нет

Мы рассмотрим, что означает каждый из этих пунктов в этой статье.

Далее — установка. На этот раз вам не нужно ничего делать. Просто расслабьтесь и ждите, пока yarn сделает свою работу! (конечно, вы также можете использовать npm, если вам так больше нравится).

cd ithink
yarn install # or npm install
Вход в полноэкранный режим Выйти из полноэкранного режима

Эта команда создаст очень простой шаблон, который поставляется с некоторыми настройками по умолчанию. Запустите yarn dev или npm run dev и откройте http://localhost:3000 в браузере. Все уже выглядит отлично, не так ли?

Вы увидите полуполезное приложение, описывающее Vue, с добавлением некоторых ссылок на его внутреннюю работу.

К сожалению, большая часть этого материала бесполезна для нас. Поэтому мы сотрем большую его часть, чтобы можно было продолжить работу над этим руководством:

rm src/components/* src/assets/ src/stores/*
echo '' > src/App.vue
Вход в полноэкранный режим Выход из полноэкранного режима

Потратьте некоторое время на изучение структуры папок. Обратите внимание на то, что в ней особое внимание уделяется развязке. Все находится там, где должно быть, что делает эту архитектуру очень чистой и элегантной для работы.

tree .

├── index.html
├── package.json
├── public
│   └── favicon.ico
├── README.md
├── src
│   ├── App.vue
│   ├── components
│   ├── index.css
│   ├── main.js
│   └── stores
├── vite.config.js
└── yarn.lock
Вход в полноэкранный режим Выход из полноэкранного режима

Не волнуйтесь, если все это выглядит пугающе. Мы расскажем обо всем этом в этом документе.

Настройка SEO

Вы, наверное, заметили, что заголовок нашего документа — «Vite App», и нет очевидного способа изменить его.

Оказывается, это отличная возможность для нас углубиться в первый элемент: index.html. Вы, несомненно, уже видели это имя файла в миллиарде других мест, но, вероятно, не в контексте Vue.

Итак, давайте погрузимся в index.html и обновим некоторые настройки по умолчанию:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" href="/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    - <title>Vite App</title>
    + <title>ithink</title>
    + <meta name="description" content="Global online messaging, void of any borders." />
  </head>
  <body>
    <div id="app"></div>
    <script type="module" src="/src/main.js"></script>
  </body>
</html>
Войти в полноэкранный режим Выход из полноэкранного режима

Мы изменили заголовок и добавили описание. Таким образом, проще говоря, index.html — это точка входа в наше приложение. Именно здесь вы изменяете мета-теги <head>, добавляете имена классов к самым верхним элементам, таким как <body> и <html>, и меняете язык вашего сайта с английского ("en") на, скажем, французский ("fr").

Также невозможно избежать разговора о <div id="app"></div>. Как мы увидим в одном из следующих разделов, именно сюда внедряется все, что связано с Vue. Так что давайте продолжать исследовать!

Изменение цвета тела

Сайт, который мы создаем, имеет общую темную тему, а это значит, что цвет <body> должен быть изменен с белого на темный.

После настройки TailwindCSS мы начнем добавлять эти изменения в наш документ.

Вы, должно быть, уже создали файл src/index.css во время настройки Tailwind и импортировали его из src/main.js. Итак, давайте добавим наши пользовательские стили:

/* index.css */
@tailwind base;
@tailwind utilities;
@tailwind components;

body {
    @apply bg-stone-900;
}
Вход в полноэкранный режим Выход из полноэкранного режима

Как вы видите, мы используем директиву @apply для применения утилиты bg-stone-900 к телу. Если вы не очень разбираетесь в Tailwind, то bg-stone-900 эквивалентно следующему объявлению CSS:

body {
    background: #1c1917;
}
Войти в полноэкранный режим Выход из полноэкранного режима

Добавление заголовка

Пришло время углубиться в наш второй по значимости элемент: App.vue. Это самый верхний компонент нашего приложения.

Все, что вы разместите в этом файле, попадет прямо в <div id="app"></div>. Помните об этом? Мы упоминали его в последнем абзаце раздела, в котором рассматривался index.html. Видите, как все это сочетается? Волшебно!

И со всем этим, давайте реализуем наш слишком упрощенный заголовок:

<template>
    <!-- App.vue -->

    <header>
        <h1>ithink</h1>
        <button>New</button>
    </header>
</template>
Вход в полноэкранный режим Выход из полноэкранного режима

Как видите, мы сделали все довольно просто.

Эй! Я скрыл имена классов Tailwind из приведенного выше кода. Просмотрите наш репозиторий кода, чтобы увидеть полный код.

Кхм, давайте обратимся к слону в комнате. Что такое <template>?

  • Прежде чем мы разберемся в этом, позвольте мне объяснить вам фундаментальную концепцию… Веб-компоненты!

    Веб-компоненты дают разработчикам возможность создавать свои собственные теги:

    <profile-card>, <emoji>, <close-button>, <code-editor>.

    Это похоже на волшебство! Вы получаете возможность инкапсулировать свой собственный HTML, CSS и JavaScript. Веб-компоненты — это элегантный способ создания пользовательских интерфейсов, напоминающих нативные приложения. Они — будущее веба.

    Если для вас это звучит круто, вам, вероятно, интересно, как создавать такие вещи.

Позвольте представить вам… Однофайловые компоненты! (SFCs)

<script setup>
    // Javascript here
</script>

<template>
    <!-- HTML here -->
</template>

<style scoped>
    /* CSS here */
</style>
Вход в полноэкранный режим Выход из полноэкранного режима

Как вы можете видеть, SFC разделяют дизайн компонента на три логические части, и они могут располагаться в любом порядке:

  • Логика JavaScript: (<script setup>) Мозг вашего компонента. Обрабатывает состояние, события, сетевое взаимодействие и т.д….
  • HTML-документ: (<template>) Семантика вашего компонента.
  • CSS декларации: (<style scoped>) Стиль вашего компонента.

Довольно аккуратно, да?

Прослушивание событий нажатия

В текущем виде кнопка «new» ничего не делает, а бесполезная кнопка никому не нужна.

Так давайте изменим это! Мы должны прослушать событие щелчка, а в Vue события работают следующим образом:

<script setup>
    function openModal() {
        // TODO
        alert('button clicked!')
    }
</script>
<template>
    <button @click="openModal">New</button>
</template>
Вход в полноэкранный режим Выход из полноэкранного режима

Мы вернемся к функции openModal позже, когда мы действительно создадим модальный компонент!

Создание модального компонента

Пришло время использовать папку src/components. Именно здесь мы будем размещать все наши пользовательские компоненты.

На изображении выше вы видите компонент «New Modal», записанный в файл NewModal.vue.

Создайте новый файл по адресу src/components/NewModal.vue. Наполните его обычным шаблоном SFC:

<script setup>
    // NewModal.vue
</script>

<template>
</template>

<style scoped>
</style>
Войти в полноэкранный режим Выйти из полноэкранного режима

Давайте разберемся с разметкой: (не забывайте использовать правильную семантику)

<template>
  <div>
    <dialog open>
      <main>
        <form method="dialog">
            <label for="content">Content</label>
            <textarea id="content"></textarea>

            <button value="cancel">Cancel</button>
            <button value="default">Post</button>
        </form>
      </main>
      <footer>
        <p>Whatever you write will become public.</p>
      </footer>
    </dialog>
  </div>
</template>
Войти в полноэкранный режим Выйти из полноэкранного режима

Разметка сама по себе не очень полезна без логики JavaScript. Давайте прикрепим обработчики событий к нашим элементам кнопок:

<script setup>
    function close() {
        // TODO
    }
</script>

<template>
    ...
    <button value="cancel" @click="close">Cancel</button>
    <button value="default">Post</button>
    ...
</template>
Вход в полноэкранный режим Выход из полноэкранного режима

Хорошо, это замечательно! Но обратите внимание, что когда вы нажимаете на кнопку «post», страница перезагружается. Это поведение HTML-форм по умолчанию.

Обычно мы меняем это, вызывая e.preventDefault. Но в Vue так много внимания уделяется простоте, что есть очень удобное сокращение:

<script setup>
    function submit() {
        // TODO
    }
</script>

<template>
    ...
    <form method="dialog" @submit.prevent="submit">
        ...
    </form>
    ...
</template>
Войти в полноэкранный режим Выйти из полноэкранного режима

Вы только посмотрите на это! Мы можем сократить e.preventDefault() до @submit.prevent. Теперь нам даже не нужно больше рассматривать объект Event!

Отслеживание состояния загрузки

Пользователь должен знать, когда его приложение загружается, отправляет ли оно новый текст на сервер или просто получает его.

Давайте создадим переменную, которая будет отслеживать, когда наш компонент отправляет/получает данные на/от сервера. Мы будем использовать реактивные переменные.

  • Что такое реактивная переменная?

    Рассмотрим следующий сценарий:

    let a = 4
    document.getElementById('container').textContent = a // <p id="container">4</p>
    

    Допустим, мы обновили значение в a до 5. Одно мы знаем наверняка: элемент <p> не изменится. В нем всегда будет написано «4», если мы явно не изменим его.

    Но нам и не нужно этого делать! С реактивностью DOM автоматически обновляется, как только связанная переменная изменяется.

    В Vue реактивные переменные создаются с помощью ref, функции, которая принимает любое значение и делает его реактивным.

    import { ref } from 'vue'
    const a = ref(4)
    a.value = 5
    

    Обратите внимание на добавление .value. Это важно, потому что если бы мы сделали a = 5, то полностью лишили бы переменную реактивности.

    Также не имеет значения, что a является постоянной переменной, потому что мы присваиваем только ее свойству .value.

    Теперь посмотрите на HTML:

    <template>
        <p>{{ a }}</p>
    </template>
    

    Vue заменит {{ a }} его значением: a.value, а двойные скобки не являются необязательными.

Итак, давайте вернемся к логике submit. Мы создадим новую реактивную переменную isLoading, которая будет указывать, когда наше приложение находится в контакте с сервером.

import { ref } from 'vue'

const isLoading = ref(false)
async function submit() {
    isLoading.value = true

    // TODO: send data to server

    isLoading.value = false
}
Вход в полноэкранный режим Выход из полноэкранного режима

Получение пользовательского ввода от элементов формы

Наш «новый модальный» компонент не может существовать без элемента формы <textarea>. Но это поднимает важный вопрос: как нам сохранить синхронизацию кода javascript с содержимым DOM?

В двух словах, в Vue есть короткий способ работы с элементами форм. И это довольно удобно!

<script setup>
    import { ref } from 'vue'
    const message = ref('')

    async function submit() {
        // TODO: implement addItem
        addItem(message.value)
    }
</script>

<template>
    ...
    <textarea id="content" v-model="message"></textarea>
</template>
Войти в полноэкранный режим Выход из полноэкранного режима

Как вы видите, message — это реактивная переменная, поэтому всякий раз, когда пользователь вводит что-то в textarea, message будет мгновенно обновляться. В этом и заключается магия v-model!

Эй! Возможно, вы уже устали от постоянного ввода .value. К счастью, Vue планирует изменить это.

Создание инертного модала во время загрузки

Когда наше приложение загружается (что отслеживается переменной isLoading, которую мы видели в предыдущем разделе), мы должны отключить взаимодействие с ним.

Для этого необходимо получить доступ к элементу DOM в JavaScript. Чистый способ сделать это — body.querySelector(), но этот подход нереактивен. Кроме того, существует более простая альтернатива, предлагаемая Vue:

<script setup>
    import { ref } from 'vue'
    const container = ref(null)
</script>

<template>
    <div ref="container"></div>
</template>
Войти в полноэкранный режим Выход из полноэкранного режима

Это называется шаблонные ссылки! И хотя здесь говорится null, Vue заполнит container элементом DOM <div ref="container"> при его создании. Он также вернется к null, когда исчезнет.

Имея это на руках, мы можем сделать наш компонент инертным во время загрузки:

<script setup>
    import { ref } from 'vue'
    const container = ref(null)

    async function submit() {
        isLoading.value = true;
        dialog.value.setAttribute("inert", true);

        // TODO: send data to server

        dialog.value.removeAttribute("inert");
        isLoading.value = false;
    }
</script>

<template>
    <div ref="container"></div>
</template>

<style scoped>
dialog[inert] {
  @apply filter brightness-90;
}
</style>
Войти в полноэкранный режим Выйти из полноэкранного режима

Эй! Хотя есть более простой способ добиться вышеописанного (<div :inert=»isLoading»), я просто должен был создать возможность научить вас рефлексам шаблона, которые являются довольно важной функцией в VueJS.

Автоматическая фокусировка <textarea>

Когда пользователь открывает компонент NewModal, мы знаем, что его целью является ввод в <textarea>. Так не будет ли удобно избавить их от необходимости перемещать курсор к этому элементу формы?

Давайте реализуем эту функцию! Как ни странно, мы не можем этого сделать:

<script setup>
    import { ref } from 'vue'
    const textarea = ref(null)

    textarea.value.focus() // this is WRONG!
</script>

<template>
    <textarea ref="textarea"></textarea>
</template>
Войти в полноэкранный режим Выйти из полноэкранного режима

Приведенный выше код не будет работать, потому что это то же самое, что сказать null.focus().

Итак, как же нам сказать Vue, чтобы он фокусировал текстовую область только после того, как она станет доступной? Ответ кроется в крючках жизненного цикла.

Мы хотим использовать крючок «mounted», который вызывается сразу после добавления компонента в DOM. Именно в этот момент <textarea> отображается, а это значит, что она не null:

import { onMounted } from 'vue'

onMounted(() => {
    textarea.value.focus() // CORRECT! :)
})
Вход в полноэкранный режим Выход из полноэкранного режима

Существует еще много крючков жизненного цикла, и мы обычно используем большинство из них. Однако в нашем приложении хука «mounted» было более чем достаточно. Но имейте в виду одну вещь: вы будете видеть эту концепцию снова и снова в каждом фреймворке.

Закрытие модала при клике снаружи

Это довольно распространенное поведение в каждом модале — закрывать его, когда пользователь нажимает на кнопку «наружу».

В этом шаге мы воспользуемся ОГРОМНЫМ сокращением. Вместо того чтобы вручную реализовывать это поведение, мы воспользуемся другой библиотекой, которая сделает это за нас.

Сообщество Vue огромно и бесконечно полезно, поэтому неудивительно найти такую библиотеку, как @vueuse: более 200 полезных методов, которые охватывают почти все возможные случаи использования.

Нас интересует событие [onClickOutside](https://vueuse.org/core/onClickOutside/), которое срабатывает каждый раз, когда пользователь щелкает мышью вне определенного элемента DOM.

После настройки @vueuse, давайте добавим его в наш компонент:

<script setup>
    import { ref } from 'vue'
    import { onClickOutside } from '@vueuse/core'

    // Close dialog when clicked outside
    const container = ref(null)
    onClickOutside(container, close)

    function close() {
        // TODO
    }
</script>

<template>
    <dialog ref="container">...</dialog>
</template>
Вход в полноэкранный режим Выход из полноэкранного режима

Удивительно, как в одной строке кода мы смогли реализовать такую, казалось бы, сложную функцию!

Закрытие модала

У модала нет возможности узнать, открыт он или закрыт, поскольку эта информация известна только его родителю — App.vue.

Но есть одна вещь, которую мы знаем точно, и это то, что модальное окно знает, когда оно должно быть закрыто:

  • пользователь щелкнул снаружи,
  • пользователь отправил,
  • пользователь отменил.

Поэтому нам нужен способ для дочернего модуля NewModal общаться со своим родителем, App. И решение довольно простое: События!

В Vue очень просто определять собственные события и диктовать, когда они должны происходить, используя собственные правила.

const emit = defineEmits(['close'])

function close() {
    emit('close')
}
Вход в полноэкранный режим Выход из полноэкранного режима

Отныне, всякий раз, когда нужно закрыть модальное окно, мы передаем событие «close» его родителю.

Обратите внимание, что мы не импортировали defineEmits. Это потому, что это макрос компилятора, поэтому он всегда присутствует по умолчанию.

Использование нашего пользовательского компонента

До сих пор вы не могли видеть NewModal, потому что он еще не был добавлен в App.vue. Итак, давайте изменим это:

<script setup>
    import NewModal from './components/NewModal.vue'
</script>

<template>
    <NewModal />
</template>
Войти в полноэкранный режим Выйти из полноэкранного режима

С приведенным выше фрагментом кода модальное окно всегда будет открыто. Так что давайте добавим немного магии переключения:

<script setup>
    import NewModal from './components/NewModal.vue'
    import { ref } from 'vue'

    const isModalOpen = ref(false)

    function openModal() {
        isModalOpen.value = true
    }
    function closeModal() {
        isModalOpen.value = false
    }
</script>

<template>
    <NewModal v-if="isModalOpen" />
</template>
Войти в полноэкранный режим Выйти из полноэкранного режима

Мы воспользовались преимуществами v-if для показа/скрытия модала.

И теперь мы закончили с компонентом NewModal. Довольно просто, да?

Создание динамического списка элементов DOM

Пришло время перейти к новому компоненту ThoughtList. Этот компонент будет отображать список элементов, причем длина этого списка нам заранее неизвестна.

Давайте создадим новый файл по адресу src/ThoughtList.vue. В Vue вот как мы повторяем элемент:

<script setup>
    import { ref } from 'vue'
    const items = ref(['hello', 'world!'])
</script>

<template>
    <ul>
        <li v-for="item in items">
            <p>{{ item }}</p>
        </li>
    </ul>
</template>
Войти в полноэкранный режим Выйти из полноэкранного режима

Удивительно просто! Возможно, будет полезно разделить этот компонент на два: ThoughtList.vue и ThoughtItem.vue.

Итак, изменим src/ThoughtList.vue на:

<script setup>
    import { ref } from 'vue'
    import ThoughtItem from './ThoughtItem.vue'

    const items = ref(['hello', 'world!'])
</script>

<template>
    <ul>
        <li v-for="item in items">
            <ThoughtItem />
        </li>
    </ul>
</template>
Войти в полноэкранный режим Выйти из полноэкранного режима

и новый ThoughtItem.vue будет содержать:

<template>
  <p>
    Hello world!
  </p>
</template>

Войти в полноэкранный режим Выйти из полноэкранного режима

И, конечно, не забудьте добавить его в App.vue:

<script setup>
import ThoughtList from "./components/ThoughtList.vue";
</script>

<template>
  <main>
    <ThoughtList />
  </main>
</template>

Вход в полноэкранный режим Выход из полноэкранного режима

Передача текста от родителя к ребенку

Мы уже видели, как передавать события от дочернего элемента к родительскому, а теперь пришло время перевернуть эту формулу.

В настоящее время наш ThoughtItem отображает один и тот же текст: «Hello world!».

Но мы хотим показать реальные данные, которые хранятся у родителя — ThoughtList — в реактивной переменной items.

Решение этой проблемы называется Props (свойства). Они похожи на атрибуты HTML, если бы они могли содержать что угодно! (массивы, объекты и даже функции!).

Итак, давайте внесем небольшое изменение в ThoughtList.vue:

<template>
    <ul>
        <li v-for="item in items">
            <ThoughtItem :message="item" />
        </li>
    </ul>
</template>
Войти в полноэкранный режим Выход из полноэкранного режима

Эй! Важно добавить двоеточие : перед именем реквизита. Это говорит Vue рассматривать содержимое между «…» как код JavaScript (переменная item), а не как строку (текст «item»).

И теперь мы можем легко получить доступ к текстовому сообщению в ThoughtItem:

<script setup>
defineProps({
  message: String, // `message` has type String.
});
</script>

<template>
  <p>
    {{ message }}
  </p>
</template>
Войти в полноэкранный режим Выйти из полноэкранного режима

Эй! Как и defineEmits, defineProps является макросом компилятора и не требует импорта.

Загрузка данных с сервера

До сих пор мы использовали только mock-данные. Так давайте изменим это! Мы будем получать данные из внешнего API.

Но сначала обратите внимание, что нам понадобится доступ к API из двух отдельных компонентов: ThoughtList для получения всех элементов, и NewModal для добавления новых элементов в список.

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

Именно здесь мы начнем работать с папкой stores/. Итак, давайте создадим файл src/stores/thoughts.js.

Магазин всегда должен быть похож на этот шаблон, поэтому используйте его в качестве отправной точки:

// thoughts.js
import { defineStore } from 'pinia'

export default defineStore('thoughts', {
    state: () => ({
        // Reactive variables here
    }),

    actions: {
        // Methods here
    }
})
Вход в полноэкранный режим Выход из полноэкранного режима

Мы создали магазин с идентификационным именем "thoughts".

Но что такое магазин, спросите вы? Это просто набор реактивных переменных и методов, которые действуют на них:

// thoughts.js
import { defineStore } from 'pinia'

export default defineStore('thoughts', {
    state() {
        return {
            reactive_var: 1,
            another_reactive_var: 'awesome!',
            again_another_reactive_var: [0, 2, 4]
        }
    },

    actions: {
        my_method() {
            // We can access all reactive variables here using `this.`
            // NOTE: we don't have to use `.value` here!
            this.reactive_var++
            return this.another_reactive_var * 4.5
        }
    }
})
Войти в полноэкранный режим Выход из полноэкранного режима

Обратите внимание, что здесь нам не нужно использовать .value. Потрясающе!

Теперь давайте создадим действительно полезный магазин:

// thoughts.js
import { defineStore } from "pinia";

export default defineStore("thoughts", {
  state() {
    return {
      items: [],
    };
  },
  actions: {
    async load() {
      // Fetch data from the Cyclic API
      const res = await fetch("https://ithink-api.cyclic.app/", {
        headers: {
          "Content-Type": "application/json",
        },
      });
      const items = await res.json();

      this.items = items;
    },
    async add(message) {
      // Post data to the Cyclic API
      await fetch("https://ithink-api.cyclic.app/", {
        method: "post",
        headers: {
          "Content-Type": "application/json",
        },
        body: JSON.stringify({
          text: message,
        }),
      });

      this.items.unshift(message);
    },
  },
});
Войти в полноэкранный режим Выход из полноэкранного режима

Все, что мы здесь сделали, это объявили реактивную переменную items, которая будет содержать наши самые важные данные. Затем мы определили метод load, который должен быть вызван для загрузки данных с сервера и присвоения их items.

Мы также определили действие add, которое асинхронно отправляет данные на сервер и добавляет их в список items.

Использование реальных данных в нашем приложении

Давайте соединим наш магазин thoughts с нашим приложением! Мы начнем с NewModal:

<script setup>
    import getThoughtsStore from '../stores/thoughts.js'

    const { add: addItem } = getThoughtsStore()

    async function submit() {
        await addItem(message.value)
        close()
    }
</script>
Вход в полноэкранный режим Выход из полноэкранного режима

Мы извлекли функцию add из магазина, вызвав ее как функцию. Здесь мы назвали ее getThoughtsStore, что является условностью при использовании Pinia.

Поверите ли вы мне, если я скажу, что это все? Ну, я никогда не вру.

Давайте перейдем к ThoughtList, который будет загружать данные с сервера, чтобы отобразить их.

<script setup>
import getThoughtsStore from "../stores/thoughts";
const { load: loadItems } = getThoughtsStore();

await loadItems();
</script>
Вход в полноэкранный режим Выход из полноэкранного режима

И мы также должны получить доступ к реактивной переменной items прямо из магазина. Так что здесь давайте действовать по той же схеме:

const { items } = getThoughtsStore(); // this is WRONG
Вход в полноэкранный режим Выход из полноэкранного режима

Но, как ни странно, это нарушает реактивность переменной. Вот правильный способ сделать это:

import { storeToRefs } from "pinia";
const { items } = storeToRefs(getThoughtsStore()); // CORRECT :)
Войти в полноэкранный режим Выйти из полноэкранного режима

Идеально!

Обратите внимание, что в нашем предыдущем примере кода мы используем await верхнего уровня для загрузки данных в ThoughtList:

<script setup>
import getThoughtsStore from "@/stores/thoughts";
const { load: loadItems } = getThoughtsStore();

await loadItems();
</script>
Вход в полноэкранный режим Выйти из полноэкранного режима

Компоненты с ожиданием на верхнем уровне называются асинхронными компонентами.

Если мы оставим все как есть, то получим следующую ошибку: (проверьте консоль devtools)

Component <Anonymous>: setup function returned a promise, but no <Suspense> boundary was found in the parent component tree. A component with async setup() must be nested in a <Suspense> in order to be rendered.
Войти в полноэкранный режим Выйти из полноэкранного режима

Это говорит нам, что мы должны использовать компонент под названием <Suspense> в качестве родительского для компонента async. [<Suspense>](https://vuejs.org/guide/built-ins/suspense.html) является встроенным компонентом Vue, поэтому мы можем использовать его в любом месте нашего приложения. Давайте используем его в App.vue:

<script setup>
import ThoughtList from "./components/ThoughtList.vue";
</script>

<template>
  <main>
    <Suspense>
      <ThoughtList />
    </Suspense>
  </main>
</template>
Вход в полноэкранный режим Выйти из полноэкранного режима

Теперь все работает отлично! Ура!

Добавление состояния загрузки в наш список

В настоящее время наш компонент ThoughtList будет невидим, пока не загрузятся данные. Это неудобно и является отличным примером плохого UX.

К счастью, поскольку мы уже используем <Suspense>, мы можем прямо сказать ему показать что-то другое, пока его дочерний компонент загружается. Добавьте это в App.vue:

<Suspense>
    <ThoughtList />

    <template #fallback>
        <p>Loading...</p>
    </template>
</Suspense>
Войти в полноэкранный режим Выйти из полноэкранного режима

Как вы можете видеть, все, что находится внутри <template #fallback>, будет показано во время загрузки ThoughtList. Потрясающе!

Но мы можем сделать еще круче. Давайте покажем загружающийся скелет!

<Suspense>
    <ThoughtList />

    <template #fallback>
        <div class="flex flex-wrap gap-2">
            <div v-for="i in 15" class="h-16 w-48 animate-pulse rounded bg-stone-50/10"></div>
        </div>
    </template>
</Suspense>
Вход в полноэкранный режим Выход из полноэкранного режима

Вы должны знать, что i in 15 — это ярлык, который Vue предлагает нам, чтобы зациклить диапазон [1, ..., 15]. Супер!

И с этим все готово, наше приложение завершено! Теперь это было не так уж и сложно, не так ли?

Заключение

Мы рассмотрели удобное количество функций Vue:

  • Suspense и async компоненты,
  • библиотека @vueuse,
  • реактивные переменные,
  • загрузка скелета,
  • пользовательские события,
  • крючки жизненного цикла,
  • ссылки на шаблоны,
  • v-for и v-if,
  • пинии.

Некоторые из них являются общими для всех веб-фреймворков, а некоторые — нет. Мы сравним Vue с остальными аналогами в конце этой серии, поэтому я советую вам продолжать читать! Поверьте, вам предстоит узнать еще много интересного! 😄

Оцените статью
devanswers.ru
Добавить комментарий