Вероятно, Vue.js — это один из приятнейших JavaScript-фреймворков. У него имеется интуитивно понятный API, он быстрый, гибкий, им легко пользоваться. Однако гибкость Vue.js соседствует с определёнными опасностями. Некоторые разработчики, работающие с этим фреймворком, склонны к небольшим оплошностям. Это может плохо влиять на производительность приложений, или, в долгосрочной перспективе, на возможность их поддержки.

Автор материала, перевод которого мы сегодня публикуем, предлагает разобрать некоторые распространённые ошибки, совершаемые теми, кто разрабатывает приложения на Vue.js.
Вычисляемые свойства — это очень удобный механизм Vue.js, позволяющий организовывать работу с фрагментами состояния, зависящими от других фрагментов состояния. Вычисляемые свойства следует использовать только для вывода данных, хранящихся в состоянии и зависящих от других данных из состояния. Если оказывается, что вы вызываете внутри вычисляемых свойств некие методы или выполняете запись неких значений в другие переменные состояния, это может означать, что вы что-то делаете неправильно. Рассмотрим пример.
Если мы попытаемся вывести
Это так из-за того, что вычисляемое свойство
Пр��дположим, что у нас имеется компонент, который выводит подробные сведения о цене товаров или услуг, включённых в некий заказ.
Здесь мы создали вычисляемое свойство, которое выводит общую стоимость заказа с учётом налогов и скидок. Так как мы знаем, что общая стоимость заказа здесь меняется, мы можем попытаться породить событие, которое уведомляет родительский компонент об изменении
Теперь представим, что иногда, хотя и очень редко, возникают ситуации, в которых мы работаем с особенными покупателями. Этим покупателям мы даём дополнительную скидку в 10%. Мы можем попытаться изменить объект
Это, однако, приведёт к нехорошей ошибке.

Сообщение об ошибке

Неправильное вычисление стоимости заказа для особенного покупателя
В подобной ситуации происходит следующее: вычисляемое свойство постоянно, в бесконечном цикле, «пересчитывается». Мы меняем скидку, вычисляемое свойство на это реагирует, пересчитывает общую стоимость заказа и порождает событие. При обработке этого события скидка снова увеличивается, это вызывает пересчёт вычисляемого свойства, и так — до бесконечности.
Вам может показаться, что подобную ошибку невозможно совершить в реальном приложении. Но так ли это на самом деле? Наш сценарий (если нечто подобное произойдёт в настоящем приложении) будет очень сложно отладить. Подобную ошибку будет крайне непросто отследить. Дело в том, что для возникновения этой ошибки нужно, чтобы заказ оформлял бы особенный покупатель, а на один такой заказ, возможно, приходится 1000 обычных заказов.
Иногда у разработчика может появиться соблазн отредактировать что-то в свойстве из
Здесь у нас имеется компонент
Представим себе гипотетическую ситуацию, в которой другой разработчик впервые сталкивается с нашим кодом и видит родительский компонент.
Ход мыслей этого разработчика может быть следующим: «Видимо, мне нужно уменьшить
Представьте себе, что это — особый случай, когда подобная проверка производится только для редкого товара или в связи с наличием специальной скидки. Если этот код попадёт в продакшн, то всё может закончиться тем, что наши клиенты будут, вместо 1 экземпляра товара, покупать 2 экземпляра.
Если этот пример показался вам неубедительным — представим себе ещё один сценарий. Пусть это будет форма, которую заполняет пользователь. Сущность
Здесь легко наладить работу с
Простой способ «исправления» проблемы может заключаться в клонировании объекта
Хотя это может и сработать, мы лишь обходим проблему, но не решаем её. Наш компонент
Хотя этот код, определённо, кажется довольно сложным, он лучше, чем предыдущий вариант. Он позволяет избавиться от вышеописанных проблем. Мы ожидаем (
Если компонент обращается к другому компоненту и выполняет над ним некие действия — это может привести к противоречиям и ошибкам, это может выразиться в странном поведении приложения и в появлении в нём связанных компонентов.
Рассмотрим очень простой пример — компонент, реализующий выпадающее меню. Представим, что у нас имеется компонент
Обратите внимание на метод
На первый взгляд может показаться, что подобный код работает правильно. Но тут можно усмотреть пару проблем:
Этот код, опять же, из-за изменения
Свойства передаются вниз по иерархии компонентов, события передаются вверх. В этих словах заключён смысл правильного подхода к решению нашей задачи. Вот наш пример, модифицированный в расчёте на использование событий.
Сейчас, благодаря использованию событий, дочерний компонент больше не привязан к родительскому. Мы можем свободно менять свойства с данными в родительском компоненте и пользоваться анимированными переходами. При этом мы можем не думать о том, как наш код может повлиять на родительский компонент. Мы просто уведомляем этот компонент о том, что произошло. При этом компонент
Самый короткий код не всегда является самым удачным. У методик разработки, предусматривающих «простое и быстрое» получение результатов, часто имеются недостатки. Для того чтобы правильно пользоваться любым языком программирования, библиотекой или фреймворком, нужно терпение и время. Это справедливо и для Vue.js.
Уважаемые читатели! Сталкивались ли вы на практике с неприятностями, подобными тем, о которых идёт речь в этой статье?


Автор материала, перевод которого мы сегодня публикуем, предлагает разобрать некоторые распространённые ошибки, совершаемые теми, кто разрабатывает приложения на Vue.js.
Побочные эффекты внутри вычисляемых свойств
Вычисляемые свойства — это очень удобный механизм Vue.js, позволяющий организовывать работу с фрагментами состояния, зависящими от других фрагментов состояния. Вычисляемые свойства следует использовать только для вывода данных, хранящихся в состоянии и зависящих от других данных из состояния. Если оказывается, что вы вызываете внутри вычисляемых свойств некие методы или выполняете запись неких значений в другие переменные состояния, это может означать, что вы что-то делаете неправильно. Рассмотрим пример.
export default { data() { return { array: [1, 2, 3] }; }, computed: { reversedArray() { return this.array.reverse(); // Побочный эффект - изменение свойства с данными } } };
Если мы попытаемся вывести
array и reversedArray, то можно будет заметить, что оба массива содержат одни и те же значения.исходный массив: [ 3, 2, 1 ] модифицированный массив: [ 3, 2, 1 ]
Это так из-за того, что вычисляемое свойство
reversedArray модифицирует исходное свойство array, вызывая его метод .reverse(). Это — довольно простой пример, демонстрирующий неожиданное поведение системы. Взглянем на ещё один пример.Пр��дположим, что у нас имеется компонент, который выводит подробные сведения о цене товаров или услуг, включённых в некий заказ.
export default { props: { order: { type: Object, default: () => ({}) } }, computed:{ grandTotal() { let total = (this.order.total + this.order.tax) * (1 - this.order.discount); this.$emit('total-change', total) return total.toFixed(2); } } }
Здесь мы создали вычисляемое свойство, которое выводит общую стоимость заказа с учётом налогов и скидок. Так как мы знаем, что общая стоимость заказа здесь меняется, мы можем попытаться породить событие, которое уведомляет родительский компонент об изменении
grandTotal.<price-details :order="order" @total-change="totalChange"> </price-details> export default { // другие свойства в этом примере неважны methods: { totalChange(grandTotal) { if (this.isSpecialCustomer) { this.order = { ...this.order, discount: this.order.discount + 0.1 }; } } } };
Теперь представим, что иногда, хотя и очень редко, возникают ситуации, в которых мы работаем с особенными покупателями. Этим покупателям мы даём дополнительную скидку в 10%. Мы можем попытаться изменить объект
order и увеличить размер скидки, прибавив 0.1 к его свойству discount.Это, однако, приведёт к нехорошей ошибке.

Сообщение об ошибке

Неправильное вычисление стоимости заказа для особенного покупателя
В подобной ситуации происходит следующее: вычисляемое свойство постоянно, в бесконечном цикле, «пересчитывается». Мы меняем скидку, вычисляемое свойство на это реагирует, пересчитывает общую стоимость заказа и порождает событие. При обработке этого события скидка снова увеличивается, это вызывает пересчёт вычисляемого свойства, и так — до бесконечности.
Вам может показаться, что подобную ошибку невозможно совершить в реальном приложении. Но так ли это на самом деле? Наш сценарий (если нечто подобное произойдёт в настоящем приложении) будет очень сложно отладить. Подобную ошибку будет крайне непросто отследить. Дело в том, что для возникновения этой ошибки нужно, чтобы заказ оформлял бы особенный покупатель, а на один такой заказ, возможно, приходится 1000 обычных заказов.
Изменение вложенных свойств
Иногда у разработчика может появиться соблазн отредактировать что-то в свойстве из
props, являющемся объектом или массивом. Подобное желание может быть продиктовано тем фактом, что сделать это очень «просто». Но стоит ли так поступать? Рассмотрим пример.<template> <div class="hello"> <div>Name: {{product.name}}</div> <div>Price: {{product.price}}</div> <div>Stock: {{product.stock}}</div> <button @click="addToCart" :disabled="product.stock <= 0">Add to card</button> </div> </template> export default { name: "HelloWorld", props: { product: { type: Object, default: () => ({}) } }, methods: { addToCart() { if (this.product.stock > 0) { this.$emit("add-to-cart"); this.product.stock--; } } } };
Здесь у нас имеется компонент
Product.vue, который выводит название товара, его стоимость и имеющееся у нас количество товара. Компонент, кроме того, выводит кнопку, которая позволяет покупателю положить товар в корзину. Может показаться, что очень легко и удобно будет уменьшать значение свойства product.stock после щелчка по кнопке. Сделать это, и правда, просто. Но если поступить именно так — можно столкнуться с несколькими проблемами:- Мы выполняем изменение (мутацию) свойства и ничего не сообщаем об этом родительской сущности.
- Это может привести к неожиданному поведению системы, или, что ещё хуже, к появлению странных ошибок.
- Мы вводим в компонент
productнекую логику, которая, вероятно, не должна в нём присутствовать.
Представим себе гипотетическую ситуацию, в которой другой разработчик впервые сталкивается с нашим кодом и видит родительский компонент.
<template> <Product :product="product" @add-to-cart="addProductToCart(product)"></Product> </template> import Product from "./components/Product"; export default { name: "App", components: { Product }, data() { return { product: { name: "Laptop", price: 1250, stock: 2 } }; }, methods: { addProductToCart(product) { if (product.stock > 0) { product.stock--; } } } };
Ход мыслей этого разработчика может быть следующим: «Видимо, мне нужно уменьшить
product.stock в методе addProductToCart». Но если так и будет сделано — мы столкнёмся с небольшой ошибкой. Если теперь нажать на кнопку, то количество товара будет уменьшено не на 1, а на 2.Представьте себе, что это — особый случай, когда подобная проверка производится только для редкого товара или в связи с наличием специальной скидки. Если этот код попадёт в продакшн, то всё может закончиться тем, что наши клиенты будут, вместо 1 экземпляра товара, покупать 2 экземпляра.
Если этот пример показался вам неубедительным — представим себе ещё один сценарий. Пусть это будет форма, которую заполняет пользователь. Сущность
user мы передаём в форму в качестве свойства и собираемся отредактировать имя (name) и адрес электронной почты (email) пользователя. Код, который показан ниже, может показаться «правильным».// Родительский компонент <template> <div> <span> Email {{user.email}}</span> <span> Name {{user.name}}</span> <user-form :user="user" @submit="updateUser"/> </div> </template> import UserForm from "./UserForm" export default { components: {UserForm}, data() { return { user: { email: 'loreipsum@email.com', name: 'Lorem Ipsum' } } }, methods: { updateUser() { // Отправляем на сервер запрос на сохранение данных пользователя } } } // Дочерний компонент UserForm.vue <template> <div> <input placeholder="Email" type="email" v-model="user.email"/> <input placeholder="Name" v-model="user.name"/> <button @click="$emit('submit')">Save</button> </div> </template> export default { props: { user: { type: Object, default: () => ({}) } } }
Здесь легко наладить работу с
user с помощью директивы v-model. Vue.js это позволяет. Почему бы не поступить именно так? Подумаем об этом:- Что если имеется требование, в соответствии с которым необходимо добавить на форму кнопку Cancel, нажатие на которую отменяет внесённые изменения?
- Что если обращение к серверу оказывается неудачным? Как отменить изменения объекта
user? - Действительно ли мы хотим выводить изменённые имя и адрес электронной почты в родительском компоненте перед сохранением соответствующих изменений?
Простой способ «исправления» проблемы может заключаться в клонировании объекта
user перед отправкой его в качестве свойства:<user-form :user="{...user}">
Хотя это может и сработать, мы лишь обходим проблему, но не решаем её. Наш компонент
UserForm должен обладать собственным локальным состоянием. Вот что мы можем сделать.<template> <div> <input placeholder="Email" type="email" v-model="form.email"/> <input placeholder="Name" v-model="form.name"/> <button @click="onSave">Save</button> <button @click="onCancel">Save</button> </div> </template> export default { props: { user: { type: Object, default: () => ({}) } }, data() { return { form: {} } }, methods: { onSave() { this.$emit('submit', this.form) }, onCancel() { this.form = {...this.user} this.$emit('cancel') } } watch: { user: { immediate: true, handler: function(userFromProps){ if(userFromProps){ this.form = { ...this.form, ...userFromProps } } } } } }
Хотя этот код, определённо, кажется довольно сложным, он лучше, чем предыдущий вариант. Он позволяет избавиться от вышеописанных проблем. Мы ожидаем (
watch) изменений свойства user и копируем его во внутренние данные form. В результате у формы теперь есть собственное состояние, а мы получаем следующие возможности:- Отменить изменения можно, переназначив форму:
this.form = {...this.user}. - У нас имеется изолированное состояние для формы.
- Наши действия не затрагивают родительский компонент в том случае, если нам это не нужно.
- Мы контролируем то, что происходит при попытке сохранения изменений.
Прямой доступ к родительским компонентам
Если компонент обращается к другому компоненту и выполняет над ним некие действия — это может привести к противоречиям и ошибкам, это может выразиться в странном поведении приложения и в появлении в нём связанных компонентов.
Рассмотрим очень простой пример — компонент, реализующий выпадающее меню. Представим, что у нас имеется компонент
dropdown (родительский), и компонент dropdown-menu (дочерний). Когда пользователь щёлкает по некоему пункту меню, нам нужно закрыть dropdown-menu. Скрытие и отображение этого компонента выполняется родительским компонентом dropdown. Взглянем на пример.// Dropdown.vue (родительский компонент) <template> <div> <button @click="showMenu = !showMenu">Click me</button> <dropdown-menu v-if="showMenu" :items="items"></dropdown-menu> </div> <template> export default { props: { items: Array }, data() { return { selectedOption: null, showMenu: false } } } // DropdownMenu.vue (дочерний компонент) <template> <ul> <li v-for="item in items" @click="selectOption(item)">{{item.name}}</li> </ul> <template> export default { props: { items: Array }, methods: { selectOption(item) { this.$parent.selectedOption = item this.$parent.showMenu = false } } }
Обратите внимание на метод
selectOption. Хотя подобное случается и очень редко, у кого-то может возникнуть желание напрямую обратиться к $parent. Подобное желание можно объяснить тем, что сделать это очень просто.На первый взгляд может показаться, что подобный код работает правильно. Но тут можно усмотреть пару проблем:
- Что если мы изменим свойство
showMenuилиselectedOption? Выпадающее меню не сможет закрыться и ни один из его пунктов не окажется выбранным. - Что если нужно будет анимировать
dropdown-menu, использовав какой-нибудь переход?
// Dropdown.vue (родительский компонент) <template> <div> <button @click="showMenu = !showMenu">Click me</button> <transition name="fade"> <dropdown-menu v-if="showMenu" :items="items"></dropdown-menu> </dropdown-menu> </div> <template>
Этот код, опять же, из-за изменения
$parent, работать не будет. Компонент dropdown больше не является родителем dropdown-menu. Теперь родителем dropdown-menu является компонент transition.Свойства передаются вниз по иерархии компонентов, события передаются вверх. В этих словах заключён смысл правильного подхода к решению нашей задачи. Вот наш пример, модифицированный в расчёте на использование событий.
// Dropdown.vue (родительский компонент) <template> <div> <button @click="showMenu = !showMenu">Click me</button> <dropdown-menu v-if="showMenu" :items="items" @select-option="onOptionSelected"></dropdown-menu> </div> <template> export default { props: { items: Array }, data() { return { selectedOption: null, showMenu: false } }, methods: { onOptionSelected(option) { this.selectedOption = option this.showMenu = true } } } // DropdownMenu.vue (дочерний компонент) <template> <ul> <li v-for="item in items" @click="selectOption(item)">{{item.name}}</li> </ul> </template> export default { props: { items: Array }, methods: { selectOption(item) { this.$emit('select-option', item) } } }
Сейчас, благодаря использованию событий, дочерний компонент больше не привязан к родительскому. Мы можем свободно менять свойства с данными в родительском компоненте и пользоваться анимированными переходами. При этом мы можем не думать о том, как наш код может повлиять на родительский компонент. Мы просто уведомляем этот компонент о том, что произошло. При этом компонент
dropdown сам принимает решения о том, как ему обрабатывать выбор пользователем пункта меню и операцию закрытия меню.Итоги
Самый короткий код не всегда является самым удачным. У методик разработки, предусматривающих «простое и быстрое» получение результатов, часто имеются недостатки. Для того чтобы правильно пользоваться любым языком программирования, библиотекой или фреймворком, нужно терпение и время. Это справедливо и для Vue.js.
Уважаемые читатели! Сталкивались ли вы на практике с неприятностями, подобными тем, о которых идёт речь в этой статье?

