Добрый день. Меня зовут Тимофей, я фронт-тимлид в диджитал-продакшене ДАЛЕЕ. В данном цикле статей я поделюсь подходами и инструментами фронтенд-разработки на аутсорсе, которые помогут создать качественный продукт без кошмарного instant-legacy и значительно облегчат жизнь команде разработчиков и не только.

Типичные веб-приложения в аутсорс-командах разрабатываются, как правило, год-два. За это время не раз успевают смениться как разработчики, так и менеджеры с заказчиками. Однотипных проектов в агентстве сразу несколько,  разработчикам приходится переключаться между ними. Про отсутствие юнит-тестов на аутсорс-фронте, думаю, упоминать не нужно. Унифицировать процесс разработки в таких условиях не просто полезно — это нужно делать обязательно.

Расскажу, почему не стоит излишне усложнять архитектуру фронтенда, и дам примеры удобных и эффективных инструментов разработки с точки зрения DX (developer experience. Это важно) и дальнейшей поддержки.

Используй принцип KISS в своей архитектуре

Исторически сложилось так, что к бэку люди относятся серьёзнее, чем к фронту — мол, там же данные (!), и всё такое важное, а на фронте у вас только картинки и кнопки разноцветные. Поэтому подходы в разработке бэкенда более-менее стандартизированы: используются шаблоны построения архитектуры и взаимодействия между элементами, для них даже названия есть — MVC, MVP, и т.п.

С фронтом всё не так. Шаблонов и стандартов из коробки, которые пропагандировали бы крупные акулы вроде Laravel, нет. Люди начинают экспериментировать. 

Эксперименты обычно заключаются в том, что разработчики тянут на фронт подходы из бэкенда или используют молодые архитектурные решения. Например, Feature-Sliced Design (FSD), в рамках которого элементы распределяются на слои, слайсы и сегменты. Рекомендации по этому распределению есть, но это именно рекомендации — каждый волен использовать подход по своему усмотрению, что зачастую и происходит.

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

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

Если вдруг в команду пришел новый разработчик, который раньше работал по FSD, это не значит, что в новом проекте он разберётся быстро.

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

В аутсорсе с таким походом сложности возникнут еще быстрее. Как я уже упоминал, разработчики часто переключаются с проекта на проект или одновременно работают над несколькими приложениями. Если архитектура и инструменты отличаются, приходится тратить лишнее время на то, чтобы погрузиться и разобраться, как и что работает. 

Речь не только об одном FSD-подходе. Современные архитектурные решения для фронта, в которых нет жестких правил, в конце концов приведут к росту трудозатрат, а разработчикам будет сложнее. 

Как-то я писал приложение с нуля и тоже решил поэкспериментировать с моделями архитектуры. Сначала пробовал FSD на Vue, затем поставил vue-class-components и углубился в хардкор на TypeScript с Vue 2. В результате понял, что эти попытки не отвечают цели. Моя задача как разработчика — создать качественный продукт, который будет легко поддерживать и дорабатывать, а Vue 2 к тому времени уже был deprecated. 

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

Изобретать велосипед не нужно: если вы пишете проект на Vue.js или React, не стесняйтесь — используйте архитектуру, которую предполагают эти библиотеки из коробки. Все компоненты в одной папке, максимально плоская структура, никакой вложенности. Keep it simple, stupid (KISS). Таким образом вы создадите удобренную почву для теоретически возможного крупного развития проекта в будущем, когда он перейдет из стадии быстрой аутсорс-поделки в полноценный продукт. То есть основная идея такого подхода — сознательное соглашение на, возможно, отталкивающий «внешний» вид проекта (короче как папки выглядят в IDE; тайлвинда дальше, кстати, тоже касается), в угоду скорости и качества разработки. Папка components будет выглядеть примерно так:

Связанные компоненты сразу можно заметить по названию, базовые — по префиксу App, страницы — по суффиксу Page. Важно заметить необычный порядок слов в названиях, к примеру InputDefault, LayoutDefault — это нужно для создания групп компонентов в пределах одной директории, а очевидная идея про «положить инпуты в /inputs, страницы в /pages...» не имеет практического смысла, так как это — не архитектурный подход, как многие думают, а просто дополнительная вложенность.

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

Унифицируй проекты 

Строить однотипные проекты с точки зрения архитектуры — отличное решение, но есть инструменты, которые ещё больше упростят разработку. В качестве примера расскажу о TanStack Query и Tailwind.

Используй TanStack Query с микро-хранилищами клиентского состояния

TanStack Query — это бывший React Query. Раньше он работал только для React, но после ребрендинга стал официально доступен для Vue и других фреймворков. До этого для Vue был неофициальный адаптер vue-query, который неплохо справлялся с задачей.

Это единое хранилище серверного состояния, которое зачастую используется в связке с микро-хранилищами клиентского состояния, например Recoil, Zustand, Pinia. TanStack Query унифицирует подход к созданию рутинных вещей.

Подавляющее большинство приложений пишутся по шаблону REST API с точки зрения взаимодействия серверного и клиентского состояния. Намного реже некоторые заказчики-олигархи решают затянуть в проекты GraphQL или RPC.

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

Пример для понимания (я пойду all-in, допустим, против Redux). Нам нужно получить постраничный список пользователей. Даже проще — нам нужно получить плоский список избитых todo-items. При этом сделать это по всем канонам: и чтобы код был хороший, и чтобы оверхеда много не было.

// функция, тянущая данные с апи
const fetchTodoItems = () => {
 return api.getTodoItems()
}


// и дальше начинается наша фронтовая работа


// делаем "экшен"
// делаем "редусер"
// делаем "селектор"
// делаем "диспач экшена"
// тянем данные с помощью "селектора"


// ...поддерживаем этот код через пару-тройку лет

Мне нужны todo-items, черт возьми. При чем тут «диспачи» и «редюсеры»? Я не хочу заниматься хранением серверных данных — я хочу их получить и использовать. Не заниматься самоутверждением за счет «идеального контроля над данными», «чёткого и предсказуемого потока» и прочих «аргументов» за использование Redux. В продукте — пожалуйста, на аутсорсе — ни за что.

Использование TanStack Query упрощает такие задачи, потому что даёт возможность описать процесс одной строчкой кода. Такой код уже охватывает полный цикл работы с данными. После первого запроса система сама кладёт данные в кэш, при следующем запросе обновляет или выдает текущую информацию, в зависимости от настроенного таймаута. 

При этом нам вообще не важно, с какой страницы был совершен запрос. Прописанная схема является сквозной: данные будут доступны из любой части приложения, из коробки, в одну строчку.

// функция, тянущая данные с апи
const fetchTodoItems = () => {
 return api.getTodoItems()
}


// опять фронтовая UI-работа...


// делаем простейший хук-обертку.
// для большинства проектов подойдут дефолтные настройки
export const useTodoItems = () => useQuery('todo-items', fetchTodoItems)


const { data: todoItems } = useTodoItems()
// да. и все

Я хочу todo-items. Я говорю «дай мне todo-items». Я получаю todo-items. Как они кэшируются, обновляются, переиспользуются — меня не волнует. Захочу обновить — скажу «обнови» (вызову refetch), захочу избавиться — скажу «пометь как старье и удали, когда посчитаешь нужным» (вызову remove).

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

Сейчас TanStack Query не единственный в своем роде инструмент, альтернативы есть, например RTK Query, который позволяет Redux как-то жить на аутсорсе.

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

Используй Tailwind полностью

Раньше я, как и все фронтендеры, активно использовал частичный БЭМ: разделение сущностей в верстке на блок, элемент и модификатор.

БЭМ возможно и хорош, если его использовать правильно. Сразу пример: мне нужно добавить position: relative или display: flex на новый div — для этого по правилам я должен пойти в стили, придумать (да, иногда это тяжело) новое название, проследить чтобы это название, не дай бог, не конфликтовало с другими в проекте и написать одно CSS-свойство. Еще нужно всегда держать в голове принятый в данном проекте (какой же это тогда стандарт, черт возьми) стиль написания БЭМ-классов. Двойной ли дефис (--) или нижнее подчеркивание (_) — пойди разберись. 

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

Дальше разработчики начинают стрельбу по коленям и создают себе «помощников»: классы flex, relative, margin-auto, hidden; активно их используют, и, глядите — внезапно БЭМ уже не БЭМ, а смесь непонятно чего:

<section class="salary-screen base-screen display-flex relative">
 <div class="container">
   <div
     class="salary-screen__title mb-14.5 mobile:mb-10 text-center"
     v-html="data.title"
   />
 </div>
</section>

В следующем проекте им надоедает создавать этих помощников вручную и они докидывают Tailwind, думая, что это набор так им нужных «хелперов» и получается такое:

.ui-toggler {
 @media (max-width: theme('screens.mobile.max')) {
   width: 100%;
   @apply mb-8;
 }


 &__button {
   @media (max-width: theme('screens.mobile.max')) {
     @apply w-1/2;
   }
 }
}

Дизайн-переменные зачастую будут объявлены как в tailwind-конфиге, так и в css-файлах, какие же использовать при перекраске кнопки из синей в красную; какие из них актуальные? Правильно — никакие. Когда источников «правды», которой в данном случае является дизайн-система, несколько, у большинства разработчиков отношение к такому проекту стремительно катится на дно. «Да ладно, хуже уже не будет», — и вместо использования хотя бы какой-то переменной происходит вставка кода прямо из Figma для быстрого закрытия задачи.

Тут наступает пересменка, приходит другой человек, времени на то, чтобы разобраться, нет. В результате мы получаем ужас, три разных структуры в верстке: БЭМ, Tailwind и ручной код. Поможет только рефакторинг, но времени и ресурсов на него, конечно же, нет.

Выход из положения — использовать Tailwind для стилизации абсолютно всех элементов: 

const MyFancyButton = ({ children, className, ...attrs }) => {
 return (
   <button
     className={clsx(
       'hover:bg-pink absolute top-1/2 z-10 hidden h-[49px] w-[49px] -translate-y-1/2 items-center justify-center rounded-full bg-white transition-colors hover:text-white hover:shadow-transparent xl:flex',
       className
     )}
     {...attrs}
   >
     {children}
   </button>
 )
}

В последней на текущий момент версии появилась возможность использовать JIT-компиляцию для создания классов по типу h-[49px], mb-[12px], что позволяет использовать Tailwind повсеместно. Исключение будут составлять сложные тени, анимации и те вещи, которые в наборе Tailwind отсутствуют, что встречается довольно редко.

Разобраться в таком коде, на удивление, гораздо проще, чем в БЭМ. Все перед глазами, как и в случае с директорией components выше; стили внезапно (!) не протекают в другие места, не нужно придумывать названия и тащить вспомогательные средства. 

Сильно углубляться в преимущества использования Tailwind не стану — у автора библиотеки есть потрясающая статья на этот счет, написал он её ещё когда подобные utility-фреймворки использовали только два с половиной фрика на pet-проектах. Настоятельно рекомендую ознакомиться, там подробно описано все вышесказанное с огромным количеством примеров.

Эпилог

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

Приведенные в качестве примеров библиотеки не являются стандартом де-факто — мол, писать только так — но хорошо зарекомендовали себя в сообществе и стоят того, чтобы, как минимум, рассмотреть их как варианты. 

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

Комментарии (3)


  1. konclave
    22.12.2023 10:15

    Прочитав статью, я вспомнил, почему мне не понравилось работать с аутсорс разработкой. Когда-то я работал в стартапе и у нас не хватало ресурсов, чтобы доставлять фичи. Мы заказали часть проекта в аутсорс компании. В итоге, весь код, при первой же возможности весь код, который нам написали аутсорс разработчики, был переписан. Потому что с точки зрения выполнения задачи, придраться было не к чему – все работало, как и задумывалось. Но код был такой, что поддерживать это всё было совершенно невозможно.

    И читая про ваш подход "нормально будешь делать в продукте, а раз ты аутсорс, то делай так, чтобы отдать и забыть", я для себя понимаю, что заказывать разработку у вас бы не стал.


    1. ustyantsevxx Автор
      22.12.2023 10:15

      Вы, явно не поняв суть статьи про "совмещение хорошего качества кода и скорости разработки", вытянули из контекста фразы, внаглую их перефразировав и даже взяв в "кавычки" аки цитата. Затем на основе своих собственных надумок и неудачного опыта с командой сомнительного уровня (я так понимаю, прецедент единственный) сделали вывод.

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

      Если подкрепите свой пример фактическим сравнением уровня аутсорс-команды с которой вы работали с вашей собственной командой из стартапа - можно будет сделать объективные выводы.

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


  1. mdlufy
    22.12.2023 10:15

    Спасибо за статью!

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

    С фронтом всё не так. Шаблонов и стандартов из коробки, которые пропагандировали бы крупные акулы вроде Laravel, нет

    Как нет? Был AngularJs, который реализовал дизайн-паттерн MVVM. Angular 2 с MVC

    Опять таки, не стоит путать архитектуру, которую предоставляет фреймворк из коробки со способом организации кода в проекте на этом фреймворке, который действительно никто не навязывает. FSD, FBT, FBF - всего лишь паттерны организации кода

    это нужно для создания групп компонентов в пределах одной директории, а очевидная идея про «положить инпуты в /inputs, страницы в /pages...» не имеет практического смысла, так как это — не архитектурный подход, как многие думают, а просто дополнительная вложенность.

     Все компоненты в одной папке, максимально плоская структура, никакой вложенности.

    Это просто способ организации кода в проекте - folder by type. Такой подход неплохо работает на небольших проектах. При росте проекта уже тяжело становиться следить за кучей компонентов и лучше присмотреться к folder by feature, который позволяет выделить верхнеуровневые абстракции и разбить код на вертикальные слои

    Откладывать же принятие решения о выделении дополнительного уровне вложенности, как вы предлагаете, на самом деле только ухудшит читаемость даже на начальном этапе разработки, а тем более, когда уже есть 15-20 сущностей