Простое фото приложение на Vue.js, Axios и Flickr API — Часть 1

Spread the love

Добро пожаловать в первую часть серии учебных статей для новичков в Vue.js. В этой серии мы создадим простое приложение для работы с фотографиями, используя Vue.js, Axios и Flickr API. Целью этой серии является отработка основных концепций Vue и ознакомление с работой с API. Я предполагаю что у вас есть базовые знания HTML, CSS и Javascript, а также некоторое знакомство с Vue.

Шаг 1: Создание проекта

Предполагая, что у вас уже установлена последняя версия Vue CLI, создайте новый проект, перейдя в каталог в котором будет размещен проект и введите следующую команду:

vue create vue-flickr

Далее выберите следующие функции:


В итоге должно получиться что то типа такого:

Далее перейдите в каталог vue-flickr и запустите команду npm run serve для запуска проекта. Вы должны увидеть что то типа такого:

$ cd vue-flickr
$ npm run serve

Шаг 2: Установка дополнительных пакетов

Чтобы отправлять запросы в Flickr, нам понадобиться популярная библиотека Axios. С Axios мы сможем:

  • Делать XMLHttpRequests из браузера
  • Делать http запросы из node.js
  • В ней есть поддержка Promise API
  • Есть возможность перехватывать запросы и ответы на них
  • Трансформировать данные запросов и ответов
  • Отменять запросы
  • Автоматически преобразовывать данные в JSON
  • Есть встроенная защита от XSRF

Не беспокойтесь, если вы не знаете, что все это значит. Просто знайте, что axios поможет нам общаться с Flickr. Установим ее, запустив в своем терминале следующую команду:

npm install axios --save

Если вы увидите что то типа такого то это будет значить что установка прошла успешно:

+ axios@0.18.0
added 1 package from 1 contributor and audited 24563 packages in 17.862s
found 0 vulnerabilities

Шаг 3: API

Для работы с API потребуется нечто, называемое ключом, уникальный идентификатор, который позволяет Flickr знать, что мы являемся законным источником запроса. Чтобы получить этот ключ, вы должны иметь учетную запись Flickr. Для его получения сделайте следующее:

  1. Залогинтесь/Зарегистрируйтесь в Flickr
  2. Посетите Flickr API Services и выберите “Create an App”

4. Далее кликните “Request an API Key”.

5. Далее кликните “Apply for a non-commercial key”.

6. Введите имя и описание вашего приложения. Затем нажмите на “Submit”.

7. Затем вы попадете на страницу, где сможете скопировать предоставленный ключ.

8. В корневом каталоге проекта (на том же уровне, что и README.md) создайте файл с именем config.js и добавьте в него следующее содержимое:

export default {
api_key: 'YOUR_KEY_HERE'
}

Шаг 4: Делаем наш первый вызов API

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

<template>
  <div class="home">

  </div>
</template>

<script>
export default {
  name: 'home',
};
</script>

Первый делом мы создадим способ ввода и отправки поискового запроса. Для этого добавим в шаблон форму, содержащую один элемент ввода текста input и кнопку отправки submit. Для ввода текста используем атрибут v-model и подключим к нему переменную tag. Кнопка отправки будет запускать метод search, НО она не будет перезагружать страницу (мы воспользуемся модификатором prevent события click).

Для этого обязательно добавьте в функцию data() свойство tag и в раздел methods функцию search. На данный момент функция search может просто вывести this.tag в консоль:

<template>
  <div class="home">
    <form>
      <label>
        Search:
        <input v-model="tag" type="text">
      </label>
      <button type="submit" class="go-button" @click.prevent="search">Search</button>
    </form>
  </div>
</template>

<script>
export default {
  name: 'home',
  data() {
    return {
      tag: ''
    }
  },
  methods: {
    search() {
      console.log("Searching for: ", this.tag)
    }
  }
};
</script>

Далее нам нужно сделать следующее. После того, как пользователь отправит свой поисковый запрос, на странице должны появиться фотографии, найденные согласно поисковому запросу. Во время запроса данных, шаблон должен отображать сообщение loading … , а затем когда запрос будет завершен отобразить неупорядоченный список фотографий (используя v-for).

Исходя из вышеизложенного, наш шаблон теперь будет использовать два новых свойства данных: loading и images, поэтому обязательно добавим их в data().

Далее, наш метод search должен сделать пару вещей. Когда он вызывается впервые, он должен изменить loading на true, поскольку процесс извлечения данных уже начался. Затем он должен сделать запрос к API Flickr с помощью axios, и когда будет получен ответ, заполните images массивом изображений и затем установить loading в значение false.

Давайте еще немного рассмотрим вызова API через axios. Для ясности я выделил фактический вызов API в отдельный метод fetchImages(). В начале, чтобы использовать axios, я импортировал его в верхнюю часть раздела script вместе с config, где хранится наш ключ api_key (потому что он нам тоже понадобится). Затем axios нужен объект, который указывает, какой тип запроса мы делаем, где мы делаем запрос (URL), и набор параметров (params), которые дополнительно определяют, какую информацию мы хотим получить от URL. Объект params имеет следующее конфигурацию:

  • method —  для поиска мы используем метод flickr.photos.search
  • api_key — мы передаем наш api_key что Flickr мог доверять нам
  • tags —условия поиска, хранящимся в переменной tag
  • extras —  любая дополнительная информация, которую мы хотим получить в фотографиях. Мы хотим URL фотографии, поэтому нам нужна строка url_n, owner_name содержит автора фото, datetaken содержит дату фотографии и views содержит количество, сколько людей просмотрели фото.

Вы можете найти полный список параметров которые доступны для фотографии здесь, и вы можете поиграть с методом flickr.photos.search здесь.

Остальные параметры params указывают, сколько фотографий мы получаем и в каком формате представлены данные. Мы выбрали Json так как он позволит нам легко перемещаться по структуре данных в Javascript.

Метод fetchImages() обрабатывает фактический поиск фотографий, но сам по себе не отображает изображения. Это происходит при последующем обратном вызове, когда будут получены данные. Мы узнаем от том что, данные готовы, когда вернется Promise. Promise – это то, что позволяет получать данные асинхронно. Другими словами, мы можем попросить axios поработать над получением данных, а тем временем позволить Vue заняться другими делами. Короче говоря, Promise помогают нашему приложению работать быстрее и выполнять несколько задач одновременно.

Когда Promise вернет наши данными, мы сообщаем Vue, в методе then() что с ним делать. В этот метод будет передан response, который содержит объект data. Внутри data мы найдем объект photos, который содержит массив фотографий с именем photo. Как только мы это сделаем, мы можем пометить loading как false и сбросить свойство tag.

.then((response) => {
  this.images = response.data.photos.photo;
  this.loading = false;
  this.tag = "";
})
<template>
  <div class="home">
    <form>
      <label>
        Search:
        <input v-model="tag" type="text">
      </label>
      <button type="submit" class="go-button" @click.prevent="search">Search</button>
    </form>
    <p v-if="loading">
      Loading...
    </p>
    <ul v-else>
      <li v-for="image in images" :key="image.id">{{image}}</li>
    </ul>
  </div>
</template>

<script>
import config from '../../config';
import axios from 'axios';
export default {
  name: 'home',
  data() {
    return {
      loading: false,
      tag: '',
      images: []
    }
  },
  methods: {
    search() {
      this.loading = true;
      this.fetchImages()
        .then((response) => {
          this.images = response.data.photos.photo;
          this.loading = false;
        })
    },
    fetchImages() {
      return axios({
        method: 'get',
        url: 'https://api.flickr.com/services/rest',
        params: {
          method: 'flickr.photos.search',
          api_key: config.api_key,
          tags: this.tag,
          extras: 'url_n, owner_name, date_taken, views',
          page: 1,
          format: 'json',
          nojsoncallback: 1,
          per_page: 30,
        }
      })
    },
  }
};
</script>

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

Шаг 5: Делаем компонент ImageCard

Ура! У нас есть данные. Теперь давайте сделаем их удобочитаемыми. Внутри неупорядоченного списка Home.vue добавьте:

...
<li v-for="image in images" :key="image.id">
  <img :src="image.url_n" :alt="image.title">
  <div>
    <p v-if="image.title">{{image.title}}</p>
    <p v-else>No Title Found</p>
    <p>By {{image.ownername}}</p>
    <section>
      <p>{{image.datetaken}}</p>
      <p>Views: {{image.views}}</p>
    </section>
  </div>
</li>
...

Теперь стало лучше, но страница все еще нуждается в некотором оформлении. Чтобы упростить Home.vue необходимо перетащить указанную выше разметку в собственный компонент. Внутри каталога компонентов удалите HelloWorld.vue и создайте файл с именем ImageCard.vue со следующим кодом:

<template>
  <li>
    <img :src="image.url_n" :alt="image.title">
    <div>
      <p v-if="image.title">{{image.title}}</p>
      <p v-else>No Title Found</p>
      <p>By {{image.ownername}}</p>
      <section>
        <p>{{image.datetaken}}</p>
        <p>Views: {{image.views}}</p>
      </section>
    </div>
  </li>
</template>

<script>
export default {
  name: 'ImageCard',
  props: [ 'image' ]
}
</script>

Вышеуказанный компонент имеет ту же разметку что и в Home.vue и получает переменную image. Чтобы использовать ImageCard, импортируйте его в Home.vue и зарегистрируйте:

...
import ImageCard from '@/components/ImageCard';

export default {
  name: 'home',
  components: {
    ImageCard
  },
  ...
}

Затем добавьте его в шаблон вместо предыдущей разметки:

...
<ul v-else>
  <image-card
    v-for="image in images"
    :key="image.id"
    :image="image" />
</ul>
....

Результат в браузере должен быть таким же, как и прежде.

Теперь добавим стили в Home.vue и ImageCard.vue, чтобы сделать все немного по красивее.

Во время настройки проекта мы не устанавили препроцессор, поэтому для использования SCSS выполним следующую команду:

npm install -D sass-loader node-sass

Далее обновим ImageCard.vue:

<template>
  <li class="image-card">
    <img class="image-card__image" :src="image.url_n" :alt="image.title">
    <div class="image-card__body">
      <p v-if="image.title" class="image-title">{{image.title}}</p>
      <p v-else class="image-title">No Title Found</p>
      <p class="image-owner">By {{image.ownername}}</p>
      <section class="image-date-view-wrapper">
        <p class="image-date">{{image.datetaken}}</p>
        <p class="image-views">Views: {{image.views}}</p>
      </section>
    </div>
  </li>
</template>

<script>
export default {
  name: 'ImageCard',
  props: [ 'image' ]
}
</script>

<style lang="scss">
.image-card {
  width: calc(33% - 1rem);
  margin: .5rem;
  border-radius: 5px;
  box-shadow: 0 5px 15px rgba(0, 0, 0, .15);
  background: white;
  @media only screen and (max-width: 799px) {
    width: calc(50% - 1rem);
  }
  @media only screen and (max-width: 549px) {
    width: 100%;
    margin: .5rem 0;
  }
}
.image-card__image {
  border-radius: 5px 5px 0 0;
  width: 100%;
  height: 200px;
  object-fit: cover;
}
.image-card__body {
  padding: .5rem 1rem 1rem;
}
.image-title {
  font-weight: bold;
  margin: 0;
}
.image-owner {
  margin-top: 0;
  font-size: .8rem;
}
.image-title,
.image-owner {
  white-space: nowrap;
  text-overflow: ellipsis;
  overflow: hidden;
}
.image-date-view-wrapper {
  display: flex;
  align-items: center;
  justify-content: space-between;
}
.image-date,
.image-views {
  margin-bottom: 0;
  font-size: .8rem;
}
</style>

Home.vue также нуждается в небольших изменениях. Сделаем так, чтобы панель навигации была полноразмерной, но оставьте список фотографий по-прежнему с некоторыми полями. Я также сделал несколько других структурных модификаций шаблона и, конечно, добавил новые стили классов:

<template>
  <div>
    <nav class="navbar">
      <form class="searchbar">
        <label>
          <span class='screen-reader-only'>Search:</span>
          <input 
            v-model="tag" 
            placeholder="Search for photos"
            type="text" 
            class="searchbar-input">
        </label>
        <button 
          type="submit" 
          class="btn btn--green btn--go" 
          @click.prevent="search">
            Go
        </button>
      </form>
    </nav>
   <div class="wrapper">
      <p v-if="loading" class="text-centered">
        Loading...
      </p>
      <ul v-else class="image-card-grid">
        <image-card
          v-for="image in images"
          :key="image.id"
          :image="image" />
      </ul>
   </div>
  </div>
</template>

<script>
import config from '../../config';
import axios from 'axios';
import ImageCard from '@/components/ImageCard';
export default {
  name: 'home',
  components: {
    ImageCard
  },
  data() {
    return {
      loading: false,
      tag: '',
      images: []
    }
  },
  methods: {
    search() {
      this.loading = true;
      this.fetchImages()
        .then((response) => {
          this.images = response.data.photos.photo;
          this.loading = false;
        })
        .catch((error) => {
          console.log("An error ocurred: ", error);
        })
    },
    fetchImages() {
      return axios({
        method: 'get',
        url: 'https://api.flickr.com/services/rest',
        params: {
          method: 'flickr.photos.search',
          api_key: config.api_key,
          tags: this.tag,
          extras: 'url_n, owner_name, date_taken, views',
          page: 1,
          format: 'json',
          nojsoncallback: 1,
          per_page: 30,
        }
      })
    },
  }
};
</script>

<style lang="scss">
.screen-reader-only {
  height: 1px;
  width: 1px;
  position: absolute;
  left: -100000px;
}
.text-centered {
  text-align: center;
}
.wrapper {
  margin: 0 auto;
  max-width: 800px;
  @media only screen and (max-width: 799px) {
    max-width: 100%;
    margin: 0 1.5rem;
  }
}
.image-card-grid {
  list-style: none;
  margin: .5rem 0;
  padding: 0;
  display: flex;
  align-items: flex-start;
  flex-wrap: wrap;
}
.navbar {
  display: flex;
  align-items: center;
  justify-content: center;
  padding: 1rem;
  background: #F0F0F0;
}
.searchbar {
  width: 300px;
  display: flex;
  align-items: center;
  justify-content: center;
  @media only screen and (max-width: 549px) {
    width: 100%;
    label {
      width: 80%;
    }
  }
}
.searchbar-input {
  padding: .5rem 1rem;
  border-radius: 20px;
  font-size: 1rem;
  border: 1px solid gray;
  min-width: 300px;
  @media only screen and (max-width: 549px) {
    min-width: 0;
    width: 100%;
  }
}
.btn {
  padding: .5rem 1rem;
  font-size: 1rem;
  border-radius: 20px;
  background: transparent;
  border: none;
}
.btn--green {
  background: #42b983;
  color: white;
  font-weight: bold;
}
.btn--go {
  padding: .5rem 2rem;
  margin-left: 1rem;
}
</style>

В заключительной части я внес несколько CSS-изменений в App.vue:

<template>
  <div id="app">
    <router-view/>
  </div>
</template>

<style>
* {
  box-sizing: border-box;
}
body {
  margin: 0;
  padding: 0;
}
#app {
  font-family: 'Avenir', Helvetica, Arial, sans-serif;
  -webkit-font-smoothing: antialiased;
  -moz-osx-font-smoothing: grayscale;
  color: #2c3e50;
}
#nav {
  padding: 30px;
}
#nav a {
  font-weight: bold;
  color: #2c3e50;
}
#nav a.router-link-exact-active {
  color: #42b983;
}
</style>

Должно получиться что то типа такого:

Шаг 6: Форматирование дат с помощью Moment.js

Наши изображения выглядят великолепно, но даты немного некрасивые. Moment.js – популярная библиотека для работы с датами. Установите его с помощью команды:

npm install moment --save

Внутри ImageCard.vue импортируем moment и создадим filter для форматирования даты. Если вы не знаете что такое filter то думайте о них как о функциях, которые получают данные, нуждающиеся в форматировании а возвращают результат такой как мы хотим. Если нужно можете посетить страницу форматирования строк, чтобы узнать больше о способах отображения дат:

<script>
import moment from 'moment';

export default {
  name: 'ImageCard',
  props: [ 'image' ],
  filters: {
    moment(date) {
      return moment(date).format("MMMM Do, YYYY");
    }
  }
}
</script>

Используем фильтр в шаблоне, добавив символ канала ( | ) после даты, которую мы хотим отформатировать, и ссылаясь на наш фильтр по его имени:

<p class="image-date">{{image.datetaken | moment}}</p>

Шаг 7: Основы очистки данных

Вы могли заметить, что иногда изображения не отображаются, и в вашем браузере появляется следующее:

Вероятно, это связано с несогласованностью данных в Flickr (то есть отсутствием строки url_n). Простое решение – создать вычисляемое свойство с именем cleanImages, которое отфильтровывает любое изображение, не имеющее url_n, и использовать cleanImages для циклического прохождения в нашем шаблоне вместо изображений.

В Home.vue добавим вычисляемое свойство:

...
computed: {
  cleanImages() {
    return this.images.filter(image => image.url_n)
  }
},

Затем в шаблоне измените images на cleanImages:

<template>
...
<image-card
  v-for="image in cleanImages"
  :key="image.id"
  :image="image" />
...
</template>

Завершение

Мы использовали axios и Vue.js, чтобы сделать простой запрос к API Flickr, и сделали его хорошо выглядящим с помощью CSS стилей и некоторой помощью Moment.js! Далее, во второй части, мы добавим страницы с подробными изображениями и немного изменим наше приложение с точки зрения маршрутизации страниц.

Оригинал: Simple Photo App with Vue.js, Axios and Flickr API — Part 1


Spread the love

Добавить комментарий

Ваш e-mail не будет опубликован. Обязательные поля помечены *