Если вы используете веб-сайты поиска изображений, такие как Google Image Search или Flickr, вы заметите, что их изображения отображаются в виде сетки, которая выглядит как стена из кирпичей. Изображения неравномерны по высоте, но равны по ширине. Это называется эффектом кладки, потому что выглядит как стена из кирпича.
Чтобы реализовать эффект каменной кладки, мы должны установить ширину изображения, пропорциональную ширине экрана, и установить высоту изображения, пропорциональную соотношению сторон изображения.
Это мучительно, если это делается без каких-либо библиотек, поэтому люди создали пакеты для создания этого эффекта.
В этой статье мы создадим приложение для фотографий, которое позволит пользователям искать изображения и отображать изображения в сетке каменной кладки. Сетка изображений будет иметь бесконечную прокрутку, чтобы получить больше изображений. Мы будем использовать библиотеку vue-masonry
для рендеринга сетки изображения и vue-infinite-scroll
для эффекта бесконечной прокрутки.
Наше приложение будет отображать изображения из API Pixabay. Вы можете просмотреть документацию по API и зарегистрироваться для получения ключа на странице https://pixabay.com/api/docs/
Начиная
Получив ключ API Pixabay, мы можем приступить к написанию нашего приложения. Для начала создадим проект под названием photo-app
. Запустить:
npx @vue/cli create photo-app
Это создаст файлы для нашего приложения и установит пакеты для встроенных библиотек. Мы выбираем «выбрать функции вручную» и выбираем Babel, Vue Router и CSS Preprocessor.
Далее мы устанавливаем собственные пакеты. Нам нужны упомянутые выше vue-masonry
библиотека и vue-infinite-scroll
. Кроме того, нам понадобится BootstrapVue для стилизации, Axios для выполнения HTTP-запросов и Vee-Validate для проверки формы.
Устанавливаем все пакеты, запустив:
npm i axios bootstrap-vue vee-validate vue-infinite-scroll vue-masonry
Создание приложения
Установив все пакеты, мы можем приступить к написанию нашего приложения. Создайте папку mixins
в каталоге src
и создайте файл requestsMixin.js
.
Затем мы добавляем в файл следующее:
const axios = require("axios"); const APIURL = "https://pixabay.com/api"; export const requestsMixin = { methods: { getImages(page = 1) { return axios.get(`${APIURL}/?page=${page}&key=${process.env.VUE_APP_API_KEY}`); }, searchImages(keyword, page = 1) { return axios.get( `${APIURL}/?page=${page}&key=${process.env.VUE_APP_API_KEY}&q=${keyword}` ); } } };
Мы вызываем здесь конечные точки для поиска изображений. process.env.VUE_APP_API_KEY
извлекается из файла .env
в корневой папке нашего проекта. Обратите внимание, что используемые нами переменные среды должны иметь ключи, начинающиеся с VUE_APP
.
Затем в Home.vue
замените существующий код на:
<template> <div class="page"> <h1 class="text-center">Home</h1> <div v-infinite-scroll="getImagesByPage" infinite-scroll-disabled="busy" infinite-scroll-distance="10" > <div v-masonry="containerId" transition-duration="0.3s" item-selector=".item" gutter="5" fit-width="true" class="masonry-container" > <div> <img :src="item.previewURL" v-masonry-tile class="item" v-for="(item, index) in images" :key="index" /> </div> </div> </div> </div> </template> <script> import { requestsMixin } from "../mixins/requestsMixin"; export default { name: "home", mixins: [requestsMixin], data() { return { images: [], page: 1, containerId: null }; }, methods: { async getImagesByPage() { const response = await this.getImages(this.page); this.images = this.images.concat(response.data.hits); this.page++; } }, beforeMount() { this.getImagesByPage(); } }; </script>
Здесь мы используем пакеты vue-infinite-scroll
и vue-masonry
. Обратите внимание, что мы указали transition-duration
, чтобы настроить переход от отображения ничего к отображению изображений, а fit-width
делает столбцы подходящими для контейнера. gutter
определяет ширину промежутка между каждым столбцом в пикселях. Мы также устанавливаем имя класса CSS в контейнере v-masonry
, чтобы позже изменить стили.
Внутри v-masonry
div
мы перебираем изображения в цикле, мы устанавливаем v-masonry-tile
, чтобы указать, что это плитка, чтобы он изменил их размер до сетки каменной кладки.
В объекте script
мы получаем изображения при загрузке страницы с помощью хука beforeMount
. Поскольку мы добавляем бесконечную прокрутку, мы продолжаем добавлять изображения в массив по мере того, как пользователь прокручивает вниз. Мы вызываем getImagesByPage
, когда пользователь прокручивает страницу вниз, как указано в опоре v-infinite-scroll
. Мы устанавливаем infinite-scroll-disabled
на busy
, чтобы отключить прокрутку. infinite-scroll-distance
указывает расстояние от низа страницы в процентах до начала прокрутки.
Затем создайте ImageSearchPage.vue
в папке views
и добавьте:
<template> <div class="page"> <h1 class="text-center">Image Search</h1> <ValidationObserver ref="observer" v-slot="{ invalid }"> <b-form @submit.prevent="onSubmit" novalidate> <b-form-group label="Keyword" label-for="keyword"> <ValidationProvider name="keyword" rules="required" v-slot="{ errors }"> <b-form-input :state="errors.length == 0" v-model="form.keyword" type="text" required placeholder="Keyword" name="keyword" ></b-form-input> <b-form-invalid-feedback :state="errors.length == 0">Keyword is required</b-form-invalid-feedback> </ValidationProvider> </b-form-group> <b-button type="submit" variant="primary">Search</b-button> </b-form> </ValidationObserver> <br /> <div v-infinite-scroll="searchAllImages" infinite-scroll-disabled="busy" infinite-scroll-distance="10" > <div v-masonry="containerId" transition-duration="0.3s" item-selector=".item" gutter="5" fit-width="true" class="masonry-container" > <div> <img :src="item.previewURL" v-masonry-tile class="item" v-for="(item, index) in images" :key="index" /> </div> </div> </div> </div> </template> <script> import { requestsMixin } from "../mixins/requestsMixin"; export default { mixins: [requestsMixin], data() { return { form: {}, page: 1, containerId: null, images: [] }; }, methods: { async onSubmit() { const isValid = await this.$refs.observer.validate(); if (!isValid) { return; } this.page = 1; await this.searchAllImages(); }, async searchAllImages() { if (!this.form.keyword) { return; } const response = await this.searchImages(this.form.keyword, this.page); if (this.page == 1) { this.images = response.data.hits; } else { this.images = this.images.concat(response.data.hits); } this.page++; } } }; </script>
Бесконечная прокрутка и макет каменной кладки почти такие же, за исключением случаев, когда keyword
изменяется, мы переназначаем массив this.images
на новые элементы вместо того, чтобы продолжать добавлять их в существующий массив, чтобы пользователи видели новые результаты.
Форма завернута внутрь ValidationObserver
, чтобы мы могли получить статус проверки всей формы внутри. В форме мы оборачиваем ввод с помощью ValidationProvider
, чтобы можно было проверить поле формы и отобразить сообщение об ошибке проверки для ввода. Проверяем, заполнено ли keyword
.
Как только пользователь нажимает кнопку «Поиск», запускается onSubmit
обратный вызов, который выполняет await this.$refs.observer.validate();
для получения статуса проверки формы. Если это приведет к true
, то для получения изображений будет запущено searchAllImages
.
Затем мы заменяем существующий код в App.vue
на:
<template> <div> <b-navbar toggleable="lg" type="dark" variant="info"> <b-navbar-brand href="#">Photo App</b-navbar-brand> <b-navbar-toggle target="nav-collapse"></b-navbar-toggle> <b-collapse id="nav-collapse" is-nav> <b-navbar-nav> <b-nav-item to="/" :active="path == '/'">Home</b-nav-item> <b-nav-item to="/imagesearch" :active="path == '/imagesearch'">Image Search</b-nav-item> </b-navbar-nav> </b-collapse> </b-navbar> <router-view /> </div> </template> <script> export default { data() { return { path: this.$route && this.$route.path }; }, watch: { $route(route) { this.path = route.path; } } }; </script> <style lang="scss"> .page { padding: 20px; } .item { width: 30vw; } .masonry-container { margin: 0 auto; } </style>
Мы добавляем сюда BootstrapVue b-navbar
, чтобы отобразить верхнюю панель со ссылками на наши страницы. В разделе script
смотрим текущий маршрут, получая this.$route.path
. Мы устанавливаем свойство active
, сверяя путь с нашим наблюдаемым path
, чтобы выделить ссылки.
В разделе style
мы устанавливаем отступы наших страниц с помощью класса page
, мы устанавливаем ширину фотографии с помощью класса item
, как указано в item-selector
нашего v-masonry
div, и устанавливаем поле masonry-container
на 0 auto
, чтобы оно было сосредоточено на страница.
Затем в main.js
замените существующий код на:
import Vue from "vue"; import App from "./App.vue"; import router from "./router"; import "bootstrap/dist/css/bootstrap.css"; import "bootstrap-vue/dist/bootstrap-vue.css"; import BootstrapVue from "bootstrap-vue"; import { ValidationProvider, extend, ValidationObserver } from "vee-validate"; import { required } from "vee-validate/dist/rules"; import { VueMasonryPlugin } from "vue-masonry"; import infiniteScroll from "vue-infinite-scroll"; Vue.config.productionTip = false; extend("required", required); Vue.component("ValidationProvider", ValidationProvider); Vue.component("ValidationObserver", ValidationObserver); Vue.use(VueMasonryPlugin); Vue.use(infiniteScroll); Vue.use(BootstrapVue); new Vue({ router, render: h => h(App) }).$mount("#app");
Это добавляет все библиотеки, которые мы использовали в компонентах, и правила проверки Vee-Validate, которые мы использовали. Кроме того, мы импортируем сюда наши стили Bootstrap, чтобы видеть стили везде.
Затем в router.js
замените существующий код на:
import Vue from "vue"; import Router from "vue-router"; import Home from "./views/Home.vue"; import ImageSearchPage from "./views/ImageSearchPage.vue"; Vue.use(Router); export default new Router({ mode: "history", base: process.env.BASE_URL, routes: [ { path: "/", name: "home", component: Home }, { path: "/imagesearch", name: "imagesearch", component: ImageSearchPage } ] });
Это добавляет наши маршруты.
После того, как все будет сделано, запустим наше приложение, выполнив npm run start
, и мы должны получить: