Модульный фронтенд для репликационного масштабирования или как перестать копировать репозитории целиком

В этой статье будут изложены основные идеи и показаны простые примеры для  грамотной организации, скажем так — «репликационного масштабирования» проектов на фронтенде. То есть, само понятие масштабирования здесь будет рассматриваться скорее с той точки зрения и в одном из смыслов как это понимает бизнес, но, при этом, речь будет пойдет именно о технической стороне процесса, правда, сугубо в контексте браузерной клиентской части информационных систем. Ближе к реальной ситуации: предположим что ваша компания разрабатывает, условно — некий 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/

Добавить комментарий

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