Реализация кроссфреймворковых Web Components (использование в React/Vue/Angular)

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

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация кроссфреймворковых Web Components (использование в React/Vue/Angular)
Сложная
~5 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Реализация кроссфреймворковых Web Components (использование в React/Vue/Angular)

Главное обещание Web Components — «напиши один раз, используй везде». Реализация этого обещания требует решить ряд нетривиальных проблем: типизация в каждом фреймворке, обработка событий, двусторонний биндинг, SSR-совместимость.

Проблемы при использовании Web Components в фреймворках

React (до версии 19) не передаёт объекты и массивы через атрибуты. Событийная модель React не подхватывает кастомные DOM-события автоматически. С React 19 ситуация улучшилась, но требует проверки.

Angular требует схему CUSTOM_ELEMENTS_SCHEMA для работы с нестандартными тегами.

Vue — наиболее дружественный к Web Components фреймворк, обрабатывает большинство кейсов нативно.

SSRcustomElements.define не существует в Node.js. Компоненты нельзя рендерить на сервере без специальных решений.

React: корректная интеграция

// Проблема: React передаёт объекты как строку "[object Object]"
// Решение: ref + useEffect для установки свойств напрямую

import { useRef, useEffect, forwardRef } from 'react'

// Обёртка-компонент для Web Component с объектными пропами
interface DataTableProps {
  columns: Column[]
  rows: Row[]
  onRowSelect?: (row: Row) => void
  onSort?: (column: string, direction: 'asc' | 'desc') => void
}

export const DataTable = forwardRef<HTMLElement, DataTableProps>(
  ({ columns, rows, onRowSelect, onSort }, forwardedRef) => {
    const ref = useRef<HTMLElement>(null)

    // Пробрасываем forwardedRef
    useEffect(() => {
      if (typeof forwardedRef === 'function') forwardedRef(ref.current)
      else if (forwardedRef) forwardedRef.current = ref.current
    }, [forwardedRef])

    // Объекты передаём через свойства, не атрибуты
    useEffect(() => {
      if (ref.current) {
        (ref.current as any).columns = columns
      }
    }, [columns])

    useEffect(() => {
      if (ref.current) {
        (ref.current as any).rows = rows
      }
    }, [rows])

    // Кастомные события
    useEffect(() => {
      const el = ref.current
      if (!el) return

      const handleRowSelect = (e: Event) => {
        onRowSelect?.((e as CustomEvent).detail)
      }
      const handleSort = (e: Event) => {
        const { column, direction } = (e as CustomEvent).detail
        onSort?.(column, direction)
      }

      el.addEventListener('row-select', handleRowSelect)
      el.addEventListener('sort', handleSort)

      return () => {
        el.removeEventListener('row-select', handleRowSelect)
        el.removeEventListener('sort', handleSort)
      }
    }, [onRowSelect, onSort])

    return <data-table ref={ref} />
  }
)

React 19: улучшенная поддержка

// React 19 нативно поддерживает передачу объектов в Web Components
// и подписку на кастомные события через on* пропы

// Типизация для нового поведения React 19
declare module 'react' {
  namespace JSX {
    interface IntrinsicElements {
      'data-table': {
        ref?: React.Ref<HTMLElement>
        columns?: Column[]      // React 19: объект передаётся напрямую
        rows?: Row[]
        'on-row-select'?: (e: CustomEvent<Row>) => void
        onRowSelect?: (e: CustomEvent<Row>) => void  // React 19
      }
    }
  }
}

Angular: схема и обёртки

// app.module.ts — разрешить неизвестные элементы
import { NgModule, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'
import { BrowserModule } from '@angular/platform-browser'

@NgModule({
  declarations: [AppComponent],
  imports: [BrowserModule],
  schemas: [CUSTOM_ELEMENTS_SCHEMA],  // ← обязательно
  bootstrap: [AppComponent],
})
export class AppModule {}
// Standalone component — без NgModule
@Component({
  selector: 'app-page',
  standalone: true,
  schemas: [CUSTOM_ELEMENTS_SCHEMA],
  template: `
    <ui-button
      variant="primary"
      [disabled]="isLoading"
      (uiClick)="handleClick($event)"
    >
      Сохранить
    </ui-button>
  `,
})
export class PageComponent {
  isLoading = false

  handleClick(e: CustomEvent) {
    this.isLoading = true
    // ...
  }
}

Angular директива-обёртка для двустороннего биндинга:

// Директива для <ui-input> с поддержкой [(ngModel)]
import { Directive, forwardRef, HostListener, ElementRef } from '@angular/core'
import { ControlValueAccessor, NG_VALUE_ACCESSOR } from '@angular/forms'

@Directive({
  selector: 'ui-input[formControlName], ui-input[ngModel]',
  standalone: true,
  providers: [{
    provide: NG_VALUE_ACCESSOR,
    useExisting: forwardRef(() => UiInputValueAccessor),
    multi: true,
  }],
})
export class UiInputValueAccessor implements ControlValueAccessor {
  private onChange: (value: string) => void = () => {}
  private onTouched: () => void = () => {}

  constructor(private el: ElementRef) {}

  @HostListener('sl-input', ['$event.target.value'])
  @HostListener('sl-change', ['$event.target.value'])
  onInput(value: string) {
    this.onChange(value)
  }

  @HostListener('sl-blur')
  onBlur() { this.onTouched() }

  writeValue(value: string) {
    this.el.nativeElement.value = value ?? ''
  }

  registerOnChange(fn: (v: string) => void) { this.onChange = fn }
  registerOnTouched(fn: () => void) { this.onTouched = fn }

  setDisabledState(disabled: boolean) {
    this.el.nativeElement.disabled = disabled
  }
}

Vue: нативная поддержка

Vue 3 работает с Web Components почти без настройки:

// vite.config.ts — не парсить кастомные теги как Vue компоненты
export default defineConfig({
  plugins: [
    vue({
      template: {
        compilerOptions: {
          // Элементы с '-' в имени — Web Components
          isCustomElement: (tag) => tag.includes('-'),
        },
      },
    }),
  ],
})
<template>
  <!-- Props передаются как атрибуты для примитивов -->
  <ui-button variant="primary" :disabled="isLoading" @ui-click="handleClick">
    Отправить
  </ui-button>

  <!-- Объекты через .prop модификатор -->
  <data-table
    .columns="columns"
    .rows="rows"
    @row-select="handleRowSelect"
  />

  <!-- v-model для кастомного инпута -->
  <ui-input v-model="formValue" label="Email" />
</template>

<script setup lang="ts">
import { ref } from 'vue'
import '@company/ui-core/components/button.js'
import '@company/ui-core/components/input.js'
import '@company/ui-core/components/data-table.js'

const isLoading = ref(false)
const formValue = ref('')

// v-model для Web Component — нужен defineCustomElement или вручную:
// v-model компилируется в :modelValue + @update:modelValue
// Web Component должен эмитить 'update:modelValue' событие
</script>

Svelte

<script lang="ts">
  import '@company/ui-core/components/button.js'

  let loading = false

  function handleClick(e: CustomEvent) {
    loading = true
    // ...
  }
</script>

<!-- Svelte: on:событие для кастомных событий -->
<ui-button
  variant="primary"
  disabled={loading}
  on:ui-click={handleClick}
>
  Отправить
</ui-button>

SSR: решение проблемы server-side rendering

На сервере нет customElements, HTMLElement, window. Варианты:

// 1. Lazy import только на клиенте (Next.js)
// components/UiButton.tsx
import dynamic from 'next/dynamic'

const UiButtonClient = dynamic(
  () => import('./UiButtonClient').then(m => m.UiButtonClient),
  { ssr: false }
)

export function UiButton(props: ButtonProps) {
  return <UiButtonClient {...props} />
}
// 2. Полифил для SSR (experimental)
// @lit-labs/ssr — серверный рендер Lit компонентов
import { renderToString } from '@lit-labs/ssr'
import { html } from 'lit'

const result = renderToString(html`
  <ui-button variant="primary">Click</ui-button>
`)
// Отдаёт declarative shadow DOM разметку
<!-- Declarative Shadow DOM — SSR-compatible -->
<ui-button>
  <template shadowrootmode="open">
    <style>/* ... */</style>
    <button class="btn btn--primary">Click</button>
  </template>
  Click
</ui-button>

Универсальные обёртки: @lit/react

Официальное решение от команды Lit для React-интеграции:

import { createComponent } from '@lit/react'
import React from 'react'
import { UiButton } from '@company/ui-core'

export const Button = createComponent({
  tagName: 'ui-button',
  elementClass: UiButton,
  react: React,
  events: {
    onUiClick: 'ui-click',
    onUiFocus: 'ui-focus',
    onUiBlur: 'ui-blur',
  },
})

// Теперь Button работает как React компонент с типизацией
function App() {
  return (
    <Button
      variant="primary"
      onUiClick={(e) => console.log(e.detail)}
    >
      Click
    </Button>
  )
}

Чеклист совместимости

Перед публикацией кроссфреймворковой библиотеки:

  • Все кастомные события используют composed: true и bubbles: true
  • Объектные свойства не зеркалятся в атрибуты (reflect: false для объектов)
  • Компонент корректно работает с disabled через ElementInternals
  • Нет прямых обращений к window, document в constructor — только в connectedCallback
  • Экспортируются типы для каждого фреймворка
  • Добавлен custom-elements.json (CEM) для IDE-автодополнения

Сроки

Интеграция существующей Web Components библиотеки в один фреймворк с типами и обёртками — 3–5 дней. Поддержка React + Vue + Angular + SSR с полным набором типов и документации — 3–4 недели.