В этой статье будут изложены основные идеи и показаны простые примеры для грамотной организации, скажем так — «репликационного масштабирования» проектов на фронтенде. То есть, само понятие масштабирования здесь будет рассматриваться скорее с той точки зрения и в одном из смыслов как это понимает бизнес, но, при этом, речь будет пойдет именно о технической стороне процесса, правда, сугубо в контексте браузерной клиентской части информационных систем. Ближе к реальной ситуации: предположим что ваша компания разрабатывает, условно — некий OLAP-продукт, и перед вами как фронтенд-разработчиком ставят задачи по развертыванию и поддержке более или менее сходных новых проектов фронтенда для самых разных клиентов. После скандальной критической статьи о, имхо, сомнительных дурных современных подходах и тенденция в верстке веб-интерфейсов — моя карма на Хабре, наконец-то упала ниже нуля, а я, если честно, не очень хорошо понимаю правила игры, увидят ли эту статью читатели… Но, с другой стороны, готов изложить все просто «в стол», так как считаю что лучшая мотивация для написания чего либо — это если «просто очень хочется написать», сформулировать, прежде всего — для себя самого.
Эта статья логично продолжает тематику первой статьи о модулях позволяющих сделать разработку фронтенда качественнее и эффективнее. Но если в первом материале речь шла, прежде всего, об замечательном атомарном тренде в вебдизайне и простом надежном способе доставки его в код компонентных фреймворков с помощью препроцессоров, построении простой кастомной библиотеки UI-компонент для единообразного оформления разных проектов, то новый пример станет немного сложнее — хочется сосредоточиться уже не на «внешних», «оформительских» моментах, а на функциональных и организационных. Для наглядной демонстрации практического применения изложенных в статье идей снова написаны примеры: небольшой модуль-библиотека (и документация к нему), а также использующий его проект, на этот раз с более актуальным стеком Vue3+TypeScript/Vuex4/VuePress2. В отличие от более примитивной либы из первой статьи, этот модуль:
-
Использует хранилище, то есть содержит состояние
-
Может запускаться в полноценном режиме разработки, как будто это собственно уже сам конечный проект
-
Поддерживает темизацию и локализацию
Пример модуля содержит совсем немного компонент и документация на новой версии VuePress, в отличии от первой версии модуля, не кастомизируется под фирменный стиль который предоставляет сама библиотека. Так сделано не только по причине лени и экономии времени, но, прежде всего, потому что кажется излишним — то что призваны продемонстрировать примеры — этого совсем не требует.
Зачем?
Пилите вы и без того малыми ресурсами фронтенд для некоего OLAP-продукта и поначалу у вашей фирмы всего один заказчик — все идет нормально. Но потом вдруг появляется еще один клиент и руководство требует от вас, конечно же, максимально быстро сделать еще один проект — «точно такой же, но немного другой». Что вы будете делать в этом случае? По опыту, особенно если как обычно «нужно еще вчера», а «рук все время не хватает» — вы просто скопируете легаси содержимое первого репозитория в новый. Со всеми его недостатками и недоработками.
Наверное понятно что будет происходить дальше:
Выражающий почти одно и тоже код в репозиториях начнет «разъезжаться», «расползаться». Важные фиксы с высокой вероятностью станут попадать только в один репо. А если вы будете стараться следить за этим — вам придется уныло доставлять одно и тоже в два разных места «ручками». Новый функционал — точно также. А если разработчиков несколько, проекты пилятся разными составами? А если проекта уже три, четыре?… Мрак, хаос и отчаяние…
Очевидно, что все работы на фронте если проектов основанных на одном визуальном языке (то есть в идеале — с почти полностью сходной кодовой базой) больше одного — должны вестись через единое универсальное решение-модуль. Только в этом случае можно говорить о какой-то эффективности и переиспользовании — фирменного стиля, дизайна и верстки . Но это как раз о проблеме которая решалась в первой статье — «модулем-библиотекой статичных UI-элементов» — «вьюх»:
Но «некий OLAP-продукт» скорее всего и на фронтенде требует более сложных компонент, чем просто получающих данные и модификаторы состояния по пропсам. Поэтому сама архитектура дочерних проектов в этом случае будет далеко не идеальной. Нам придется добавлять более сложные компоненты — сообщающиеся с хранилищем или запрашивающие данные с бэкенда в сами дочерние проекты, что по сути дела по-прежнему будет являться дублированием и будет по-прежнему сильно затруднять рефакторинг и модификацию, доставку новых фич:
Модуль-библиотека с состоянием, темезацией, локализацией, документацией и режимом разработки (на Vue3+TS/Vuex4/VuePress2/i18n)
Для решения этих проблем вы можете создать более продвинутый модуль-библиотеку:
Вот теперь, наконец-то можно расслабиться и смело смотреть в будущее. Такое решение позволяет надеяться на то что вы действительно сможете эффективно поддерживать большое количество схожих проектов малыми силами, при обычной нехватке рабочих рук, светлых голов, трудовых ресурсов. Кроме того — вы сможете быть уверены что код во всех ваших проектах оптимальный. Все изменения и улучшения которые необходимо распространить по всем копиям системы будут отправляться строго в одно единственное место:
Мы должны воспринимать как продукт — прежде всего сам модуль. И поэтому он должен обладать всей необходимой общей функциональностью которая затребована от вашей системы, то есть, вероятно — содержать хранилище. Также необходимо иметь возможность запустить и тестировать всю кухню как будто это конечный проект — и поэтому вероятно ей будет нужен собственный роутер. Хранилище мы экспортируем в дочернии проекты, а роутер — нет (так как роутинг реального проекта и для разработки-тестирования центрального ядра — библиотеки — разные сущности). Главная функция библиотеки — предоставление фирменного стиля, компонент и всего специфического функционала. Единственная в идеале функция дочернего проекта — запросы к бэкенду на видах (pages) и проксирование полученных данных в основные компоненты модуля.
Это была мотивационная часть, сейчас адекватно будет перейти к простому примеру реализации. Чтобы наглядно увидеть различия между модулем и использующим его проектом стоит взглянуть на их главные файлы, точки входа:
@/src/main.ts библиотеки
import { App } from 'vue'; import { createApp } from 'vue'; import { createI18n } from 'vue-i18n'; import store, { key } from './store'; import { createRouter, createWebHistory } from 'vue-router'; // UI Components import * as components from './components'; // Dev and test components import Development from './Development.vue'; import TestComponent from './components/TestComponent/TestComponent.vue'; // Constants import { LANGUAGES, MESSAGES } from '@/utils/constants'; // Localization const i18n = createI18n({ legacy: true, locale: store.getters['layout/language'] ? store.getters['layout/language'] : LANGUAGES[0].name, fallbackLocale: LANGUAGES[0].name, messages: MESSAGES, }); // UI Components library with store and localization const ComponentLibrary = { // eslint-disable-next-line @typescript-eslint/explicit-module-boundary-types install(app: App) { // localization app.use(i18n); // store app.use(store, key); // components for (const componentName in components) { // eslint-disable-next-line @typescript-eslint/no-explicit-any const component = (components as any)[componentName]; app.component(component.name, component); } }, }; // ATTENTION! Set to true if you want // to develop a module (not documentation) // and false before publishing for use in projects const isDevelopmentModuleMode = false; if (isDevelopmentModuleMode) { console.log('Start development module!'); // eslint-disable-next-line @typescript-eslint/no-explicit-any const routes: any = [ { path: '/', name: 'TestComponent', component: TestComponent, }, { path: '/route/:id', name: 'TestRoute', component: () => import( /* webpackChunkName: "TestRoute" */ './components/TestRoute/TestRoute.vue' ), }, { path: '/:catchAll(.*)', name: 'NotFound', component: () => import( /* webpackChunkName: "NotFound" */ './components/NotFound/NotFound.vue' ), }, ]; const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes, }); createApp(Development).use(i18n).use(store, key).use(router).mount('#app'); } export default ComponentLibrary;
@/src/main.ts проекта
import { createApp } from 'vue'; import App from './App.vue'; import ComponentLibrary from 'ui-library-starter-2'; import 'ui-library-starter-2/dist/ui-library-starter-2.css'; import { createRouter, createWebHistory } from 'vue-router'; import Home from './views/Home.vue'; // eslint-disable-next-line @typescript-eslint/no-explicit-any const routes: any = [ { path: '/', name: 'Home', component: Home, }, { path: '/route/:id', name: 'Test', component: () => import(/* webpackChunkName: "TestRoute" */ './views/Test.vue'), }, { path: '/:catchAll(.*)', name: 'NotFound', component: () => import(/* webpackChunkName: "NotFound" */ './views/NotFound.vue'), }, ]; const router = createRouter({ history: createWebHistory(process.env.BASE_URL), routes, }); export default router; createApp(App).use(ComponentLibrary).use(router).mount('#app');
Для того чтобы запустить режим разработки нужно выставить флаг isDevelopmentModuleMode
в значение true
. А перед отправкой модуля на npm — переключить его обратно. Это, мягко говоря, не очень изящно, но как сделать лучше — я пока не придумал. Если у этой статьи будут читатели — может кто-нибудь подскажет более красивое решение.
Темизация
Очень часто может оказаться что очередной клиент «хочет кнопочки другого цвета». Сложно поверить, но может даже встретится реальный кейс (мне арт-директор сказал) когда темы всех проектов должны быть доступны в одном интерфейсе. Поэтому я организовал возможность простого добавления и простого переключения между любым количеством тем, каждая с двумя режимами (дневной и ночной). Переменные препроцессора предоставляют атомы единственной основной дефолтной темы:
@/src/stylus/utils/_variables.styl
// Palette ////////////////////////////////////////////////////// $colors = { cat: #fed564, dog: #8bc24c, bird: #7e746e, wood: #515bd4, stone: #ffffff, sea: #13334c, sky: #0d2233, ball: #b1b1b1, rain: #efefef, } // Dependencies colors $colors["text"] = $colors.sky $colors["header"] = $colors.stone $colors["content"] = $colors.rain $colors["placeholder"] = rgba($colors.sea, $opacites.pop)
Добавление новых тем происходит в константах javascript — объект конкретного режима темы должен содержать поля с именами повторяющими набор атомов в препроцессоре:
@/src/utils/constants.ts
export const THEMES: TConfig = { theme1: 'theme1', theme2: 'theme2', }; export const MODES: TConfig = { mode1: 'light', mode2: 'dark', }; // Design constants ////////////////////////////////////////////////////// export const DESIGN: TConfig = { V: '1.0.0', BREAKPOINTS: { tablet: 768, desktop: 1025, }, THEMES: { [THEMES.theme1]: { // Light [MODES.mode1]: { // Palette cat: '#fed564', dog: '#8bc24c', bird: '#fd5f00', wood: '#515bd4', stone: '#ffffff', sea: '#13334c', sky: '#dddddd', ball: '#b1b1b1', rain: '#efefef', // Dependencies colors text: '#0d2233', header: '#ffffff', content: '#efefef', }, // Dark [MODES.mode2]: { // Palette cat: '#fed564', dog: '#8bc24c', bird: '#fd5f00', wood: '#515bd4', stone: '#ffffff', sea: '#13334c', sky: '#dddddd', ball: '#b1b1b1', rain: '#efefef', // Dependencies colors text: '#ffffff', header: '#163C59', content: '#0d2233', }, }, [THEMES.theme2]: { // Light [MODES.mode1]: { // Palette cat: '#fd5f00', dog: '#8bc24c', bird: '#fed564', wood: '#515bd4', stone: '#ffffff', sea: '#3A0061', sky: '#f9f9f9', ball: '#b1b1b1', rain: '#efefef', // Dependencies colors text: '#1F0033', header: '#ffffff', content: '#efefef', }, // Dark [MODES.mode2]: { // Palette cat: '#fd5f00', dog: '#8bc24c', bird: '#fed564', wood: '#515bd4', stone: '#ffffff', sea: '#3A0061', sky: '#f9f9f9', ball: '#b1b1b1', rain: '#efefef', // Dependencies colors text: '#ffffff', header: '#5D009C', content: '#1F0033', }, }, }, };
Теперь можно использовать Custom Properties c соответствующими именами, после переменных препроцессора остающихся в качестве фоллбэка:
.selector color $colors.text color var(--text)
Потому как в лейауте:
@/src/components/Layout/Layout.vue
// ... <script> import { defineComponent, computed, onBeforeMount, watch } from 'vue'; import { useStore } from '../../store'; import { DESIGN, THEMES, MODES } from '../../utils/constants'; import LangSwitch from './LangSwitch.vue'; import Menu from '../Menu'; export default defineComponent({ name: 'Layout', components: { LangSwitch, Menu, }, setup() { const store = useStore(); let toggleLayout; let toggleMode; let toggleTheme; let setThemeOrMode; const isMenuOpen = computed(() => store.getters['layout/isMenuOpen']); const theme = computed(() => store.getters['layout/theme']); const mode = computed(() => store.getters['layout/mode']); toggleLayout = () => { store.dispatch('layout/setLayout', { field: 'isMenuOpen', value: !isMenuOpen.value, }); }; toggleMode = () => { store.dispatch('layout/setLayout', { field: 'mode', value: mode.value === MODES.mode1 ? MODES.mode2 : MODES.mode1, }); }; toggleTheme = (theme) => { store.dispatch('layout/setLayout', { field: 'theme', value: theme, }); }; watch( () => store.getters['layout/mode'], () => { setThemeOrMode(); }, ); watch( () => store.getters['layout/theme'], () => { setThemeOrMode(); }, ); setThemeOrMode = () => { for (const color in DESIGN.THEMES[theme.value][mode.value]) { document.documentElement.style.setProperty( `--${color}`, DESIGN.THEMES[theme.value][mode.value][color], ); } }; onBeforeMount(() => { setThemeOrMode(); }); return { THEMES, MODES, isMenuOpen, mode, theme, toggleLayout, toggleTheme, toggleMode, }; }, }); </script> // ...
Локализация
А вот локализация мне не совсем нравится. Я прикрутил ее до кучи в последний момент, так как такая возможность кажется очень важной-полезной в свете остальных качеств, целей и задач разработки. Но то что все переводы дочерних проектов должны скопом лежать в константах либы — кажется весьма сомнительным и не оптимальным. С другой стороны — у меня пока не было возможности проработать и улучшить это в реальной ситуации — конкретные проекты по мотивам которых написана статья и примеры — используют только один язык. У любой реализации всегда можно найти несовершенные моменты и точки роста.
Выводы которые желательно сделать в конце статьи на Хабре )
Все дизайнеры и разработчики фронтенда привычно используют различные сторонние готовые библиотеки и модули. Но существуют реальные коммерческие ситуации когда подобный модуль просто необходимо написать и внедрить самому. Бизнес и руководство, менеджеры всегда стремятся любыми способами удовлетворить клиента, часто в условиях жесткой нехватки ресурсов. Ваше качество жизни на работе в этих ситуациях напрямую зависит от того насколько качественными, аккуратными, оптимальными будут подходы и архитектуры которые вы используете на своих боевых проектах. Творите!
ссылка на оригинал статьи https://habr.com/ru/post/647179/