Часть 1:Унифицированные SVG-иконки с помощью Vite, Vue 3, Quasar и Pinia

Резюме

В этой статье мы:

  • использовать пользовательские SVG-иконки
  • создадим гибкий и настраиваемый компонент многократного использования для использования SVG иконок
  • использовать Vite, Vue 3, Quasar и Pinia
  • использовать как Composition API с настройкой скриптов, так и Options API с Vue 3
  • авторегистрация глобальных компонентов с помощью import.meta.glob и import.meta.globEager
  • связывать значения CSS с динамическим состоянием компонента с помощью функции CSS v-bind

Подождите😲! Все это в одной статье?

ДА! Давайте сделаем это! 🤹♂️

Что мы будем строить:

  • Нажмите здесь, чтобы увидеть приложение, которое мы будем создавать.
  • Поиграйте с кодом онлайн на Stackblitz (возможно, вам придется запустить vite в терминале, чтобы запустить приложение)
  • Repo с каждым уроком в ветке

Требования

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

Наш дизайнер 👨🎨/👩🎨 устал от использования наборов иконок и решил, что приложению нужны собственные уникальные SVG-иконки. Вот спецификации, которые он/она дал нам:

  • Большинство иконок имеют размеры 16, 24, 32, 48. Однако несколько иконок, некоторые специальные иконки, имеют другие размеры. Наиболее используемый размер — 48px.

  • Иконки SVG масштабируются, и их штрихи тоже, но наш умный дизайнер хочет вручную контролировать ширину штриха в разных размерах:

    • 16px и 24px: 1px ширина обводки
    • 32px: 2px ширина обводки
    • 48px: 2,5px ширина обводки
  • Все иконки имеют контурный стиль.

  • Все иконки должны иметь цвет по умолчанию, который называется «основной», и цвет наведения по умолчанию белый, однако, должна быть возможность легко переписать эти значения по умолчанию.

  • В приложении цвета определены в переменных CSS, но должна быть возможность задать цвет с помощью значения цвета (hex, rgb и т.д.).

Вот цвета переменных CSS:

  --primary: #007bff;
  --secondary: #6c757d;
  --positive: #28a745;
  --negative: #dc3545;
  --info: #17a2b8;
  --warning: #ffc107;
Войти в полноэкранный режим Выход из полноэкранного режима

Отлично! Как вы видите, нам нужен гибкий и настраиваемый компонент многократного использования. 🏯

Давайте начнем кодить! ⌨️🔥

Мы могли бы начать с создания нового проекта Vite + Vue 3, что можно сделать, выполнив npm init vue@latest в терминале, но чтобы ускорить процесс, я уже сделал это, почистил проект и добавил несколько SVG иконок.

Итак, клонируйте или скачайте репозиторий или поиграйте с кодом онлайн на Stackblitz.

Как видите, у нас есть чистое приложение Vite + Vue 3 и несколько SVG-иконок в папке src/components/icons.
Следующим шагом будет установка Quasar и Pinia. Перед этим мне понравилось, как в Vue 2 мы могли сохранить файл main.js чистым и простым, поэтому мы собираемся сделать именно это!

Сначала создадим папку plugins (src/plugins) и внутри нее файл main-app.js:

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

export const app = createApp(App)
Вход в полноэкранный режим Выйти из полноэкранного режима

Затем наш main.js должен выглядеть следующим образом:

import { app } from './plugins/main-app'

app.mount('#app')
Войти в полноэкранный режим Выход из полноэкранного режима

Чисто и просто, верно?

Установка Quasar и Pinia

Сначала выполните команду:

npm install quasar @quasar/extras pinia
Войти в полноэкранный режим Выйти из полноэкранного режима

Для того чтобы Quasar работал в Vite, нам необходимо установить соответствующий плагин:

 npm install -D @quasar/vite-plugin
Войти в полноэкранный режим Выйти из полноэкранного режима

Теперь, когда мы их установили, давайте зарегистрируем их в папке plugins:

pinia.js

import { app } from './main-app'
import { createPinia } from 'pinia'

app.use(createPinia())
Войти в полноэкранный режим Выйти из полноэкранного режима

quasar.js

import { Quasar } from 'quasar'
import { app } from './main-app'

// Import icon libraries
import '@quasar/extras/material-icons/material-icons.css'
// Import Quasar css
import 'quasar/src/css/index.sass'

app.use(Quasar, {
  plugins: {} // import Quasar plugins and add here
})
Войти в полноэкранный режим Выход из полноэкранного режима

Наконец, давайте импортируем плагины Quasar и Pinia в файл main.js:

import { app } from './plugins/main-app'
import './plugins/quasar' // +
import './plugins/pinia' // +

app.mount('#app')
Войти в полноэкранный режим Выход из полноэкранного режима

Если у вас что-то не работает, посмотрите рабочий код здесь на Github или онлайн на Stackblitz.

Создание многоразового компонента для SVG-иконок

Теперь нам нужно создать многоразовый компонент для SVG-иконок. Давайте сделаем это. 👷

Мы назовем его SvgIcon и сделаем его глобальным компонентом, чтобы мы могли использовать его везде без импорта.

Давайте создадим SvgIcon.vue и contants.js в папке src/components/global/svg-icon. Внутри components/global мы будем хранить все наши глобальные компоненты.

Помните наши требования?

  • наши общие размеры иконок — 16, 24, 32 и 48. Поэтому мы назовем их sm, md, lg, xl соответственно.
  • По умолчанию это 48, значит xl.
  • 16 и 24 имеют ширину обводки 1px, 32 — 2px, 48 — 2.5px.
  • Цвет по умолчанию — primary, а цвет hover по умолчанию — white.

Давайте определим их в файле contants.js:

export const sizes = {
  sm: {
    size: 16,
    strokeWidth: 1
  },
  md: {
    size: 24,
    strokeWidth: 1
  },
  lg: {
    size: 32,
    strokeWidth: 2
  },
  xl: {
    size: 48,
    strokeWidth: 2.5
  }
}
export const defaults = {
  color: 'var:primary',
  varPrefix: 'q-',
  hoverColor: 'white',
  size: 'xl'
}
Вход в полноэкранный режим Выход из полноэкранного режима

Переменные Quasar по умолчанию имеют префикс q-. Например, --q-primary.
Чтобы учесть это, мы определяем свойство varPrefix в объекте defaults.

var:primary: color и hoverColor могут быть либо значением цвета, например, yellow, либо переменной, например, var:primary. var:primary указывает на переменную --q-primary.

Далее, давайте напишем немного кода в файле SvgIcon.vue. Мы будем использовать API Options 😎:

<script>
import { defineAsyncComponent } from 'vue'
import { sizes, defaults } from './constants'

export default {
  props: {
    name: {
      type: String,
      required: true
    },

    color: {
      type: String,
      default: defaults.color
    },

    size: {
      type: String,
      default: defaults.size,
      validator: (val) => Object.keys(sizes).includes(val)
    },

    hoverColor: [Boolean, String]
  },

  computed: {
    dynamicComponent() {
      const name = this.name.charAt(0).toUpperCase() + this.name.slice(1) + 'Icon'

      return defineAsyncComponent(() => import(`../../icons/${name}.vue`))
    },

    colorBind() {
      const color = this.color ? this.color : defaults.color

      return this.getVarOrColorValue(color)
    },

    hoverColorBind() {
      if (this.hoverColor === false) return

      if (this.hoverColor === true || !this.hoverColor) return defaults.hoverColor
      return this.getVarOrColorValue(this.hoverColor)
    },

    svgSize() {
      return sizes[this.size].size
    },

    strokeWidth() {
      return sizes[this.size].strokeWidth
    }
  },

  methods: {
    getVarOrColorValue(str) {
      return str.startsWith('var:') ? str.replace(/^var:/, `var(--${defaults.varPrefix}`) + ')' : str
    }
  }
}
</script>
Войти в полноэкранный режим Выйти из полноэкранного режима

Я думаю, что код компонента прост, но вот некоторые пояснения:

Далее добавим теги <template> и <style>:

<template>
  <component
    :is="dynamicComponent"
    class="svg-icon"
    :width="svgSize"
    :height="svgSize"
    :stroke-width="strokeWidth"
    :class="{ 'add-hover': !!hoverColorBind }"
  />
</template>

<style lang="scss" scoped>
.svg-icon {
  color: v-bind(colorBind);
  transition: color 0.2s ease-in-out;

  &.add-hover:hover {
    color: v-bind(hoverColorBind);
  }
}
</style>
Вход в полноэкранный режим Выход из полноэкранного режима

Не так много нужно объяснить о шаблоне, но в стиле мы используем v-bind, чтобы связать вычисляемые свойства colorBind и hoverColorBind со свойством цвета CSS. Всякий раз, когда эти вычисляемые свойства изменяются, свойство color будет обновляться. Фактически, фактическое значение будет скомпилировано в хэшированную переменную CSS. Узнайте больше в документации.

Великолепно! Вот несколько простых примеров использования компонента, который мы только что создали:

<svg-icon name="home" />

<svg-icon name="home" size="sm" color="var:primary" hoverColor />

<svg-icon name="home" size="sm" color="var:primary" hoverColor="blue" />

<svg-icon name="home" size="sm" color="blue" hoverColor="var:primary" />
Вход в полноэкранный режим Выход из полноэкранного режима

Смотрите рабочий код здесь на Github или онлайн на Stackblitz.

Автоматическая регистрация компонентов

Мы еще не сделали наш компонент SvgIcon.vue глобальным, поэтому давайте зарегистрируем все компоненты в папке components/global.
В Vite мы можем сделать это с помощью Glob import.

Сначала создайте файл plugins/global-components.js:

import.meta.glob.

Благодаря использованию import.meta.glob, сопоставленные файлы будут лениво загружены через динамический импорт и будут разбиты на отдельные куски во время сборки:

import { defineAsyncComponent } from 'vue'
import { app } from './main-app'

const globalComponentsPaths = import.meta.glob('/src/components/global/**/*.vue')

Object.entries(globalComponentsPaths).forEach(([path, module]) => {
  // "./components/SvgIcon.vue" -> "SvgIcon"
  const componentName = path
    .split('/')
    .pop()
    .replace(/.vue$/, '')

  app.component(componentName, defineAsyncComponent(module))
})
Войти в полноэкранный режим Выход из полноэкранного режима
import.meta.globEager.

Если вы хотите загрузить все совпадающие файлы с нетерпением, вы можете использовать import.meta.globEager:

import { app } from './main-app'

const globalComponentsPaths = import.meta.globEager('/src/components/global/**/*.vue')

Object.entries(globalComponentsPaths).forEach(([path, module]) => {
  // "./components/SvgIcon.vue" -> "SvgIcon"
  const componentName = path
    .split('/')
    .pop()
    .replace(/.vue$/, '')

  app.component(componentName, module.default)
})
Вход в полноэкранный режим Выход из полноэкранного режима

В нашем случае нам не нужны отдельные блоки, так как у нас будет только одна страница, поэтому мы будем использовать import.meta.globEager. Это позволит загрузить все компоненты с нетерпением и будет включено в основной пакет.

Последним шагом будет импорт файла global-components.js в main.js:

import { app } from './plugins/main-app'
import './plugins/quasar'
import './plugins/pinia'
import './plugins/global-components' // +

app.mount('#app')
Вход в полноэкранный режим Выход из полноэкранного режима

Теперь мы можем использовать компонент <svg-icon> везде в нашем приложении без необходимости его импорта.
Теперь пора приступить к созданию нашей интерактивной игровой площадки. 🔥🔥

Смотрите рабочий код здесь на Github или онлайн на Stackblitz.

Создание и использование магазина Pinia 🏪

Первым шагом в создании интерактивной игровой площадки является создание глобального хранилища, чтобы все наши компоненты могли взаимодействовать с ним.
Итак, давайте создадим файл global-store.js в папке src/stores:

import { reactive, ref } from 'vue'
import { defineStore } from 'pinia'

export const useGlobalStore = defineStore('global-store', () => {
  const availableIcons = ['user', 'search', 'home']
  const selectedIcon = ref(availableIcons[0])

  const color = ref()

  const hasHoverColor = ref(false)
  const hoverColor = ref()

  const availableSizes = ['sm', 'md', 'lg', 'xl']
  const selectedSize = ref(availableSizes[3])

  const cssVarColors = reactive({
    primary: '#007bff',
    secondary: '#6c757d',
    positive: '#28a745',
    negative: '#dc3545',
    info: '#17a2b8',
    warning: '#ffc107'
  })

  return {
    availableIcons,
    selectedIcon,
    color,
    hasHoverColor,
    hoverColor,
    availableSizes,
    selectedSize,
    cssVarColors
  }
})
Вход в полноэкранный режим Выход из полноэкранного режима

Отлично! Мы создали магазин Pinia 🍍! Это было просто, верно?

Теперь давайте используем этот магазин в App.vue для привязки cssVarColors к переменным Quasar CSS. Мы будем использовать Composition API с script setup для App.vue и, наконец, использовать компонент SvgIcon.vue:

<script setup>
import { useGlobalStore } from '@/stores/global-store'

const globalStore = useGlobalStore()
</script>

<template>
  <header>
    <div class="gradient-font q-my-sm">Unified way of using SVG Icons</div>
  </header>

  <main class="">
    <svg-icon name="user" />
  </main>
</template>

<style lang="scss">
@import 'css/base';

.main {
  --q-primary: v-bind('globalStore.cssVarColors.primary');
  --q-secondary: v-bind('globalStore.cssVarColors.secondary');
  --q-positive: v-bind('globalStore.cssVarColors.positive');
  --q-negative: v-bind('globalStore.cssVarColors.negative');
  --q-info: v-bind('globalStore.cssVarColors.info');
  --q-warning: v-bind('globalStore.cssVarColors.warning');

  width: 100%;
}
</style>
Вход в полноэкранный режим Выход из полноэкранного режима

Смотрите рабочий код здесь на Github или онлайн на Stackblitz.

Следующие шаги

Статья получилась немного длинной, поэтому давайте построим интерактивную игровую площадку в следующей статье:

  • Использовать встроенный компонент: Suspense
  • создадим интерактивную игровую площадку для игры с компонентом SvgIcon
  • выделять и генерировать код с помощью Highlight.js
  • добавлять отзывчивый дизайн с помощью CSS Grid & Quasar
  • добавить CSS Gradient закругленные границы
  • более широкое использование Quasar, Pinia и Composition API с настройкой скрипта

Если вам понравилась эта статья, вы можете выразить свою поддержку, купив мне кофе. Это меня очень мотивирует.

Спасибо за прочтение, надеюсь, вам понравилось!

Обо мне

Меня зовут Роланд Дода (не стесняйтесь связаться со мной на Linkedin или следить за мной на Github, Dev.to, Hashnode или Twitter), и я работаю старшим разработчиком фронтенда в CPI technologies.
Мы также принимаем на работу! Посмотрите на CPI Career Center, а также описание вакансии VueJS Frontend Developer.

Оставьте комментарий

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