Да-да, я знаю, 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();
}
}
Такой способ мне не нравится тем, что много мусорных функций, нет понимания как встраивать данные и менять их. Да и вообще так получилось, что я не нашла ни одной статьи, которая бы показывала полноценное решение, учитывая всевозможные случаи.
Поэтому я решила создать это решение сама.
Вводные
Хочется, чтобы можно было создать сервис, который
Создавал экземпляр только по запросу, позволял создать несколько экземпляров, мог удалить нужный экземпляр
Было понятно откуда пришли данные или методы, чтобы при поиске по файлу названия можно было найти источник
Данные должны быть консистентные, при изменении в одном месте, во всех остальных местах они должны стать такими же
Если данные используются в компоненте, то должна быть возможность сделать их реактивными, то есть при отображении в template после изменения компонент должен перерендериться
Должна быть возможность защитить данные, чтобы менять их можно было только из класса, запретить изменение из компонентов
Должна быть возможность менять данные с компонента, установить валидатор для подобных изменений
Должна быть возможность получить экземпляр компонента для каких-то специфичных действий,
хоть я и не очень это поддерживаюДолжна быть возможность встраивать этот класс в любой компонент без обязательства иметь общего родителя
Спойлер
Я смогла реализовать подобную логику, оказалось, это было так просто, что даже смешно. И теперь я хочу рассказать об этом вам и предоставить функции-фабрики для обвязки подобного класса, чтобы вы могли тоже удобно пользоваться классами.
Это 1 часть статьи про реализацию сервиса во Vue 2.
Во 2 части я хочу рассказать о деталях проектирования класса, как работать с разными типами данных, каким образом они встраиваются в компонент и приобретают реактивность, как сделать геттер на свойство/свойства (аналог computed
) и передать его в компонент.
В 3 части я расскажу про экземпляры, как регулировать создание и уничтожение, покажу мои функции-фабрики для обвязки класса, как сделать удобную передачу таких свойств в компонент.
Опционально
В 4 части я бы хотела порассуждать о том, какой сервис можно считать хорошим, что стоит выносить в сервис, чтобы не выстрелить себе потом в ногу, как их тестировать и как тестировать компоненты, использующие сервис.
Буду смотреть по вашей реакции, будет ли вам интересно про это прочитать.
Если не хотите ждать, то вот ссылка на репозиторий с рабочим решением, там есть подробные комментарии о реализации. Наиболее интересные файлы: сам класс, и конечно функции для обвязки.
Этот вариант еще в стадии черновика, я готовлю для него документацию. С практикой я буду его дорабатывать, плюс по ходу написания следующих частей я могу вспомнить про какой-то кейс, который не учла.
Комментарии (41)
ionicman
22.11.2022 23:31Я не очень понял суть задачи — надо расшарить какие-то данные/события между какими-то иерархически несвязанными/связанными компонентами?
Если да, то максимально корректный в парадигме Vue — это provide/inject.
А если с этим не заморачиваться, то (имхо) самый удобный — event-bus на руте (либо внешний через mixin/globals). В нем можно как райзить события-данные, так и события-логику, получают их все подписанные на нужные события компоненты, что очень удобно. Расплата за это только одна — отследить типы событий и где они порождаются и где ловятся — сложно. Но, если именование событий стандартизировать, то ищется в проекте элементарно поиском по названию события.
Не увидел event-bus в статье, кстати — но может быть потому, что неверно задачу понял. Поясните, если не трудно, можно просто простым примером задачи.Marcelinka Автор
23.11.2022 00:08Здесь нет конкретной задачи, есть идея о том, какие сервисы мне хочется проектировать. Вводные требования я прописала в конце статьи, эти требования покрывают большую часть узких кейсов, которыми страдают остальные решения.
Provide/inject в плане выноса логики страдает проблемой общего родителя (он обязателен) + необходимость встраивать в родителя, что засоряет код ненужными вещами собственно говоря для родителя. + еще несколько минусов, описанных в секции cons, но эти 2 самые весомые для меня.
Да, я действительно забыла указать EventBus, но он слишком неочевидный, как вы уже сами сказали, отследить вызов события и его listener - не очень удобно.
Я хотела сделать логику, при которой сервис как таковой вообще не будет связан с Vue, потому что если мы выносим логику в сервис, то фреймворк там уже не должен иметь значения. То есть я нацелилась конкретно на классы, и как можно удобно использовать их в компонентах.
Вам будет понятнее, если вы посмотрите реализацию (ссылка на репозиторий указана в конце статьи), либо дождетесь второй и третьей части, где я буду более подробно рассказывать о том, что я сделала.
ionicman
23.11.2022 10:41+1Общий родитель есть всегда - это рут. Чтобы его не загрязнять, можно то что провайдится, затащить внешним файлом js.
Про отслеживание событий я написал ниже.
А так вы затащили в проект ещё одну зависимость, которую, во-первых, нужно понимать всем, кто работает в проекте, а во-вторых - поддерживать. Вместо того, чтобы использовать сам vue и его парадигмы, которые понятны всем и доступны из коробки.
ИМХО, это пятая нога - вы облегчили одно и усложнили другое.
Но, вообще, чтобы понять что решали, было бы очень неплохо увидеть реальную задачу.
ArutaGerman
23.11.2022 09:23Event-bus - мега не очевидно. provide/inject - тоже, но в меньшей степени.
В большом проекте надо постараться, чтобы найти родителя в котором сделан provide. Можно ставить коммент, как вариант, у inject, но родителя перенесли/переименовали и финита ля комедия - ищи снова родителя.
Сам я использовал provide/inject и это удобно, но это был маленький проект и там было всё очевидно. В больших проектах лучше искать более удобные решения, даже если это "велосипеды"
ionicman
23.11.2022 10:37+2Чем event-bus меганеочевиден?
Если договориться о стандарте именования, например, сделать события вида [НазваниеКомпонента]_[Событие]_[ID], где ID - это идентификатор экземпляра компонента, если компонентов такого вида может быть несколько.
После такого соглашения понимать что за компонент райзнул событие очень просто, как и найти подписчиков на него. Можно для дебага перехватывать все события и кидать в консоль и тд.
Я бы не сказал, что это неочевидней provide/inject, а самое главное - никаких внешних зависимостей и парадигм тащить не надо - работает из коробки.
Плюсов масса - если компоненты каким-то образом подписаны на события друг-друга, например когда есть модальный лоадер, а другие компоненты при асинхронке кидают события типа Loader_On/Off, то, достаточно просто подключить компонент лоадера и любой другой компонент, вообще ничего не зная про связь - и они подружатся сами. А если вы не подключили лоадер - никакой ошибки не будет, тк событие будет уходить, но подписчиков у него нет.
borovinskiy
23.11.2022 02:26Ну если надо хранить глобально несколько экземпляров вместо синглтона, то шаблон давно известен: мультитон. Правда тут вопрос как понять, когда экземпляры в мультитоне надо убить, если вдруг их там может быть порождено очень много.
Marcelinka Автор
23.11.2022 08:12Я не знала про этот паттерн, но это по сути то, что я и сделала для себя
zizop
23.11.2022 07:23+1Я раньше очень много писал на Symfony, и там просто великолепный Dependency Injection Container. Очень сильно не хватало этого на стороне фронта, на Vue. Использовать контейнеры с декораторами не хотелось ( ну не перевариваю я их).
И вот недавно я открыл для себя RSDI. Оказалась потрясная штука, сейчас использую вовсю. В потом еще я подключил авто-маппинг сервисов из rsdi с помощью provide/inject, и теперь могу вызвать сервис из любого компонента, где мне это нужно и в тоже самое воемя четко видить все зависимости и в одном месте иметь описание состава контейнера. Мне прям зашло. Если интересно - могу поделиться деталями.
P.S. rsdi и под vue 2 будет работать.Marcelinka Автор
23.11.2022 08:17+1Напишите статью!) Очень мало способов решения именно на просторах интернета, что и подтолкнуло меня написать статью
BruTO8000
23.11.2022 08:01Я правильно понял, что вы создали свой DI контейнер, когда могли просто использовать inversifyjs?
Marcelinka Автор
23.11.2022 08:10Про этот пакет в курсе, я использовала его на практике на проекте, где компоненты были написаны как классы. Но при изменениях в классе ломался hot-reloading, при этом ломался так, как будто у тебя ошибка какая-то происходит, что очень сильно путало. И по какой-то причине не удалось встроить в стор и роутер файлы, но уже не помню по какой, пришлось там просто синглтон получать, что свело на нет пользу пакета.
Я создала типа упрощённую версию, которая будет работать при любых вводных, так как она максимально простая.
BruTO8000
25.11.2022 02:54Идея мне понравилась. Это как MobX в реакте, посмотрите provide/inject в библиотеке mobx-react. Там не нужно писать так много кода, и запоминать работу фреймворка (вы переопределили сеттер, чтобы vue мог установить proto).
То есть можно использовать классы, а с помощью mobX они уже реактивны, а если нужен мультитон, то оборачивать сервис в свой декоратор (написать самостоятельно).
Marcelinka Автор
25.11.2022 09:14Я например сейчас работаю на проекте, где использован Vuex, и другие зависимости вводить соответственно не очень. Плюс в нативке как никак разобраться можно без дополнительных запросов в гугл.
В целом я знакома с mobx, и работала с ним на реакте. Вроде как есть реализация под Vue, надо будет глянуть, как они это сделали.
Насчёт того, что приходится помнить особенности фреймворка, согласна. У меня была идея сделать декораторы, чтобы объявление свойства в классе могло выглядеть как-то так:
@primitive @readonly _prop = 'something' @object @validated _obj = { }
Тогда в этих декораторах можно было бы зашить Vue-специфичную логику. Но в нативном JS декораторы ещё не въехали в стандарт, вот в TS - это возможно.
js_n00b
23.11.2022 09:27Почему пропущен вариант использовать Composables?
Marcelinka Автор
23.11.2022 13:39Потому что статья про Vue 2
js_n00b
23.11.2022 13:53В чем проблема использовать Composables во Vue 2 ?
ionicman
23.11.2022 14:10"composable" is a function that leverages Vue's Composition API to encapsulate and reuse stateful logic.
Из документации по VUE.
VUE Composition API появилась только во VUE3.
js_n00b
23.11.2022 14:29+1Спасибо за слив кармы за абсолютно оправданное замечание(смотрите ссылку ниже). Парни и девчонки, которые знают, что я был прав, пожалуйста, накидайте плюсов в карму! Верните мне веру в Хабр, что здесь еще можно нормально общаться хотя бы на технические темы!
Marcelinka Автор
23.11.2022 15:23Статья написана про старые проекты на Vue 2, написанные на Options Api, которые не используют Composition Api. Там нет такой опции.
js_n00b
23.11.2022 15:50Начиная с версии 2.7 во Vue 2 появилась опция setup. В доказательство этому, выше я уже скидывал ссылку на Changelog. Даже если Вам не нравится это вариант, мне было бы интересно прочитать сравнение с плюсами и минусами. Только поэтому я спрашиваю.
Marcelinka Автор
23.11.2022 16:10Я не говорила, что мне не нравится этот вариант, мне впринципе Vue 3 больше нравится, и там я использовала только Composition Api.
Просто тема статьи другая, я это наверное явно не обозначила, но опция Composable на некоторых проектах не стоит, так как весь проект на Options Api, и composable не совсем вписывается.
Для 3 версии Vue я потом хотела сделать решение, где как раз таки класс встраивается в компонент через composable.
gmtd
23.11.2022 09:56+2Смешались в кучу
кони, людистейты, функции, миксины...Автор понимает, что такое сервис? Верней, как автор понимает что такое сервис? Судя по тексту, Math.round() для нее сервис. И весь Vuex стор тоже.
О каких сервисах речь? Stateless, stateful? Откуда к нему нужен доступ? Какие данные передаются? Какие задачи решаются? Нужна ли реактивность? О чём статья, вообще?
"Как можно инкапсулировать логику", "Сервисная архитектура во Vue 2" - статья точно не об этом.
Marcelinka Автор
23.11.2022 13:46Это статья-рассуждение о том, что я чаще всего видела в проектах, когда появлялась необходимость вынести логику. И рассуждение как раз и сводилось к тому, что чаще используют инструменты, не приспособленные хранить и реализовывать внутри себя логику, которая уже начинает оформляться в сервис.
В блоке вводные я как раз описала, как на мой взгляд сервис должен работать, и какие ситуации покрывать.
gmtd
23.11.2022 14:28+1Претенциозное название статьи абсолютно не соответствует её содержимому. Погуглите термин "сервисная архитектура" и посмотрите, что обычно публикуется в этом контексте в IT
Kirill_94
23.11.2022 17:38-2Претенциозный ваш комментарий. Если у вас не хватает мозгов понять суть и идею статьи, то это проблемы не автора статьи. Мне лично все понятно.
Mecitan
Объясните мне, человеку, который интересуется разработкой сайтов, зачем выносить бизнес-логику на сторону клиента? Для чего? Какой прок от этого?
Мои взгляды могут показаться консервативными, но всё же такие штуки должны обрабатываться на стороне сервера, а на стороне клиента должен происходить вывод всего этого. При этом сайт должен быть легковесным, интуитивно понятным и доступным. То бишь каждый занят своим дело. Фронт раскрашивает кнопочки и добавляет "реактивность", бэк пишет логику и хранит данные. Всё чётко.
Marcelinka Автор
На данный момент на стороне бэкенда популярен паттерн REST-api, что говорит о том, что они отдают чистые данные, связанные с конкретными сущностями. Клиент на данный момент не только рисует кнопки и добавляет реактивность, а в том числе сращивает все эти запросы между собой.
Часто данные приходят не в удобном виде, и нам приходится писать адаптеры, добавлять форматирование и так далее.
Часть логики, связанную с рендерингом, считается хорошей практикой выносить клиент.
Например, последняя моя задача: есть страница для сбора паспортных данных, она включает в себя с 10 разных экранов, которые должны показаться в разных случаях. Условно, сначала мы приходим на первый экран, при успехе попадаем на экран 2, при провале попадаем на экран 3, если у экрана 2 успех, то мы переходим на экран 4 и т.д. Где хранить эту логику? Она очень прямо связана с бизнес-логикой - это флоу клиента, но при этом она связана с отображением экранов на клиенте. Мы храним эту логику на клиенте, потому что нам проще регулировать отображение экранов, чем если бы это делали на сервере.
Mecitan
Но вы сами в своём примере разбили логику действий на составляющие. Что позволит бэк-енд разработчику подготовить представление данных на своей стороне и правильно их подать на фронт. Привет взаимодействию в команде.
У сервера мощностей по более будет, чем у клиента в браузере, который будет тратить некоторое время и самое главное трафик, пока будут производиться вычисления. Всё таки скрипты порядком больше потребляют трафика, даже с учётом сжатия данных и вот это вот всё. Это нам повезло, что у нас трафик дешёвый, в той же Америке и Европе он стоит порядком дороже.
Marcelinka Автор
Не понимаю смысла спора, это современный стандарт, часть бизнес-логики так или иначе переносится на фронт, дабы уменьшить нагрузку на сервер и убрать зависимость сервера от клиента. То есть АПИ строится таким образом, что им не нужно знать о том, как работает клиент, и любой клиент сможет использовать это АПИ.
Mecitan
Я не спорил с вами, а всего лишь пытаюсь понять, когда наступил этот переломный момент когда часть тех.процессов отдали на откуп клиентам. И да, популярность не всегда залог успешного.
Marcelinka Автор
С момента, как появилась технология SPA, она впринципе возникла из-за потребности разделить логику между клиентом и сервером. Это не "популярно", это стандарт и считается нормой на сегодняшний день. Пользователям нравится SPA, когда все плавно и без задержек, без выноса части логики на клиент - это невозможно.
Mecitan
Каким образом нынче сохраняетеся микроразметка? Как решается вопрос с seo?
Marcelinka Автор
Фронты за это отвечают, они могут подключить SSR при необходимости, либо сделать статическую генерацию для ботов, зависит от проекта все
А вообще я давненько не вижу запроса на какую-то работу с микроразметкой/seo, сейчас часто используют другие каналы продвижения (типа соцсетей или мессенджеров)
Иногда бизнес просит сделать рекламный статичный сайт-визитку для продвижения через поиск, а с основным сайтом не запариваются
Ilusha
Бэк хранит данные, валидирует их, управляет ими и предоставляет API для доступа для клиентов. Которыми может быть не только люди.
Фронт - это “человеческий API”. На один и тот же бэк может быть: мобилка, десктоп, браузер-экстеншен, генератор pdf/cvs/etc.
Современный фронт (реакт/вью/etc) - это, обычно, модель данных, которая определять визуальное представление. Эта модель, обычно, подвергается нормализации. И обновляется не полностью по действию пользователя, а инкрементально: нажали кнопку, поменяли один флаг, отправили на бэк, получили результат, изменили эту часть модели (а еще отработали состояние ожидания ответа, отработали ошибки).
А дальше аппетиты растут: кеши на фронте - чтобы снизить нагрузку на сеть и бэк (и механизмы их инвалидации), использование параметров урла для хранения данных, шеринг каких-нибудь данных на весь апликейшн (ACL, например). Работа со сложными формами: это все вложенные переиспользуемые компоненты, которые валидируются и мапятся на модель, которая, в свою очередь, законтрактована с моделью на бэке.
На самом деле на фронте бизнес-логики часто не катастрофически много.
Mecitan
Но на стороне сервера тоже есть механизмы, которые которые позволяют кешировать данные, подготовленные запросы и прочее.
Ilusha
Конечно.
Использование сессионного кеша на фронте позволяет превратить асинхронную операцию в синхронную. Что делает интерфейс при нестабильном интернете более отзывчивым.
ILaeeeee
Сеть является самым тормозным местом. Поэтому, чтобы интерфейс работал максимально быстро, часть вещей переносят на фронт, где это можно и имеет смысл. Я анимирую часто на фронте элементы, и уже интервал 100ms (0.1 s) хорошо заметен: можно с уверенностью сказать стало быстрее или медленнее. Поэтому этот интервал использую как шаг для грубой настройки анимации.
SkiperX
На фронте логика интерфейса, на сервере логика обработки данных. Каждый занят своим делом и у каждого есть подходящие возможности для реализации этой логики. На бекенде делать что-то связанное с интерфейсом и фронтом не так удобно. Тогда фронт находится на в своей стихии.