Реализация Web Components для микрофронтендной архитектуры

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

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

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

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация Web Components для микрофронтендной архитектуры
Сложная
от 2 недель до 3 месяцев
Часто задаваемые вопросы

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

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

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

  • 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 для микрофронтендной архитектуры

Микрофронтенды решают одну конкретную проблему: как дать нескольким командам независимо деплоить части одного интерфейса, не превращая сборку в монолит. Web Components — нативный браузерный механизм, который даёт технологическую изоляцию без единого фреймворка-диктатора.

Почему Web Components, а не Module Federation

Module Federation (Webpack 5) — мощный инструмент, но он завязывает все команды на одну систему сборки. Если одна команда хочет Vite, другая — Rollup, третья вообще пишет на Svelte — начинаются компромиссы.

Web Components работают на уровне браузера. customElements.define('order-cart', OrderCartElement) — и этот компонент подключает любой хост, не зная про React, Vue или ванильный JS внутри.

Ограничения тоже реальные: Shadow DOM усложняет глобальные стили, событийная коммуникация требует дисциплины, SSR — отдельная боль (хотя Declarative Shadow DOM в Chrome 90+ частично закрывает вопрос).

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

Типовая схема: shell-приложение (хост) + N микрофронтендов, каждый публикует один или несколько кастомных элементов.

monorepo/
├── shell/                    # хост, маршрутизация, layout
├── mfe-catalog/             # продуктовый каталог
├── mfe-cart/                # корзина и чекаут
├── mfe-account/             # личный кабинет
└── shared/
    ├── design-tokens/       # CSS-переменные, общие токены
    └── events/              # типизированные события (TypeScript)

Каждый mfe-* собирается в один JS-файл и публикуется в CDN или внутренний npm. Shell подключает их через <script type="module">.

Реализация базового Web Component

// mfe-cart/src/CartWidget.ts
export class CartWidget extends HTMLElement {
  private shadow: ShadowRoot;
  private _items: CartItem[] = [];

  static get observedAttributes() {
    return ['user-id', 'currency'];
  }

  constructor() {
    super();
    this.shadow = this.attachShadow({ mode: 'open' });
  }

  connectedCallback() {
    this.render();
    this.loadItems();
    // слушаем события от других MFE
    window.addEventListener('product:added', this.handleProductAdded);
  }

  disconnectedCallback() {
    window.removeEventListener('product:added', this.handleProductAdded);
  }

  attributeChangedCallback(name: string, _old: string, next: string) {
    if (name === 'user-id' && next) {
      this.loadItems();
    }
  }

  private handleProductAdded = (e: Event) => {
    const { productId, qty } = (e as CustomEvent).detail;
    this.addToCart(productId, qty);
  };

  private async loadItems() {
    const userId = this.getAttribute('user-id');
    if (!userId) return;

    const res = await fetch(`/api/cart/${userId}`);
    this._items = await res.json();
    this.render();
  }

  private async addToCart(productId: string, qty: number) {
    await fetch('/api/cart', {
      method: 'POST',
      headers: { 'Content-Type': 'application/json' },
      body: JSON.stringify({ productId, qty }),
    });
    await this.loadItems();
    // сообщаем shell и другим MFE
    this.dispatchEvent(new CustomEvent('cart:updated', {
      detail: { count: this._items.length },
      bubbles: true,
      composed: true, // пробивает Shadow DOM
    }));
  }

  private render() {
    this.shadow.innerHTML = `
      <style>
        :host {
          display: block;
          font-family: var(--font-sans, system-ui);
        }
        .cart-count {
          background: var(--color-accent, #e53e3e);
          color: white;
          border-radius: 50%;
          padding: 2px 6px;
          font-size: 12px;
        }
      </style>
      <button part="trigger">
        Корзина
        <span class="cart-count">${this._items.length}</span>
      </button>
    `;

    this.shadow.querySelector('button')
      ?.addEventListener('click', () => this.openCart());
  }

  private openCart() {
    window.dispatchEvent(new CustomEvent('cart:open'));
  }
}

customElements.define('cart-widget', CartWidget);

Межкомпонентная коммуникация

Прямые вызовы между MFE — антипаттерн. Нужна шина событий. Самый простой вариант — window с типизацией:

// shared/events/index.ts
export type AppEvents = {
  'product:added': { productId: string; qty: number };
  'cart:updated': { count: number };
  'user:authenticated': { userId: string; token: string };
  'navigation:requested': { path: string };
};

type EventMap = {
  [K in keyof AppEvents]: CustomEvent<AppEvents[K]>;
};

declare global {
  interface WindowEventMap extends EventMap {}
}

export function emit<K extends keyof AppEvents>(
  type: K,
  detail: AppEvents[K],
  target: EventTarget = window
) {
  target.dispatchEvent(new CustomEvent(type, { detail, bubbles: true }));
}

export function on<K extends keyof AppEvents>(
  type: K,
  handler: (detail: AppEvents[K]) => void,
  target: EventTarget = window
) {
  const listener = (e: Event) => handler((e as CustomEvent<AppEvents[K]>).detail);
  target.addEventListener(type, listener);
  return () => target.removeEventListener(type, listener);
}

Shell-приложение и динамическая загрузка

Shell не знает про внутренности MFE — только про их URL и публичное API (атрибуты + события).

// shell/src/registry.ts
interface MFEManifest {
  name: string;
  url: string;
  elements: string[];
}

const manifest: MFEManifest[] = [
  {
    name: 'cart',
    url: 'https://cdn.example.com/[email protected]/index.js',
    elements: ['cart-widget', 'cart-drawer'],
  },
  {
    name: 'catalog',
    url: 'https://cdn.example.com/[email protected]/index.js',
    elements: ['product-card', 'product-list', 'product-filter'],
  },
];

export async function loadMFE(name: string): Promise<void> {
  const entry = manifest.find(m => m.name === name);
  if (!entry) throw new Error(`Unknown MFE: ${name}`);

  // проверяем, что элементы ещё не зарегистрированы
  const alreadyLoaded = entry.elements.every(
    el => customElements.get(el) !== undefined
  );
  if (alreadyLoaded) return;

  await import(/* @vite-ignore */ entry.url);
}

// shell/src/router.ts
import { loadMFE } from './registry';

const routes: Record<string, () => Promise<void>> = {
  '/catalog': () => loadMFE('catalog'),
  '/cart': () => loadMFE('cart'),
  '/account': () => loadMFE('account'),
};

export async function navigate(path: string) {
  const loader = routes[path];
  if (loader) await loader();

  document.querySelector('#app-root')!.innerHTML = getTemplate(path);
  history.pushState(null, '', path);
}

Стили: изоляция vs дизайн-система

Shadow DOM изолирует стили полностью. CSS-переменные пробиваются насквозь — это и есть механизм для дизайн-системы:

/* shell/src/global.css — токены, доступные всем MFE */
:root {
  --color-primary: #1a56db;
  --color-accent: #e3a008;
  --color-surface: #f9fafb;
  --font-sans: 'Inter', system-ui, sans-serif;
  --font-mono: 'JetBrains Mono', monospace;
  --radius-md: 8px;
  --shadow-sm: 0 1px 3px rgba(0,0,0,.1);
  --spacing-unit: 4px;
}

Для более сложной передачи стилей (например, шрифт через @font-face) — Constructable Stylesheets:

// shared/design-tokens/stylesheet.ts
const sheet = new CSSStyleSheet();
sheet.replaceSync(`
  :host { font-family: var(--font-sans, system-ui); }
  * { box-sizing: border-box; }
`);

export const baseStyles = sheet;

// В компоненте:
constructor() {
  super();
  const shadow = this.attachShadow({ mode: 'open' });
  shadow.adoptedStyleSheets = [baseStyles];
}

Сборка и версионирование

Каждый MFE собирается независимо. Пример конфига Vite:

// mfe-cart/vite.config.ts
import { defineConfig } from 'vite';

export default defineConfig({
  build: {
    lib: {
      entry: 'src/index.ts',
      formats: ['es'],
      fileName: 'index',
    },
    rollupOptions: {
      // React/Vue как внешние зависимости только если shell их предоставляет
      // Иначе бандлим внутрь — каждый MFE самодостаточен
      external: [],
    },
    target: 'es2020',
  },
});

Версионирование через семантический тег в URL CDN. При несовместимом изменении API — мажорная версия, shell переезжает на новый URL явно. Никакой автоматической подтяжки latest.

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

Unit-тесты компонентов через @web/test-runner (поддерживает реальный DOM, в отличие от jsdom):

// mfe-cart/test/cart-widget.test.ts
import { fixture, html, expect } from '@open-wc/testing';
import '../src/CartWidget';

describe('cart-widget', () => {
  it('renders empty cart count', async () => {
    const el = await fixture<HTMLElement>(
      html`<cart-widget user-id=""></cart-widget>`
    );
    const count = el.shadowRoot!.querySelector('.cart-count');
    expect(count?.textContent).to.equal('0');
  });

  it('dispatches cart:updated after add', async () => {
    const el = await fixture<HTMLElement>(
      html`<cart-widget user-id="user-123"></cart-widget>`
    );

    let eventFired = false;
    el.addEventListener('cart:updated', () => { eventFired = true; });

    window.dispatchEvent(new CustomEvent('product:added', {
      detail: { productId: 'prod-1', qty: 1 }
    }));

    await new Promise(r => setTimeout(r, 50));
    expect(eventFired).to.be.true;
  });
});

E2E через Playwright — запускаем shell локально и проверяем интеграцию всех MFE вместе.

Сроки и этапы

Проект с нуля для трёх-четырёх микрофронтендов занимает от шести до десяти недель:

Первые две недели — проектирование: определяем границы MFE, схему событий, стратегию стилей, CI/CD под независимые деплои.

Третья-четвёртая неделя — инфраструктура: shell, шина событий, дизайн-токены, сборочные конфиги, CDN-публикация.

Пятая-восьмая неделя — разработка MFE командами параллельно.

Девятая-десятая — интеграционное тестирование, нагрузочные проверки (lazy-loading не должен давать заметных задержек), production-деплой.

Зрелость подхода прямо зависит от дисциплины команды с версионированием и контрактами событий. Без формализованного shared/events с типами и чейнджлогом микрофронтенды быстро превращаются в распределённый монолит.