Хоть UI библиотек или фреймворков для Vue.js не так уж и много, но Vuetify как раз здесь больше всего выделяется и по функционалу просто впереди других. Сегодня в этой небольшой статье я расскажу об данном UI фреймворке, его особенности, его структура и вообще к чему его можно применять. И честно в интернете не так уж и много русскоязычных материалов под данной теме, поэтому я решил что-то написать по Vuetify.
P.S. Вот сыллка на GitHub с кодом приложения.
Введение/Установка
Окей, представим что вы решили все таки начать пользоваться Vuetify и написать какое-то своё первое приложение используя его. И начинается все конечно же с установки, для начала давайте создадим новое Vue CLI приложение с помощью npm:
vue create vuetify-app
Важно, надо чтобы приложение имело версию Vue 2.0, поскольку на время написания этой статьи в Vuetify поддержка идёт только для Vue 2.0 приложений. После того как нового приложение было создано — следующим шагом будет установить UI фреймворк как плагин:
vue add vuetify
Нас попросит в дальнейшем выбрать какой-то пресет, и поскольку мы не собираемся пробовать версию V3 (которая нестабильна), то мы выбираем пресет “Default”:
Когда процесс установки Vuetify завершиться, то можно будет запустить локальный сервер приложения:
npm run serve
И открыв https://localhost:8080 мы увидим по-умолчанию созданную страницу от Vuetify с полезными сыллками:
Как мы видим то все работает и самое время начать уже писать наше приложение.
Так о чем же будет наше приложение?
Хороший вопрос, ведь мы создали уже Vue CLI приложение и установили Vuetify, но я так и не сказал что же будет делать именно наше приложение. Мой любимый пример, это мобильное приложение по доставке еды — поэтому давайте это будет тема нашего приложения.
Но перед этим давайте также установим Vue Router:
vue add router
Как обычно при добавлении нового плагина к приложению, терминал может сказать что есть изменения которые не были добавлены в commit, мы здесь пишем “y” (т.е. yes) и двигаемся дальше:
Дальше при установке нас спросит хотим ли использовать режим истории в маршутизации, и здесь мы также тыкаем “y”:
Вот теперь Vue Router установлен, и самое время убрать код примеров в src/router/index.js чтобы когда мы сейчас запустили снова локальный сервер — не было ошибок:
// Файл src/router/index.js
import Vue from 'vue'
import VueRouter from 'vue-router'
Vue.use(VueRouter)
const routes = [
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
Файл маршутизации теперь настроен и не содержит в себе каких-то несуществующих маршрутов, ну а теперь нужно также почистить файл App.vue:
<!-- App.vue -->
<template>
<div id="app">
<!-- Сейчас мы будем здесь писать свой код-->
</div>
</template>
<script>
export default {
name: 'App'
}
</script>
Хорошо, теперь мы настроили наши два файла и при запуске не будет никаких ошибок — а самое лучшее это то — что мы можем начинать разрабатывать наше приложение.
Layout
В Vuetify уже есть своя придуманная гибкая система построения сетки в приложении. И давайте сейчас с ней ознакомимся. Для начала нам нужно будет воспользоваться двумя компонентами, это v-app и v-main. Открываем App.vue и добавляем их:
<template>
<v-app>
<v-main>
</v-main>
</v-app>
</template>
<script>
export default {
name: 'App'
}
</script>
С компонента v-app и начинается наше приложение, этот компонент является псевдонимом тега <div> с id=”app”, а в компоненте v-main мы уже начинаем размещать контент самого приложения. Следующим шагом будет добавить хедер с помощью компонента v-app-bar:
<template>
<v-app>
<v-app-bar
app
>
</v-app-bar>
<v-main>
</v-main>
</v-app>
</template>
<!-- <script> -->
Если мы перейдем на локальный сервер — то увидим следующее:
На странице нету ничего кроме нашего хедера. Объясню также для чего я добавил атрибут app в v-app-bar. Когда Vuetify видит что в компоненте v-app-bar есть атрибут app, то он понимает что это часть layout’a приложения. Многие также могут спросить почему я добавил хедер перед v-main, ответ простой: в v-main будет уже располагаться компонент router-view который будет показывать содержимое маршрутов, а поскольку наш хедер это не сам контент приложения — то я и вынес его перед компонентом v-main.
Мне не очень нравится цвет хедера, поэтому давайте его поменяем добавив атрибут color:
<template>
<v-app>
<v-app-bar
app
color="red"
>
<!-- Сейчас добавим сюда title -->
</v-app-bar>
<v-main>
<!-- Контент -->
</v-main>
</v-app>
</template>
Окей, и что если мы перейдем на локальный сервер? То вот что мы с вами увидим:
Круто, а теперь мы можем начинать добавлять уже какой-то контент? Да, но перед этим добавим также заголовок в хедер и компонент просмотра маршутов внутри v-main:
<template>
<v-app>
<v-app-bar
app
color="red"
>
<v-app-bar-title>
<h3 class="text-h4 white--text">iFood</h3>
</v-app-bar-title>
</v-app-bar>
<v-main>
<router-view></router-view>
</v-main>
</v-app>
</template>
Заголовок мы добавили с помощью компонента v-app-bar-title, и уже внутри прописали тег h4 с классом от Vuetify которые помогают стилизировать текст. Давайте посмотрим как выглядит страница на локальном сервере:
Супер, заголовок есть. Хорошо, теперь самое время начать создавать компоненты в которых и будет контент приложения.
Главная страница
Давайте создадим компонент src/components/List.vue в которым и будет выводиться весь список заведений с помощью JSON массива. Для начала внутри этого компонента создадим переменную items_list, которая и будет хранить в себе список всех заведений:
<!-- src/components/List.vue -->
<template>
<div>
<!-- Пока здесь будет пустой div -->
</div>
</template>
<script>
export default {
name: 'List',
data: () => ({
items_list: [
{
id: 0,
title: 'Заведение 1',
description: 'Это наверное самое крутое заведение которое вы можете знать',
img: '' // Здесь добавляем любое изображение
},
{
id: 1,
title: 'Заведение 2',
description: 'Lorem ipsum',
img: '' // Здесь добавляем любое изображение
}
// Дальше можете добавить другие заведения в список
]
})
}
</script>
Окей, список у нас есть, а с помощью каких компонентов от Vuetify будем его выводить? Сейчас воспользуемся компонентом v-card, который позволяет удобно в карточках выводить подобную информацию. Но при этом мы также воспользуемся Bootstrap Vue чтобы элементы красиво располагались на странице.
<!-- src/components/List.vue -->
<template>
<v-container> <!-- Создаем контейнер внутри которого и будут элементы компонента -->
<v-row class="list__cafes-title"> <!-- В это row выводим заголовок компонента -->
<v-col>
<h2 class="text-center text-h3 py-3">List of cafes</h2>
</v-col>
</v-row>
<v-row class="list__cafes-content">
<v-col md="4" v-for="item in items_list" :key="item.id">
<v-card>
<v-img
height="250"
:src="item.img"
></v-img> <!-- С помощью v-img добавляем изображение карточки -->
<v-card-title> <!-- Заголовок заведения -->
<h3 class="text-h4">{{ item.title }}</h3>
</v-card-title>
<v-card-text> <!-- Описание заведения -->
<p class="text-body-1">{{ item.description }}</p>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
Многие могут заметить что в row’e где мы уже выводим сами карточки заведения с помощью v-for в компоненте v-col я добавляю атрибут md=”4". Те кто учили Bootstrap уже должны догадаться для чего нужен этот атрибут и как именно он задает размеры колонок. С помощью этого атрибута я указал чтобы каждая колонка имела размеры 4/12 и благодаря этому на странице карточки будут выводиться по 3 штуки в один ряд. Окей, компонент вывода заведений готов, надо теперь добавить его маршрут в файл src/router/index.js:
import Vue from 'vue'
import VueRouter from 'vue-router'
import List from '../components/List.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'List',
component: List
}
]
const router = new VueRouter({
mode: 'history',
base: process.env.BASE_URL,
routes
})
export default router
А теперь давайте перейдем на локальный сервер и посмотрим на результат вывода:
Супер У нас есть вывод заведений на главной странице. А теперь я думаю самое время добавить отдельную страницу заведения, которая будет открываться при клике на заголовок карточки. Поскольку у нас нету базы данных с которой мы могли бы брать определенное заведение через API — то сама страница будет статичной.
Страница просмотра заведения
Для начала создаём компонент src/components/CafeView.vue и первое что в нем будет выводиться это изображение с заведением, его заголовок и описание. Для этой базовой информации мы создадим отдельный компонент с названием CafeInfo.vue, но перед этим заполним уже чем-то компонент CafeView.vue:
<!-- src/components/CafeView.vue -->
<template>
<v-container>
<CafeInfo></CafeInfo> <!-- Этот компонент мы сейчас создадим -->
</v-container>
</template>
<script>
import CafeInfo from './Cafe/CafeInfo.vue'
export default {
name: 'CafeView',
components: {
CafeInfo
}
}
</script>
Хорошо, а теперь создаём компонент CafeInfo.vue:
<!-- src/components/Cafe/CafeInfo.vue -->
<template>
<v-row>
<v-col md="6">
<v-card class="h-100">
<v-img
height="350"
:src="main_info.img"
class="d-flex align-end"
>
<!--
Выше можете заметить что с помощью классов d-flex и align-end, мы прописали CSS свойства display: flex; и align-items: flex-end. Это сделает так что название заведения будет выводиться внизу внутри изображения
-->
<h2 class="text-h4 ml-4 mb-4 white--text">{{ main_info.title }}</h2>
</v-img>
</v-card>
</v-col>
<v-col md="6">
<v-card class="h-100">
<v-card-text>
<p class="font-weight-light">{{ main_info.description }}</p>
</v-card-text>
</v-card>
</v-col>
</v-row>
</template>
<script>
export default {
name: 'CafeInfo',
data: () => ({
main_info: {
img: 'https://example.com/image_1.jpg',
title: 'Заголовок заведения',
description: 'А здесь уже описание заведения'
}
})
}
</script>
<style>
.h-100 {
height: 100%;
}
</style>
Для удобства я специально поместил данные в data функцию. Теперь когда наш компонент готов — самое время сделать маршрут для компонента CafeView.vue в src/routes/index.js:
import Vue from 'vue'
import VueRouter from 'vue-router'
import List from '../components/List.vue'
import CafeView from '../components/CafeView.vue'
Vue.use(VueRouter)
const routes = [
{
path: '/',
name: 'List',
component: List
},
{
path: '/cafe/view',
name: 'CafeView',
component: CafeView
}
]
// ...
Окей, а теперь давайте посмотрим как это выглядит вместе на локальном сервере на url http://localhost:8080/cafe/view:
Вид можно было б ещё больше кастомизировать чтобы выглядело намного красивее. Но пусть будет так с стилями Vuetify по-умолчанию. Теперь давайте также пропишем сыллку на переход на эту страницу в List.vue:
<!-- src/components/List.vue -->
<template>
<v-container>
<v-row class="list__cafes-title">
<v-col>
<h2 class="text-center text-h3 py-3">List of cafes</h2>
</v-col>
</v-row>
<v-row class="list__cafes-content">
<v-col md="4" v-for="item in items_list" :key="item.id">
<v-card>
<v-img
height="250"
:src="item.img"
></v-img>
<v-card-title>
<router-link to="/cafe/view">
<h3 class="text-h4">{{ item.title }}</h3>
</router-link>
</v-card-title>
<v-card-text>
<p class="text-body-1">{{ item.description }}</p>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-container>
</template>
В массив объектов заведений можно было бы ещё добавить отдельное поле url, которое хранило в себе линки на страницу заведения — но поскольку само приложение у нас статичное, то мы сделали именно так. Вывода такой информации на странице заведения — недостаточно. Нужно ещё реализовать компонент вывода меню товаров (еды) заведения. Поэтому для этого мы сейчас создадим отдельный компонент CafeMenu.vue.
Компонент меню заведения
Сначала создайте компонент src/components/Cafe/CafeMenu.vue, и внутри него сначала создаем переменную с массивом внутри которого будут объекты с информацией об товарах:
<!-- src/components/Cafe/CafeMenu.vue -->
<template>
<v-row>
<!-- Пока здесь будет пустой row -->
</v-row>
</template>
<script>
export default {
name: 'CafeMenu',
data: () => ({
menu_items: [
{
id: 0,
name: 'King Burger',
img: 'https://example.com/burger_1.png',
price: 34.99
},
{
id: 1,
name: 'Dark Burger',
img: 'https://example.com/burger_2.png',
price: 39.99
},
// Можете также дальше добавить другие товары
]
})
}
</script>
Рекомендую также добавить другие товары в список. А теперь самое время вывести их в отдельной колонке:
<!-- src/components/Cafe/CafeMenu.vue -->
<template>
<v-row>
<v-col md="6">
<v-card>
<v-img
height="250"
src="https://example.com/menu_img.png"
class="d-flex align-end"
>
<h2 class="ml-4 mb-4 text-h3 white--text">MENU</h2>
</v-img>
</v-card>
</v-col>
<v-col md="6">
<v-card>
<v-card-text>
<!--
Здесь внутри начинаем выводить товары,
для этого создаем список
-->
<v-list-item-group>
<v-list-item v-for="item in menu_items" :key="item.id">
<v-list-item-avatar>
<!-- Выводим изображение товара -->
<img :src="item.img" fluid />
</v-list-item-avatar>
<v-list-item-content>
<v-list-item-title>
<!-- Выводим название товара -->
<h5 class="text-h5">{{ item.name }}</h5>
</v-list-item-title>
<v-list-item-subtitle>
<!-- Выводим цену товару -->
<b>{{ item.price }}</b>
</v-list-item-subtitle>
</v-list-item-content>
<v-list-item-action>
<!-- Добавляем кнопку для покупки товара -->
<v-btn icon>
<v-icon>mdi-plus</v-icon>
</v-btn>
</v-list-item-action>
</v-list-item>
</v-list-item-group>
</v-card-text>
</v-card>
</v-col>
</v-row>
</template>
С помощью компонента v-list-item-group мы создали список, внутри которого с помощью v-for в v-list-item вывели список всех товаров. Компонент v-list-item-content используется для того чтобы выводить контент самого элемента списка, в нашем случае мы выводим заголовок (v-list-item-title) и цену (с помощью v-list-item-subtitle). А с помощью v-list-item-action, внутри мы добавили кнопку покупки товара (v-btn), атрибут icon мы добавили чтобы указать что внутри кнопки выводиться только иконка.
Внутри v-icon мы указали иконку от Material Design. Окей, теперь когда я объяснил что к чему — можем подключить данный компонент:
<!-- src/components/CafeView.vue -->
<template>
<v-container>
<CafeInfo></CafeInfo>
<CafeMenu></CafeMenu>
</v-container>
</template>
<script>
import CafeInfo from './Cafe/CafeInfo.vue'
import CafeMenu from './Cafe/CafeMenu.vue'
export default {
name: 'CafeView',
components: {
CafeInfo,
CafeMenu
}
}
</script>
Открываем локальный сервер и смотрим на результат:
Круто, но нашему приложению не хватает менюшки в хедере очень сильно. Поэтому давайте её сейчас добавим.
Создание навигации
Сейчас нам нужно перейти в компонент App.vue и добавить новый компонент v-navigation-drawer:
<template>
<v-app>
<v-navigation-drawer
app
left
></v-navigation-drawer>
<v-app-bar
app
color="red"
>
<v-app-bar-title>
<h3 class="text-h4 white--text">iFood</h3>
</v-app-bar-title>
</v-app-bar>
<v-main>
<router-view></router-view>
</v-main>
</v-app>
</template>
<script>
export default {
name: 'App'
}
</script>
Этот компонент позволяет создавать меню навигации, я специально прописал атрибут left чтобы пустое меню навигации появилось с левой стороны:
Выглядит немного страшновато, но сейчас мы это пофиксим. Добавлю также что v-navigation-drawer является одним из компонентов layout системы Vuetify, и именно он отвечает за меню навигации. Давайте добавим следующий код чтобы это меню срабатывало при клике на иконку в хедере:
<template>
<v-app>
<v-navigation-drawer
app
left
absolute
temporary
v-model="drawer"
></v-navigation-drawer>
<v-app-bar
app
color="red"
>
<v-app-bar-nav-icon @click="drawer = !drawer">
</v-app-bar-nav-icon>
<v-app-bar-title>
<h3 class="text-h4 white--text">iFood</h3>
</v-app-bar-title>
</v-app-bar>
<v-main>
<router-view></router-view>
</v-main>
</v-app>
</template>
<script>
export default {
name: 'App',
data: () => ({
drawer: false
})
}
</script>
Зачем я добавил атрибуты absolute, temporary и v-model в v-navigation-drawer? Во-первых, absolute задает CSS свойство position: absolute; а это позволит сделать так чтобы эта менюшка и на мобайл хорошо отображалась. Во-вторых, temporary задает z-index для навигации — чтобы другие элементы не залазили на неё.
И во-третьих, v-model имеет в себе переменную drawer, которая в зависимости от значения true или false будет показывать меню. К примеру, если переменная drawer равняется в текущую момент значению true — то менюшка будет показываться. Если false, то менюшка будет спрятана. Если вы сейчас перейдете на локальный сервер — то увидите в хедере иконку и при клике на неё будет вылазить наше меню:
Теперь нужно чем-то заполнить меню, для этого создадим сейчас отдельную переменную с пунктами для меню:
<!-- ./ template -->
<script>
export default {
name: 'App',
data: () => ({
drawer: false,
menu_items: [
{
id: 0,
name: 'Список заведения',
url: '/'
},
{
id: 1,
name: 'О нас',
url: '#'
},
{
id: 2,
name: 'Контакты',
url: '#'
},
{
id: 3,
name: 'Помощь',
url: '#'
}
]
})
}
</script>
А теперь выведем все это внутри v-navigation-drawer:
<template>
<v-app>
<v-navigation-drawer
app
left
absolute
temporary
v-model="drawer"
>
<v-list-item-group>
<v-list-item v-for="item in menu_items" :key="item.id">
<v-list-item-title>
<router-link :to="item.url">{{ item.name }}</router-link>
</v-list-item-title>
</v-list-item>
</v-list-item-group>
</v-navigation-drawer>
<v-app-bar
app
color="red"
>
<v-app-bar-nav-icon @click="drawer = !drawer"></v-app-bar-nav-icon>
<v-app-bar-title>
<h3 class="text-h4 white--text">iFood</h3>
</v-app-bar-title>
</v-app-bar>
<v-main>
<router-view></router-view>
</v-main>
</v-app>
</template>
Готово, теперь если мы откроем меню то увидим следующее:
И вот у нас есть менюшка которая выглядет не прям красиво, но она работает и позволяет уже переходить на страницы. Окей, и что у нас получается? У нас есть простое приложение с выводом заведений, и просмотром этих заведений. Все это довольно таки примитивно — то мы смогли уже сделать что-то с Vuetify.
Итог
Окей, нам получилось сделать просто приложение с неким дизайном с помощью UI фреймворка для Vue.js — Vuetify. Можно было б и больше рассказать — но это сделало статью ещё более длинной, поэтому если кто-то хочет узнать более подробно по данной теме, то может посмотреть моё видео где мы создаём аналогичное приложение — только здесь уже идёт больше объяснений что и как и куда. Кому интересно может заглянуть.
Я лишь скажу что считаю Vuetify лучшим UI фреймворком для Vue.js, и использование его сильно облегчает разработку нового Vue приложения. Ведь тут уже столько всего поддерживает, и нам не приходится изобретать велосипеды.
Комментарии (17)
flancer
28.08.2021 07:50Я лишь скажу что считаю Vuetify лучшим UI фреймворком для Vue.js
А какое место в вашем рейтинге занимает Quasar UI? В чём он хуже Vuetify?
DemianKost Автор
28.08.2021 12:41Не скажу что Quasar UI какой-то плохой или хуже, да, у него тоже отличная документация, очень много компонентов которые имеют схожесть с Vuetify. Но лично для меня фаворитом остается Vuetify, да и Quasar впереди - ведь уже поддерживает v3. Касательно фразы "Я лишь скажу что считаю Vuetify лучшим UI фреймворком для Vue.js" - не надо прям считать что автор опровергает другие инструменты для UI, они тоже полезны и пользуются многими другими разработчиками.
Bromka
28.08.2021 11:42На v3 то сделали его наконец?
DemianKost Автор
28.08.2021 12:36v3 пока ещё в разработке, но в дальнейшем будет уже релизнут, честно мне было интересно можно ли уже взять текущую версию и её "портировать" по vue 3 - но получалось с костылями и не очень)
Sergo_1969
28.08.2021 17:05Как урок для новичка эта статья мало подходит - данные автором подготовлены плохо, хочешь видеть картинки, ищи их в интернетах сам, работали мы с компонентом "Лист" - в какой-то момент перескочили на группу компонентов "Кафе" - и о листе том больше не упоминалось, необходимых обьяснений абсолютный минимум, многие действия автора непонятны, ссылок на действующий код на гитхабе - нет. Данные разнесены по компонентам, что, конечно, недопустимо - тем более для в учебных материалах для начинающих.С другой стороны - для опытного разработчика - ничего интересного нет, тонкостей работы с фреймворком нет. Таким образом единственной целью автора была реклама его канала на ютубе. В моем случае цели автор не достиг, после такого "тьюториала" подписываться на канал - никакого желания нет.
DemianKost Автор
28.08.2021 17:19-1Да, может и так, но новички могут как раз заюзать видео в котором абсолютно все показано и объясняется, я бы мог бы здесь выписать прям все подробно - но для этого я готовлю отдельное краткое пособие - пока уже 21 страница (если тебе интересно и хочешь помочь то могу линку кинуть) - статья была больше как обзор и здесь не стояло цели чтобы прям нереально глубоко залезть на нижние уровни
Vadiok
28.08.2021 17:37+1Стоит ли новичкам изучать туториал по Vue 2, в то время как актуальная версия уже 3? Только из-за привязки к Vuetify, но, как уже написали выше, есть визуально очень похожий Quasar.
Также новичкам стоит в первую очередь изучить Composition API, а не объектный вариант описания компонентов.
DemianKost Автор
28.08.2021 17:43-1Да - брать сразу Vue 3 и Quasar, но все таки идёт рассмотрение именно функционала Vuetify а не самого Vue
DemianKost Автор
28.08.2021 17:22И также почему "недопустимо" разносить данные по отдельным компонентам? Разве лучше чтобы все было собрано в одном большом компоненте где смешаны сразу все row'ы, col'ы от Boostrap Vue?
Vadiok
28.08.2021 17:50Данные должны быть вообще не в компоненте. Компонент должен их получать из API или ещё откуда-то, а не хранить в data. Также у вас нерациональное использование реактивности, когда в data вы храните массивы или объекты, которые никогда не меняются, их не стоит делать реактивными.
А по поводу Vuetify, работаю с ним чуть больше года, визуально он мне нравится, но штука очень тормознутая и местами плохо кастомизируемая, иногда теряет реактивность, плюс ещё периодически разные баги возникают. Я бы не стал его называть лучшим, с Element UI, мне как-то больше понравилось работать, хотя и внешний вид у него похуже на мой взгляд.
DemianKost Автор
28.08.2021 17:53Да, знаю, там в статье говорилось что поскольку приложение статичное и мы не работаем с API/DB то сами данные в компонентах. А за реактивность спасибо возьму во внимание
Vadiok
28.08.2021 17:56Ну, вот для статьи их можно хотя бы было хранить в отдельных файлах, и очень не хватает посмотреть весь код в репозитории.
DemianKost Автор
28.08.2021 17:59кстати как раз думал об том чтобы в файлы сохранить и кстати это было бы очень хорошо, ведь потом просто взять и сделать запрос через fetch или axios чтобы получать эти данные. А по коду репозитория - сейчас иду засетаплю репозиторий и закину сыллку сюда
Sergo_1969
28.08.2021 20:24Как один из коллег уже ответил - данным в компонентах нет места. Не усложняя урок сетевыми запросами - можно было-бы создать класс провайдера данных - и обращатся к нему - обьяснив что это так называемый "mock" созданный в целях ускорения разработки.
derikn_mike
мне не нравится