Вероятно, Vue.js — это один из приятнейших JavaScript-фреймворков. У него имеется интуитивно понятный API, он быстрый, гибкий, им легко пользоваться. Однако гибкость 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
};
}
}
}
};
order
и увеличить размер скидки, прибавив 0.1
к его свойству discount
.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.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 это позволяет. Почему бы не поступить именно так? Подумаем об этом: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
сам принимает решения о том, как ему обрабатывать выбор пользователем пункта меню и операцию закрытия меню.К сожалению, не доступен сервер mySQL