Vue и Веб-компоненты
Веб-компоненты (Web Components) — общий термин набора нативных API, которые позволяют веб-разработчикам создавать переиспользуемые пользовательские элементы.
Vue и веб-компоненты — взаимодополняющие технологии. Vue имеет отличную поддержку и чтобы использовать и чтобы создавать пользовательские элементы. Независимо от того, интегрируете пользовательские элементы в приложение Vue, или используете Vue для разработки и распространения пользовательских элементов, не прогадаете.
Использование пользовательских элементов во Vue
Vue безупречно получает 100% в тестах Custom Elements Everywhere. Использование в приложении Vue пользовательских элементов в целом работает аналогично обычному использованию нативных HTML-элементов. Несколько моментов, о которых стоит помнить:
Пропуск определения компонента
По умолчанию Vue будет пытаться разрешить ненативный HTML-тег зарегистрированным компонентом Vue, прежде чем вернуться к его отрисовке как пользовательского элемента. Это приведет к тому, что во время разработки Vue выдаст предупреждение "не удалось определить компонент" (англ.: "failed to resolve component"). Чтобы сообщить Vue, что некоторые элементы должны рассматриваться как пользовательские и предотвратить их определение как компонентов, мы можем указать параметр compilerOptions.isCustomElement
.
В приложениях Vue с использованием шага сборки, опцию необходимо передавать через конфигурацию сборки, поскольку она используется во время компиляции.
Пример конфигурации в браузере
js
// Работает только при использовании компиляции шаблонов в браузере.
// При использовании системы сборки, см. примеры ниже.
app.config.compilerOptions.isCustomElement = (tag) => tag.includes('-')
Пример конфигурации Vite
js
// vite.config.js
import vue from '@vitejs/plugin-vue'
export default {
plugins: [
vue({
template: {
compilerOptions: {
// считать все теги с тире как пользовательские элементы
isCustomElement: (tag) => tag.includes('-')
}
}
})
]
}
Пример конфигурации Vue CLI
js
// vue.config.js
module.exports = {
chainWebpack: (config) => {
config.module
.rule('vue')
.use('vue-loader')
.tap((options) => ({
...options,
compilerOptions: {
// считать все теги начинающиеся с ion- пользовательскими элементами
isCustomElement: (tag) => tag.startsWith('ion-')
}
}))
}
}
Передача свойств DOM
Поскольку атрибуты DOM могут быть только строками, то появляется необходимость передавать сложные данные в пользовательские элементы через свойства DOM. При установке входных параметров для пользовательского элемента, Vue 3 автоматически проверяет наличие DOM-свойства с помощью оператора in
и предпочтёт установить значение как DOM-свойство, если ключ присутствует. Чаще всего об этом и не придётся думать, если пользовательский элемент разработан следуя рекомендуемым практикам.
Возможны крайние случаи, когда данные должны быть переданы как свойство DOM, но пользовательский элемент не объявляет/отражает свойство должным образом (приводя к неудаче при проверке с помощью in
). В таких случаях, форсировать привязку v-bind
для установки свойства DOM можно использованием модификатора .prop
:
template
<my-element :user.prop="{ name: 'jack' }"></my-element>
<!-- сокращенная запись -->
<my-element .user="{ name: 'jack' }"></my-element>
Создание пользовательских элементов с помощью Vue
Главное преимущество пользовательских элементов в том, что их можно использовать с любым фреймворком или даже без него. Это делает их идеальными для распространения компонентов, когда конечный пользователь может использовать во фронтенде другой стек, или когда следует изолировать конечное приложение от деталей реализации компонентов, которые оно использует.
defineCustomElement
Vue позволяет создавать пользовательские элементы с точно таким же же API компонентов Vue с помощью метода defineCustomElement
. Метод принимает такой же аргумент, что и defineComponent
, но вместо этого будет возвращать конструктор пользовательского элемента, расширяющего HTMLElement
:
template
<my-vue-element></my-vue-element>
js
import { defineCustomElement } from 'vue'
const MyVueElement = defineCustomElement({
// обычные опции компонента Vue
props: {},
emits: {},
template: `...`,
// только для defineCustomElement: CSS, внедряемый в shadow root
styles: [`/* inlined css */`]
})
// регистрация пользовательского элемента.
// после регистрации все теги `<my-vue-element>`
// на странице будут обновлены.
customElements.define('my-vue-element', MyVueElement)
// Вы также можете программно создать экземпляр элемента:
// (это может быть сделано только после регистрации)
document.body.appendChild(
new MyVueElement({
// начальные входные параметры (опционально)
})
)
Жизненный цикл
Пользовательский элемент Vue будет монтировать внутренний экземпляр компонента Vue внутри своего shadow root при первом вызове
connectedCallback
на элементе.При вызове
disconnectedCallback
на элементе Vue будет проверять, отсоединён ли элемент от документа после microtask тика.Если элемент будет всё ещё находиться в документе, то это считается перемещением и экземпляр компонента будет сохранён;
Если элемент будет отсоединён от документа, то это считается удалением и экземпляр компонента будет размонтирован.
Входные параметры
Все входные параметры для пользовательского элемента, объявленные в опции
props
, будут определены в пользовательском элементе как свойства. Vue автоматически обработает соответствие между атрибутами / свойствами, где это необходимо.Атрибуты всегда отражаются в соответствующих свойствах
Свойства с примитивными значениями (
string
,boolean
ornumber
) отражаются как атрибуты.
Vue автоматически приведёт входные параметры, объявленные с типами
Boolean
илиNumber
, к нужному типу, когда они устанавливаются в качестве атрибутов (которые всегда являются строками). К примеру, рассмотрим объявление входных параметров::jsprops: { selected: Boolean, index: Number }
Использование пользовательского элемента:
template<my-element selected index="1"></my-element>
В компоненте значение
selected
будет приведено кtrue
(boolean), аindex
- к1
(number).
События
События, генерируемые через this.$emit
или setup emit
,будут генерироваться на пользовательском элементе как нативные CustomEvents. Дополнительные аргументы события (payload / данные передаваемые с событием) будут содержаться в объекте CustomEvent в свойстве details
в виде массива.
Слоты
Внутри компонента слоты могут указываться как обычно, с помощью элемента <slot/>
. Но при получении результирующего элемента должен быть только нативный синтаксис слотов:
Слоты с ограниченной областью видимости не поддерживаются.
При передаче именованных слотов используйте атрибут
slot
вместо директивыv-slot
:template<my-element> <div slot="named">hello</div> </my-element>
Provide / Inject
Provide / Inject API и его эквивалент в Composition API также работают между пользовательскими элементами, определяемыми Vue. Однако обратите внимание, что это работает только между пользовательскими элементами. То есть пользовательский элемент, определяемый Vue, не сможет инжектировать свойства, предоставляемые компонентом Vue, не являющимся пользовательским элементом.
App Level Config
You can configure the app instance of a Vue custom element using the configureApp
option:
js
defineCustomElement(MyComponent, {
configureApp(app) {
app.config.errorHandler = (err) => {
/* ... */
}
}
})
Однофайловые компоненты как пользовательские элементы
Метод defineCustomElement
также работает с однофайловыми компонентами Vue (SFC). Однако при стандартной настройке инструментов <style>
внутри SFC все равно будут извлечены и объединены в один CSS-файл при production сборке. При использовании SFC в качестве пользовательского элемента часто более желательным вариантом будет внедрение тегов <style>
в shadow root пользовательского элемента.
Официальные инструменты для однофайловых компонентов поддерживают их импорт в "режиме пользовательского элемента" (требуется @vitejs/plugin-vue@^1.4.0
или vue-loader@^16.5.0
). Однофайловый компонент, загруженный в режиме пользовательского элемента, вставляет свои теги <style>
как строки CSS и раскрывает их в параметре компонента styles
. Их использует defineCustomElement
и при инициализации внедрено в shadow root элемента.
Для переключения в этот режим, требуется завершить имя файла компонента на .ce.vue
:
js
import { defineCustomElement } from 'vue'
import Example from './Example.ce.vue'
console.log(Example.styles) // ["/* инлайн css */"]
// преобразование в конструктор пользовательского элемента
const ExampleElement = defineCustomElement(Example)
// регистрация
customElements.define('my-example', ExampleElement)
Если нужно настроить, какие файлы должны импортироваться в режиме пользовательских элементов (например, чтобы пользовательскими элементами считались все однофайловые компоненты), можно передать параметр customElement
соответствующим плагинам системы сборки:
Советы по созданию библиотеки пользовательских элементов на Vue
При создании пользовательских элементов с помощью Vue элементы будут опираться на среду выполнения Vue. В зависимости от количества используемых функций, базовый размер будет составлять ~16 кб. Это означает, что использование Vue не является идеальным, если вы поставляете один пользовательский элемент - возможно, вам лучше использовать ванильный JavaScript, petite-vue или фреймворки, которые специализируются на небольшом размере среды выполнения. Но такой базовый размер более чем оправдан, если поставлять коллекцию пользовательских элементов со сложной логикой, так как Vue позволит создавать каждый компонент с гораздо меньшим количеством кода. Чем больше элементов будет поставляться, тем выгоднее окажется этот компромисс.
Если пользовательские элементы будут использоваться в приложении, которое также использует Vue, то можно настроить экстернализацию Vue, чтобы убрать его из итоговой сборки, а элементы стали использовать Vue из основного приложения.
Рекомендуется экспортировать отдельные конструкторы элементов, чтобы позволить их импортировать по необходимости или регистрировать с нужными именами тегов. Также можно экспортировать удобную функцию для автоматической регистрации всех элементов. Вот пример точки входа пользовательской библиотеки пользовательских элементов на Vue:
js
import { defineCustomElement } from 'vue'
import Foo from './MyFoo.ce.vue'
import Bar from './MyBar.ce.vue'
const MyFoo = defineCustomElement(Foo)
const MyBar = defineCustomElement(Bar)
// экспорт отдельных элементов
export { MyFoo, MyBar }
export function register() {
customElements.define('my-foo', MyFoo)
customElements.define('my-bar', MyBar)
}
Если компонентов слишком много, можно воспользоваться такими возможностями систем сборки, как glob import в Vite или require.context
в webpack для загрузки всех компонентов из определённого каталога
Веб-компоненты и Typescript
Если вы разрабатываете приложение или библиотеку, вам может потребоваться проверять типы ваших Vue компонентов, включая те, которые определены как пользовательские элементы.
Пользовательские элементы регистрируются глобально с использованием нативного API, поэтому по умолчанию у них нет вывода типов при использовании в шаблонах Vue. Чтобы предоставить поддержку типов для Vue компонентов, зарегистрированных как пользовательские элементы, мы можем регистрировать глобальные типы компонентов, используя интерфейс GlobalComponents в шаблонах Vue и/или в JSX:
typescript
import { defineCustomElement } from 'vue'
// однофайловый компонент Vue
import CounterSFC from './src/components/counter.ce.vue'
// определение его как пользовательского элемента
export const Counter = defineCustomElement(CounterSFC)
// регистрация глобальных типов
declare module 'vue' {
export interface GlobalComponents {
Counter: typeof Counter
}
}
Веб-компоненты против Vue компонентов
Некоторые разработчики считают, что стоит избегать моделей компонентов проприетарных фреймворков, и что использование исключительно пользовательских элементов сделает приложение «готовым к будущему». Поэтому далее попытаемся объяснить своё мнение, почему считаем это слишком упрощённым взглядом на проблему.
Между пользовательскими элементами и компонентами Vue действительно есть некоторый уровень совпадающих функций: они оба позволяют определять многократно используемые компоненты с передачей данных, генерацией событий и управлением жизненным циклом. Но API веб-компонентов относительно низкоуровневое и «сырое». Для создания реального приложения часто требуется довольно много дополнительных возможностей, которые эта платформа не охватывает:
Декларативная и эффективная система шаблонов;
Реактивная система управления состоянием, которая облегчает извлечение и переиспользование логики между компонентами;
Производительный способ отрисовки компонентов на сервере и гидратация на клиенте (SSR), что важно для SEO и метрик Web Vitals, таких как LCP. Отрисовка на стороне сервера нативных пользовательских элементов обычно состоит из симулирования DOM в Node.js и последующую сериализацию изменённого DOM, в то время как SSR для Vue когда это возможно компилируется в конкатенацию строк, что гораздо эффективнее.
Компонентная модель Vue разработана как целостная система, с учётом этих потребностей.
При наличии компетентной команды инженеров, вероятно, возможно построить эквивалент всего этого поверх родных пользовательских элементов — но это значит, что берёте на себя долгосрочное бремя поддержки собственного фреймворка, теряя при этом преимущества зрелой экосистемы фреймворка Vue и его сообщества.
Существуют фреймворки, построенные с использованием пользовательских элементов в качестве основы своей компонентной модели, но всем им неизбежно требуется внедрять свои собственные решения проблем, перечисленных выше. Применение таких фреймворков подразумевает принятие их технических решений для решения озвученных проблем, но, несмотря на рекламу, не защищает автоматически от потенциальных будущих изменений.
Также есть некоторые области, в которых пользовательские элементы могут быть ограничивающим фактором:
Нетерпеливая оценка слотов не позволяет компоновать компоненты. Слоты с ограниченной областью видимости во Vue - это мощный механизм для композиции компонентов, который не может поддерживаться пользовательскими элементами из-за нетерпеливой природы нативных слотов. Нетерпеливые слоты также означают, что принимающий компонент не может контролировать, когда и нужно ли выводить содержимое слота.
Доставка пользовательских элементов с shadow DOM и локальным (scoped) CSS сейчас требует встраивания CSS в JavaScript, чтобы их можно было внедрить в shadow root в runtime. Это также приводит к дублированию стилей в разметке в сценариях с SSR. В этой области работают над новыми возможностями платформы, но на данный момент они ещё не поддерживаются повсеместно, и всё ещё есть проблемы с производительностью в production и SSR, которые требуется решить. В тоже время, однофайловые компоненты Vue предоставляют механизм локализации CSS, который поддерживает извлечение стилей в обычные CSS-файлы.
Vue всегда будет идти в ногу с последними стандартами веб-платформ, и использовать всё, что предоставляет платформа, если это может облегчить работу. Но текущая цель — предоставлять решения, которые работают хорошо и работают уже сегодня. Это значит, что новые возможности платформы следует включать лишь критически поразмыслив — и это подразумевает заполнение пробелов, где стандарты ещё не соответствуют требованиям.