Написать пост

Обзор Vue Composition API. Реальность оказалась сложнее

В рунете немало обзоров, посвященных Composition API. Зачастую авторы восхищены новым инструментом, но у него есть и минусы.

В рунете немало статей, посвященных Composition API. Зачастую авторы с восхищением описывают свой опыт взаимодействия с новым инструментом, уделяя недостаткам и подводным камням в лучшем случае пару-тройку абзацев. Приведу несколько цитат:

  • «Composition API – это мощнейшее решение, которое в полной мере разрешает проблемы Options API Vue 2 версии, а также предоставляет расширенные возможности в декларировании и управлении данными в фреймворке. Новый API позволяет облегчить реализацию сложных архитектурных решений в крупных и средних приложениях».
  • «Я очень взволнован этим новым API. Он убирает зуд, который у меня был в течение долгого времени, пытаясь применить принципы чистого кода в моих интерфейсных приложениях».
  • «Код, написанный с помощью нового API, лучше читается, что делает его более лёгким для понимания».
  • «Это, безусловно, самое ожидаемое изменение в Vue3. Оно должно помочь с организацией и возможностью многократного переиспользования кода компонентов».

Вот оно, новое слово в компонентостроении, решение всех наболевших проблем, сейчас наконец заживём…

Однако как часто это бывает, реальность оказалась сложнее. Мы активно использовали Composition API более полутора лет в довольно крупном enterprise приложении и мне есть что вам об этом рассказать.

Мы точно идем вперед? Или все новое это хорошо забытое старое

Зайдём издалека, что вообще такое фреймворки и зачем они нам нужны? Отличный ответ на этот вопрос дал Илья Климов в рамках курса по Vue:

Фреймворк инструктирует вас, как писать код.

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

До фреймворков разработчику предоставляли значительно больше возможностей влиять на систему. Однако зачастую дополнительная свобода приводила к увеличению проблем, о многих из которых мы в 2022 уже успели позабыть. Отчасти фреймворки появились как ответ на эту огромную неопределённость (= чрезмерную свободу) предыдущих более низкоуровневых решений (JQuery, Backbone, Vanila JS и т.д.). И Vue отлично справлялся с этой задачей, возможно даже лучше, чем конкуренты.

Чем же так плоха свобода в коде? Прежде всего тем, что ведёт к увеличению проектных знаний, а это, в свою очередь, мешает компании и проекту масштабироваться и развиваться. К примеру, чем больше проектных знаний, тем больше новому разработчику нужно времени на онбординг. Отдельно остро стоит вопрос качества этих знаний — повышается значимость решений отдельного человека, возрастает вероятность ошибок. В данном контексте cвобо­да действий — враг стандартизации и качества.

И с этой точки зрения Composition API — инструмент, увеличивающий свободу. Это движение против стандартизации, предложенной в Options API. Ниже приведу несколько примеров:

			<script setup>
  …
  const {...} = useFeature ();
  …
</script>

…

export function useFeature () {
  …
  const {...} = useEntity ();
  …
}

…

import {ref, onMounted, onUnmounted} from 'vue';
import {entityApi} from '@api/entityApi'; // обертка для запросов
import {addSocketEventListener, removeSocketEventListener} from '@socket'; // обертка для работы с WebSocket

export function useEntity () {
  const state = ref (null);

  async function onSocketEvent (data) {
    state.value = await entityApi.update (data.id);
  }

  onMounted ( () => addSocketEventListener ('socket-event', onSocketEvent));
  onUnmounted ( () => removeSocketEventListener ('socket-event', onSocketEvent));
  …
}
		

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

  • Вложенность composable. Ограничений по вложенности composable нет, в документации также отсутствуют рекомендаций на этот счёт.
  • Хуки жизненного цикла компонента доступны всем вложенным composable. Фактически мы обращаемся к данным из другого уровня (из самого верхнего, уровня самого компонента) абстракции, нарушается принцип одного уровня абстракции.
  • Работа с внешними данными во вложенных composable.

А когда это трио собирается вместе, получаем коктейль из сайдэффектов. Но на этом примеры не заканчиваются:

			<script>
export default {
  setup (_, ctx) {
    …
    const {...} = useFeature (ctx);
    …
  },
}
</script>

…

export function useFeature (ctx) {
  …
  const {...} = useSubfeature (ctx);
  …
}

…

export function useSubfeature (ctx) {
  …
  const {...} = useDoubleSubfeature (ctx);
  …
}

export function useDoubleSubfeature (ctx) {
  …
  function save () {
    ctx.emit (‘save’);
  }
  …
}
		

И вновь вложенность, вновь сложность кода возросла, вновь сайдэффекты цветут и пахнут. Что повлияло в данном случае?

  • Вложенные composable.
  • SetupContext и возможность пробрасывать его на любую глубину.
  • Emit события из вложенной use-функции. Сайдэффект, оказывающий влияние на весь компонентСтоит также добавить, что переиспользование use-функций из примеров выше в других компонентах может привести к кратному увеличению негативных эффектов.

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

Composition API подталкивает нас к созданию многофункциональных компонентов

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

  • Single responsibility principle. Чем больше у компонента responsibilities (зон ответственности, функций), тем сложнее им управлять и его расширять. К примеру, если мы решим хранить все данные в одном месте, компонент будет становиться все больше и больше, что впоследствии может привести к потере контроля.
  • F.I.R.S.T. Следует стремиться создавать сфокусированные (Focused), независимые (Independent), переиспользуемыме (Reusable), небольшие (Small) и тестируемые (Testable) компоненты или сокращенно FIRST компоненты. Многофункциональные компоненты зачастую большие, не сфокусированные и их трудно тестировать.

Сравним подходы в аспекте многофункциональных компонентов.

В Options API разрастающиеся опции намекали разработчику о том, что компонент следует декомпозировать. Чем больше функций выполнял компонент, чем толще становились опции, тем менее комфортно становилось с ним работать. Vue выполнял одну из своих важнейших задач — подталкивал разработчика к соблюдению стандартов (в данном случае к декомпозиции). Для нас проблемы начинались примерно с 500 строк — при достижении этой отметки мы сразу приступали к декомпозиции. Это правило позволило нам строить сложные интерфейсы из относительно небольших и простых компонентов-кирпичиков, каждый из которых имел 1–2 ответственности.

В Composition API разрастающийся setup намекает разработчику о том, что следует добавить новую use-функцию. То есть подталкивает нас к наделению компонента новыми responsibilities. Сколькими ответственностями можно наделить компонент перед тем, как он превратится в многофункциональный монстер-компонент? Точный ответ дать трудно, но по нашему опыту, сложность и связность компонента значимо возрастают начиная с 3–4 use-функций.

Пожалуй, стоит подкрепить вышесказанное примером:

			<script setup>
  const {updateA, stateA} = useFeatureA (); // stateA загружается при маунте (хук onBeforeMount)
  
  const {stateB} = useFeatureB (stateA); // происходит запрос на получение данных для stateB при маунте (хук onBeforeMount) и при изменении stateA (watch на одно из полей stateB) 

  const {saveС} = useFeatureC (updateA); // stateA обновляется при сохранении С
</script>
		

Благодаря разделению кода на use функции, мы близки к тому чтобы заблудиться в трёх соснах! При этом соблюдены все конвенции из документации vue. Скажу даже больше, нам никто не мешает добавить ещё одну фичу (в моей практике встречались компоненты с 5–6 фичами-use функциями), никаких соглашений этот шаг не нарушит. Трудно представить, насколько сильно может усложниться код при дальнейшем расширении компонента.

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

Component vs composable

Как писал выше, Options API подталкивает нас к декомпозиции большого компонента на маленькие, в то время как Composition API предлагает создавать новые use функции.

Попытаемся сопоставить эти два подхода к декомпозиции. Удобнее будет рассматривать компоненты как абстракции в шаблоне, а use-функции – абстракции в логике. Позволю себе громкое заявление: абстракции в логике строить труднее, чем абстракции в шаблоне.

Хорошие абстракции строить сложно. Сразу возникает куча вопросов: На основании чего нам строить абстракции? По фичам? По задачам? Или по сущностям? Что есть сущность? Как отделить одну сущность от другой? Как сформировать барьеры абстракции?

При создании composable разработчику нужно самостоятельно найти ответы на эти вопросы. Для принятия правильных решений необходимы компетенции и серьёзный опыт, которыми обладает далеко не каждый кодер. По сути, необходимы навыки, используемые при построении js-библиотек. Если же их нет, то начинается импровизация (часто с неблагоприятным исходом). Так было и у нас

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

Любой разработчик в состоянии на макете отделить один блок от другого, часто удаётся даже прикинуть схему взаимодействия между ними.

Условно в таблице есть header, есть body, есть строка, несколько типов ячеек и т. д. и дерево компонентов будет строиться похожим образом. Кроме того, интерфейс подталкивает разработчика создавать компоненты, к которым существует визуальный референс.

К примеру, разработчик вряд ли выдумает новый тип ячейки или решит вдруг таблицу делить не по строкам, а по произвольным группам ячеек. Источник истины в виде интерфейса значительно снижает неопределённость и улучшает восприятие абстракций.

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

Обзор Vue Composition API. Реальность оказалась сложнее 1

Отдельно стоит рассмотреть вопрос взаимодействия между абстракциями.

Правила взаимодействия между компонентами известны, они ограничены и задокументированы (как хорошие, так и плохие практики), довольно просты и прошли проверку временем. props и emits; слоты; сторы; provide/inject.

Правила взаимодействия между composable необходимо сформировать самостоятельно, ведь по сути, use-функции — это кастомные абстракции. При создании composable разработчику необходимо ответить на ряд вопросов:

  1. Можно ли менять стейт одного composable в другом composable? (В компонентах нельзя, для изменения стейта есть props и emits).
  2. Как быть с вложенными composable? Насколько глубокой может быть вложенность? (Компоненты можно беспрепятственно вкладывать друг в друга и группировать без сильного увеличения сложности).
  3. Как работать с данными из внешних источников? Какие сайдэффекты в composable допустимы? (Так как работать с сайдэффектами в компонентах приходится часто, то в большинстве проектов уже пришли к консенсусу по этому вопрос).
  4. Как при добавлении нового composable не слишком сильно увеличить связность и сложность? (Добавить новый компонент в разметку проще. Следуем общим правилам и стараемся не плодить многофункциональные компоненты).
  5. Какие функции будет брать на себя composable? (Для компонентов есть подсказка в виде интерфейсов) И чем больше use-функций, тем сложнее найти верные ответы.

Откуда корни растут?

Evan You не скрывает, что при создании Composition API вдохновлялся React хуками. В документации даже есть подраздел «Сравнение с React Hooks». За время существования хуков вышло несколько статей с критикой подхода. Обвиняют в том числе за чрезмерную свободу действий и, как следствие, спагетти-код.

Наши правила работы с Composition API

Для того чтобы избежать негативных последствий, описанных выше, мы в Yclients придерживаемся нескольких базовых правил работы с Composition API (перечень не является исчерпывающим и может быть расширен под ваш проект):

  • Не рекомендуется использовать watch во вложенных use-функциях. Предпочтительнее watch в корне setup функции (чем очевиднее сайдэффекты, тем лучше).
  • Не рекомендуется эмитить события из вложенных use-функций. Предпочтительнее эмитить события в корне setup функции.
  • Настоятельно не рекомендуется мутировать аргументы use-функции напрямую. Только через сеттеры. readonly и shallowReadonly приветствуются.
  • C осторожностью работать с источниками внешних данных в use-функциям. Работу с данными извне рекомендуется вынести в отдельную абстракцию. Лучше всего взаимодействовать с этой абстракцией в корне setup функции (чем очевиднее сайдэффекты, тем лучше).
  • С осторожностью использовать хуки жизненного цикла компонента во вложенных use-функциях. Помнить о том, что хуки компонента во вложенных composable — это сайдэффект.
  • Не рекомендуется передавать setupContext в use-функции. Если всё же решили пойти на этот шаг, то лучше передавать только ту часть setupContext, которая необходима этой use-функции. Также настоятельно не рекомендуется использовать getCurrentInstance.
  • Всё взаимодействие между composable рекомендуется производить в корне setup функции.
  • Настоятельно рекомендуется избегать вложенности в composable. Чем меньше вложенность, тем лучше. Предпочитать композицию use-функции вложенности.

И самое главное: декомпозиция через компоненты предпочтительнее декомпозиции через composable!

Если не получается решить проблему с помощью декомпозиции компонента, переходим к декомпозиции логики через composable

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

Так ли Composition API необходим?

Поделюсь инсайдами с интервью на позицию frontend developer к нам в Yclients. Первая часть собеседования — беседа с кандидатом, целью которой является оценка общего кругозора разработчика. Одним из наиболее популярных топиков является как раз Composition API. Сначала я прошу кандидата поделиться его личным мнением и опытом использования. Чаще всего это восторг и пересказ всё тех же хвалебных статей. В ответ от меня следует вопрос: “Какие главные проблемы предыдущего подхода решает Composition API?”.

Большинство кандидаты в этот момент немного теряются.

Не стану вас томить и сразу перейду к ответу, который дал Evan You в одном из обсуждений на GitHib:

  • Большие компоненты (длиной в сотни строк) инкапсулирующие несколько логических задач.
  • Потребность в переиспользовании логики между несколькими компонентами.

И я с ним полностью согласен, в этих аспектах Composition API действительно показывает себя с лучшей стороны. А теперь зададимся вопросом, как часто мы сталкиваемся с проблемами выше? В нашей кодовой базе под эти критерии подходят лишь 10–15% от всех компонентов, что также соответствует опыту Anthony Gore:

«Я просмотрел Vue 2 приложения, над которыми работал ранее (в основном небольшие), и обнаружил, что лишь 10–20% компонентов являлись многофункциональными или использовали миксины и могли бы выиграть от Composition API».

Стоит ли ради 10–20% кодовой базы переходить на новый подход? Не приведёт ли этот переход к ухудшению качества кода и усложнению логики (как это произошло в нашем случае)?

Итоги

Использовать Composition API можно и даже в некоторых случаях нужно, однако стоит помнить, что этот подход значительно более требователен к разработчику, нежели Options API. В неумелых руках этот инструмент способен принести больше вреда, чем пользы. Для снижения негативных последствий рекомендуется сформировать правила работы с Composition API. Кроме того, проблемы, для решения которых создавался Composition API, встречаются не так часто, как может показаться на первый взгляд.

И не бойтесь использовать Options API, он живее всех живых:

–  Станет ли Options API deprecated?

–  Нет, мы не планируем этого делать.

Следите за новыми постами
Следите за новыми постами по любимым темам
12К открытий12К показов