Оглавление
Vue
Сложные выражения в шаблоне
Вы можете использовать инлайн-выражения в шаблонах Vue - это самые обычные JavaScript выражения. Они дают максимальную свободу и мощность, однако из-за этого они могут стать слишком сложными. Не злоупотребляйте этим — оставляйте инлайн-выражения простыми.
- Сложные выражения сложнее прочесть и понять.
- Инлайн-выражения нельзя переиспользовать, это очевидно, ведёт дублированию кода и ухудшению его качества.
- Редакторы и IDE обычно не могут парсить такие выражения, а значит у вас не будет автодополнения и валидации.
Простое правило — если код JavaScript инлайн-выражения становится слишком сложным — выносите его как отдельный метод в блок methods или computed свойство, соответственно в блок computed.
<!-- 😊 Хорошо: шаблон удобно читать -->
<template>
<h1>
{{ `${year}-${month}` }}
</h1>
</template>
<script>
export default {
computed: {
month() {
return this.twoDigits((new Date()).getUTCMonth() + 1);
},
year() {
return (new Date()).getUTCFullYear();
}
},
methods: {
twoDigits(num) {
return ('0' + num).slice(-2);
}
},
};
</script>
<!-- 💩 Плохо: большое выражение в шаблоне -->
<template>
<h1>
{{ `${(new Date()).getUTCFullYear()}-${('0' + ((new Date()).getUTCMonth()+1)).slice(-2)}` }}
</h1>
</template>
Примитивные входящие параметры
Хотя Vue и поддерживает передачу параметров в виде сложных объектов, старайтесь избегать этого. Лучше ограничиться простыми типами JavaScript и функциями для этого. Не передавайте сложные объекты в компоненты-наследники.
- Используя для каждого свойства отдельный атрибут - API вашего компонента будет более наглядным.
- Такой подход совместим с API к которому мы все привыкли у нативных HTML элементов.
- Созданные вами атрибуты будет легче понять другим членам команды.
- При передаче сложных объектов сразу не видно, какие из его свойств далее используются, - это затруднит рефакторинг.
Используйте отдельные атрибуты для каждой опции и передавайте в неё примитив (флаг, строку, число).
<!-- 😊 Хорошо: все параметры передаются отдельно -->
<range-slider
:values="[10, 20]"
min="0"
max="100"
step="5"
:on-slide="updateInputs"
:on-end="updateResults">
</range-slider>
<!-- 💩 Плохо: все параметры приходят как один объект -->
<range-slider :config="complexConfigObject"></range-slider>
Наследование атрибутов
Когда компонент возвращает какой-либо один HTML элемент – нет нужны делать входящие параметры для каждого атрибута.
Если возвращается несколько DOM-элементов, можно указать inheritAttrs: false, а на нужном элементе сделать v-bind="$attrs", чтобы прокинуть атрибуты через родительский элемент.
<!-- 😊 Хорошо: атрибуты наследуются, s-input не принимает type явно -->
<s-input type="number"></s-input>
<template>
<input v-bind="$attrs">
</template>
<script>
export default {
name: 'SInput'
}
</script>
<!-- 💩 Плохо: для каждого атрибута придётся делать свой входящий параметр -->
<s-input type="number"></s-input>
<template>
<input :type="type">
</template>
<script>
export default {
name: 'SInput',
props: {
type: {
type: String,
default: 'text'
}
}
}
</script>
this.$parent
Vue поддерживает вложенности компонентов, поэтому дочерние компоненты могут обращаться к данным родителя. Обращение к внутреннему состоянию компонента снаружи нарушает принцип FIRST. Старайтесь избегать конструкции this.$parent. Возможны случаи, когда это разумный выход, но это слишком плохая практика, чтобы использовать его всегда.
Компонент Vue, как и любой другой, должен работать изолированно. Если для работы требуется взаимодействия с соседними скоупами, то нарушается принцип компонентной разработки.
Если компоненту требуется обращение к соседям — такой компонент не может быть полноценно переиспользован.
- Передавайте данные из родителя в дочерний компонент используя атрибуты и свойства.
- Передавайте методы используя коллбеки и выражениях в атрибутах.
- В обратную сторону: дочерние компоненты должны генерировать события, которые будет перехватывать родитель.
<!-- 😊 Хорошо: по клику ловим событие и обрабатываем -->
<template>
<base-button @click="onClick"></base-button>
</template>
<script>
export default {
name: 'ParentComponent',
methods: {
onClick () {
console.log('Я родитель')
}
}
}
</script>
<!-- 💩 Плохо: кнопка вызывает метод родителя -->
<template>
<button @click="onClick"></button>
</template>
<script>
export default {
name: 'BaseButton',
methods: {
onClick () {
this.$parent.onClick()
}
}
}
</script>
this.$refs
Vue, как и React поддерживает обращение к другим компонентам и html-элементам с использованием атрибута ref. Через обращение к this.$refs разработчик может получить доступ к контексту других компонентов или тегов. В большинстве случаев можно не использовать this.$refs для обращения к другим компонентам.
Если возникает потребность вызвать методы другого компонента – хорошо подумайте, всё ли сделано правильно. Зачастую можно найти элегантное решение, но если всё же сильно хочется – стоит прибегнуть к паттерну Event Bus, используя, новый Vue-экземпляр или библиотеку mitt.
- Серьёзно относитесь к дизайну API ваших компонентов.
- Старайтесь избегать умножений и ветвлений пути исполнения кода в компонентах. Наличие таких фрагментов является признаком того, что API недостаточно общее, либо вам нужно создать и использовать другие компоненты для других юзкейсов.
- Используя компонент, обратите внимание на входящие параметры: если какого-то из них не хватает, то добавьте их сами.
- То же самое с событиями — если чего-то не хватает, значит другой разработчик (или вы сами, в прошлом) не добавил их. Для исправления добавьте отсутствующее или проверьте бизнес-логику компонента, возможно, это событие уже не используется, тогда его можно просто удалить.
- Используйте
this.$refs, только если других путей нет и вам никак не обойтись событиями и свойствами.
<!-- 😊 Хорошо: используется паттерн Event Bus -->
<template>
<child></child>
</template>
<script>
import mitt from 'mitt'
export default {
name: 'ParentComponent',
provide () {
return {
bus: this.bus,
}
},
data () {
return {
bus: mitt(),
}
},
mounted () {
this.bus.emit('focus')
},
beforeDestroy () {
this.bus.all.clear()
}
}
</script>
<!-- 💩 Плохо: вызывается метод ребёнка -->
<template>
<child ref="child"></child>
</template>
<script>
export default {
name: 'ParentComponent',
mounted () {
this.$refs.onFocus()
},
}
</script>
Документация компонента
Для компонента, его входящих параметров и слотов, передаваемых событий следует писать документацию в формате JSDoc от styleguidist. В @maxus/svztify по таким комментариям собирается API для каждого компонента, который показывается по наведению курсора в IDE.
<!-- 😊 Хорошо: API задокументировано -->
<template>
<button class="base-button">
<!-- @slot Контент внутри кнопки -->
<slot></slot>
</button>
</template>
<script>
/**
* Компонент базовой кнопки.
* @displayName BaseButton
*/
export default {
name: 'BaseButton',
props: {
/**
* Тип кнопки
*/
type: {
type: String,
default: 'primary'
},
}
}
</script>
<!-- 💩 Плохо: комментарии отсутствуют -->
<template>
<button class="base-button">
<slot></slot>
</button>
</template>
<script>
export default {
name: 'BaseButton',
props: {
type: {
type: String,
default: 'primary'
},
}
}
</script>
Функциональные компоненты
Для увеличения производительности можно использовать функциональные компоненты.
Поскольку функциональные компоненты — это просто функции, их отрисовка значительно быстрее.
Кроме того, они очень удобны в качестве обёрток. Например, если вам нужно:
- Выбрать один из компонентов для последующей отрисовки в данной точке
- Произвести манипуляции над дочерними элементами, входными параметрами или данными, перед тем как передать их в дочерний компонент
Такие компоненты лучше делать через рендер-функции, так как не все Vue-директивы работают при использовании шаблонов. Для избежания багов при слиянии атрибутов рекомендуется использовать библиотеку vue-functional-data-merge.
Использование template c функциональными компонентами не рекомендуется, так как в таком случае не все директивы корректно отрабатывают.
<script>
import { mergeData } from 'vue-functional-data-merge'
/**
* Компонент для резервирования места под картинки или фреймы
* за счёт нативного aspect-ratio или фолбэком через паддинги.
* Применяется, чтобы избежать CLS для контента.
*/
export default /* #__PURE__ */{
name: 'SAspectRatio',
functional: true,
props: {
/**
* Тэг контейнера
*/
tag: {
type: String,
default: 'div',
},
/**
* Соотношение сторон
* width / height
*/
ratio: {
type: Number,
default: 1,
},
},
render (h, { children, data, props }) {
const { ratio, tag } = props
const componentData = {
class: [
's-aspect-ratio',
],
style: {
'--s-aspect-ratio': ratio,
},
}
/**
* Рисуем контейнер
*/
return h(
tag,
mergeData(data, componentData),
[
h(
'span',
{
class: ['s-aspect-ratio__content'],
},
/**
* @slot Контент внутри контейнера
*/
children,
),
],
)
},
}
</script>
Пустые строки между опциями компонента
Когда компоненты кажутся неразборчивыми и становятся трудными для чтения, то добавление пустых строк между многострочными свойствами может облегчить их беглое изучение во время просмотра.
<script>
<!-- 😊 Хорошо: свойства отбиты пустыми строками -->
export default {
props: {
value: {
type: String,
required: true
},
isFocused: {
type: Boolean,
default: true
},
label: {
type: String,
default: "",
}
}
}
</script>
<script>
<!-- 💩 Плохо: свойства трудно читать -->
export default {
props: {
value: {
type: String,
required: true
},
isFocused: {
type: Boolean,
default: true
},
label: {
type: String,
default: "",
}
}
}
</script>
Реактивный provide
Опция provide позволяет нам указать данные/методы, которые мы хотим предоставить всем компонентам-потомкам. Тем не менее у внедрения зависимостей есть недостатки – свойства, указанные в provide, не будут реактивными. Чтобы обойти этот недостаток для provide используется функция, а в компоненте-потомке inject оборачиваем в computed.
<script>
export default {
name: 'Parent',
provide () {
return {
/**
* @link https://github.com/vuejs/vue/issues/7017#issuecomment-480906691
*/
$article: () => this.article,
}
},
data() {
return {
article: {
title: 'Статья',
text: 'Описание'
}
}
}
}
</script>
<script>
export default {
name: 'Child',
inject: ['$article'],
computed: {
/**
* Получаем наш объект статьи, после чего работает всегда с этим `computed`, вместо `$article`
*/
reactiveArticle () {
return this.$article()
}
}
}
</script>
Входные параметры должны быть определены как можно более подробно
Определение входных параметров всегда должно быть максимально подробным. Необходимо обязательно указывать type и одно из двух: required (если true) или default (если required: false).
<script>
<!-- 😊 Хорошо: входные параметры описаны -->
export default {
props: {
value: {
type: String,
required: true
},
isFocused: {
type: Boolean,
default: true
},
label: {
type: String,
default: "",
}
}
}
</script>
<script>
<!-- 💩 Плохо: у свойств нет типов -->
export default {
props: ['value', 'isFocused', 'label']
}
</script>
Уникальные ключи для v-for
key с v-for всегда обязателен для компонентов, для поддержания внутреннего состояния компонента и его поддерева.
Не рекомендуется использовать индекс из цикла. Исключение — статические списки, которые не меняются после инициализации.
<template>
<!-- 😊 Хорошо: ключи добавлены -->
<ul class="shops">
<li
v-for="shop of shops"
:key="shop.id"
>
{{ shop.name }}
</li>
</ul>
</template>
<template>
<!-- 💩 Плохо: используется индекс -->
<ul class="shops">
<li
v-for="(shop, index) of shops"
:key="index"
>
{{ shop.name }}
</li>
</ul>
</template>
Простые вычисляемые свойства
Комплексные вычисляемые свойства должны быть разделены на максимально простые свойства.
Проще говоря, хорошие вычисляемые свойства будет:
-
Легче тестировать
Когда каждое вычисляемое свойство содержит только очень простое выражение, с очень небольшим набором зависимостей, то будет гораздо проще писать тесты, подтверждающие его правильную работу.
-
Легче читать
Упрощение вычисляемых свойств заставляет вас давать каждому значению понятное имя, даже если оно не будет использоваться повторно. Это облегчает другим разработчикам (и вам в будущем) сосредоточиться на коде, который им нужен и выяснить что происходит.
-
Лучше приспособлены к изменяющимся требованиям
Любое значение, которое можно назвать, может быть полезным для представления. Например, мы можем решить отображать сообщение пользователю с информацией сколько денег сэкономил. Мы также можем решить рассчитывать налог с продаж, но, возможно, отображать его отдельно, а не как часть окончательной цены.
Небольшие, сфокусированные вычисляемые свойства создают меньше предположений о том, как информация будет использована, поэтому при изменениях требований потребуется меньше рефакторинга.
<script>
<!-- 😊 Хорошо: большое свойство разбито на несколько мелких -->
export default {
computed: {
basePrice: function () {
return this.manufactureCost / (1 - this.profitMargin)
},
discount: function () {
return this.basePrice * (this.discountPercent || 0)
},
finalPrice: function () {
return this.basePrice - this.discount
}
}
}
</script>
<script>
<!-- 💩 Плохо: большое вычисление в одном свойстве -->
export default {
computed: {
price: function () {
const basePrice = this.manufactureCost / (1 - this.profitMargin)
return (
basePrice -
basePrice * (this.discountPercent || 0)
)
}
}
}
</script>
Стиль именования входных параметров и событий
Входные параметры должны всегда использовать camelCase при определении, но kebab-case в шаблонах.
Мы просто придерживаемся соглашений каждого языка. Для JavaScript использовать camelCase является более естественным. Для HTML — kebab-case.
В отличие от компонентов и входных параметров, имена событий не предоставляют никакой автоматической трансформации стиля именования события. Вместо этого, имя генерируемого события должно точно соответствовать имени, используемому при прослушивании события.
Кроме того, директивы прослушивания событий v-on внутри DOM-шаблонов автоматически преобразуются в нижний регистр (из-за нечувствительности HTML к регистру), поэтому v-on:myEvent станет v-on:myevent — что делает прослушивание события myEvent невозможным.
По этим причинам рекомендуется всегда использовать kebab-case для имён событий.
<script>
<!-- 😊 Хорошо: свойста в camelCase, события в kebabCase -->
export default {
name: 'Article',
props: {
articleTitle: {
type: String,
required: true
},
},
methods: {
onTitleClick () {
this.$emit('title-click')
}
}
}
</script>
<script>
<!-- 💩 Плохо: свойста в kebabCase, события в camelCase -->
export default {
name: 'Article',
props: {
'article-title': {
type: String,
required: true
},
},
methods: {
onTitleClick () {
this.$emit('titleClick')
}
}
}
</script>
Фильтры
Фильтры были удалены из Vue 3.x и больше не поддерживаются. Чтобы не усложнять рефакторинг в будущем, вместо них рекомендуется использовать вызовы методов или вычисляемые свойства.
<!-- 😊 Хорошо: фильтры не используются -->
<template>
<h1>Остаток на банковском счёте</h1>
<p>{{ accountInUSD }}</p>
</template>
<script>
export default {
props: {
accountBalance: {
type: Number,
required: true
}
},
computed: {
accountInUSD() {
return '$' + this.accountBalance
}
}
}
</script>
<!-- 💩 Плохо: используются фильтры -->
<template>
<h1>Остаток на банковском счёте</h1>
<p>{{ accountBalance | currencyUSD }}</p>
</template>
<script>
export default {
props: {
accountBalance: {
type: Number,
required: true
}
},
filters: {
currencyUSD(value) {
return '$' + value
}
}
}
</script>
Избегаем XSS
Динамическая отрисовка произвольного HTML-кода на вашем сайте крайне опасна, так как может легко привести к XSS-уязвимостям. Используйте интерполяцию HTML только для доверенного кода, и никогда не подставляйте туда содержимое, создаваемое пользователями. Для безопасной вставки контента вместо v-html следует использовать v-dompurify-html.
<template>
<!-- 💩 Плохо: потенциальный XSS -->
<div v-html="content"></div>
<!-- 😊 Хорошо: фильтруем контент перед вставкой -->
<div v-dompurify-html="content"></div>
</template>
Передача текстового контента
Для передачи текста лучше использовать именовые слоты, а не пропсы, так как это добавляет больше гибкости компоненту. Мы можем контролировать все классы, события на переданных элементах.
<template>
<!-- 💩 Плохо: компонент не очень гибкий -->
<some-article
title="Заголовок статьи"
description="Описание"
></some-article>
<!-- 😊 Хорошо: компонент принимает слоты с любым контентом -->
<some-article>
<template name="title">
Заголовок статьи
</template>
<template name="description">
Описание
</template>
</some-article>
</template>
Деструктуризация this
Во vue компонентах следует деструктуризировать только вычисляемые свойства или пропсы, так как это дает небольшие оптимизации. В остальных случаях следует обращаться ко всем полям через this, так как иногда можно выстрелить себе в ногу, деструктуризировав какие-нибудь данные, которые не являются ссылочным типом, а затем попытаться записать туда какое-либо значение.
Ещё частой ошибкой является деструктуризация vue методов из инстанса компонента, например const { $emit } = this, что также не будет работать должным образом.
<!-- 💩 Плохо: деструктурируются методы, а компьютеды наоборот нет -->
<script>
export default {
computed: {
someComputed () {
return 1
},
someOtherComputed () {
return this.someComputed + 1
},
},
methods: {
someMethod (a, b) {
const { $emit } = this
$emit('someEvent', a, b)
},
},
mounted () {
const { someComputed, someOtherComputed, someMethod } = this
someMethod(someComputed, someOtherComputed)
}
}
</script>
<!-- 😊 Хорошо: деструктурируются только компьютеды в угоду оптимизациям -->
<script>
export default {
computed: {
someComputed () {
return 1
},
someOtherComputed () {
const { someComputed } = this
return someComputed + 1
},
},
methods: {
someMethod (a, b) {
this.$emit('someEvent', a, b)
},
},
mounted () {
const { someComputed, someOtherComputed } = this
this.someMethod(someComputed, someOtherComputed)
}
}
</script>
Стиль именования вычисляемых свойств
В названиях вычисляемых свойств стоит избегать слово get, так как это может ввести в заблуждение, что функция вызывается, а на самом деле это уже заранее вычисленное поле.
<!-- 💩 Плохо: вычисляемые свойства начинаются с get -->
<script>
export default {
computed: {
getClasses () {
return ['button']
},
},
}
</script>
<!-- 😊 Хорошо: вычисялемые свойства именуются правильно -->
<script>
export default {
computed: {
classes () {
return ['button']
},
},
}
</script>
Стиль именования сущностей vuex
Actions
load – если запрос в апи и нужно положить в стор
fetch – если запрос в апи и нужно вернуть в ответе
Mutations
set – если нужно записать данные в стейт
Getters
get – если это функция
is – булево значение
<!-- 💩 Плохо: сущности заименованы не по правилам -->
<script>
export default {
actions: {
getProduct () {},
},
mutations: {
addProduct () {},
},
getters: {
productAdded () {},
}
}
</script>
<!-- 😊 Хорошо: сущности названы верно -->
<script>
export default {
actions: {
loadProduct () {},
},
mutations: {
setProduct () {},
},
getters: {
isProductAdded () {},
}
}
</script>
Мета-тэги
Для страниц рекомендуется задавать мета-информацию. Для обычных страниц сайта это может быть название раздела. Для промо страниц помимо заголовка, рекомендуется заполнять все тэги для шеринга в социальных сетях. Если картинки в макете нет, то можно сделать её самому. Рекомендуемый размер – 1200x630.
Если после обновления превью отображается по прежнему старое изображение, то в телеграме можно сбросить кэш у бота @WebpageBot.
<!-- 💩 Плохо: страница без заголовка -->
<script>
export default {
name: 'PromoIndex',
}
</script>
<!-- 😊 Хорошо: заполнена мета информация -->
<script>
export default {
name: 'PromoIndex',
head () {
return {
title: 'HONOR',
meta: [
{ hid: 'description', name: 'description', content: 'Тот самый ноутбук.' },
{ hid: 'og:title', property: 'og:title', content: 'HONOR' },
{ hid: 'og:image', property: 'og:image', content: '//cdn.svyaznoy.ru/upload/web/nuxt/svyaznoy/promo/honor/sharing/fb-sharing.png' },
{ hid: 'og:image:width', property: 'og:image:width', content: '1200' },
{ hid: 'og:image:height', property: 'og:image:height', content: '630' },
{ hid: 'og:description', property: 'og:description', content: 'Тот самый ноутбук.' },
{ hid: 'og:url', property: 'og:url', content: 'https://www.svyaznoy.ru/promo/honor' },
{ hid: 'og:locale', property: 'og:locale', content: 'ru_RU' },
{ hid: 'vk:image', property: 'vk:image', content: '//cdn.svyaznoy.ru/upload/web/nuxt/svyaznoy/promo/honor/sharing/fb-sharing.png' },
],
}
}
}
</script>
Утечка памяти: сторонние константы
Когда нужно использовать сторонние константы в data, обязательно применяйте Object.freeze, чтобы ограничить реактивность. Такие импорты создают утечку памяти на сервере.
<!-- 💩 Плохо: на объект навесится реактивность, будут проблемы -->
<script>
import STEPS from './steps.js'
export default {
name: 'PromoIndex',
data() {
return {
steps: STEPS,
}
}
}
</script>
<!-- 😊 Хорошо: утечки не будет -->
<script>
import STEPS from './steps.js'
export default {
name: 'PromoIndex',
data() {
return {
steps: Object.freeze(STEPS),
}
}
}
</script>