Организация работы с Vue: к чему я пришёл
Моя первая публикация в блоге…
…и сразу же про Vue.js. На самом деле, меня довольно многое связывает с этим фреймворком. Моё первое SPA-приложение было написано на jQuery: использовало AJAX для загрузки уже отрендеренного HTML и замены его в определённом блоке, который сейчас бы я назвал router-outlet.
Vue.js стал первым фреймворком в моей жизни и сразу показал мне, что SPA - это не больно. Круто было осознавать, что больше не нужно изобретать кучу велосипедов, чтобы “сайт менял страницы не перезагружаясь”.
В течение двух лет я пишу на Vue, и выработал несколько практик, которые с каждым днём делают мою работу легче. Прежде всего это касается организации работы с данными.
Я жалею только об одном: что не додумался об этом раньше.
Небольшой дисклеймер: я не претендую на первенство. Скорее всего, так до меня уже кто-то делал. Я всего лишь делюсь своими соображениями.
Что предполагает Vue
Основной способ разработки на Vue.js - использование однофайловых компонентов. На первый взгляд это безумно удобно: все составляющие вашего приложения под рукой, в пределах одного файла, главное лишь правильно “разделить” весь интерфейс на логические блоки.
И роутер работает с компонентами, и основной блок приложения - компонент. Это очень удобно, когда приложение небольшого размера и логики в нём не очень много. Но стоит подумать о чём-то более сложном, как сразу же возникают проблемы.
Если проблему с хранением данных решает Vuex, то от избытка кода никуда не уйти. И хотя Webpack успешно решает вынос javascript-части приложения в отдельный файл через script src, размер этого файла легко может выйти из-под контроля.
Можно перенести часть логики во Vuex, но это также может оказаться не самой лучшей идеей. Во-первых потому, что рано или поздно хранилище разрастётся (даже если выносить всё в отдельные модули), а во-вторых потому, что (и это моё субъективное мнение) работа с Vuex довольна громоздкая в плане написания кода (this.$store.commit( ‘NameOfMutation’, payload )).
В итоге, спустя два года активного написания приложений на Vue, я пришёл к простой истине: разделение - это хорошо.
Разделение - это хорошо
Мой подход заключается в простой вещи: мы должны поделить приложение на “зоны ответственности”:
- Шаблон. Файл с расширением .vue, который нужен исключительно для отрисовки данных. В нём не должно происходить ничего, кроме того, что нужно для отрисовки данных. Исключение составляют методы и computed-свойства, отвечающие за финальную подготовку данных к показу в шаблоне.
- Стили. Если компонент или страница имеют большое количество стилей, то их лучше вынести в отдельный css-файл (или файл других стилевых движков). Для себя я определил порог в 100 строк. Если стилей больше, они переезжают в свой файл.
- Логика. Я предпочитаю выносить всю работу с логикой компонента в отдельный сервис. Благо, новые стандарты javascript уже имеют синтаксический сахар для классов, поэтому я использую именно их. Если ваш проект пишется на typescript, то использовать сервисы-классы становится ещё на порядок приятнее.
- Данные. Здесь нам приходит на помощь Vuex и его именованные пространства имён. Важно, что в vuex-хранилище я не провожу никакие операции с данными, несмотря на наличие actions. Сторы я использую только для хранения, а все операции производятся в сервисе.
На практике
Давайте представим, что мы решили написать компонент поиска фильмов по названию, с автокомплитом и обращением к серверу. Мы начинаем вводить название фильма, часть введённой строки отправляется на сервер, который ищет все подходящие варианты и отправляет их в ответе. Далее компонент показывает поле со списком фильмов, удовлетворивших условию поиска.
Весь дальнейший код – не пример готового приложения. Моя цель просто показать, как я организую работу с файлами и разделением кода.
Итак, представим, что мы находимся в папке src приложения. В ней я создаю директорию components/FilmSearcher. Создадим в ней следующие файлы:
# ./src/components/FilmSearcher/ ./FilmSearcher.vue ./FilmSearcher.service.js ./FilmSearcher.store.js ./FilmSearcher.style.scss
Думаю, не трудно догадаться, за что отвечает каждый из этих файлов.
Файл шаблона будет выглядеть примерно так:
<template> <div class="film-searcher-component"> <input class="film-searcher-input" type="text" @input="searchFilms" v-model="filmSearcherText" > <div class="film-searcher-autocomplete" v-if="filmVariants.length"> <template v-for="(variant, idx) in filmVariants"> <div class="film-searcher-variant" :key="idx" v-html="variant.title"></div> </template> </div> </div> </template> <script> import FilmSearcherService from './FilmSearcher.service'; export default { name: 'FilmSearcher', data() { return { service: null, filmSearcherText: '' } }, computed: { filmVariants () { return this.service.filmVariants; } }, methods: { async searchFilms () { await this.service.search(this.filmSearcherText); } }, created () { this.service = new FilmSearcherService({ store: this.$store }); }, beforeDestroy () { // Удаляем хранилище компонента this.service.unregisterStorage() } } </script> <style lang="scss"> @import "./FilmSearcher.style"; </style>
В хуке created важно инициализировать наш сервис. Ещё важнее передать в него ссылку на глобальное хранилище приложения.
Пример файла-сервиса:
import http from 'http'; import FilmSearcherModule from './FilmSearcher.store' // Импортируем стор export default class FilmSearcherService { constructor(payload) { this.payload = payload; this.registerStorage(); // Вызов создания хранилища } // Динамически создаём хранилище для нашего компонента registerStorage () { this.payload.store.registerModule('filmSearcher', FilmSearcherModule); } // Удаляем хранилище компонента unregisterStorage () { this.payload.store.unregisterModule('filmSearcher'); } async search (filmTitle) { const foundVariants = await this.http.get('https://server.com', {title: filmTitle}); const result = // ...здесь обрабатываем результаты, если нужно // Записываем обработанные данные в хранилище this.payload.store.commit('filmSearcher/setFilms', result); } get filmVariants () { return this.payload.store.getters['filmSearcher/films']; } }
Мы видим, что в сервисе происходят все операции, которые нужны для получения данных. Главный метод сервиса – search – возвращает результат, который передаётся в компонент. Задача сервиса – максимально подготовить данные к как можно более быстрой отрисовке.
Также здесь мы используем возможность динамической регистрации модулей Vuex. Вы можете использовать и глобальный модуль, если это нужно.
Вот так выглядит vuex-модуль компонента
export default FilmSearcherModule { namespaced: true, state: { _films: [] }, mutations: { setFilms(state, payload) { state._films = payload; } }, getters: { films: (state) => state.films; } }
Здесь наглядно видно, что vuex-модуль не используется для обработки данных. Только хранение, запись и чтение. Всё.
Что касается файла со стилями, я не думаю, что есть смысл приводить его содержимое. Стилизация компонента в данной статье особой роли не играет, да и делается там всё как обычно.
Проговорим вслух
Чтобы закрепить вышесказанное, давайте пропишем путь нашего компонента внутри приложения.
- При создании компонента вызывается конструктор его сервиса. В нём же мы автоматически инициализируем хранилище компонента.
- Вся работа с данными в компоненте никогда не происходит напрямую. Компонент отдаёт приказ сервису, который уже залезает в хранилище, обрабатывает всё должным образом и отдаёт своему “начальнику”.
- Сервис не только забирает данные из хранилища, но и добавляет их туда, обращаясь к мутациям.
- Все операции с данными внутри хранилища происходят исключительно посредством мутаций и геттеров.
- При запуске ивента beforeDestroy внутри компонента хранилище компонента автоматически удаляется.
А не лишнее?
Кому-то такой подход может показаться чрезмерным усложнением, и такое мнение, безусловно, имеет право на существование. Однако, мой опыт vue-разработки показал, что очень часто приложение дорабатывается, переделывается и дополняется, и такое разделение очень хорошо помогает управлять имеющимся функционалом.
Поначалу применение такого подхода немного увеличивает время, потраченное на начальном этапе разработки, но это время с лихвой компенсируется впоследствии, когда вы возвращаетесь с новыми задачами в старый компонент. Все преимущества разделения становятся прозрачнее при разработке больших проектов (особенно если в разработке используется TypeScript с class-based синтаксисом).
Итого
Во всех проектах, которыми я сейчас занимаюсь на работе и в качестве хобби, я использую именно такую разбивку. И, пока такой подход не покажет свою несостоятельность, я вряд ли вернусь к однофайловым компонентам.
Интересно мнение сообщества на этот счёт!