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

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

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

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

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка библиотеки Web Components (Lit)
Сложная
~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 (Lit)

Lit — библиотека от Google для создания Web Components. Весит ~5kb gzipped, строится поверх нативных API (Custom Elements, Shadow DOM, Templates), добавляя реактивные свойства, декларативные шаблоны через tagged template literals и автоматические обновления DOM.

Выбор Lit против «голых» Custom Elements оправдан когда компонентов больше 3–5: Lit убирает шаблонный код ручного обновления DOM, реактивных атрибутов и lifecycle-управления.

Установка и структура проекта

npm create vite@latest my-components -- --template lit-ts
cd my-components
npm install

Или в существующий проект:

npm install lit

Типовая структура библиотеки:

src/
├── components/
│   ├── button/
│   │   ├── button.ts
│   │   ├── button.styles.ts
│   │   └── button.test.ts
│   ├── modal/
│   └── tooltip/
├── styles/
│   └── tokens.css      ← CSS custom properties
├── index.ts            ← реэкспорт всех компонентов
└── types.ts

Базовый компонент

import { LitElement, html, css, PropertyValues } from 'lit'
import { customElement, property, state, query } from 'lit/decorators.js'
import { classMap } from 'lit/directives/class-map.js'
import { ifDefined } from 'lit/directives/if-defined.js'

@customElement('ui-button')
export class UiButton extends LitElement {
  // Реактивные свойства — изменение вызывает перерендер
  @property({ type: String })
  variant: 'primary' | 'secondary' | 'ghost' = 'primary'

  @property({ type: String })
  size: 'sm' | 'md' | 'lg' = 'md'

  @property({ type: Boolean, reflect: true })
  disabled = false

  @property({ type: Boolean })
  loading = false

  @property({ type: String })
  type: 'button' | 'submit' | 'reset' = 'button'

  // Внутреннее состояние — не атрибут, не публичное свойство
  @state()
  private _focused = false

  // Доступ к DOM элементу внутри shadow
  @query('button')
  private _button!: HTMLButtonElement

  static styles = css`
    :host {
      display: inline-flex;
    }

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

    button:disabled {
      opacity: 0.5;
      cursor: not-allowed;
    }

    button:active:not(:disabled) {
      transform: scale(0.97);
    }

    /* Варианты */
    :host([variant="primary"]) button,
    button.variant--primary {
      background: var(--ui-color-primary, #7000ff);
      color: #fff;
    }

    :host([variant="secondary"]) button,
    button.variant--secondary {
      background: transparent;
      border: 1.5px solid var(--ui-color-primary, #7000ff);
      color: var(--ui-color-primary, #7000ff);
    }

    :host([variant="ghost"]) button,
    button.variant--ghost {
      background: transparent;
      color: var(--ui-color-primary, #7000ff);
    }

    /* Размеры */
    .size--sm { padding: 6px 14px;  font-size: 13px; }
    .size--md { padding: 10px 22px; font-size: 15px; }
    .size--lg { padding: 14px 30px; font-size: 17px; }

    /* Loading spinner */
    .spinner {
      width: 1em;
      height: 1em;
      border: 2px solid transparent;
      border-top-color: currentColor;
      border-radius: 50%;
      animation: spin 0.6s linear infinite;
    }

    @keyframes spin {
      to { transform: rotate(360deg); }
    }
  `

  render() {
    const classes = classMap({
      [`variant--${this.variant}`]: true,
      [`size--${this.size}`]: true,
      'is-loading': this.loading,
    })

    return html`
      <button
        class=${classes}
        type=${this.type}
        ?disabled=${this.disabled || this.loading}
        aria-disabled=${this.disabled || this.loading}
        @focus=${() => { this._focused = true }}
        @blur=${() => { this._focused = false }}
      >
        ${this.loading ? html`<span class="spinner" aria-hidden="true"></span>` : ''}
        <slot name="icon-start"></slot>
        <slot></slot>
        <slot name="icon-end"></slot>
      </button>
    `
  }

  // Lifecycle: после первого рендера
  protected firstUpdated() {
    this._button.addEventListener('click', this._handleClick)
  }

  // Lifecycle: после каждого обновления
  protected updated(changedProps: PropertyValues) {
    if (changedProps.has('disabled')) {
      this.setAttribute('aria-disabled', String(this.disabled))
    }
  }

  private _handleClick = (e: Event) => {
    if (this.disabled || this.loading) {
      e.preventDefault()
      e.stopPropagation()
      return
    }

    // Всплывающий кастомный ивент
    this.dispatchEvent(new CustomEvent('ui-click', {
      bubbles: true,
      composed: true,
      detail: { originalEvent: e },
    }))
  }

  // Публичный метод (вызывается снаружи)
  focus() {
    this._button?.focus()
  }

  disconnectedCallback() {
    super.disconnectedCallback()
    this._button?.removeEventListener('click', this._handleClick)
  }
}

declare global {
  interface HTMLElementTagNameMap {
    'ui-button': UiButton
  }
}

Реактивные свойства подробнее

@customElement('ui-tabs')
export class UiTabs extends LitElement {
  // reflect: true — синхронизирует JS свойство с HTML атрибутом
  @property({ type: Number, reflect: true })
  activeIndex = 0

  // converter — кастомное преобразование строки атрибута в тип
  @property({
    converter: {
      fromAttribute: (value: string | null) =>
        value ? value.split(',').map(Number) : [],
      toAttribute: (value: number[]) => value.join(','),
    }
  })
  selectedIds: number[] = []

  // hasChanged — пропуск лишних обновлений
  @property({
    hasChanged: (newVal: object, oldVal: object) =>
      JSON.stringify(newVal) !== JSON.stringify(oldVal),
  })
  config: Record<string, unknown> = {}
}

Directives в шаблонах

import { repeat } from 'lit/directives/repeat.js'
import { cache } from 'lit/directives/cache.js'
import { asyncReplace } from 'lit/directives/async-replace.js'
import { ref } from 'lit/directives/ref.js'

render() {
  return html`
    <!-- repeat с key для эффективного diff -->
    <ul>
      ${repeat(
        this.items,
        (item) => item.id,  // key
        (item) => html`<li>${item.name}</li>`
      )}
    </ul>

    <!-- cache — не уничтожает DOM при переключении -->
    ${cache(this.showDetails
      ? html`<details-panel></details-panel>`
      : html`<summary-view></summary-view>`
    )}

    <!-- ref — доступ к DOM элементу -->
    <canvas ${ref(this._canvasRef)}></canvas>
  `
}

Контроллеры (Reactive Controllers)

Переиспользуемая логика, независимая от компонента:

// controllers/mouse-controller.ts
import { ReactiveController, ReactiveControllerHost } from 'lit'

export class MouseController implements ReactiveController {
  host: ReactiveControllerHost
  x = 0
  y = 0

  constructor(host: ReactiveControllerHost) {
    this.host = host
    host.addController(this)
  }

  hostConnected() {
    window.addEventListener('mousemove', this._onMouseMove)
  }

  hostDisconnected() {
    window.removeEventListener('mousemove', this._onMouseMove)
  }

  private _onMouseMove = (e: MouseEvent) => {
    this.x = e.clientX
    this.y = e.clientY
    this.host.requestUpdate()  // триггерит перерендер компонента
  }
}

// Использование в компоненте
@customElement('cursor-tracker')
class CursorTracker extends LitElement {
  private mouse = new MouseController(this)

  render() {
    return html`
      <p>Курсор: ${this.mouse.x}, ${this.mouse.y}</p>
    `
  }
}

Сборка библиотеки для публикации

// package.json
{
  "name": "@myorg/ui-components",
  "version": "1.0.0",
  "type": "module",
  "main": "./dist/index.js",
  "module": "./dist/index.js",
  "types": "./dist/index.d.ts",
  "exports": {
    ".": {
      "import": "./dist/index.js",
      "types": "./dist/index.d.ts"
    },
    "./button": {
      "import": "./dist/components/button/button.js",
      "types": "./dist/components/button/button.d.ts"
    }
  },
  "files": ["dist"],
  "customElements": "custom-elements.json"
}
// vite.config.ts для библиотеки
export default defineConfig({
  build: {
    lib: {
      entry: 'src/index.ts',
      formats: ['es'],
      fileName: 'index',
    },
    rollupOptions: {
      external: ['lit'],  // не бандлить Lit — peer dependency
    },
  },
})

Сроки

3–5 компонентов с Lit, декораторами и базовой сборкой — 1 неделя. Полная библиотека из 10–15 компонентов с контроллерами, типами, storybook-документацией, тестами (Playwright) и npm-публикацией — 3–5 недель.