Blog
Back

Six simple principles for writing a Vue app that's easy to maintain (Part 1)

#Development 14 march 2023
Привет! Меня зовут Наташа Калачева. Я Frontend-разработчик в компании AGIMA. Vue — один из самых популярных фреймворков JS, его используют для разработки SPA и PWA. А его главные плюсы — это понятная, четкая документация, готовая структура и низкий порог входа.
Тем не менее, Frontend сегодня — это сложные приложения, которые содержат не только красивые элементы интерфейса, но и большую часть логики и функциональности всего продукта. Это требует от нас тщательного планирования и организации проекта, чтобы сделать его масштабируемым и простым.
В этой статье поделюсь правилами, которых придерживаюсь в работе и которые помогают упростить поддержку и расширение приложения. Мы рассмотрим, как организовать хранение компонентов, стилей и плагинов, когда использовать стор и полезные функции Vue.
Следуя этим рекомендациям, вы сможете создавать более эффективные проекты.

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

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

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

Assets: здесь организуем хранение файлов CSS, шрифтов и изображений.

Components: это автономные компоненты Vue, которые одновременно инкапсулируют структуру шаблона, логику JavaScript и представление CSS.

Router: хранит все настройки роутинга и маршруты.

Store: содержит конфиг и данные хранилища (Vuex, Pinia).

Хорошая практика — придерживаться уже готового решения и расширять по мере необходимости. Хотя Vue не дает строгих рамок и мы можем менять файловую структуру как хотим, всё же использование знакомых стандартов делает проект более предсказуемым и простым.
Расширение базовой структуры
Это хороший старт, но в больших приложениях мы неизбежно расширяем эту структуру. Лучше сделать это в начале разработки, а не пытаться изменить, когда дедлайн близко. О чем же нужно задуматься на берегу?
Первое — это компоненты и принципы их разделения. Всё приложение состоит из набора компонентов. Если хранить их в одной папке, мы вскоре столкнемся с огромным количеством неструктурированного контента.
На какие папки можно разделить компоненты?
Components (ui). Здесь хранятся компоненты дизайн-системы. Это самые простые элементы интерфейса, которые часто переиспользуются. Обычно эти компоненты взаимодействуют с «внешним миром» через пропсы и события. Обращение из них к стору и роутингу будет лишним. Чаще всего они не отправляют запросов к серверу и не содержат сложной бизнес-логики.
Примеры таких компонентов: инпуты, кнопки, алерты и другие UI-элементы.
Фотография

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 создаем отдельную папку в корне проекта, где хранятся краткие запросы к сервисам, возвращающие промис.
Фотография

Каждый файл хранит и экспортирует нужные функции по категориям. Например, products.js может содержать следующее:
                    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)
}
                
Таким образом, мы документируем все доступные методы общения с сервером по категориям. И если путь изменится, мы поменяем его в одном файле, а не во всех местах, где используем запрос. Здесь важно, что мы отделяем чистый запрос от любой бизнес-логики.
Далее мы можем использовать расширенный вариант функции в сторе или методах компонента, нанизывая бизнес-логику, хранение данных, отлов ошибок. Например, в сторе:
                    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)
   }
 }
}
                
Плохим вариантом будет просто выполнять запросы к урл в компоненте. Например, так:
                    methods: {
   getProductById (id) {
     return axios.get(`https://example.com/api/products/${id}`)
   }
}
                
Когда новому разработчику понадобится этот запрос в другом компоненте, ему придется копипастить или писать заново. А если поменяется урл запроса, то менять его в нескольких местах.
Основной путь к серверу лежит в .env, например VUE_APP_API_URL. Так что доступ к этой переменной возможно получить в любом месте приложения, и он может динамически изменяться в зависимости от окружения.
                    const API_URL = process.env.VUE_APP_API_URL
                
Основные плюсы этого подхода в том, что он достаточно прост и гибок, обеспечивает хорошую читабельность, сразу видно, какие запросы возможны в любой части приложения.
Это отлично работает для небольших и средних приложений, в которых не хочется усложнять структуру. Хотя для больших систем можно посмотреть в сторону разделения данных ORM. Подробнее об этом в документации для Vuex ORM. Это плагин Vuex, который позволяет разделять состояние приложения с точки зрения объектов данных (продуктов, пользователей и т. д.) и операций CRUD (создания, обновления, удаления).

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

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

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

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

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

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

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

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

Создание модуля ничем не отличается от обычного центрального хранилища Vuex. Синтаксис почти такой же, за исключением того, что мы не создаем новый экземпляр хранилища в модуле. Вместо этого мы экспортируем объект, содержащий все его свойства, в экземпляр центрального хранилища:
                    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
}
                
В то же время в основном хранилище мы импортируем модули:
                    import example from '@/store/example'
…
…

export default {
 state: defaultState,
 getters,
 actions,
 mutations,
 modules: { example }
}
                
Хорошая практика использовать атрибут namespaced:true у модуля, чтобы обеспечить его автономность. В этом случае вы сможете повторно использовать одно и то же имя для своего состояния, изменения и действий, не вызывая ошибок. В компонентах мы можем обращаться напрямую к модулю по его названию:
                    computed: {
   ...mapGetters('example', ['exampleGetter'])
}
                

Продолжение завтра

Написав первые 3 пункта, я поняла, что статья получается большой. Поэтому пришлось разбить ее на две части. Завтра в блоге AGIMA выйдет продолжение. Ссылка на него появится тут. А если у вас возникли вопросы по первым трем пунктам — задавайте в комментариях. Постараюсь оперативно ответить.
P. S. Вопросы также можно задать в нашем телеграм-канале для разработчиков. В нем уже более 500 человек — присоединяйтесь.

Content-hub

0 / 0
+7 495 981-01-85 + Become a customer
Services Cases Content-hub