Эта статья — перевод оригинальной статьи Lindsay Wardell "Building Web Components with Vue 3.2"

Также я веду телеграм канал “Frontend по-флотски”, где рассказываю про интересные вещи из мира разработки интерфейсов.

Вступление

Вы когда-нибудь работали над несколькими проектами и хотели иметь набор настраиваемых компонентов, которые можно было бы использовать во всех из них? Будь то работа или просто побочные проекты, набор компонентов, к которым вы можете обратиться, - отличный способ ускорить работу в новом или существующем проекте. Но что, если не все ваши проекты используют одну и ту же структуру пользовательского интерфейса? Или, что, если у вас есть тот, который вообще не использует какой-либо фреймворк JavaScript и полностью отрисован на сервере?

Как разработчик Vue, в идеале мы хотели бы просто использовать наш фреймворк для создания сложных пользовательских интерфейсов. Но иногда мы оказываемся в описанной выше ситуации, работая с другим фреймворком JavaScript, таким как React или Angular, или используя внутреннюю систему рендеринга, такую как Rails или Laravel. Как мы можем создать многоразовый пользовательский интерфейс для различных вариантов внешнего интерфейса?

В Vue 3.2 у нас теперь есть решение этой проблемы: веб-компоненты на базе Vue!

Веб-компоненты

Согласно MDN, «Веб-компоненты - это набор различных технологий, позволяющих вам создавать многоразовые настраиваемые элементы - с их функциональностью, инкапсулированной отдельно от остальной части вашего кода - и использовать их в ваших веб-приложениях». Рассмотрим несколько существующих элементов HTML, например select или video. Эти интерактивные элементы содержат собственный базовый стиль (обычно предоставляемый браузером), некоторую внутреннюю логику и способ прослушивания событий. Веб-компоненты позволяют разработчикам создавать свои собственные элементы и ссылаться на них в своем HTML - никакой инфраструктуры не требуется.

Вот очень простой пример веб-компонента, который будет отображать текущее время.

// Create the Web Component
class CurrentTime extends HTMLElement {
  connectedCallback() {
    this.innerHTML = new Date();

    setInterval(() => this.innerHTML = new Date(), 1000)
  }
}

// Define it as a custom element
customElements.define('current-time', CurrentTime);

После того, как пользовательский веб-компонент определен, он может отображаться как часть модели DOM, как и любой стандартный элемент HTML. Мы можем использовать этот элемент в нашем HTML так:

<body>
  <h1>The time is:</h1>
  <current-time></current-time>
</body>

Мы также можем использовать настраиваемые атрибуты с этими элементами, что позволяет нам передавать в них данные (аналогично props во Vue). Обратите внимание, что объекты нельзя передавать как атрибуты, потому что это концепция JavaScript, а не HTML.

<body>
  <h1>The time is:</h1>
  <current-time time-zone="America/Los_Angeles"></current-time>
</body>

Хотя мы могли бы довольно легко написать эту логику в теге script, используя обычный JavaScript, использование веб-компонентов дает нам возможность инкапсулировать определенную логику и функции внутри компонента, тем самым сохраняя наш код более организованным и понятным. По этой же причине мы используем компонентные фреймворки, такие как Vue и React. Кроме того, как мы обсуждали ранее, веб-компоненты являются гибкими в том смысле, что их можно использовать без JS-фреймворка, но они также совместимы с современными фреймворками (React и Vue и другие поддерживают использование веб-компонентов).

Веб-компоненты на базе Vue

Vue 3.2 включает встроенную поддержку для создания пользовательских веб-компонентов при использовании Vue API. Таким образом, мы получаем лучшее из обоих миров - настраиваемые повторно используемые компоненты во фреймворках / интерфейсах, а также превосходный API Vue. Давайте возьмем наш пример получения текущего времени и переведем его в компонент Vue. Мы будем использовать <script setup>, который сегодня является рекомендуемым способом написания однофайловых компонентов Vue.

Для начала создадим наш новый файл CurrentTime.ce.vue (ce в данном случае означает пользовательский элемент).

<template>
  {{ currentDateTime }}
</template>

<script setup>
import { ref } from 'vue'

let currentDateTime = ref(new Date())

setInterval(() => {
  currentDateTime.value = new Date()
})
</script>

Отлично, наш компонент делает именно то, что мы делали раньше. Затем нам нужно импортировать его где-нибудь в наш основной Javascript и определить его как настраиваемый элемент.

import { defineCustomElement } from 'vue'
import CurrentTime from './CurrentTime.ce.vue'

const CurrentTimeElement = defineCustomElement(CurrentTime)
customElements.define('current-time', CurrentTimeElement)

Что мы здесь сделали?

  • Сначала мы импортируем функцию defineCustomElement из Vue, которая преобразует компонент Vue в настраиваемый элемент.

  • Затем мы импортируем наш Vue SFC и передаем его в defineCustomElement, создавая конструктор, необходимый для API веб-компонентов.

  • Затем мы определяем настраиваемый элемент в DOM, снабжая его тегом, который мы хотим использовать (current-time), и конструктором, который он должен использовать для рендеринга.

Теперь наш веб-компонент Vue может отображаться в нашем приложении! А поскольку веб-компоненты работают во всех современных фреймворках (а также в фреймворках, отличных от JS, таких как Ruby on Rails или Laravel), теперь мы можем создать набор веб-компонентов для нашего приложения с помощью Vue, а затем использовать их в любом интерфейсе, который мы захотим. .

Вот базовый пример использования ванильного JS и шаблона Vite по умолчанию:

<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="UTF-8" />
    <link rel="icon" type="image/svg+xml" href="favicon.svg" />
    <meta name="viewport" content="width=device-width, initial-scale=1.0" />
    <title>Vite App</title>
  </head>

  <body>
    <div id="app">
      <h1>Hello Vue Web Components!</h1>
      <current-time></current-time>
    </div>
    <script type="module" src="/main.js"></script>
  </body>
</html>

Вы можете увидеть рабочий пример на Stackblitz.

Больше возможностей

Однако создание базового компонента Vue - не всегда то, чем вы хотите заниматься. Что произойдет, если нам понадобится использовать props или события? К счастью, Vue тоже позаботился о нас. Давайте рассмотрим некоторые из основных функций, которые мы ожидаем от наших пользовательских компонентов Vue.

Props

Первое, что мы хотим сделать, это передать свойства нашему веб-компоненту. Используя наш компонент <current-time>, мы хотим иметь возможность устанавливать часовой пояс. В этом случае мы можем использовать props, как обычно для элемента HTML.

В нашем HTML-шаблоне давайте изменим наш код на следующий:

<div id="app">
  <h1>Hello Vue Web Components!</h1>
  <current-time time-zone="America/New_York"></current-time>
</div>

Если вы сохраните файл сейчас, вы, вероятно, получите в консоли такую ошибку:

Cannot read properties of undefined (reading 'timeZone')

Это потому, что наш props еще не определен в компоненте. Давай позаботимся об этом сейчас. Вернитесь к своему компоненту Vue и внесите следующие изменения:

<template>
  <div>
    {{ displayTime }}
  </div>
</template>

<script setup>
import { ref, computed } from 'vue';

const props = defineProps({
  timeZone: {
    type: String,
    default: 'America/Los_Angeles',
  }
});

const currentDateTime = ref(new Date());
const displayTime = computed(() =>
  currentDateTime.value.toLocaleString('en-US', {
    timeZone: props.timeZone,
  })
);

setInterval(() => {
  currentDateTime.value = new Date();
});
</script>

В нашем компоненте Vue мы теперь определяем props (используя Vue 3.2 defineProps, который нам не нужно импортировать), а затем используем свойство timeZone для перевода даты в правильную строку часового пояса. Отлично!

Сохраните свои файлы, и наше приложение должно снова заработать, как и ожидалось, но на этот раз оно будет отображать дату в другом часовом поясе. Не стесняйтесь немного поиграть с ним, пробуя разные часовые пояса.

По умолчанию Vue переводит props в их заданные типы. Поскольку HTML позволяет передавать только строки в качестве атрибутов, Vue выполняет перевод в разные типы за нас.

События

Из документации: «События, генерируемые через this.$emit или setup emit, отправляются как нативные CustomEvents в собственном элементе. Дополнительные аргументы события будут представлены в виде массива в объекте CustomEvent в качестве его свойства details».

Давайте добавим базовую отправку события из нашего компонента, которое будет запускать console.log. В Vue компоненте обновите наш блок скрипта следующим образом:

import { ref, computed, watch } from 'vue';

const props = defineProps({
  timeZone: {
    type: String,
    default: 'America/Los_Angeles',
  },
});

const emit = defineEmits(['datechange']);

const currentDateTime = ref(new Date());
const displayTime = computed(() =>
  currentDateTime.value.toLocaleString('en-US', {
    timeZone: props.timeZone,
  })
);

setInterval(() => {
  currentDateTime.value = new Date();
  emit('datechange', displayTime);
}, 1000);

Основное изменение, которое мы здесь вносим, - это добавление defineEmit (также доступно без импорта, аналогично defineProps), чтобы определять, какие события делает этот компонент. Затем мы начинаем использовать это в нашем setInterval, чтобы выдать новую дату как событие.

В нашем main.js мы добавим прослушиватель событий в наш веб-компонент.

document
  .querySelector('current-time')
  .addEventListener('datechange', recordTime);

function recordTime(event) {
  console.log(event.detail[0].value);
}

Благодаря этому, всякий раз, когда первый компонент <current-time> генерирует событие datechange, мы сможем его прослушать и действовать соответствующим образом. Отлично!

Slots

Слоты работают точно так же как и в обычном Vue приложении, включая именованные слоты. Scoped слоты не поддерживаются, так как они являются расширенной функцией в Vue. Кроме того, при использовании именованных слотов в вашем приложении вам нужно будет использовать нативный синтаксис слотов, а не специальный синтаксис слотов Vue.

Давайте попробуем сейчас. В вашем компоненте Vue измените свой шаблон на следующей:

<div>
  <slot /> {{ displayTime }}
</div>

В этом примере мы используем стандартный слот (мы вернемся к именованным слотам позже). Теперь в вашем HTML добавьте текст между тегами <current-time>:

<current-time> The time is </current-time>

Если вы сохраните и перезагрузите свою страницу, вы должны теперь увидеть, что ваш текст (или любой другой контент, который вы хотите) правильно передается в ваш веб-компонент. Теперь давайте сделаем то же самое, используя именованные слоты. Измените свой компонент Vue, чтобы он имел именованный слот, и затем измените ваш HTML-код следующим образом:

<current-time>
  <span slot="prefix">The time is</span>
</current-time>

Результат должен быть таким же! И теперь мы можем использовать именованные слоты в наших веб-компонентах.

Стили

Одна из замечательных особенностей веб-компонентов заключается в том, что они могут использовать shadow root, инкапсулированную часть модели DOM, которая содержит их собственную информацию о стилях. Эта функция также доступна для наших веб-компонентов Vue. Когда вы называете свой файл расширением .ce.vue, по умолчанию он имеет встроенные стили. Это идеально, если вы хотите использовать свои компоненты в качестве библиотеки в приложении.

Provide/Inject

Provide и Inject также работают в веб-компонентах Vue. Однако следует иметь в виду, что они могут передавать данные только другим веб-компонентам Vue. Из документации, «пользовательский элемент, определенный в Vue, не сможет принимать props, предоставляемые компонентом Vue, не являющимся пользовательским элементом».

Заключение

Vue 3.2 предоставляет возможность писать собственные веб-компоненты с использованием знакомого синтаксиса Vue и гибкости Composition API. Однако имейте в виду, что это не рекомендуемый подход к написанию приложений Vue или разработке приложений в целом. В документации очень много говорится о различиях между веб-компонентами и компонентами, специфичными для Vue, а также о том, почему команда Vue считает, что их подход предпочтительнее для веб-разработки.

Однако веб-компоненты по-прежнему являются прекрасной технологией для создания кросс-фреймворковых приложений. В экосистеме веб-разработки существует множество инструментов, ориентированных исключительно на веб-компоненты, такие как Lit или Ionic. Хотя это может быть не рекомендуемый подход к созданию приложений с помощью Vue, он может обеспечить инкапсулированный способ разработки и функционирования определенных функций в командах или проектах.

Независимо от вашего отношения к веб-компонентам, я настоятельно рекомендую вам ознакомиться с этой новой функцией Vue и поэкспериментировать с ней самостоятельно. Вы даже можете попробовать вставить свой компонент в React или Svelte и посмотреть, насколько легко работать с другими фреймворками JavaScript. Самое главное, помните, что экосистема разработки всегда улучшается и растет, и вы должны быть готовы расти вместе с ней.

StackBlitz Demo

Поиграйтесь с StackBlitz demo и посмотрите мой пример веб-компонента на Vue, используемый в моём побочном проекте. Веселитесь!

Комментарии (8)


  1. korsetlr473
    06.10.2021 19:17

    жду не дождусь когда о новой фичи портал ктото напишет для создание модалок...


  1. NerVik
    07.10.2021 07:13
    -1

    Мы будем использовать <script setup>, который сегодня является рекомендуемым способом написания однофайловых компонентов Vue.

    Нет не является script setup рекомендованным способом написания компонентов


    1. qmzik Автор
      07.10.2021 11:44
      +2

      Из документации: "<script setup> is a compile-time syntactic sugar for using Composition API inside Single File Components (SFCs). It is the recommended syntax if you are using both SFCs and Composition API."


      1. NerVik
        07.10.2021 11:48
        -1

        It is the recommended syntax if you are using both SFCs and Composition API.

        тут главное словосочетание both SFCs and Composition API, т.е. не юзаешь композишн - script setup не является рекомендованным способом.

        При этом сам композишн не является рекомендованным стилем/синтаксисом. из чего следует вывод, что скрипт сетап не является рекомендованным способом написания однофайловых компонентов.


        1. Vadiok
          07.10.2021 14:16

          не юзаешь композишн - script setup не является рекомендованным способом.

          Это очевидно, т.к. если используешь ``script setup``, то автоматически используешь и Composition API. Нельзя использовать setup и старое объектное описание компонента.

          А вот насчет того, какой способ описания компонента использовать - я думаю, что создатели фреймворка задумывали Composition API как приоритетный, т.к. он позволяет хранить логику отдельно от компонентов, а также корректнее управлять реактивностью.


          1. NerVik
            07.10.2021 14:28
            -1

            я думаю, что создатели фреймворка задумывали Composition API как приоритетный

            зачем думать, если о том что композишен дополнительный, а не основной писали еще в 2019 году? https://github.com/vuejs/rfcs/blob/master/active-rfcs/0013-composition-api.md#adoption-strategy

            The Composition API is purely additive and does not affect / deprecate any existing 2.x APIs


            1. Demivan
              13.10.2021 23:26
              +1

              https://github.com/vuejs/rfcs/discussions/378#discussioncomment-1158805

              Composition API + <script setup> will be the recommended API moving forward, especially if you want good TS support.

              The current recommended approach is: Use SFC + <script setup> + Composition API


  1. justboris
    07.10.2021 15:57
    +2

    Маленькое пожелание – при использовании setInterval нужно не забывать их очищать при удалении компонента. Даже если это просто демка для примера, лучше следовать хорошим практикам