Мы используем файлы cookie. Чтобы улучшить работу сайта и предоставить вам больше возможностей. Продолжая использовать сайт, высоглашаетесь с условиями использования cookie.

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

AGIMA14 марта 2023Наталья Калачева·Фронтенд-разработчик

Поддерживаемое приложение на Vue начинается с предсказуемой структуры, изолированного слоя API и обоснованного использования стора. В первой части — три практики, которые упрощают развитие проекта и адаптацию разработчиков.

Коротко

ПринципПрактический эффект
Простая структура каталоговБыстрее находить код и подключать новых разработчиков
Единый слой APIМенять транспорт и обработку ошибок в одном месте
Модульный сторНе превращать глобальное состояние в монолит

1. Делать простую масштабируемую структуру

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

После инициализации приложения с Vue CLI мы уже видим предложенную структуру.

Базовая структура каталогов Vue-приложения: assets, components, router и store
КаталогНазначение
assetsCSS, шрифты и изображения
componentsАвтономные Vue-компоненты: шаблон, логика и стили
routerМаршруты и настройки роутинга
storeКонфигурация и данные Vuex или Pinia

Хорошая практика — придерживаться уже готового решения и расширять по мере необходимости. Хотя Vue не дает строгих рамок и мы можем менять файловую структуру как хотим, всё же использование знакомых стандартов делает проект более предсказуемым и простым.

Расширение базовой структуры Это хороший старт, но в больших приложениях мы неизбежно расширяем эту структуру. Лучше сделать это в начале разработки, а не пытаться изменить, когда дедлайн близко. О чем же нужно задуматься на берегу?Первое — это компоненты и принципы их разделения. Всё приложение состоит из набора компонентов. Если хранить их в одной папке, мы вскоре столкнемся с огромным количеством неструктурированного контента.

На какие папки можно разделить компоненты?Components (ui).

Здесь хранятся компоненты дизайн-системы. Это самые простые элементы интерфейса, которые часто переиспользуются. Обычно эти компоненты взаимодействуют с «внешним миром» через пропсы и события. Обращение из них к стору и роутингу будет лишним. Чаще всего они не отправляют запросов к серверу и не содержат сложной бизнес-логики.

Примеры таких компонентов: инпуты, кнопки, алерты и другие UI-элементы.

Пример организации переиспользуемых компонентов интерфейса Vue по назначению

Blocks.

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

Views/pages.

Страницы собираются из блоков и компонентов, но сами по себе являются более сложными компонентами, из которых мы обращаемся к стору, роутингу и т. д.

Layouts.

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

Помимо компонентов, важно организовать хранение дополнительного JS-кода.

Plugins.

В этой папке храним все сторонние библиотеки, там же их инициализируем и настраиваем.

Hooks.

Можно выделить отдельную папку для хранения кода, использующегося в Setup-компонентах (composition API).

Helpers.

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

API/services.

Папка содержит все функции вызова API.

Constants.

Всё, на что в приложении будут ссылаться глобально, но не хранится в .env, можно хранить здесь. Это могут быть статические данные или, например, список типов глобальных окон, которые можно вызвать глобально (через эмиттеры).

Interfaces, enums.

Если вы используете Typescript, то сразу можно выделить папку для типов и перечислений.

Эти папки описаны для примера и общего представления о том, как можно разделять кодовую базу. Конечно, можно видоизменять это под потребности проекта. Например, кто-то предпочитает хранить в /pages не только сам компонент страницы, но и папку этой страницы вместе со всеми используемыми блоками. Может быть удобно в папке /views хранить pages, store, blocks для каждого сервиса приложения.

Можно выбрать любой подходящий вариант, важно понимать, что несмотря на то, что компонент vue хранит в себе и template, и js, и css это не повод нагружать его слишком сильно.

2. Выделять все запросы к API

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

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

Каталог API с отдельными файлами запросов для коллекций, продуктов и пользователей

Каждый файл хранит и экспортирует нужные функции по категориям. Например, products.js может содержать следующее:

php
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)
}

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

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

text
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)
 }
 }
}

Плохим вариантом будет просто выполнять запросы к урл в компоненте. Например, так:

yaml
methods: {
 getProductById (id) {
 return axios.get(`https://example.com/api/products/${id}`)
 }
}

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

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

text
const API_URL = process.env.VUE_APP_API_URL

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

Это отлично работает для небольших и средних приложений, в которых не хочется усложнять структуру. Хотя для больших систем можно посмотреть в сторону разделения данных ORM. Подробнее об этом в документации для Vuex ORM. Это плагин Vuex, который позволяет разделять состояние приложения с точки зрения объектов данных (продуктов, пользователей и т. д.) и операций CRUD (создания, обновления, удаления).

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

Стор — централизованное хранилище для всех данных приложения с гарантированной предсказуемостью смены состояний.

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

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

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

Например, при переходе с одного продукта на другой данные не обновляются сразу, и пользователь видит какое-то время «старые» данные. Нужно дополнительно заботиться не только о получении и хранении данных, но и об их обновлении/удалении.

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

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

Схема выбора между локальным состоянием, provide/inject, PortalVue и Vuex
  • Будут ли данные повторно использоваться где-то еще?
  • Могу ли я вместо этого использовать здесь локальное состояние?
  • Способствует ли использование стора улучшению архитектуры приложения?

Если ответы на эти вопросы вызывают сомнения, то, возможно, стоит использовать props или provide/inject.

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

Поэтому важно разделять хранилище на модули.

Модули Vuex — это, по сути, небольшие независимые хранилища, которые объединены в более крупное центральное хранилище.

Примеры разделения на модули:

  • index.js — основной файл стора, который импортирует и хранит все модули;
  • auth.js — хранит состояние авторизации, логин, логаут, рефреш токена и т.д.;
  • user.js — хранит данные юзера и методы, связанные с ними;
  • config.js — хранит настройки приложения.

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

yaml
const defaultState = () => {
 return {
 exampleData: {
 prop1: '1',
 prop2: '2'
 }
 }
}

const getters = {
 exampleGetter: (state) => state.exampleData.prop1
} const mutations = {
 setExampleData (state, data) {
 state.exampleData = data
 }
} const actions = {
 async fetchData ({ commit }) {
 try {
 const response = await getData()
 commit('setExampleData', response.data)
 } catch (err) {
 console.error(err)
 }
 },

}

export default {
 namespaced: true,
 state: defaultState,
 getters,
 actions,
 mutations
}

В то же время в основном хранилище мы импортируем модули:

yaml
import example from '@/store/example'
…
…

export default {
 state: defaultState,
 getters,
 actions,
 mutations,
 modules: { example }
}

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

yaml
computed: {
 ...mapGetters('example', ['exampleGetter'])
}

Продолжение

Оставшиеся три принципа — обёртки для библиотек, типизация пропсов и организация стилей — разобраны во второй части.

VueFrontendJavaScriptVuexАрхитектура frontendAPI
Загружаем чат

Напишите в поддержку

Опишите проблему — команда разберётся.