Как стать автором
Обновить

6 простых принципов написания приложения на Vue, которое легко поддерживать (часть 1)

Время на прочтение8 мин
Количество просмотров22K
Всего голосов 50: ↑45 и ↓5+40
Комментарии70

Комментарии 70

Почему бы не использовать axios.defaults.baseURL = 'https://api.example.com' ?

Чтобы "https://api.example.com" лежал в env файле, а не в коде

Удобней при сборке в различных окружениях (CI/CD)

Так можете задать эту переменную в каком удобно файле и импортировать ее туда, где загружаете axios и прицепить axios.defaults.baseURL = API_URL.

В итоге не потребуется писать каждый раз в определении функций "axios.post(`${API_URL}/строка_запроса`), а просто ограничиться axios.post(строка_запроса).

PS Возможно я не прав, так как только учусь :)

А, ну это да

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

Насчет API - все-таки сейчас OpenApi/Swagger для rest is a must, так же как codegen для graphql.

Вроде как vue уходит от единого vuex store к pinia с multiple stores, поддержка не сложнее, универсальности больше.

Люди, вопрос глупый, но все же. Есть идея пет-проекта и встал вопрос что выбрать: React или Vue? Сделал пару мелких приложений на одном и на втором. Пока что опыта мало и не могу понять какой-же из фреймворков выбрать. У них есть какие-либо ключевые различия? Или может по удобству один предпочтительнее другого? В общем, у кого есть опыт просветите пожалуйста.

От скиллов зависит... Но я всегда выбираю vue. Мне кажется что на нем гораздо быстрее получается что-то готовое

Спасибо. А за счет чего быстрее?

Меньше религии... сопряжения всяких версий библиотек... но это ИМХО

Во vue очень много магии и сахара. На vue можно сделать одну задачу кучу разными способами и это в большинстве случаев плохо.

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

У vue кривая входа на много легче, "easy to learn hard to master". Но чем сложнее начинается ваше приложение, тем больше появляется трудностей.

Если вас не смущает все выше перечисленное, берите vue.

Как будто бы с react ситуация лучше.

Vue - легче и быстрее писать. Single file components существенно облегчают работу.

Откройте в браузере facebook.com, поработайте в этом тормозном монстре, и потом подумайте - могут ли создавшие его люди придумать эффективный фреймворк

Ну такое себе под постом про vue спрашивать об этом. Но мое мнение, что Angular или Vue. При этом Vue кажется более перспективным.

В Angular уже есть дефолтаная архитектура, которая уменьшает боль при разработке.

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

В React разброд и шатание, это же просто библиотека. Я так и ни разу не увидел хорошей архитектуры на реакте. Киллер-фича реакта это jsx/tsx, так как можно гибко переиспользовать UI компоненты. Хуки, это красивый костыль, но все равно костыль. Composition Api во Vue получше будут. Да и jsx во Vue можно прикрутить.

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

После vue3 на реакт обратно не хочется. Но у vue сильно меньшая экосистема, чем у реакта. Разнообразия меньше.

Для пет-проекта вообще без разницы.

В целом, попробовать: возьмите фреймворк quasar на vue, там из коробки есть всё, что нужно: и uikit, и всё настроено.

Хабр на vue.

Как уже писали, я тоже выбираю Vue. Количество стандартного кода сильно меньше, и нет JSX.

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

Как зачем, чтобы страничка дёргалась и батарейка жралась на лишних рендерах

Их же все равно нужно хранить.

В компоненте или сторе - не так принципиально. Но стор позволяет абстрагировать логику работы с данными: пагинация, фильтрация, сортировки. Можно и в хук вынести, не принципиально.

Плюс стор позволяет кешировать: в спа можем снова маунтить компонент без запроса на данные и писать «было загружено 2 минуты назад».

Кароч, причин может быть множество. Где-то нужно, а где-то нет.

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

Сторы во вью берут на себя роль middleware для выделения бизнес-логики. В реакте это танк/саги. Операции над данными лучше уносить в сторы (или в хуки), а не хранить в компоненте.

Pinia-сторы - это изолированные истории, которые могут использоваться ток для одного компонента. Типа мобх. Вообще, pinia-сторы очень близки к хукам.

«Отдельный скриптик» будет хранить мне все параметры сортировок, фильтраций и пагинации, которые нужно восстановить во вьюхе? Да еще и process-стейтом будет управлять? У меня там вообще ошибка может быть. Или не быть респонса. Но хочу хранить состояние компонента, даже когда размаунчен.

Простейший пример:

  • Есть компонент Список товаров, в нем есть пагинация и фильтры, а еще в нем в попапе можно вызвать редактирование Товара.

  • В этом попапе Товара я могу привязать связанные товары, которые выбираются из того же компонента Списка товаров, но уже внутри этого попапа, там также есть фильтр.

Вот и вопрос, как я тут со стором это реализую, если это один компонент, но в 2 местах и у каждого свои списки? И самое главное, зачем это надо делать через стор?

Сторы во вью берут на себя роль middleware для выделения бизнес-логики

Где вы такое определение взяли? Сторы во Vue - это реактивное хранилище данных. Накрутить в него можно все что угодно, но это нарушит Single Responsibility Principle, и, я считаю, что нарушит совершенно бесмысленно.

«Отдельный скриптик» будет хранить мне все параметры...

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

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

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

SRP - это абстракция. Дробить/обобщать - это уже на разработчике: как он видит смысл «единства».

Давайте в Ваш пример добавим:

  • списков «товаров» может быть много.

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

  • Переключение между списками не сбрасывает параметры

  • Закрытие всех компонентов не сбрасывает установленные пользователем во время сессии параметры

  • Сами товары в разных списках - это разные типы

Ну, добавили всё это в мой пример - значит, надо хранить в сторе настройки фильтров. Зачем там хранить сами списки товаров, я до сих пор не понимаю. Это не бесплатная операция вообще-то.

Это можнт сказать бесплатная операция. Не нужно экономить на спичках.

Список храним, чтобы не запрашивать снова. Это не очевидно?

Список храним, чтобы не запрашивать снова

Отлично, пользователь со списка переходит на редактирование сущности, сохраняет ее, возвращается, и что же он видит?

Старые данные

Нет. Фронт данные обновил в модели.

Т.е. стор у вас ещё и отвечает за получение одной сущности, ее сохранение и обработку кеша? Как-то слишком много у него обязанностей.

А стор с другим типом сущностей эту же логику будет дублировать?

import fetchList from '@/api/fetchList'

// типизация будет чуть сложнее, естественно
export function getStore(name: string) {
  return defineStore(name, {
    // ...
    actions: {
      async fetch() {
        if (['process', 'fetched'].includes(this.fetchStatus)) {
          return;
        }
        this.fetchStatus = 'process'
        this.list = await fetchList()
        this.fetchStatus = 'fetched'
      }
    }
    // ...
  })
}

Ужас какой, 5 строчках кода и получение, и сохранение, и обработка кеша на уровне стора. Как много обязанностей.

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

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

Ужас какой, 5 строчках кода и получение, и сохранение, и обработка кеша на уровне стора.

На уровне стора у вас нет обработки кеша, нет никакого сохранения в этих 5 строчках. Обработка кеша у вас если и есть, то где-то внутри @/api/fetchList.

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

Список храним, чтобы не запрашивать снова

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

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

Если эти данные требуются в нескольких компонентах, либо если эти данные надо хранить после дестроя компонента, чтобы получить при повторном вызове, то ОК. Но только список сущностей к этому никак не относится, т.к. в вашем же примере он повторно запрашивается со стороны.

Как же нет кеширования, если есть? Первое уже условие в экшене. Это самое элементарное кеширование последнего респонса. Кеширование по определению. Не похоже на парметризированное кеширование на уровне api? Ну так оно не нужно в данном контексте. Прикрутим пагинацию, фильтрацию и сортировки - все становится несколько сложнее. Но суть останется той же.

Зачем: чтобы перед маунтом компонента всегда делать fetch и не париться.

И как нет сохранения? А куда сохраняется результат запроса?

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

Делаем запрос данных в компоненте: имеем самые свежие при открытии. Храним в сторе: получаем уже загруженные и доп нагрузку на бизнес-логику фронта: обновление моделей или сброс флага на "нужно снова сфетчить в след раз".

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

Простите, но у вас вообще какая-то билеберда, а не стор. Вы такой код с кем-нибудь в команде используете?

Я вам про концепции, а вы про код, которого даже не видели :)

Думаю, вы просто не поняли идею, потому что то, что я говорю - простейший паттерн, который еще в backbone использовали.

И в догонку, комментарием выше вы писали:

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

Как вы думаете, что произойдет в плане потребления оперативки в двух вариантах:

  1. Компонент вызвался, список каких-то сущностьей пришёл с бэка, сохранился в компоненте, компонент убрался (например при смене роута).

  2. Все аналогично, но список сохранился в сторе.

Список не весит ничего, на уровне погрешности.

Сетевое взаимодействие - это уже затраты времени. С мобильным третьим - существенные. В Африке - жопа.

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

На самом деле, адекватного ответа не будет. Мы пришли к решению в виде tanstack query для vue. Там и в документации достаточно популярно объясняется разница между состоянием приложения и асинхронным состоянием сервера. Решение хорошее, очень много проблем решает.

Статья как будто устарела на год минимум

Vuex?

Где composables?

axios, кстати, лучше тоже обернуть (в http), и вызывать через обертку

axios лучше закопать и один раз разобраться с нативным fetch

Его в nodejs завезли буквально недавно. Не смотрел, он из экспериментальной фичи вышел?

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

Вы шутите, он в хроме и огнелисе с 2015 года в продакшн!

С бэком да, сталкивался с рукожопами, которые определяют ajax запрос по наличию http заголовка x-requested-with: xmlhttprequest (вместо нормального accept). Но это было ещё во времена миграции с Yii 1.1 и $.ajax .

Вам про ноду, а вы про браузеры. Есть такая вещь - ssr. И гораздо проще использовать одну библиотеку и на сервере и на клиенте.

Спасибо, за ssr я немного в курсе. На ноде он с 2021, а полифил (node-fetch) доступен 8 лет как.

Ну и да, хотелось бы посмотреть соотношение количество проектов на vue которые используют ssr и вообще бэкенд на nodejs по отношению ко всем проектам на vue. Там хотя бы 1% есть?

Ну, например, весь nuxt. С quasar тоже включается парой строк в конфиге. Да и в принципе не так, чтобы сложно сделать (но со всякими сторами, конечно, есть нюансы).

Ну даже по статистике npmjs.com соотношение загрузок nuxt/vue 1:8. Но это конечно ни о чём, дофиглиард сайтов подключают vue.js тупо c CDN или тащат к себе как тот же битрикс. Потому что одно дело подсунуть json с какого угодно бэка и тэг <template> и оно тупо работает, совсем другое подписаться на вечные пляски с черной дырой node_modules.

Там хотя бы 1% есть?

vs

по статистике npmjs.com соотношение загрузок nuxt/vue 1:8

Предлагаю на этом остановиться.

Разве все проекты на Накст используют SSR?
Это лишь одна из его особенностей

А в safari с 2017. А относительно массовый отказ от ie пошел вот ток недавно.

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

Ссылка на pinia есть, насколько понял vuex остается пока для больших приложений: https://habr.com/ru/post/666250/ Конечно у pinia логика более прозрачная, чем у vuex с кучей экшн и их мутациями.

axios, кстати, лучше тоже обернуть (в http), и вызывать через обертку

Можно в двух словах зачем так делать? У меня тут опыта не хватает понять функционал заворачивания :*-)

Vuex уже год как не рекомендуется для использования в официальной документации Vue 3. Размер приложения тут ни при чем.

Axios обернуть лучше чтобы при необходимости/желании можно было легко заменить его на что-то другое - тот же fetch или еще что-то - сейчас хватает более современных и удобных библиотек для сетевых запросов

Точно так же заворачивание компонента в свою обертку позволяет потом легко заменить его. Например завертка v-btn в свой BaseButton позволяет сделать замену в одном месте, а не по всему коду.

Спасибо!

На что заменить axios?

хочу попробовать got

это под ноду, axios это же фронт

axios это и фронт и бэк, что незаменимо при SSR

Можно выбрать любое хранилище, в статье про стор в принципе. Моя ошибка, что не упомянула про pinia. Vuex или Pinia в данном случае не играет роли, для pinia +- те же принципы работают

сomposables здесь лежат в папке hooks (тут так назвала, но сomposables даже более очевидно +)

Про использование обертки - отличное замечание, это как раз выйдет во второй части статьи

Тогда и типизацию нужно обернуть.

Views/pages

Так вьюшки или страницы? Или это два разных типа компонент? Как это вы называете? Давайте придем к единому стандарту.

API/services

Аналогичный вопрос.

Interfaces, enums

Вы действительно считаете, что так удобно? Быть может стоит хранить интерфейсы енумы по месту первичного использования? Например если это используется в пропсах блока, то лучше к блоку. Если возвращается api, то рядом с api.

export function getProduct (id) { return axios.get(`${API_URL}/products/${id}`) } export function postProduct (data) { return axios.post(`${API_URL}/products`, data) } export function patchProduct (data) { return axios.patch(`${API_URL}/products`, data) }

Почему это три функции, а не класс с двумя методами (я видел, что именно так в Angularделают)? А почему нет ts-типов?

import { getProduct } from '@/api/products' import { IProduct } from '@/interfaces' const actions = { async fetchProduct ({ commit }, id: string): Promise<IProduct> { try { commit('setIsLoading', true) const response = await getProduct(id) commit('setProduct', response.data) return response.data } catch (err) { // отлавливаем ошибки } finally { commit('setIsLoading', false) } } }

А почему типы здесь появились?

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

Зачем здесь стор? Почему мы не можем сделать глобальный объект-экземпляр класса, который хранит эти данные? Зачем писать куча каши со стором? Мне кажется что использовать сервисы как в Angular и получать их через inject, было бы удобнее (Там правда ручками надо поколдовать с созданием зависимостей), но у меня нет опыта реализации этого на множестве промышленных проектах.

Вообще весь 3-й совет, я так и не понял. Если данные могут использоваться на нескольких страницах то кажется должен подходить provide/inject. В класс-обертка для api-сервиса, может помочь реализовать кэширование? В Angular не используют глобальный стор (ну кроме пары извращенцев) и как-то живут. Почему этот подход нельзя использовать во Vue?

САМОЕ ГЛАВНОЕ!!! Вы забыли совет номер 0: используйте Typescript. Настройте линтер на strict mode, запрет any, а так же js-файлов.

@nkalacheva

1) Vue дает много свободы и возможность формировать свою архитектуру. На вкус разработчика, тут views = pages, главное чтоб логика разделения сохранялась и там хранились именно страницы. 

2) В небольшом/среднем проекте сразу папка api.

В проектах побольше services, внутри уже папка api + дополнительные папки (helpers, generators и т.д), где можем хранить функции сериализации, слипы и др

2) Для простоты расписывала плоскую структуру, папки можно менять или вкладывать. Да, можно выделять интерфейсы по месту хранения, если это удобно. Иногда их вообще не выделяют, как итог - несколько одинаковых интерфейсов разбросаны по проекту в разных папках на больших проектах.

Мне удобнее видеть все в одном месте. Файл с типами тоже можно сделать композитным.

3) Здесь просто пример выделения, можно использовать, как классы, так и функции. В зависимости от проекта. Можно сделать объект product и внутри него методы get, post, patch.

4) Про стор, как раз пишу, что он не везде необходим и можно обойтись без него. (https://github.com/vuejs/vuex/issues/236#issuecomment-231754241)

 На сколько знаю, в ангуляре просто вместо стора используются сервисы. Могу ошибаться, +- та же штука, где данные доступны глобально. Во вью просто нет DI движка из под коробки и зависимости явно импортируются.

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

 provide/inject подойдет, если у компонентов общий родитель, но мы не сможем делиться данными с соседями, + жирный минус, что при использовании TS теряем типизацию

5) Согласна, что typescript очень удобен и мне лично упрощает жизнь его использование. Линтеры и прекоммиты маст хев

Есть вполне себе даже не костыльный способ не терять типы: https://vuejs.org/guide/typescript/composition-api.html#typing-provide-inject

Зачем использовать provide/inject, когда есть pinia? Ноль каши. И очень удобно.

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

Смотрел на pinia-di, но пока не понял, зачем мне это нужно. Тоже хочется увидеть сложные кейсы, особенно в контексте ts.

Тут из без di мучаюсь с выводом типов через pinia в сложных кейсах (типа динамической генерации однотипных сторов).

provide/inject - элемент языка(фреймворка)

pinia - сторонняя библиотека

Может человек хочет создать свой переиспользуемый модуль без зависимостей. Тогда provide/inject вполне спасает.

Это была ирония. Что одно, что втрое - лиши инструменты

А для чего отдельная папка под интерфейсы и енамы? Кажется, они должны лежать рядом с тем местом, где они подходят по смыслу (рядом с функциями, например, которые их принимают/возвращают), а не в отдельной папке чисто для них.

Пока из первых 3-х пунктов ничего нового, прописные истины описаны. Пишите хороший код, а плохой не пишите!

Всем привет!

По первому принципу, рекомендовал бы ознакомится с https://feature-sliced.design/ (FSD). Отличное решение для масштабирования больших проектов. По началу кажется сложным, но через месяц обсуждений и привыкания - все встает на свои места. По итогу у вас будет уже готовая документация того, как хранить файлы и как они могут между собой взаимодействовать.

По третьему принципу достаточно спорно. В приведенном примере "карточки продукта". Обычно, подобные карточки включают в себя несколько вложенных компонент. И тогда локальное хранение состояния приведет к "props drilling", что не есть хорошо. Да и в целом, как по мне, pinia гораздо удобнее vuex. Кстати pinia идеально работает с FSD.

Зарегистрируйтесь на Хабре, чтобы оставить комментарий