Vue.js. Создание динамических пользовательских интерфейсов с помощью компонентов и реактивных свойств

от автора

Привет, Хабр!

Меня зовут Сергей Журавлев, я участник профессионального сообщества NTA.

Проведение опросов, викторин и других игровых обучающих активностей среди сотрудников является частью корпоративной культуры многих компаний. При этом по ряду причин использование существующих веб приложений бывает затруднительным, либо невозможным. В связи с чем, задача разработки собственной легковесной, адаптируемой и расширяемой платформы игровых активностей с динамическими пользовательскими интерфейсами (DUI) является актуальной. Отмечу, что DUI плотно вошли в практику современной веб‑разработки. Зачастую, без них невозможно представить себе реализацию ключевого функционала приложения, поскольку они предоставляют значительную гибкость и интерактивность.

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

В этом отношении, React, например, имеет несколько иной подход, при котором синтаксис html «растворяется» в JavaScript коде, расширяясь в вариацию языка JSX. Что, в конечном итоге, требует более узкой специализации от разработчиков и более сложного взаимодействия между участниками.

Тем не менее, важно понимать, что выбор фреймворка среди «большой тройки» React, Angular, Vue.js, часто является делом вкуса, поскольку все они позволяют реализовывать требуемый функционал в полной мере. И сложно представить себе задачу, которую можно решить только на React, но не получится на Vue.js и наоборот. А, учитывая вышесказанное, для написания своего приложения окончательный выбор пал на Vue.js.

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

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

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

Компоненты могут быть созданы для любых элементов интерфейса, таких как кнопки, поля ввода, меню и т. д. Каждый компонент может содержать свои собственные CSS‑стили, JavaScript и HTML‑разметку. Это позволяет разработчикам легко настраивать и изменять каждый компонент в соответствии с требованиями проекта.

Структурно компоненты состоят из трех блоков: <template>, <script> и <style>:

<template>   <p>Привет, {{name}}</p> </template> <script>   export default {     name: ‘NewComponent’,     data() {return {name: «Vue»}},   }; </script> <style>   p{color: #fff} </style>

В примере раздел <template>,представляет собой html‑разметку страницы с системой интерполяции данных, позволяющей передавать значение переменных в структуру разметки. Раздел <style> для настройки стиля страницы. Вся же логика и главный функционал приложения содержится в блоке <script> .

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

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

Component.vue <template>     <div>         <p>Привет, {{ customName }}!</p>         <button v-on:click="answer()">Приветствую</button>     </div> </template> <script> export default {     props: ["customName"],     data() {         return {             defaultName: "пользователь",         }     },     created () {         if (!this.customName) {             return this.customName = this.defaultName         }     },     methods: {         answer() {             this.customName = "Vue"         }     }, }; </script> <style>     div{         background-color: #0000001f;     }     p{         color: #fff;     } </style>

Создание компонента начинается с определения входных свойств, которые должны быть переданы извне. В моем случае — это имя пользователя customName, которое ожидается в качестве значения в атрибуте props. Также в качестве внутренней переменной объявляю и устанавливаю значение по умолчанию defaultName, на случай, если никаких свойств в props передано не будет.

props: ["customName"], data() {     return {         defaultName: "пользователь",     } },

Исходя из этого, мне следует определить, было ли фактически передано значение customName и, если нет — подменить ожидаемое дефолтным. Это возможно сделать с помощью специального метода created(), который автоматически вызывается после создания экземпляра компонента. Created относится к одному из хуков жизненного цикла компонентов, о которых подробнее можно узнать в официальной документации (https://vuejs.org/guide/essentials/lifecycle.html).

created () {         if (!this.customName) {             return this.customName = this.defaultName         }     },

Также я хочу, чтобы мой компонент при нажатии на кнопку, выводил ответное приветствие ко Vue, для чего напишу соответствующий метод answer():

methods: {     answer() {         this.customName = "Vue"     } },

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

<template>   <div>     <p>Привет, {{ customName }}!</p>     <button v-on:click="answer()">Приветствую</button>   </div> </template>

Как говорил ранее, главная особенность компонентов — это их переиспользуемость. Это означает, что я могу импортировать полученный компонент в родительский и вызывать его многократно. Привожу ниже код родительского компонента Parent.vue:

Parent.vue <template>   <div>       <my-component />       <my-component customName="Sergey"/>       <my-component v-bind:customName="newName" />   </div> </template>  <script> import Component from "./Component.vue";  export default {   components: {     my-component: Component,   },   data() {     return {       newName: "Vasya"     }   }, } </script>

Здесь вызывается три экземпляра дочернего компонента Component.vue, причем каждый экземпляр является самостоятельным и не зависит от других. Это, в свою очередь, позволяет вызывать их с собственными атрибутами. Например, первый вызывается без передачи атрибутов, что должно привести к замене имени пользователя дефолтным значением. Во второй экземпляр я передаю строку с именем пользователя, а в третий — переменную newName с заранее заданным значением. Таким образом, на странице отобразятся все три экземпляра с разным приветствием, а при нажатии на каждую из кнопок произойдет независимый от остальных ответ.

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

Реактивность в Vue.js — это механизм, который автоматически обновляет представление компонентов при изменении данных, от которых они зависят. Реактивные свойства реализованы с помощью автоматического метода Vue.observable(). При вызове в него передается объект, который нужно сделать реактивным. В результате получается новый объект, который содержит все свойства и методы оригинального объекта, но с возможностью отслеживания изменений. Когда свойство реактивного объекта изменяется, Vue.js автоматически обновляет все зависимые свойства, в том числе обновляет пользовательский интерфейс.

Для работы с реактивными свойствами Vue.js предоставляет ряд атрибутов, методов и директив, такие как watch, computed, v‑model и другие. Метод watch, например, позволяет отслеживать изменения конкретного свойства данных и выполнять определенные действия при их изменении. Computed свойства позволяют вычислять значения на основе данных и обновляться только при изменении зависимых свойств. Директива v‑model позволяет связать данные с элементом формы, таким как input, select или textarea. При изменении значения элемента, данные автоматически обновляются, а при изменении данных — обновляется значение элемента формы.

Ниже приведен пример использования директивы v‑model, которая связывает новый элемент input, в написанном ранее компоненте, с переменной enteredName. В результате, при вводе имени в поле input автоматически последует изменение приветствия, что является наглядным примером реактивности Vue.js.

Component.vue <template>     <div>         <label>Имя пользователя:</label>         <input v-model="enteredName">         <p>Привет,             <span v-if="enteredName">{{ enteredName }}</span>             <span v-else>{{ customName }}</span>!         </p>         <button v-on:click="answer()">Поприветствовать</button>     </div> </template> <script> export default {     props: ["customName"],     data() {         return {             defaultName: "пользователь",             enteredName: "",         }     },     created () {         if (!this.customName) {             return this.customName = this.defaultName         }     },     methods: {         answer() {             this.enteredName = ""             this.customName = "Vue"         }     }, }; </script>

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

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

На схеме указаны пять компонентов:

— Title.vue — заголовки и инструкции;

— Main.vue — текст вопроса, объяснение ответа, если предусмотрено;

— Info.vue — информация о верном/неверном ответе;

— Button.vue — кнопка с вариантом ответа;

— Next.vue — переход к следующему вопросу.

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

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

Title.vue <template>     <div>         <p class="number">{{ number }}/{{ quiz_length }}</p>     </div> </template>  <script> export default {     props: ['number', 'quiz_length'], }; </script>
Main.vue <template>     <div>         <div class="question">         <div class="center-field">           <p class="justify">{{ question }}</p>         </div>       </div>     </div> </template> <script> export default {     props: ["question"], }; </script>

Чуть интереснее с точки динамичности компонент Info.vue. Он также получает свойства из родительского, однако в зависимости от их значения ведет себя по‑разному. Первоначально ожидается, что компонент не виден пользователю до выбора варианта ответа. А затем он должен не только проинформировать о правильности ответа, но и применить к своему содержимому разный цветовой стиль. Это достигается использованием системы директив <v‑if> <v‑else>, и присвоением элементу <div> разного дополнительного класса в зависимости от значения переменной <is_right>.

Info.vue <template>     <div>         <div class="info-field">             <p v-if="disabled"             class="info"             :class="{                 right: is_right,                 false: !is_right,             }">                 <span v-if="is_right">ВЕРНО!</span>                 <span v-else>НЕВЕРНО!</span>             </p>         </div>     </div> </template> <script> export default {     props: ["is_right", "disabled"], }; </script>

Аналогичный подход я использую в компоненте Next.vue, с той лишь разницей, что кнопка «Далее» должна быть неактивной до выбора варианта ответа, чтобы не позволить пользователю пропустить вопрос, а затем так же как и Info.vue менять стиль и содержимое при разных сценариях:

Next.vue <template>     <div>         <button           :disabled="!disabled"           class="next-button"           :class="{ inactive: !disabled }"           @click="getNext()"         >           <p v-if="!is_full">Далее</p>           <p v-else>Завершить</p>         </button>     </div> </template> <script> export default {     props: ["is_full", "disabled"],     data() {return {}},     methods: {         getNext() {             this.$emit('getNext', {})         }     }, }; </script>

Стоит обратить внимание, что компонент Next.vue не только принимает данные из родительского компонента, но и передает событие и данные (в нашем случае пустой объект) обратно в родительский с помощью такой конструкции:

getNext() {     this.$emit('getNext', {}) }

Здесь метод getNext() создает одноименное событие, которое будет отслеживаться в родительском компоненте. Таким образом, при его срабатывании родительский компонент получит данные переданного в событии массива, что позволит благодаря реактивным свойствам выполнить последующие действия. В моем случае, при нажатии кнопки «Далее» в дочернем компоненте, отправляется запрос на получение нового задания в родительском. Более широко это используется в компоненте Button.vue:

Button.vue <template>   <div     :class="{       right: buttonProps.is_right && (buttonProps.is_pushed || disabled),       false: !buttonProps.is_right && buttonProps.is_pushed,       disabled: disabled}"   @click="answered()">   <p>{{ buttonProps.title }}</p>   </div> </template> <script> import axios from "axios"; import config from "@/scripts/api-config"; const API_URL = config["API_LOCATION"];  export default {     props: ['buttonProps', 'is_answered', 'disabled'],     data() {         return {             answer: {}         }     },     methods: {         answered() {             if (!this.is_answered) {                 if (this.buttonProps.is_right) {                     this.answer = { answer_field: 0, score: 1 }                 } else {                     this.answer = { answer_field: 1, score: 0 }                 }                 axios.patch(API_URL, this.answer);             }             this.buttonProps.is_pushed = true             this.$emit('pushAnswer', {                 is_pushed: true,                 is_right: this.buttonProps.is_right,             })         },     } }; </script>

В Button.vue используются практически все вышеперечисленные методы и директивы. Из наиболее интересного здесь, как по директиве v‑on:click, запускается метод answered(), который отправляет на сервер служебную информацию, а затем возвращает в родительский компонент событие с массивом данных о том, какой из вариантов был выбран и был ли этот вариант правильным. Что важно, на основе этих данных родительский компонент реактивно меняет значения, которые передаются во все экземпляры текущего и в другие компоненты, заставляя их динамически реагировать. т. е. происходит своеобразный цикл: из родительского компонента в дочерние передается значение, в одном из дочерних оно изменяется, возвращается в родительский, из которого уже в измененном виде снова передается в дочерние, меняя их свойства.

Благодаря этому, в моем приложении при клике на один из вариантов ответа, родительский компонент не только заставляет перекраситься ошибочному варианту в красный цвет, но и сообщает экземпляру Button.vue с верным вариантом подсветиться зеленым. В то же время, на основе этих данных меняется отображение и цвет компонента Info.vue с полем верно/неверно. Наконец, кнопка «Далее» в компоненте Next.vue меняет стиль и становится активной для запроса следующего задания. т. е. динамически изменяются практически все элементы интерфейса.

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


ссылка на оригинал статьи https://habr.com/ru/articles/749886/