Frontend developement on vue and daisyui (tailwind css) for backend developer
Замысел и ограничения
Для пет-проекта chipa решил я сделать админку посимпатичнее, заодно окунуться в современный фронтенд и вспомнить vuejs + typescript.
Я не дизайнер и не фронтенд-разработчик, потому к разработке фронтенд подход был следующий:
- Для админки дизайн не нужен: достаточно симпатичного современного стиля, чтобы глаз не резало и пользователю было удобно решать свою задачу. P.S. речь конечно про MVP или пет-проект. Крутой админке дизайн нужен :)
- Нужно опираться на какую-то достаточно высокую фронтенд-абстракцию, где решены вопросы стиля, и для разработки достаточно разместить нужные компоненты на странице (избежать работы с css, с выбором цветов, layout, с отступами на странице и т.п.)
- В opensource достаточно кодовой базы, над которой потрудились дизайнеры (в виде библиотек, шаблонов): можно найти нужный мне уровень абстрации.
Уровень абстрации и daisyui
Если идти по уровням абстрации разработки фронтенд снизу вверх, то будет так:
- Чистый CSS
- Фреймворк над CSS, упрощающий работу со стилями. Один из самых популярных - tailwind css. На нем я сделал первую версию админки. Получилось очень быстро, но не очень красиво (посмотреть можно в первой технической демке »)
- Фреймворк компонентов над tailwind css - много их. Я выбрал самый популярный - daisyui
Daisyui дал мне ощущение создания фронтенда комбинированием кубиков (компонентов), чего я и добивался как бекенд-разработчик желающий создать симпатичный фронтенд минимальными усилиями, без навыков frontend.
Реализация
vue
В приложении chipa самое главное - форма редактирования бота.
Основные задачи которые решает Vue:
- Декомпозиция сложной формы на простые компоненты.
- Логика работы компонентов (реактивное отображение состояния)
Например, так вылдядит код компонента описания бота, source »:
<script setup lang="ts">
import BotDescription from './composables/BotDescription.vue';
import CsvUpload from './composables/CsvUpload.vue'
import BotChat from './composables/BotChat.vue'
import BotTelegram from './composables/BotTelegram.vue';
import { onBeforeMount } from 'vue';
import { useBotStore } from '@/stores/currentBot';
const botStore = useBotStore()
onBeforeMount(()=>{
console.debug('EditBot component before mount')
})
</script>
<template>
<div id="edit-bot-ctr" class="mt-6 space-y-6">
<BotDescription />
<div v-if="botStore.bot!.id">
<div class="divider">csv</div>
<!-- <CsvUpload v-if="!botStore.bot!.csvSaved" /> -->
<CsvUpload />
<div v-if="botStore.bot!.csvSaved">
<!-- <CsvReupload/> -->
<div class="divider">testing (chat)</div>
<BotChat/>
<div class="divider">telegram integration</div>
<BotTelegram/>
</div>
</div>
</div>
</template>
BotDescription, CsvUpload и т.п. - это компоненты вкладки редактирования бота, каждый с понятной единой ответственностью.
Как видно, код компонентов тривиальный. Меньше сложность = меньше багов + быстрее разработка + счастливее разработчик!
state, pinia
Вопрос, который возникает при декомпозиции формы на компоненты: как организовать модель компонентов. Пользователь редактирует одного бота, но конкретный кусок информации бота редактируется в компоненте.
В общем случае, состояние компонентов Vue можно задавать через свойства (props), но тогда, если дочерний компонент меняет свойство, и об этом должны узнать другие компоненты (например, родительский), то дочерний должен отправить событие. Сложно для моего случая.
Наиболее простым вариантом я счел использование глобального состояния, которое хранится в pinia store:
Код компонента редактирования основной информации бота при этом выглядит так, full source »:
<script setup lang="ts">
import router from '@/router';
import { useBotStore } from '@/stores/currentBot';
const botStore = useBotStore()
...
</script>
<template>
<div id="bot-description-ctr" class="space-y-6">
<div id="bot-info" className="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- <div class="form-control w-full max-w-xs"> -->
<div class="form-control w-full">
<label class="label">
<span class="label-text">Name</span>
</label>
<input v-model="botStore.bot!.name" type="text" placeholder="Type name here"
class="input input-bordered w-full" />
</div>
...
</div>
</template>
Обратите внимание на botStore и input v-model=“botStore.bot!.name”: так для input element устанавливается редактирование поля бота (name).
Бот устанавливается в store при открытии формы редактирования бота, source »:
onBeforeMount(() => {
botStore.bot = undefined
if (isNewBot()) {
console.info("Initializing new bot...")
initNewBot()
console.info("New bot initialized")
} else {
console.info(`Loading bot; id=${botId}...`)
fetch('/api/bots/' + botId, {headers: authHeaders()})
.then((response) => {
if (response.ok) {
return response.json();
}
throw new Error(response.statusText);
})
.then((data: Bot) => {
botStore.bot = data
console.info(`Bot loaded {id: ${data.id}, name: ${data.name}}`)
})
.catch((err) => {
console.error(err)
router.go(0)
})
}
})
daisyui
Демонстрация мощи daisyui.
- Layout приложения выглядит так:
<div class="drawer">
<div class="drawer-content flex flex-col bg-base-200">
<Navbar />
<!-- Page content here -->
<RouterView />
</div>
</div>
Здесь используется компонент drawer
- Карточки ботов, source »
<div class="card bg-base-100">
<div class="card-body items-center text-center">
<div class="flex gap-4 flex-wrap overflow-x-auto">
<BotCardParrot />
<h2 class="card-title max-w-xs whitespace-nowrap">{{ bot.name }}</h2>
</div>
<div class="badge badge-outline badge-ghost">{{ lang(bot.language) }}</div>
<p class="">{{ bot.description }}</p>
<div class="card-actions justify-end">
<RouterLink :to="`/bots/${bot.id}/info`">
<button class="btn btn-primary">Edit</button>
</RouterLink>
<RouterLink :to="`/bots/${bot.id}/stats`">
<button v-if="bot.csvSaved" class="btn btn-primary">Stats</button>
</RouterLink>
</div>
</div>
</div>
Здесь используется компонент card »
Результат:
tabs, vue router
Вкладки реализованы с использованием Vue Router.
Форма бота выглядит так, source »:
<template>
<div id="form-card" class="card w-full lg:w-2/3 bg-base-100 shadow-xl p-6 space-y-6">
<h2 class="card-title">Bot</h2>
<!-- tab border full width: https://stackoverflow.com/a/76161483/827704 -->
<div id="tabs" v-if="botStore.bot!.id">
<div
class="tabs bg-[linear-gradient(theme(colors.base-300),theme(colors.base-300))] bg-bottom bg-no-repeat bg-[length:100%_1px]">
<RouterLink :to="{ name: 'editBot' }" class="tab tab-lifted"
:class="route.name?.toString() == 'editBot' ? 'tab-active' : ''">Edit bot</RouterLink>
<RouterLink v-if="botStore.bot!.csvSaved" :to="{ name: 'botApi' }" class="tab tab-lifted"
:class="route.name?.toString() == 'botApi' ? 'tab-active' : ''">API</RouterLink>
<RouterLink v-if="botStore.bot!.csvSaved" :to="{ name: 'botStats' }" class="tab tab-lifted"
:class="route.name?.toString() == 'botStats' ? 'tab-active' : ''">Stats</RouterLink>
</div>
<RouterView />
</div>
</div>
</template>
Каждая кнопка вкладки является RouterLink, а контент вкладки отображается в RouterView. Конкретный компонент, который отображается в RouterView задан в конфиге роутера, source »:
{
path: '/bots/:id',
name: 'bot',
component: BotView,
children: [
{
path: '',
name: 'editBotHome',
redirect: { name: 'editBot' },
},
{
name: 'editBot',
path: 'info',
component: EditBot,
},
{
name: 'botApi',
path: 'api',
component: BotApi,
},
{
name: 'botStats',
path: 'stats',
component: BotStats,
},
],
},
Благодаря глобальному state (см. раздел pinia), каждая вкладка открывает компонент с редактируемым в настоящий момент ботом.
json-server, vite = developer joy
Developer joy один из принципов проекта chipa ».
Он соблюдается в таких вещак как:
- Quarkus dev mode
- Simple SOLID microservices (see chipa nlp services)
- Using temporal to bind microservices
Демонстрация workflow разработки frontend в экосистеме Vite, Vue + json server:
keycloak
Для security приложения используется keycloak. Keycloak предоставляет библиотеку keycloak-js.
keycloak сохранен в pinia store.
Пример вызова защищенного API
opensource
Проект выложен на https://github.com/achernetsov/chipa-admin2