Вероятно, 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.
Уважаемые читатели! Сталкивались ли вы на практике с неприятностями, подобными тем, о которых идёт речь в этой статье?