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

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

При этом по ряду причин использование существующих веб приложений бывает затруднительным, либо невозможным. В связи с чем, задача разработки собственной легковесной, адаптируемой и расширяемой платформы игровых активностей с динамическими пользовательскими интерфейсами (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> <p>Привет, {{name}}</p> </template> <script> export default { name: ‘NewComponent’, data() {return {name: «Vue»}}, }; </script> <style> p{color: #fff} </style>

В примере синим шрифтом выделен раздел

Основываясь на описанной структуре, для примера создам компонент, который приветствует пользователя по имени, в том числе позволяет нажатием кнопки поприветствовать 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.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.

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>

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

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

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

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

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

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

2
Начать дискуссию
\n","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

В примере синим шрифтом выделен раздел

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"Component.vue\n\n\n","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"props: [\"customName\"],\ndata() {\n return {\n defaultName: \"пользователь\",\n }\n},","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"created () {\n if (!this.customName) {\n return this.customName = this.defaultName\n }\t\n },","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"methods: {\n answer() {\n this.customName = \"Vue\"\n }\n},","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"Parent.vue\n\n\n","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"b1b82900-3131-559d-9d60-abadb911ccf3","width":898,"height":510,"size":110341,"type":"png","color":"9e9e9f","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAJQAlAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFwAAAwEAAAAAAAAAAAAAAAAABAUHCP/EACYQAAAEAwcFAAAAAAAAAAAAAAECAwUABBEGEhMhIzHRIiU1Q5P/xAAVAQEBAAAAAAAAAAAAAAAAAAAAAf/EABURAQEAAAAAAAAAAAAAAAAAAAAR/9oADAMBAAIRAxEAPwDSVmXefVZLWqKuahgTCUAhjLKGKXVHYK3ggEJ3mYvm7qluPsX5iisM7Y2g2Pgg3ywCsEviaReuh8q5ZxAGLS1V8ZKfEvEKP//Z"}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"4568b934-7e91-570b-8691-29151f359715","width":941,"height":307,"size":58663,"type":"png","color":"9c9d9e","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAJQAlAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAACAIH/8QAKBAAAQIEAwgDAAAAAAAAAAAAAQIDAAQFBgcRMRITFCNBQlNhkZLT/8QAFgEBAQEAAAAAAAAAAAAAAAAAAQAC/8QAGBEBAQEBAQAAAAAAAAAAAAAAAAERAiH/2gAMAwEAAhEDEQA/AHRal23pdmGVxVGrSxptTlytuWU0y40oDZQQoA5qzzJ0jXVnV8mCTGa8fil0uqqfMz+cGEni86Vjmr0PcfUCVvnvKv7GJP/Z"}}}]}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"Component.vue\n\n","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"491561ac-411d-597d-9a10-03b1e2a04088","width":955,"height":526,"size":104286,"type":"png","color":"cfe1e9","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAJQAlAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFgABAQEAAAAAAAAAAAAAAAAABgcJ/8QAJBAAAQQBAwMFAAAAAAAAAAAAAQIDBAURAAYhBxITMTJBUXH/xAAXAQADAQAAAAAAAAAAAAAAAAACAwUE/8QAIhEBAAIBAwMFAAAAAAAAAAAAAQACAwQRIQUS8BQxQWLh/9oADAMBAAIRAxEAPwDRGdc7gG6U11UK1cNxppYefcIUCQc4AGD6fY0y2O5UQkrLqNT6jswle3jlXf8AYXPU2yYJYXGaKmz2HAGMjjjnRWw5Rfaba9Q6YAWuj59ZJup1nZRN6xWYthJZb8LB7W3VJHvPwDqziBo7yDfix58wNLvrxMp5KbmcAHFAASF8DP7p4G0TZ5Z//9k="}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"media","cover":false,"hidden":false,"anchor":"","data":{"items":[{"title":"","image":{"type":"image","data":{"uuid":"b16925ec-1844-5d52-ba2b-65c9d58afea4","width":924,"height":615,"size":78470,"type":"png","color":"edeaee","hash":"","external_service":[],"base64preview":"/9j/4AAQSkZJRgABAQIAJQAlAAD/2wBDAAMCAgICAgMCAgIDAwMDBAYEBAQEBAgGBgUGCQgKCgkICQkKDA8MCgsOCwkJDRENDg8QEBEQCgwSExIQEw8QEBD/2wBDAQMDAwQDBAgEBAgQCwkLEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBAQEBD/wAARCAAKAAoDAREAAhEBAxEB/8QAFQABAQAAAAAAAAAAAAAAAAAABAn/xAAgEAACAgIABwAAAAAAAAAAAAABAgADBBESEyExM3GS/8QAFgEBAQEAAAAAAAAAAAAAAAAAAgMB/8QAGhEBAAMAAwAAAAAAAAAAAAAAAAECIRFBUf/aAAwDAQACEQMRAD8Ap9jNZbju9dTMeJl0H0ektbJ1KulhbdeJvuHmPS0eoDk2ezNt0NSR2hJ//9k="}}}]}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"Title.vue\n\n\n","lang":""}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"Main.vue\n\n","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"Info.vue\n\n","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"Next.vue\n\n","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"getNext() {\n this.$emit('getNext', {})\n}","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"code","cover":false,"hidden":false,"anchor":"","data":{"text":"Button.vue\n\n","lang":""}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}},{"type":"text","cover":false,"hidden":false,"anchor":"","data":{"text":"

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

"}}],"summaryContent":null,"isExistSummaryContent":false,"warningFromEditor":null,"warningFromEditorTitle":null,"counters":{"comments":0,"favorites":0,"reposts":0,"views":672,"hits":11327,"reads":null,"online":0},"dateFavorite":0,"hitsCount":11327,"isCommentsEnabled":true,"isLikesEnabled":true,"isRemovedByUserRequest":false,"isFavorited":false,"isPinned":false,"repostId":null,"repostData":null,"subscribedToTreads":false,"isEditorial":false,"isAudioAvailable":false,"audioUrl":null,"isAudioAvailableToGenerate":false,"commentEditor":{"enabled":true,"who":null,"text":"","until":null,"reason":null,"type":"everybody"},"isBlur":false,"isPublished":true,"isDisabledAd":false,"withheld":[],"ogTitle":null,"ogDescription":null,"url":"https://vc.ru/id447345/765510-vuejs-sozdanie-dinamicheskih-polzovatelskih-interfeisov-s-pomoshyu-komponentov-i-reaktivnyh-svoistv","author":{"id":447345,"name":"NTA","nickname":null,"description":null,"uri":"","avatar":{"type":"image","data":{"uuid":"e7175678-aaab-09f8-0271-7b3af7ac4670","width":1207,"height":1207,"size":83090,"type":"jpg","color":"e3ebf8","hash":"","external_service":[]}},"cover":{"cover":{"type":"image","data":{"uuid":"8575b55d-a1f0-53ee-9ad6-78268f9ea45c","width":1280,"height":373,"size":56005,"type":"jpg","color":"b0b9ca","hash":"","external_service":[]}},"cover_y":0},"achievements":[{"title":"Год на vc.ru","code":"registration_1_year","description":"Первый год с vc.ru. Получена 24 июля 2025.","previewUuid":"0d11c244-49de-50e7-894e-b9b27945d42b","formats":{"glb":"https://static.vc.ru/achievements/fish.glb","usdz":"https://static.vc.ru/achievements/fish.usdz"},"viewData":{"contentColor":"#C67AA3","textMaxWidth":0.634765625,"textX":0.5888671875,"textY":0.54296875,"logoX":0.5859375,"logoY":0.6669921875,"logoXNoText":0.6044921875,"logoYNoText":0.5439453125},"id":4980908,"userId":447345,"count":0,"shareImage":"https://api.vc.ru/achievements/share/4980908"},{"title":"3 года на vc.ru","code":"registration_3_years","description":"Провёл 3 года вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"d9d72ac5-bcb5-55e0-8c72-b99251e5cdd9","formats":{"glb":"https://static.vc.ru/achievements/shark.glb","usdz":"https://static.vc.ru/achievements/shark.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.5205078125,"textY":0.341796875,"logoX":0.5205078125,"logoY":0.4609375,"logoXNoText":0.5,"logoYNoText":0.3662109375},"id":1356821,"userId":447345,"count":0,"shareImage":"https://api.vc.ru/achievements/share/1356821"},{"title":"5 лет на vc.ru","code":"registration_5_years","description":"Провёл 5 лет вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"a9140d54-73b8-5f40-afa8-449fbaafd42b","formats":{"glb":"https://static.vc.ru/achievements/whale.glb","usdz":"https://static.vc.ru/achievements/whale.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.533203125,"textY":0.658203125,"logoX":0.533203125,"logoY":0.77734375,"logoXNoText":0.4375,"logoYNoText":0.66015625},"id":130113,"userId":447345,"count":0,"shareImage":"https://api.vc.ru/achievements/share/130113"}],"lastModificationDate":1764935783,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":true,"badgeId":null,"isDonationsEnabled":false,"isPlusGiftEnabled":true,"isUnverifiedBlogForCompanyWithoutPro":true,"isRemovedByUserRequest":false,"isFrozen":false,"isDisabledAd":false,"isPlus":false,"isVerified":false,"isPro":false,"yandexMetricaId":null,"badge":null,"isOnline":false,"tgChannelShortname":null,"isUnsubscribable":true,"type":1,"subtype":"personal_blog"},"subsite":{"id":447345,"name":"NTA","nickname":null,"description":null,"uri":"","avatar":{"type":"image","data":{"uuid":"e7175678-aaab-09f8-0271-7b3af7ac4670","width":1207,"height":1207,"size":83090,"type":"jpg","color":"e3ebf8","hash":"","external_service":[]}},"cover":{"cover":{"type":"image","data":{"uuid":"8575b55d-a1f0-53ee-9ad6-78268f9ea45c","width":1280,"height":373,"size":56005,"type":"jpg","color":"b0b9ca","hash":"","external_service":[]}},"cover_y":0},"achievements":[{"title":"Год на vc.ru","code":"registration_1_year","description":"Первый год с vc.ru. Получена 24 июля 2025.","previewUuid":"0d11c244-49de-50e7-894e-b9b27945d42b","formats":{"glb":"https://static.vc.ru/achievements/fish.glb","usdz":"https://static.vc.ru/achievements/fish.usdz"},"viewData":{"contentColor":"#C67AA3","textMaxWidth":0.634765625,"textX":0.5888671875,"textY":0.54296875,"logoX":0.5859375,"logoY":0.6669921875,"logoXNoText":0.6044921875,"logoYNoText":0.5439453125},"id":4980908,"userId":447345,"count":0,"shareImage":"https://api.vc.ru/achievements/share/4980908"},{"title":"3 года на vc.ru","code":"registration_3_years","description":"Провёл 3 года вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"d9d72ac5-bcb5-55e0-8c72-b99251e5cdd9","formats":{"glb":"https://static.vc.ru/achievements/shark.glb","usdz":"https://static.vc.ru/achievements/shark.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.5205078125,"textY":0.341796875,"logoX":0.5205078125,"logoY":0.4609375,"logoXNoText":0.5,"logoYNoText":0.3662109375},"id":1356821,"userId":447345,"count":0,"shareImage":"https://api.vc.ru/achievements/share/1356821"},{"title":"5 лет на vc.ru","code":"registration_5_years","description":"Провёл 5 лет вместе с vc.ru. Получена 23 июля 2025.","previewUuid":"a9140d54-73b8-5f40-afa8-449fbaafd42b","formats":{"glb":"https://static.vc.ru/achievements/whale.glb","usdz":"https://static.vc.ru/achievements/whale.usdz"},"viewData":{"contentColor":"#8E6F09","textMaxWidth":0.66796875,"textX":0.533203125,"textY":0.658203125,"logoX":0.533203125,"logoY":0.77734375,"logoXNoText":0.4375,"logoYNoText":0.66015625},"id":130113,"userId":447345,"count":0,"shareImage":"https://api.vc.ru/achievements/share/130113"}],"lastModificationDate":1764935783,"isSubscribed":false,"isSubscribedToNewPosts":false,"isMuted":false,"isAvailableForMessenger":true,"badgeId":null,"isDonationsEnabled":false,"isPlusGiftEnabled":true,"isUnverifiedBlogForCompanyWithoutPro":true,"isRemovedByUserRequest":false,"isFrozen":false,"isDisabledAd":false,"isPlus":false,"isVerified":false,"isPro":false,"yandexMetricaId":null,"badge":null,"isOnline":false,"tgChannelShortname":null,"isUnsubscribable":true,"type":1,"subtype":"personal_blog"},"reactions":{"counters":[{"id":1,"count":2}],"reactionId":0},"isNews":false,"source":null,"clusters":[],"donations":{"amount":0,"isDonated":false},"commentsSeenCount":null,"keywords":["fff","0000001f"],"media":null,"customCover":null,"robotsTag":"noindex","categories":[],"isAnonymized":true}};