Pull to refresh

Comments 31

Ещё для меня очень странно, когда стор используют для получения каких-то сущностей с бэка (например, товаров), прописывают там логику с fetch и т.д., а потом используют это только в одном компоненте.

Такая практика часто встречается в различных туториалах по Vue.

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

А какие плюсы-то у такой логики?

Минусы вот следующие:

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

  • Если список тех же сущностей понадобится другому компоненту, но уже с другими фильтрами, сортировкой и т.п., то придется дублировать стор.

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

Внутри стора нельзя писать логику получения запросов. Для этого есть сервисы. Сервис должен получать данные с АПИ, обрабатывать и отправлять в стор

Зачем хранить данные в сторе, если они используются в 1 компоненте? 1) консистентность - данные с Бека хранятся в одном месте; 2) удобство тестов - можно легко мокать стор (попробуйте замокать локальную переменную где-то в сервисе); 3) удобная отладка - просмотр содержимого стора; ...и т.д.

Внутри стора нельзя писать логику получения запросов. Для этого есть сервисы.

Т.е. по вашей логике когда компоненту надо что-то получить с бэка, он дергает сервис, а результат берет из стора? Или все таки он запрос делает к стору и стор берет данные из сервиса, но тогда уже получается не сильно важно стор делает fetch(...) или что-то типа userService.fetchList(...).

консистентность - данные с Бека хранятся в одном месте

Так в том и проблема, что в случае списков не всем компонентам нужна консистентность. Например одному компоненту нужны товары с сортировкой по цене по 20 на страницу. Сделали с хранением в сторе. А теперь задача возникла, что другой компонент должен отображать те же сущности товаров, но самые новые, и их всего 5. Для этого придется в сторе заводить еще список, консистентность тут наоборот создает проблему.

удобство тестов - можно легко мокать стор (попробуйте замокать локальную переменную где-то в сервисе)

Так тестируйте сервис.

удобная отладка - просмотр содержимого стора

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

Ну так в вашей схеме получается, что кто угодно может изменить стор. Я вообще не представляю, как КОМПОНЕНТ может менять стор? Я всегда все операции с данными делаю через сервисы. Если есть сотня компонентов и сервисов, и при этом часть компонентов меняет данные самостоятельно - это жесть. Я через это проходил на своих проектах))

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

Не всем компонентам нужна консистентность

Вы неправильно понимаете принцип, который я описал. Вот у вас есть исходные данные в сторе. Одному компоненту требуется сортировка по цене, другому - по цвету и с фильтром по категориям. Вместо того, чтобы хранить копии данных в сторе для каждого компонента - берёте и реактивно сортируете/фильтруете данные из стора через computed. У вас будет 1 источник истины и никаких лишних копий данных

Так тестируйте сервис

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

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

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

Вот у вас есть исходные данные в сторе. Одному компоненту требуется сортировка по цене, другому - по цвету и с фильтром по категориям. Вместо того, чтобы хранить копии данных в сторе для каждого компонента - берёте и реактивно сортируете/фильтруете данные из стора через computed

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

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

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

Но тем не менее. Допустим, у вас есть множество сервисов, занимающихся обработкой и получением данных. Стора в проекте пока нет. Вы в компонентах получаете / отправляете данные только через сервисы. Но вдруг появляется первый ваш стор. И прямо в этом сторе прописано:data.value = await (await fetch(...)).json();проходит какое-то время, и в команду приходит новенький. Ему дают задачу: дописать логику получения данных с бэка. Например, поменять формат данных, возвращаемых API. Где он будет искать логику получения данных? Конечно, в сервисах. А найдёт в сторе. И где ему дописывать новую логику?

Внутри стора нельзя писать логику получения запросов.

А авторы pinia в своей документации утверждают, что можно и нужно. И я им верю больше.

Довольно странное решение... Зачем тогда сервисы?

На мой взгляд, тут чистейшей воды side effect. Если это стор - ожидаемо, что ты получаешь уже сохраненные данные. Тогда можно тупо в сервис это запихнуть. Или тут подразумевается некий кэш?

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

Например. Есть форум. В нем есть обычная модалка для редактирования профиля пользователя. Поменял он аватарку. Сразу после изменения она должна обновиться везде, где отображается (во всех отображаемых сообщениях от него).

И кто вообще сказал, что стор должен обслуживать только один компонент? И что плохого в том, что он умный?

Например. Есть форум. В нем есть обычная модалка для редактирования профиля пользователя...

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

кто вообще сказал, что стор должен обслуживать только один компонент

И кто, простите, такое сказал? Точно не я.

что плохого в том, что он умный?

Прочитайте про God-классы, чем они плохи. Примерно тем же плохи и умные сторы и все прочее. Комментарием выше @Pubert, также споря со мной, при этом пишет:

Внутри стора нельзя писать логику получения запросов...

То есть, некий класс, который внутри себя использует стор и сервис взаимодействия с сервером (2 зависимости) - это хорошо?

А стор, который использует внутри себя сервис взаимодействия с сервером (1 зависимость) - это плохо?

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

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

Я не понимаю контекст вашего ответа, явно вы отвечаете не на мои комментарии.

То есть, некий класс, который внутри себя использует стор и сервис взаимодействия с сервером (2 зависимости) - это хорошо?

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

А стор, который использует внутри себя сервис взаимодействия с сервером (1 зависимость) - это плохо?... S из solid не нарушается.

У стора S - это хранение данных, ну и плюсом идет каким-то образом их изменение. Когда он отвечает за запросы к серверу - это уже нарушение Single Responsibility Principle.

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

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

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

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

У стора S - это хранение данных, ну и плюсом идет каким-то образом их изменение.

Actions are the equivalent of methods in components. They can be defined with the actions property in defineStore() and they are perfect to define business logic:

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

нарушение Single Responsibility Principle

Паттерн Active Record тоже его формально нарушает, при этом применяется в каждом втором проекте для веба

Отвечу сразу на оба ваших комментария.

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

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

Actions ... are perfect to define business logic:

Тут обсуждаемая статья как раз про то, что использовать формат Option Store - устаревший формат. И я с автором соглашусь.

И не должны компоненты знать ничего о способе общения с сервером.

Как и сторы, для этого отдельные сервисы используются.

А уже внутри этого метода может быть как общая бизнес-логика...

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

Паттерн Active Record тоже его формально нарушает, при этом применяется в каждом втором проекте для веба

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

Тут обсуждаемая статья как раз про то, что использовать формат Option Store - устаревший формат. И я с автором соглашусь.

Причем тут option? Это же просто синтаксический сахар, преобразуемый на этапе сборки в синтаксис composition. Я привел доказательство того, что методы в сторе как раз позиционируются как место для бизнес-логики самими авторами.

Наличие actions никак не зависит от синтаксиса, это одна из базовых возможностей pinia.

но все-таки лучше для этого использовать отдельные сервисы

Минус в том, что бизнес-логика разъезжается по разным файлам, и новый разработчик (или старый, после перерыва) легко может полезть в стор напрямую. Или наоборот забыть что то поменять в нем после общения с бэком

Если список тех же сущностей понадобится другому компоненту, но уже с другими фильтрами, сортировкой и т.п., то придется дублировать стор.

Зачем? Фильтруйте и сортируйте внутри компонента (computed или вызов метода), а данные должны лежать в сторе, и не меняться

Т.е. вы предлагаете несколько тысяч товаров загрузить клиенту за 1 запрос (а может и десятков тысяч), а потом браузером работать с этим списком?

В таком случае стор вообще не нужен )

Грузите сразу из сервиса в компонент, раз кроме него, они нигде не требуются.

Мой изначальный коммент:

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

Вы отвечаете:

Вполне нормальный подход. Считаем стор единственным местом хранения данных. Все взаимодействие с бэком - скрыто внутри стора

Теперь вы же:

В таком случае стор вообще не нужен )

Грузите сразу из сервиса в компонент, раз кроме него, они нигде не требуются.

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

А вот со списком товаров не совсем понятно. Если нет задачи отображать на экране несколько списков сразу - то я не понимаю, зачем вы пишите про дублирование списков внутри стора? Пусть будет один массив "отображаемые товары". И он бы изменялся с помощью вызова методов, внутри которых выполнялась загрузка данных. Или, если вам так важно, при вызове метода в сервисе, который загрузит данные а потом вызовет метод стора, и скопирует их в него.

В начале обсуждения такого условия не ставилось. А оно принципиальное.

Так и в жизни так же, сначала такое условие не ставится, а потом может и поставиться.

Т.е., допустим, у нас есть сторы сущностей user, product, orders. Мы по вашей логике храним эти сущности в сторах, через сторы запрашиваем.

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

Тут мы либо переносим логику получения товаров в компоненты, у каждого свой список, но тогда архитектура приложения в плане работы со списками будут разной - user, orders - хранятся в сторе, а логика работы с products переносится в компоненты. Либо мы в сторе для product имеем 2 списка и загружаем их двумя методами. Но зачем это делать, если каждый список нужен своему компоненту?

А потом еще прилетит задача убрать компонент featured products - удалим ли мы список получения товаров для него из стора или забудем и тогда он останется там до тех пор, пока кто-то за глобальный рефакторинг не возьмется?

Если нет задачи отображать на экране несколько списков сразу - то я не понимаю, зачем вы пишите про дублирование списков внутри стора

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

Или, если вам так важно, при вызове метода в сервисе, который загрузит данные а потом вызовет метод стора, и скопирует их в него.

Тут получается такая странная штука:

  • Компонент вызывает сервис

  • Сервис кладет данные в стор

  • Компонент берет данные из стора

Т.е. запрос для получения идет в сервис, а данные приходит из стора. Логичнее было бы:

  • Компонент просит данные у сервиса

  • Сервис откуда-то их берет (бэк, кэш, его дело) и отдает компоненту.

Я бы не делал отдельный стор для каждой сущности. Собственно, я изначально это и имел в виду, как преимущество единого стора на все приложение (ну, как минимум для тесно связанных сущностей). В данном случае, навскидку, я бы сделал два: UserStore и ProductStore.

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

Точно так же создаем новый заказ, вызовом метода (экшна) у стора пользователя userStore.createOrder(productList);

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

Можно внутри productStore сделать переменную типа Map, где ключ - тип списка, а значение - собственно список. Тогда можно вообще не волноваться за появление/удаление вариантов списка товаров. Новый тип списка будет создан компонентом, нуждающемся в нем

Т.е. запрос для получения идет в сервис, а данные приходит из стора. Логичнее было бы Сервис откуда-то их берет (бэк, кэш, его дело) и отдает компоненту.

И эти данные потеряли реактивность

Ок, я понял вашу логику. Процитирую ваш ответ здесь же другому пользователю:

> Внутри стора нельзя писать логику получения запросов.

А авторы pinia в своей документации утверждают, что можно и нужно. И я им верю больше.

Если вы верите, то вера - это уже не дискуссионный вопрос. Просто вот в этом моменте мы с вами не согласимся.

Для меня что бы авторы Pinia не утверждали, я беру их библиотеку, чтобы во Vue приложении у меня было общее реактивное хранилище.

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

Можно внутри productStore сделать переменную типа Map, где ключ - тип списка, а значение - собственно список

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

Единствнный вопрос - вы бы для этой переменной типа Map использовали ref или shallowRef? И вообще в сторах вы списки и объекты в каком виде храните - в виде ref или shallowRef?

Единствнный вопрос - вы бы для этой переменной типа Map использовали ref или shallowRef? И вообще в сторах вы списки и объекты в каком виде храните - в виде ref или shallowRef?

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

А так, смотря что требуется. Иногда достаточно shallowRef, если меняется весь объект целиком, а не отдельные поля (элементы массива)

Для этой задачи прмименил бы ref

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

А при использовании shallowRef я вообще никакие изменения не смогу отследить. Map создается вместе со store и более не меняется.

А списки то как раз будут меняться - при пагинации, смене фильтров на странице, и тому подобного

использовании shallowRef я вообще никакие изменения не смогу отследить

Зачем же его придумали тогда?

const products = shallowRef(new Map());
products.value.set('someKey', someArray);
// Триггерим изменение
triggerRef(products);
// или
products.value = products.value;

Map создается вместе со store и более не меняется, а списки то как раз будут меняться - при пагинации, смене фильтров на странице, и тому подобного

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

Зачем же его придумали тогда?

Для этого и придумали - чтобы отслеживать явное присвоение значения свойству value и ничего кроме этого

Меняться будет только весь список целиком, а не ключи каждого товара

прмименил бы ref, т.к. с большой вероятностью нужно отслеживать изменения на всем уровне вложенности

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

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

Sign up to leave a comment.

Articles