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 в своей документации утверждают, что можно и нужно. И я им верю больше.
Ну, например, чтобы после вызова метода стора, он сам выполнил запрос к серверу, получил ответ, и обновил внутри себя связанные данные. А иначе, придется колхозить какие-нибудь шины событий или еще что.
Например. Есть форум. В нем есть обычная модалка для редактирования профиля пользователя. Поменял он аватарку. Сразу после изменения она должна обновиться везде, где отображается (во всех отображаемых сообщениях от него).
И кто вообще сказал, что стор должен обслуживать только один компонент? И что плохого в том, что он умный?
Например. Есть форум. В нем есть обычная модалка для редактирования профиля пользователя...
Я не писал про профиль пользователя, я писал про списки сущностей, которые можно получать в т.ч. с пагинацией, фильтрами, сортировкой и т.д..
кто вообще сказал, что стор должен обслуживать только один компонент
И кто, простите, такое сказал? Точно не я.
что плохого в том, что он умный?
Прочитайте про 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
actionsproperty indefineStore()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, т.к. с большой вероятностью нужно отслеживать изменения на всем уровне вложенности
Я не знаю всей задачи, и предположил, что отдельные свойства элементов массивов могут меняться независимо от получения данных с сервера. В моей практике такое встречалось часто.
(Пример - в списке товаров приходит базовая цена, но при заказе нескольких экземпляров к ней должна применяться скидка. Хотя глупый пример получился. Ладно, у меня рабочий день закончился. Спасибо за дискуссию)
Идеальный Store в Pinia (Vue)