Как стать автором
Обновить

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

Время на прочтение9 мин
Количество просмотров4.5K

В этой статье будут изложены основные идеи и показаны простые примеры для  грамотной организации, скажем так — «репликационного масштабирования» проектов на фронтенде. То есть, само понятие масштабирования здесь будет рассматриваться скорее с той точки зрения и в одном из смыслов как это понимает бизнес, но, при этом, речь пойдет именно о технической стороне процесса, правда, сугубо в контексте браузерной клиентской части информационных систем. Ближе к реальной ситуации: предположим что ваша компания разрабатывает, условно — некий OLAP-продукт, и перед вами как фронтенд-разработчиком ставят задачи по развертыванию и поддержке более или менее сходных новых проектов фронтенда для разных заказчиков. После скандальной критической статьи о, имхо, сомнительных дурных современных подходах и тенденция в верстке веб-интерфейсов — моя карма на Хабре, наконец-то упала ниже нуля, а я, если честно, не очень хорошо понимаю правила игры, увидят ли эту статью читатели… Но, с другой стороны, готов изложить все просто «в стол», так как считаю что лучшая мотивация для написания чего-либо — это если «просто очень хочется написать», сформулировать, прежде всего — для себя самого.

Эта статья логично продолжает тематику первой статьи о модулях позволяющих сделать разработку фронтенда качественнее и эффективнее. Но если в первом материале речь шла, прежде всего, об замечательном атомарном тренде в вебдизайне и простом надежном способе доставки его в код компонентных фреймворков с помощью препроцессоров, построении простой кастомной библиотеки UI-компонент для единообразного оформления разных проектов, то новый пример станет немного сложнее — хочется сосредоточиться уже не на «внешних», «оформительских» моментах, а на функциональных и организационных. Для наглядной демонстрации практического применения изложенных в статье идей снова написаны стартеры-песочницы: небольшой модуль-библиотекадокументация к нему), а также использующий его проект, на этот раз с более актуальным стеком Vue3+TypeScript/Vuex4/VuePress2. В отличие от более примитивной либы из первой статьи, этот модуль:

  • Использует хранилище, то есть содержит состояние

  • Может запускаться в полноценном режиме разработки, как будто это собственно уже сам конечный проект

  • Поддерживает темизацию и локализацию

Пример модуля содержит совсем немного компонент и документация на свежей версии VuePress, в отличие от первой версии модуля, не кастомизируется под фирменный стиль который предоставляет сама библиотека. Так сделано не только по причине лени и экономии времени, но, прежде всего, потому что кажется излишним — то что призваны продемонстрировать примеры — этого совсем не требует.

Зачем?

Пилите вы и без того малыми ресурсами фронтенд для некоего OLAP-продукта и поначалу у вашей фирмы всего один заказчик — все идет нормально. Но потом вдруг появляется еще один клиент и руководство требует от вас, конечно же, максимально быстро запустить еще один проект — «точно такой же, но немного другой». Что вы будете делать в этом случае? По опыту, особенно если как обычно «нужно еще вчера», а «рук все время не хватает» — вы просто скопируете легаси содержимое первого репозитория в новый. Со всеми его недостатками и недоработками.

Наверное понятно что будет происходить дальше:

Выражающий почти одно и тоже код в репозиториях начнет «разъезжаться», «расползаться». Важные фиксы с высокой вероятностью станут попадать только в один репо. А если вы будете стараться следить за этим — вам придется уныло доставлять одно и тоже в два разных места «ручками». Новый функционал — точно также. А если разработчиков несколько, проекты пилятся разными составами? А если проекта уже три, четыре?... Мрак, хаос и отчаяние…

Очевидно, что все работы на фронте если проектов основанных на одном визуальном языке (то есть, в идеале — с почти полностью сходной кодовой базой) больше одного — должны вестись через единое универсальное решение-модуль. Только в этом случае можно говорить о какой-то эффективности и переиспользовании — фирменного стиля, дизайна и верстки. Но это как раз о проблеме которая решалась в первой статье — «модулем-библиотекой статичных UI-элементов» — «вьюх»:

Но «некий OLAP-продукт» скорее всего и на фронтенде требует более сложных компонент, чем просто получающих данные и модификаторы состояния по пропсам. Поэтому сама архитектура дочерних проектов в этом случае будет далеко не идеальной. Нам придется добавлять более сложные компоненты — сообщающиеся с хранилищем или запрашивающие данные с бэкенда в сами дочерние проекты, что, по сути, дела по-прежнему будет являться дублированием, будет по-прежнему сильно затруднять рефакторинг и модификацию, доставку новых фич:

Модуль-библиотека с состоянием, темезацией, локализацией, документацией и режимом разработки (на Vue3+TS/Vuex4/VuePress2/i18n)

Для решения этих проблем вы можете построить более продвинутый модуль-библиотеку:

Вот теперь, наконец-то можно расслабиться и смело смотреть в будущее. Такое решение позволяет надеяться на то, что вы действительно сможете эффективно поддерживать большое количество схожих проектов малыми силами, при обычной нехватке рабочих рук, светлых голов, трудовых ресурсов. Кроме того, вы сможете быть уверены что код во всех ваших проектах оптимальный. Все изменения и улучшения которые необходимо распространить по всем копиям системы будут отправляться строго в одно единственное место:

Мы должны воспринимать как продукт — прежде всего сам модуль. И поэтому, он должен обладать всей необходимой общей функциональностью которая затребована от вашей системы, то есть, вероятно — содержать хранилище. Также, необходимо иметь возможность запустить и тестировать всю кухню как будто это конечный проект — и поэтому, вероятно, ей будет нужен собственный роутер. Хранилище мы экспортируем в дочернии проекты, а роутер — нет (так как роутинг реального проекта и для разработки-тестирования центрального ядра — библиотеки — разные сущности). Главная функция библиотеки — предоставление фирменного стиля, компонент и всего специфического общего функционала. Единственная [в идеале] функция дочернего проекта — запросы к бэкенду на видах роутера и проксирование полученных данных в основные компоненты модуля. 

Это была мотивационная часть, сейчас адекватно будет перейти к простому примеру реализации. Чтобы наглядно увидеть различия между модулем и использующим его проектом стоит взглянуть на их главные файлы, точки входа:

@/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

Добавление новых тем происходит в константах TypeScript — объект конкретного режима темы должен содержать поля с именами повторяющими набор атомов в препроцессоре:

@/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>

// ...

Локализация

А вот локализация мне не совсем нравится. Я прикрутил ее до кучи в последний момент, так как такая возможность кажется очень важной-полезной в свете остальных качеств, целей и задач разработки. Но то что все переводы дочерних проектов должны скопом лежать в константах либы — кажется весьма сомнительным и не оптимальным. С другой стороны — у меня пока не было возможности проработать и улучшить это в реальной ситуации — конкретные проекты по мотивам которых написана статья и примеры — используют только один язык. У любой реализации всегда можно найти несовершенные моменты и точки роста.

Выводы, которые желательно сделать в конце статьи на Хабре :)

Все дизайнеры и разработчики фронтенда привычно используют различные сторонние готовые библиотеки и модули. Но существуют реальные коммерческие ситуации когда подобный модуль просто необходимо написать и внедрить самому. Бизнес и руководство, менеджеры всегда стремятся любыми способами удовлетворить клиента, часто в условиях жесткой нехватки ресурсов. Ваше качество жизни на работе в этих ситуациях напрямую зависит от того насколько качественными, аккуратными, оптимальными будут подходы и архитектуры которые вы используете на своих боевых проектах. Творите!

Теги:
Хабы:
Всего голосов 2: ↑2 и ↓0+2
Комментарии6

Публикации

Истории

Работа

Веб дизайнер
50 вакансий
React разработчик
80 вакансий

Ближайшие события

27 августа – 7 октября
Премия digital-кейсов «Проксима»
МоскваОнлайн
28 сентября – 5 октября
О! Хакатон
Онлайн
3 – 18 октября
Kokoc Hackathon 2024
Онлайн
10 – 11 октября
HR IT & Team Lead конференция «Битва за IT-таланты»
МоскваОнлайн
25 октября
Конференция по росту продуктов EGC’24
МоскваОнлайн
7 – 8 ноября
Конференция byteoilgas_conf 2024
МоскваОнлайн