Разработка библиотеки Web Components (Stencil)

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

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

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

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка библиотеки Web Components (Stencil)
Сложная
~1-2 недели
Часто задаваемые вопросы

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

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

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

  • 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 (Stencil)

Stencil — компилятор компонентов от Ionic. Написанные компоненты компилируются в нативные Web Components с опциональной поддержкой Angular, React, Vue через автогенерируемые обёртки. Stencil — не runtime-библиотека, а инструмент сборки: итоговый бандл содержит только нативные APIs плюс минимальный полифилл-слой.

Ключевое отличие от Lit: Stencil генерирует framework-специфичные пакеты. Если библиотека должна работать как native в Angular (с двусторонним биндингом, формами), как React-компоненты (с типизированными props) — Stencil делает это из коробки.

Установка и инициализация

npm init stencil@latest
# Выбрать: component library
cd my-library
npm install

Структура проекта:

src/
├── components/
│   ├── ui-button/
│   │   ├── ui-button.tsx        ← компонент
│   │   ├── ui-button.css        ← стили
│   │   ├── ui-button.e2e.ts     ← E2E тесты
│   │   └── ui-button.spec.ts    ← unit тесты
│   └── ui-input/
├── utils/
├── index.ts
└── index.html

stencil.config.ts

Компонент на Stencil

Stencil использует TSX (как React) и декораторы для разметки компонента:

import {
  Component,
  Host,
  h,
  Prop,
  State,
  Event,
  EventEmitter,
  Method,
  Watch,
  Element,
  Listen,
} from '@stencil/core'

@Component({
  tag: 'ui-button',
  styleUrl: 'ui-button.css',
  shadow: true,  // включить Shadow DOM
  // scoped: true,  // вместо Shadow DOM — scoped CSS (нет slots, но работает с формами)
})
export class UiButton {
  // Ссылка на host элемент
  @Element() el!: HTMLElement

  // Props — публичные свойства/атрибуты
  @Prop() variant: 'primary' | 'secondary' | 'ghost' = 'primary'
  @Prop() size: 'sm' | 'md' | 'lg' = 'md'
  @Prop({ reflect: true }) disabled = false
  @Prop({ mutable: true }) loading = false  // mutable — компонент может менять

  // Внутреннее состояние
  @State() private focused = false

  // Events
  @Event({ eventName: 'uiClick', bubbles: true, composed: true })
  uiClick!: EventEmitter<{ nativeEvent: MouseEvent }>

  // Watch — реакция на изменение prop/state
  @Watch('disabled')
  onDisabledChange(newVal: boolean) {
    this.el.setAttribute('aria-disabled', String(newVal))
  }

  // Listen — слушаем события (на host или document)
  @Listen('focus', { target: 'window' })
  onWindowFocus(e: FocusEvent) {
    // ...
  }

  // Публичный метод — вызывается из JS
  @Method()
  async focusButton() {
    this.el.shadowRoot?.querySelector('button')?.focus()
  }

  private handleClick = (e: MouseEvent) => {
    if (this.disabled || this.loading) return
    this.uiClick.emit({ nativeEvent: e })
  }

  render() {
    return (
      <Host
        class={{
          'is-disabled': this.disabled,
          'is-loading': this.loading,
        }}
        aria-disabled={this.disabled ? 'true' : null}
      >
        <button
          type="button"
          disabled={this.disabled || this.loading}
          class={`btn btn--${this.variant} btn--${this.size}`}
          onClick={this.handleClick}
          onFocus={() => (this.focused = true)}
          onBlur={() => (this.focused = false)}
        >
          {this.loading && <span class="spinner" aria-hidden="true"></span>}
          <slot name="icon-start"></slot>
          <slot></slot>
          <slot name="icon-end"></slot>
        </button>
      </Host>
    )
  }
}
/* ui-button.css */
:host {
  display: inline-flex;
}

.btn {
  display: inline-flex;
  align-items: center;
  gap: 8px;
  border: none;
  cursor: pointer;
  font-family: inherit;
  font-weight: 600;
  border-radius: var(--ui-radius, 8px);
  transition: background 0.15s, transform 0.1s;
}

.btn:disabled { opacity: 0.5; cursor: not-allowed; }
.btn:active:not(:disabled) { transform: scale(0.97); }

.btn--primary  { background: var(--ui-primary, #7000ff); color: #fff; }
.btn--secondary { background: transparent; border: 1.5px solid var(--ui-primary, #7000ff); color: var(--ui-primary, #7000ff); }
.btn--ghost    { background: transparent; color: var(--ui-primary, #7000ff); }

.btn--sm { padding: 6px 14px;  font-size: 13px; }
.btn--md { padding: 10px 22px; font-size: 15px; }
.btn--lg { padding: 14px 30px; font-size: 17px; }

Stencil Config: несколько output targets

Главная возможность Stencil — скомпилировать один компонент в несколько форматов:

// stencil.config.ts
import { Config } from '@stencil/core'
import { angularOutputTarget } from '@stencil/angular-output-target'
import { reactOutputTarget } from '@stencil/react-output-target'
import { vueOutputTarget } from '@stencil/vue-output-target'

export const config: Config = {
  namespace: 'my-ui',

  outputTargets: [
    // 1. Нативные Web Components
    {
      type: 'dist',
      esmLoaderPath: '../loader',
    },

    // 2. dist-custom-elements — tree-shakeable
    {
      type: 'dist-custom-elements',
      customElementsExportBehavior: 'auto-define-custom-elements',
      externalRuntime: false,
    },

    // 3. React обёртки (автогенерация)
    reactOutputTarget({
      componentCorePackage: 'my-ui-core',
      proxiesFile: '../my-ui-react/src/components.ts',
      includeDefineCustomElements: true,
    }),

    // 4. Vue обёртки
    vueOutputTarget({
      componentCorePackage: 'my-ui-core',
      proxiesFile: '../my-ui-vue/src/components.ts',
    }),

    // 5. Angular обёртки с NgModule
    angularOutputTarget({
      componentCorePackage: 'my-ui-core',
      outputType: 'standalone',  // или 'component'
      directivesProxyFile: '../my-ui-angular/src/directives/proxies.ts',
    }),

    // 6. Документация
    { type: 'docs-readme' },
    { type: 'docs-json', file: './dist/docs.json' },

    // 7. Custom Elements Manifest для IDE-подсказок
    { type: 'docs-vscode', file: './dist/vscode.html-data.json' },
  ],

  testing: {
    browserHeadless: 'new',
  },
}

Автогенерированные React-обёртки

После npm run build в my-ui-react/src/components.ts появляется:

// Автогенерировано Stencil — не редактировать вручную
import { createReactComponent } from './react-component-lib'
import { defineCustomElements } from 'my-ui-core/loader'
defineCustomElements()

export const UiButton = /*@__PURE__*/ createReactComponent<
  JSX.UiButton,
  HTMLUiButtonElement
>('ui-button')

export const UiInput = /*@__PURE__*/ createReactComponent<
  JSX.UiInput,
  HTMLUiInputElement
>('ui-input')

Использование в React:

import { UiButton, UiInput } from 'my-ui-react'

function Form() {
  const handleClick = (e: CustomEvent<{ nativeEvent: MouseEvent }>) => {
    console.log('clicked', e.detail)
  }

  return (
    <form>
      <UiInput label="Email" type="email" required />
      <UiButton
        variant="primary"
        onUiClick={handleClick}  // типизированный event handler
        loading={false}
      >
        Отправить
      </UiButton>
    </form>
  )
}

Тестирование

Stencil включает Stencil Testng Utils поверх Jest + Puppeteer:

// ui-button.spec.ts — unit tests
import { newSpecPage } from '@stencil/core/testing'
import { UiButton } from './ui-button'

describe('ui-button', () => {
  it('renders with default props', async () => {
    const page = await newSpecPage({
      components: [UiButton],
      html: '<ui-button>Click me</ui-button>',
    })

    expect(page.root).toEqualHtml(`
      <ui-button>
        <mock:shadow-root>
          <button class="btn btn--primary btn--md" type="button">
            <slot name="icon-start"></slot>
            <slot></slot>
            <slot name="icon-end"></slot>
          </button>
        </mock:shadow-root>
        Click me
      </ui-button>
    `)
  })

  it('disables button when disabled prop is set', async () => {
    const page = await newSpecPage({
      components: [UiButton],
      html: '<ui-button disabled></ui-button>',
    })

    const button = page.root?.shadowRoot?.querySelector('button')
    expect(button?.disabled).toBe(true)
  })

  it('emits uiClick event', async () => {
    const page = await newSpecPage({
      components: [UiButton],
      html: '<ui-button></ui-button>',
    })

    const clickSpy = jest.fn()
    page.root?.addEventListener('uiClick', clickSpy)

    page.root?.shadowRoot?.querySelector('button')?.click()

    expect(clickSpy).toHaveBeenCalled()
  })
})

Монорепозиторий: структура мульти-пакетного проекта

packages/
├── core/           ← Stencil компоненты (my-ui-core)
├── react/          ← React обёртки (my-ui-react)
├── vue/            ← Vue обёртки (my-ui-vue)
├── angular/        ← Angular обёртки (my-ui-angular)
└── docs/           ← Storybook

Сроки

5–8 компонентов с dist и React output — 2–3 недели. Полная библиотека с Angular/Vue обёртками, E2E-тестами, Storybook, CD-пайплайном и npm-публикацией — 6–10 недель.