Оглавление

    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>