Эта статья — перевод оригинальной статьи 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, используемый в моём побочном проекте. Веселитесь!