Приложение базы данных Pokémon с Vue 3 Composition API и синтаксисом

Когда я впервые обновился с Vue 2 до Vue 3, одним из первых руководств, с которыми я ознакомился, было приложение базы данных покемонов (созданное Эриком Ханчеттом), в котором используется RESTful Pokémon API. для извлечения покемонов из библиотеки данных. На мой взгляд, это одно из лучших руководств для запуска проектов Vue 3 после изучения основ Composition API.

С тех пор, как я освоил все тонкости Vue 3, мне было любопытно, как поведет себя код этого приложения, если его рефакторинг будет выполнен с использованием Comp API с синтаксисом ‹script setup›. Поэтому основное внимание в этой статье будет уделено тому, как создать базу данных покемонов, подобную базе данных Эрика, но вместо этого использовать синтаксис ‹script setup› и (возможно) чуть меньше кода.

‹настройка сценария› Синтаксис… Если вы еще не знаете

Прежде чем приступить к проекту, давайте немного узнаем о синтаксисе ‹script setup›. Если вы использовали Vue 3 в последнее время, то вы, вероятно, знаете, что Composition API использует функцию setup() для инкапсуляции большей части функциональности данного компонента. Используя пример из оригинального кода базы данных покемонов Эрика, функциональность для возврата данных для выбранного покемона будет выглядеть примерно так:

<script>
import { reactive, toRefs } from "vue";
import { useRoute } from "vue-router";
export default {
  setup() {
    const route = useRoute();
    const pokemon = reactive({
      pokemon: null 
    });
    fetch(`https://pokeapi.co/api/v2/pokemon/${route.params.slug}/`)
      .then((res) => res.json())
      .then((data) => {
        pokemon.pokemon = data;
      });
    return { ...toRefs(pokemon) };
  }
};
</script>

В какой-то момент после выпуска Vue 3 команда Vue представила синтаксис ‹script setup›, позволяющий разработчикам еще больше упростить свои блоки кода. Используя синтаксис ‹script setup›, приведенный выше код можно сократить до следующего:

<script setup>
import { ref } from "vue";
import { useRoute } from "vue-router";

const route = useRoute();
const pokemon = ref(null)
    
fetch(`https://pokeapi.co/api/v2/pokemon/${route.params.slug}/`)
.then((res) => res.json())
.then((data) => {
  pokemon.value = data;
});
</script>

Как видите, использование ‹script setup› позволяет реализовать функциональность без явного использования функции setup() и оболочки export default. Кроме того, обратите внимание, что больше нет необходимости обрабатывать возврат свойства, так как эта функциональность обрабатывается за кулисами.

В конечном счете ‹настройка сценария› — это синтаксический сахар для использования Composition API внутри однофайловых компонентов (SFC). Преимущества этого синтаксиса включают в себя:

  • Более лаконичный код с меньшим количеством шаблонов (как видно из приведенного выше примера).
  • Возможность объявлять реквизиты и генерируемые события с использованием чистого TypeScript (не применимо к этой статье, но все же полезно знать!)
  • Лучшая производительность во время выполнения (шаблон компилируется в функцию рендеринга в той же области, без промежуточного прокси)
  • Улучшенная производительность вывода типов в среде IDE (у языкового сервера меньше работы по извлечению типов из кода)

Последнее замечание: согласно документам синтаксис ‹script setup› теперь является рекомендуемым, если вы используете как SFC, так и Composition API. Запомни!

Понятно? Хороший. Время начать!

Установка Tailwind CSS

Давайте запустим новую сборку Vue 3 Tailwind CSS + Vite. Запустите в терминале следующее:

npm init vite my-tailwind-project
cd my-tailwind-project

Затем установите и инициализируйте Tailwind CSS в новом проекте:

npm install -D tailwindcss
npx tailwindcss init -p

Добавьте следующее в tailwind.config.js:

module.exports = {
  content: [
    "./index.html",
    "./src/**/*.{vue,js,ts,jsx,tsx}",
  ],
  theme: {
    extend: {},
  },
  plugins: [],
}

Создайте новый файл index.css в ./src и добавьте следующее:

@tailwind base;
@tailwind components;
@tailwind utilities;

Обновите main.js, чтобы импортировать index.css:

import { createApp } from 'vue'
import App from './App.vue'
import './index.css'

createApp(App).mount('#app')

Установка маршрутизатора Vue

Установите пакет Vue Router с помощью npm install vue-router@4.

После установки роутера создайте новую папку router в src/. Внутри папки создайте новый файл маршрутизатора с именем index.js.

Добавьте в файл маршрутизатора index.js следующее:

import { createRouter, createWebHistory } from "vue-router";
import Home from "../views/Home.vue";

const routes = [
  {
    path: "/",
    name: "Home",
    component: Home
  },
  {
    path: "/about/:slug",
    name: "About",
    component: () =>
      import(/* webpackChunkName: "about" */ "../views/About.vue")
  }
];

const router = createRouter({
    history: createWebHistory(),
    routes,
  })

export default router;

Обновите main.js, включив в него файл маршрутизатора.

import { createApp } from 'vue'
import App from './App.vue'
import router from './router'
import './index.css'

createApp(App).use(router).mount('#app')

Создание приложения

После настройки проекта откройте App.vue и замените существующий код следующим:

<template>
  <div class="p-14 ">
    <span class="flex justify-center text-4xl text-yellow-700">
     Pokémon Picker&nbsp;
      <img src="src/assets/800px-Poké_Ball_icon.png" class="w-10 h-10"/>
    </span>
    <router-view />
  </div>
</template>

Добавьте папку views в src в проекте. Добавьте два файла в представления с именами Главная и О программе. Представление Главная будет представлением, в котором отображается поле ввода для поиска покемонов. В представлении О программе будут отображаться данные о покемонах, выбранных на Главной странице.

Home.vue

В компоненте Главная добавьте следующий шаблон:

<template>
  <div class="flex min-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
    <div class="w-full max-w-md space-y-8">
      <form class="mt-8 space-y-6" action="#" method="POST">
        <input type="hidden" name="remember" value="true" />
        <div class="-space-y-px rounded-md shadow-sm">
          <div>
             <input id="pokemon-name" v-model="text" class="relative block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm" placeholder="Enter Pokemon Here..." />
          </div>
        </div>

        <div class="items-center">
          <div
          class="flex flex-wrap ml-4 text-2xl text-blue-400"
          v-for="(pokemon, idx) in filteredPokemon"
          :key="idx"
          >
            <router-link :to="`/about/${getPokemonId(pokemon.name)}`">
                {{ pokemon.name }}
            </router-link>
          </div>
        </div>
      </form>
    </div>
  </div>
</template>

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

Ниже шаблона давайте создадим функциональность. Во-первых, мы воспользуемся встроенной функцией fetch(), чтобы получить ВСЕ 900 (или около того) доступных покемонов и вернуть их в журнал вашего браузера. Возможно, вам придется закомментировать ссылки на filteredPokemon и getPokemonвременно в шаблоне, чтобы избежать ошибок.

(TBH, я не знал, что существует более 900 покемонов, пока не начал этот проект. Я перестал смотреть покемонов после 2001 года, поэтому я все еще застрял на первых 150 🤷‍♂️)

<script setup>
import { computed, ref, onMounted } from "vue";
import { useRouter } from 'vue-router'

const router = useRouter()
const pokemons = ref([])
const text = ref("")

fetch('https://pokeapi.co/api/v2/pokemon?limit=100000&offset=0')
.then((res) => res.json())
.then((data) => {
    pokemons.value = data.results
    console.log(pokemons.value)
})
</script>

Похоже, функция fetch() сработала. Теперь мы можем видеть всех покемонов, доступных в базе данных!

Теперь давайте обновим приведенный выше код, чтобы включить функцию фильтрации:

<script setup>
import { computed, ref, onMounted } from "vue";
import { useRouter } from 'vue-router'

const router = useRouter()
const pokemons = ref([])
const text = ref("")

fetch('https://pokeapi.co/api/v2/pokemon?limit=100000&offset=0')
.then((res) => res.json())
.then((data) => {
    pokemons.value = data.results
})

const getPokemonId = (item) => {
    return pokemons.value.findIndex((p) => p.name === item) + 1;
};

const filteredPokemon = computed(() => {
    if(!text.value){
        return [] 
    }
    return pokemons.value.filter((pokemon)=> 
        pokemon.name.includes(text.value)
    )
})
</script>

Для функции фильтрации мы будем использовать вычисляемое свойство с именем filteredPokemon, чтобы вернуть отфильтрованный список покемонов на основе ввода пользователя. Затем мы воспользуемся функцией getPokemonId, чтобы вернуть имя выбранного покемона на основе индекса выбранного покемона в массиве pokemons.value + 1. >getPokemonId вызывается выбранным маршрутизатором-линком после передачи имени выбранного покемона в качестве аргумента.

Окончательный код для Home.vue должен выглядеть следующим образом:

<template>
  <div class="flex min-h-full items-center justify-center py-12 px-4 sm:px-6 lg:px-8">
    <div class="w-full max-w-md space-y-8">
      <form class="mt-8 space-y-6" action="#" method="POST">
        <input type="hidden" name="remember" value="true" />
        <div class="-space-y-px rounded-md shadow-sm">
          <div>
            <input id="pokemon-name" v-model="text" class="relative block w-full appearance-none rounded-md border border-gray-300 px-3 py-2 text-gray-900 placeholder-gray-500 focus:z-10 focus:border-indigo-500 focus:outline-none focus:ring-indigo-500 sm:text-sm" placeholder="Enter Pokemon Here..." />
          </div>
        </div>

        <div class="items-center">
          <div
          class="flex flex-wrap ml-4 text-2xl text-blue-400"
          v-for="(pokemon, idx) in filteredPokemon"
          :key="idx"
          >
            <router-link :to="`/about/${getPokemonId(pokemon.name)}`">
                {{ pokemon.name }}
            </router-link>
          </div>
        </div>
      </form>
    </div>
  </div>
</template>

<script setup>
import { computed, ref, onMounted } from "vue";
import { useRouter } from 'vue-router'

const router = useRouter()
const pokemons = ref([])
const text = ref("")

fetch('https://pokeapi.co/api/v2/pokemon?limit=100000&offset=0')
.then((res) => res.json())
.then((data) => {
    pokemons.value = data.results
})

const getPokemonId = (item) => {
    return pokemons.value.findIndex((p) => p.name === item) + 1;
};

const filteredPokemon = computed(() => {
    if(!text.value){
        return [] 
    }
    return pokemons.value.filter((pokemon)=> 
        pokemon.name.includes(text.value)
    )
})
</script>

О .vue

Добавьте следующий код шаблона в About.vue, чтобы отобразить профиль выбранного пользователем покемона:

<template>
    <div class="about">
      <div
      v-if="pokemon"
      className="w-3/12 m-auto bg-purple-100 mt-4 shadow-2xl flex justify-center flex-col items-center"
      >
        <h3 className="text-2xl text-green-900 uppercase">{{ pokemon.name }}</h3>
        <div class="flex justify-center">
            <img className="w-48" :src="pokemon.sprites.front_shiny" alt="" />
            <img className="w-48" :src="pokemon.sprites.back_shiny" alt="" />
        </div>
        <h3 class="text-yellow-400">Types</h3>
        <div v-for="(pkmn, idx) in pokemon.types" :key="idx">
            <h5 class="text-blue-900">{{pkmn.type.name}}</h5>
        </div>
      </div>
        
      <div class="p-14 flex justify-center">
          <router-link to="/"
          class="text-white 
          bg-purple-700 
          hover:bg-purple-800 
          focus:ring-4 
          focus:ring-blue-300 
          font-medium 
          rounded-lg 
          text-sm 
          px-5 
          py-2.5 
          text-center 
          mr-2 
          mb-2 
          dark:bg-purple-400 
          dark:hover:bg-purple-700 
          dark:focus:ring-purple-800">
          Return to Picker
          </router-link>
      </div>
  </div>
</template>

Под шаблоном добавьте следующие функции:

<script setup>
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";

const route = useRoute();
const pokemon = ref(null)
const isFound = ref(true)

fetch(`https://pokeapi.co/api/v2/pokemon/${route.params.slug}/`)
.then((res) => res.json())
.then((data) => {
    pokemon.value = data;
});
</script>

Функция fetch() в приведенном выше коде настроена на получение данных для выбранного покемона на основе имени покемона, переданного маршруту в виде слага в Home.vue. Данные, полученные для выбранного покемона, затем можно заполнить в шаблоне с помощью свойства pokemon.value.

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

Нажатие на эти результаты вернет ошибки.

Я все еще относительно новичок в этом API и поэтому не знаю, почему такие записи, как «пикачу-рок-звезда», включены в базу данных. Тем временем я рекомендую реализовать обработку ошибок в функции fetch(), чтобы управлять выводом при обнаружении ошибок. Для этого мы можем просто обернуть функцию fetch() внутри блока try/catch. Если обнаружена ошибка, мы переключим свойство isFound.value на false, чтобы отобразить сообщение «Pokémon Not Found» в шаблон. Затем мы поместим try/catch в новую функцию с именем fetchPkmn(), установив хук onMounted() для вызова fetchPkmn() при загрузке страницы.

<script setup>
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";

const route = useRoute();
const pokemon = ref(null)
const isFound = ref(true)

const fetchPkmn = async () => {
    try {
    await fetch(`https://pokeapi.co/api/v2/pokemon/${route.params.slug}/`)
    .then((res) => res.json())
    .then((data) => {
        pokemon.value = data;
    });
    } catch (err) {
        isFound.value = false
        console.log(err)
    }
}

onMounted(() => {
    fetchPkmn()
})
</script>

Полный компонент About.vue (с сообщением Pokemon Not Found) должен выглядеть следующим образом:

<template>
  <div class="about">
    <div v-if="(isFound === false)" class="mx-auto mt-12 flex justify-center">
        <span>Pokemon Not Found</span>
    </div>
    
    <div v-else>
      <div
      v-if="pokemon"
      className="w-3/12 m-auto bg-purple-100 mt-4 shadow-2xl flex justify-center flex-col items-center"
      >
        <h3 className="text-2xl text-green-900 uppercase">{{ pokemon.name }}</h3>
        <div class="flex justify-center">
            <img className="w-48" :src="pokemon.sprites.front_shiny" alt="" />
            <img className="w-48" :src="pokemon.sprites.back_shiny" alt="" />
        </div>
        <h3 class="text-yellow-400">Types</h3>
        <div v-for="(pkmn, idx) in pokemon.types" :key="idx">
            <h5 class="text-blue-900">{{pkmn.type.name}}</h5>
        </div>
      </div>
    </div>

    <div class="p-14 flex justify-center">
        <router-link to="/"
        class="text-white 
        bg-purple-700 
        hover:bg-purple-800 
        focus:ring-4 
        focus:ring-blue-300 
        font-medium 
        rounded-lg 
        text-sm 
        px-5 
        py-2.5 
        text-center 
        mr-2 
        mb-2 
        dark:bg-purple-400 
        dark:hover:bg-purple-700 
        dark:focus:ring-purple-800">
        Return to Picker
        </router-link>
    </div>
  </div>
</template>
  
<script setup>
import { onMounted, ref } from "vue";
import { useRoute } from "vue-router";

const route = useRoute();
const pokemon = ref(null)
const isFound = ref(true)

const fetchPkmn = async () => {
    try {
    await fetch(`https://pokeapi.co/api/v2/pokemon/${route.params.slug}/`)
    .then((res) => res.json())
    .then((data) => {
        pokemon.value = data;
    });
    } catch (err) {
        isFound.value = false
        console.log(err)
    }
}

onMounted(() => {
    fetchPkmn()
})
</script>

После повторного запуска приложения найдите Пикачу и нажмите «пикачу-рок-звезда». Посмотрите, сработала ли обработка ошибок.

И вот оно! Теперь вы узнали, как создать приложение базы данных Pokémon с помощью Vue, Composition API и синтаксиса ‹script setup›! Поздравляем!

Нажмите здесь, чтобы увидеть репозиторий Github для этого проекта:

https://github.com/jsfanatik/pokemon-vue-script-setup

Оставайтесь с нами, чтобы узнать больше об уроках на горизонте!

«Гусеница может превратиться в безмасляную, но сердце, которое бьется внутри, останется прежним», — Брок, руководитель спортзала Pewter City.