Создание приложения для поиска фильмов с помощью API Vue Composition
Самая первая альфа-версия Vue 3 уже выпущена! В версии 3 появилось множество интересных функций: Vue представляет свою новую систему реактивности с Composition API. Если вы не слышали об этом, я рекомендую прочитать этот RFC с описанием. Сначала я был немного скептически настроен на счет него, но, взглянув на API React Hooks, которое немного похоже, я решил попробовать его.
В этой статье мы будем создавать приложение для поиска фильмов с использованием Composition API. Мы не будем использовать объектно-ориентированные компоненты. В статье я объясню, как работает новое API и как мы можем структурировать приложение с помощью его.
Когда мы закончим, мы увидим нечто похожее на это:
Приложение будет искать фильмы с помощью Open Movie Database API и отображать результаты. Причиной создания этого приложения является то, что оно достаточно простое, чтобы не отвлекать внимание от изучения нового API, но достаточно сложное, чтобы показать, как работает API.
Если вас не интересуют объяснения, вы можете сразу перейти к исходному коду и финальному приложению.
Настройка проекта
Для этого урока мы будем использовать Vue CLI, с помощью которого быстро создадим необходимую среду.
npm install -g @vue/cli vue create movie-search-vue cd movie-search-vue npm run serve
Теперь наше приложение будет работает на http://localhost:8080 и выглядеть следующим образом:
Здесь вы можете увидеть структуру папок по умолчанию:
Если вы не хотите устанавливать все зависимости на локальном компьютере, вы также можете запустить проект в Codesandbox. У Codesandbox есть отличные стартовые проекты для самых важных фреймворков, включая Vue.
Подключение нового API
Сгенерированный исходный код использует Vue 2 со старым API. Чтобы использовать новое API с Vue 2, мы должны установить плагин композиции.
npm install @vue/composition-api
После установки мы должны добавить его в качестве плагина в main.js:
import Vue from 'vue'; import VueCompositionApi from '@vue/composition-api'; Vue.use(VueCompositionApi);
Плагин composition является аддитивным: что означает что вы все еще можете создавать и использовать компоненты по-старому и одновременно начать использовать Composition API для новых компонентов.
У нас будет четыре компонента:
- App.vue: Родительский компонент. Он будет обрабатывать вызовы API и общаться с другими компонентами.
- Header.vue: Основной компонент, который получает и отображает заголовок страницы
- Movie.vue: Он будет отображать каждый фильм. Объект фильма будет передаваться как свойство.
- Search.vue: Он содержит форму с элементом ввода и кнопкой поиска. Этот компонент будет предоставлять критерий поиска при отправке формы.
Создание компонентов
Давайте напишем наш первый компонент, Header:
<template> <header class="App-header"> <h2>{{ title }}</h2> </header> </template> <script> export default { name: 'Header', props: ['title'], setup() {} } </script>
В компоненте атрибуты props объявляется так же как и ранее. Вы перечисляете переменные, которые ожидаете от родительского компонента, в виде массива или объекта. Эти переменные будут доступны в шаблоне ({{ title }}) и в методе setup.
Новым здесь является метод setup. Он запускается после первоначального разрешения props. Метод setup может вернуть объект, и свойства этого объекта будут объединены с контекстом шаблона: это означает, что они будут доступны в шаблоне. Этот возвращенный объект также является местом для размещения обратных вызовов жизненного цикла. Мы увидим примеры этого в компоненте поиска.
Далее создадим компонент поиска Search:
<template> <form class="search"> <input type="text" :value="movieTitle" @keyup="handleChange" /> <input @click="handleSubmit" type="submit" value="SEARCH" /> </form> </template> <script> import { ref } from '@vue/composition-api'; export default { name: 'Search', props: ['search'], setup({ search }, { emit }) { const movieTitle = ref(search); return { movieTitle, handleSubmit(event) { event.preventDefault(); emit('search', movieTitle.value); }, handleChange(event) { movieTitle.value = event.target.value } } } }; </script>
Компонент Search отслеживает нажатия клавиш и сохраняет значение ввода в переменной movieTitle. Когда мы закончим и нажмем кнопку отправки, он отправляет текущий поисковый запрос в родительский компонент.
Метод setup имеет два параметра.
Первый аргумент — это переменная из props в виде именованного объекта. Вы можете использовать деструктуризацию объекта для доступа к его свойствам. Параметр является реактивным, что означает, что функция setup будет запущена снова при изменении входных свойств.
Второй аргумент — это объект контекста. Здесь перечисляются свойства, которые доступны для this в API 2.x: attrs, slots, parent, root, emit.
Следующим новым элементом здесь является функция ref. Функция ref часть системы реактивности во Vue. При вызове она создает реактивную изменяемую переменную, которая имеет одно значение value. Свойство value будет иметь значение аргумента, передаваемого в функцию ref. Это реактивная оболочка вокруг первоначального значения. Внутри шаблона нам не нужно ссылаться на свойство value, Vue развернет его для нас. Если мы передадим объект в ref, он будет полностью реактивным.
Реактивный означает, что когда мы изменяем значение объекта (в нашем случае свойство value), Vue будет знать, что значение изменилось, и ему нужно будет повторно отобразить подключенные шаблоны и повторно запустить отслеживаемые функции.
Он действует аналогично свойствам объекта, возвращаемым методом data.
data: function() { return { movieTitle: 'Joker' }; }
Объединяем все вместе
Следующим шагом является представление родительского компонента для компонента Header and Search, компонента App. Он будет прослушивать событие поиска, поступающее из компонента Search, и запускать API при изменении условия поиска и передавать найденные фильмы в список компонентов Movie.
Изменим компонент App следующим образом:
<template> <div class="App"> <Header :title="'Composition API'" /> <Search :search="state.search" @search="handleSearch" /> <p class="App-intro">Sharing a few of our favourite movies</p> <div class="movies"> <Movie v-for="movie in state.movies" :movie="movie" :key="movie.imdbID" /> </div> </div> </template> <script> import { reactive, watch } from '@vue/composition-api'; import Header from './components/Header.vue'; import Search from './components/Search.vue'; import Movie from './components/Movie.vue'; const API_KEY = 'a5549d08'; export default { name: 'app', components: { Header, Search, Movie }, setup() { const state = reactive({ search: 'Joker', loading: true, movies: [], errorMessage: null }); watch(() => { const MOVIE_API_URL = `https://www.omdbapi.com/?s=${state.search}&apikey=${API_KEY}`; fetch(MOVIE_API_URL) .then(response => response.json()) .then(jsonResponse => { state.movies = jsonResponse.Search; state.loading = false; }); }); return { state, handleSearch(searchTerm) { state.loading = true; state.search = searchTerm; } }; } } </script> <style> .app { text-align: center; } .header { background-color: #282c34; height: 70px; display: flex; flex-direction: column; align-items: center; justify-content: center; font-size: calc(10px + 2vmin); color: white; padding: 20px; cursor: pointer; } .spinner { height: 80px; margin: auto; } .intro { font-size: large; } /* new css for movie component */ * { box-sizing: border-box; } .movies { display: flex; flex-wrap: wrap; flex-direction: row; } .header h2 { margin: 0; } .add-movies { text-align: center; } .add-movies button { font-size: 16px; padding: 8px; margin: 0 10px 30px 10px; } .movie { padding: 5px 25px 10px 25px; max-width: 25%; } .errorMessage { margin: auto; font-weight: bold; color: rgb(161, 15, 15); } .search { display: flex; flex-direction: row; flex-wrap: wrap; justify-content: center; margin-top: 10px; } input[type="submit"] { padding: 5px; background-color: transparent; color: black; border: 1px solid black; width: 80px; margin-left: 5px; cursor: pointer; } input[type="submit"]:hover { background-color: #282c34; color: antiquewhite; } .search > input[type="text"]{ width: 40%; min-width: 170px; } @media screen and (min-width: 694px) and (max-width: 915px) { .movie { max-width: 33%; } } @media screen and (min-width: 652px) and (max-width: 693px) { .movie { max-width: 50%; } } @media screen and (max-width: 651px) { .movie { max-width: 100%; margin: auto; } } </style>
Здесь мы использовали два новых элемента: reactive и watch.
Функция reactive является эквивалентом Vue.observable() в Vue 2.
Она делает переданный объект реактивным: берет исходный объект и оборачивает его в прокси (реализация на основе ES2015 Proxy). К объектам, возвращаемых из reactive, мы можем напрямую обращаться к свойствам (не используя value, в отличие от возвращаемых объектов из функции ref). Если вы хотите найти аналогию в API Vue 2.x, метод data будет точно ей соответствовать.
Одним из недостатков объекта reactive является то, что мы не можем распространить его на возвращаемый объект из метода setup.
Функция watch ожидает функцию в качестве входного параметра. Она отслеживает все реактивные переменные внутри, так же как компонент делает это для template. Когда мы изменяем реактивную переменную, используемую внутри переданной функции, данная функция снова запускается. В нашем примере всякий раз, когда изменяется поисковая переменная state.search, будет запущен поисковый запрос.
Остался один компонент, отображающий каждую запись фильма Movie:
<template> <div class="movie"> <h2>{{ movie.Title }}</h2> <div> <img width="200" :alt="altText" :src="movie.Poster" /> </div> <p>{{ movie.Year }}</p> </div> </template> <script> import { computed } from '@vue/composition-api'; export default { name: "Movie", props: ['movie'], setup({ movie }) { const altText = computed(() => `The movie titled: ${movie.Title}`); return { altText }; } }; </script>
Компонент Movie получает переменную movie для отображения и выводит его имя вместе с изображением. Особенная часть состоит в том, что для поля alt изображения мы используем вычисленный текст, основанный на его заголовке.
Вычисляемая функция computed получает функцию getter и упаковывает возвращаемую переменную в реактивную. Возвращенная переменная имеет тот же интерфейс, что и возвращаемая из функции ref. Разница в том, что она только для чтения. Так же функция getter будет запущена снова, когда одна из реактивных переменных внутри функции getter изменится. Если функция computed вернет необработанное примитивное значение (не реактивную переменную), шаблон не сможет отслеживать изменения зависимостей.
Рефакторинг компонентов
На данный момент у нас слишком много бизнес-логики внутри компонента App. Он делает две вещи: обрабатывает вызовы API и его свои дочерние компоненты. Цель состоит в том, чтобы один компонент должен иметь одну ответственность: компонент приложения должен управлять только компонентами и не должен беспокоиться о вызовах API. Для этого нам нужно убрать вызов API поиска. Создадим новый файл ./hooks/movie-api.js, со следующим содержимым:
import { reactive, watch } from '@vue/composition-api'; const API_KEY = 'a5549d08'; export const useMovieApi = () => { const state = reactive({ search: 'Joker', loading: true, movies: [] }); watch(() => { const MOVIE_API_URL = `https://www.omdbapi.com/?s=${state.search}&apikey=${API_KEY}`; fetch(MOVIE_API_URL) .then(response => response.json()) .then(jsonResponse => { state.movies = jsonResponse.Search; state.loading = false; }); }); return state; };
И подключим его в App. Теперь компонент App будет только обрабатывать действия, связанных с представлением:
import Header from './components/Header.vue'; import Search from './components/Search.vue'; import Movie from './components/Movie.vue'; import { useMovieApi } from './hooks/movie-api'; export default { name: 'app', components: { Header, Search, Movie }, setup() { const state = useMovieApi(); return { state, handleSearch(searchTerm) { state.loading = true; state.search = searchTerm; } }; } }
Вот и все; мы закончили реализацию небольшого приложения с новым Composition API.
Заключение
Давайте подведем итоги тому, что мы рассмотрели в этой статье.
Мы можем использовать новое Composition API с текущей стабильной версией Vue 2. Для этого мы должны использовать плагин @vue/composition-api. API является расширяемым: мы можем создавать новые компоненты с новым API вместе со старыми, и существующие будут продолжать работать как прежде.
Vue 3 представит много новых функций:
- setup: находится в компоненте и будет управлять логикой компонента, запускается после первоначального разрешения props, получает props и контекст в качестве аргумента
- ref: возвращает реактивную переменную, запускает повторную визуализацию шаблона при изменении, мы можем манипулировать его значением через свойство value.
- reactive: возвращает реактивный объект (на основе прокси), запускает повторную визуализацию шаблона при изменении реактивной переменной, мы можем изменять его значение без свойства value
- computed: возвращает реактивную переменную на основе аргумента функции-получателя, отслеживает изменения реактивной переменной и переоценивает изменения
- watch: отслеживает изменения реактивных переменных и повторно запускается при изменении реактивных переменных
Я надеюсь, что этот пример познакомил вас с новым API и снял ваш скептицизм относительного его.
Оригинальная статья: Gábor Soós — Build a movie search app using the Vue Composition API