Сервисная архитектура во Vue 2 | Какие собственно варианты?

от автора

Да-да, я знаю, Vue 3 находится в stable-версии, и даже Nuxt наконец-то обновился. Но именно 3-й Vue с его provider / inject подтколкнул меня к поиску решения о том, как можно удобно инкапсулировать бизнес-логику во Vue 2.

Введение

Начнем с вопроса «Почему нельзя просто обновиться до Vue 3?». Все просто, некоторые проекты обладают таким уровнем связанности с другими проектами, что это невозможно сделать быстро. Где-то возможно используются специфичные пакеты, которые были написаны только под 2-ю версию Vue. Где-то разработчиков устраивает Vue 2, а даже если бы они и перешли на Vue 3, они продолжали бы писать на Options API (скажу сразу, я не проверяла свое решение на Vue 3, так что не уверена сработает ли такой подход там, но вообще должен).

Как можно инкапсулировать логику

На просторах интернета и участвуя в вопросах, задаваемых в чатах (по типу «Vue.js — русскоговорящее сообщество»), да и просто работая, чаще всего я видела несколько решений. Давайте разберем их плюсы и минусы.

Вынести во Vuex/Pinia/Другой стейт менеджер

На удивление один из самых популярных ответов, как только нужно делиться какими-то данными — вынеси в стор.

Доходит до смешного, человек спрашивал как можно передать строку поиска из компонента в соседний (у них общий родитель). Ему накинули несколько решений, и человек выбрал Vuex, потому что на его взгляд это проще.

Другой пример, на проекте я видела страницу, которая сохраняла временные данные форм (и в какой-то мере кешировала на сессию) с помощью.. Vuex. Я думаю, нет смысла объяснять подробно чем это чревато, но багов там связанных с этим было много, мне пришлось полностью переделать эту страницу.

Pros

  • Часто советуют, легко найти помощь

  • Использование vuex интуитивно понятно

  • Возможно внушает чувство безопасности

  • В целом легко читается, понятно откуда приходят данные

  • Можно получить только те данные, которые тебе нужны в конкретном компоненте

Cons

  • Вызывает баги, если пытаться хранить временные данные (которые должны чиститься, или меняться в зависимости от контекста)

  • Невозможно переиспользовать логику (модуль один на сайт, от этого ты никуда не денешься, дополнительный экземпляр не создашь) только если не сделать 10 одинаковых модулей

  • Используя недолго эту логику, она все равно останется в памяти, хоть и больше не требуется

  • Данные не защищены, любой компонент может изменить их с помощью прямого обращения к state

Миксины

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

Pros

  • опять же интуитивно понятно, выглядит как кусок компонента

  • другого способа интегрировать общий хук, или фреймворко-зависимые элементы особо нет

Cons

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

  • Легко затереть функциональность миксина, используя случайно те же имена свойств/методов

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

  • Невозможно расшарить данные (только если не положить их во vuex)

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

  • Выбрать какие методы/свойства тебе нужны нельзя

Provide / Inject

Это уже не самый популярный способ, не все знают про эту опцию. Многие знают про то, что это есть во Vue 3, но во Vue 2 provide появился еще в версии 2.2.0. Подробнее можно почитать по ссылке.

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

Pros

  • Позволяет передавать данные на любую глубину

  • Можно выбрать, что ты будешь инъектить

Cons

  • Требует общего родителя, в худшем случае придется делать это на странице или в App что засоряет код

  • Таким образом не получится переиспользовать код, это больше про распространение данных

  • Если сделать экземпляр класса (каким-то образом, я не пробовала), то ты его себе заберешь целиком, нет возможности выбрать, что тебе надо

  • Не интуитивно и не очевидно, что будет происходить с реактивностью, где менять эти данные, и вообще вызывает много вопросов

Почему я упомянула provide/inject в начале

Когда я писала на Vue 2, у меня даже примерной идеи не было, как бы можно было инкапсулировать логику. Мне казалось, что в том формате, как мы делаем компоненты, это будет по-любому неудобно, непонятно, и как-то костыльно.

Vue 3 с Composition API открыл для меня новую веху, там можно сделать экземпляр класса, полностью обернуть его в reactive, сделать provide прям на этапе создания приложения (т.е. в index.js условном, что было понятно).

Но и это решение меня не до конца устраивало, мне хотелось сделать это как-то по-другому. Пока я об этом думала, я перешла на другой проект, который написан на Vue 2, и это подтолкнуло меня к поиску решения.

Вынести в функции

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

Pros

  • Легко использовать, импортируешь функцию и используешь

  • Знакомо и узнаваемо

Cons

  • Для связанного процесса приходится передавать данные из одной функции в другую, что ухудшает читабельность

  • Если пытаешься сохранить данные в какой-то переменной вне функций, не будет ясности, что там находится

Глобальные переменные

Во Vue 2 я частенько записывала созданный мной класс, хранящий экземпляр Axios, в глобальные переменные. Это довольно удобно для модуля, который используется повсеместно.

Pros

  • Удобное использование через this

  • Можно в глобальную переменную записать экземпляр класса, что увеличивает ваши возможности

Cons

  • Даже если записать экземпляр класса, то он будет один

  • Непонятно, будут ли данные реактивными, не совсем очевидно

  • Использование через this удобно для тех, кто знает, какие данные в проекте глобальные. Для остальных придется еще догадаться, где искать эту запись, и что же кроется под переменной.

Вынести в класс

Кажется самым удобным методом, но те статьи, которые я находила, предлагали либо сделать singleton, либо делать статические методы и встраивать их подобным образом

methods: {   someAlias() {     Class.someMethodFromClass();   } }

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

Поэтому я решила создать это решение сама.

Вводные

Хочется, чтобы можно было создать сервис, который

  1. Создавал экземпляр только по запросу, позволял создать несколько экземпляров, мог удалить нужный экземпляр

  2. Было понятно откуда пришли данные или методы, чтобы при поиске по файлу названия можно было найти источник

  3. Данные должны быть консистентные, при изменении в одном месте, во всех остальных местах они должны стать такими же

  4. Если данные используются в компоненте, то должна быть возможность сделать их реактивными, то есть при отображении в template после изменения компонент должен перерендериться

  5. Должна быть возможность защитить данные, чтобы менять их можно было только из класса, запретить изменение из компонентов

  6. Должна быть возможность менять данные с компонента, установить валидатор для подобных изменений

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

  8. Должна быть возможность встраивать этот класс в любой компонент без обязательства иметь общего родителя

Спойлер

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

Это 1 часть статьи про реализацию сервиса во Vue 2.

Во 2 части я хочу рассказать о деталях проектирования класса, как работать с разными типами данных, каким образом они встраиваются в компонент и приобретают реактивность, как сделать геттер на свойство/свойства (аналог computed) и передать его в компонент.

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

Опционально

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

Буду смотреть по вашей реакции, будет ли вам интересно про это прочитать.

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

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

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

Используете ли вы классы-сервисы в своих проектах?

42.42% Да14
0% Да, но это неудобно0
24.24% Нет8
15.15% Нет, но хотелось бы5
18.18% Пользуюсь другим способом6

Проголосовали 33 пользователя. Воздержались 5 пользователей.

ссылка на оригинал статьи https://habr.com/ru/articles/700392/


Комментарии

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

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