Реализация Single-SPA для микрофронтендов

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

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

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

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

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Реализация Single-SPA для микрофронтендов
Сложная
~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

Реализация Single-SPA для микрофронтендов

Single-SPA — оркестратор микрофронтендов. Управляет жизненным циклом приложений: когда монтировать, когда размонтировать, как переключаться между ними без перезагрузки страницы. В отличие от Module Federation, Single-SPA фреймворко-нейтрален — один microfrontend может быть на React, другой на Vue, третий на Angular.

Каждое приложение экспортирует три функции: bootstrap, mount, unmount. Single-SPA вызывает их по расписанию на основе URL.

Что входит в работу

Настройка root config (оркестратор), регистрация microfrontend-приложений, адаптеры для React/Vue/Angular, parcel-компоненты (не привязаны к роутам), import map для управления версиями, обмен данными, CI/CD.

Архитектура

root-config             → single-spa оркестратор, import map
  ├── @company/navbar   → навигация (parcel, всегда активна)
  ├── @company/catalog  → /products/* (React)
  ├── @company/checkout → /checkout/* (React)
  ├── @company/account  → /account/* (Vue)
  └── @company/legacy   → /legacy/* (Angular, старый код)

Установка root-config

npx create-single-spa --moduleType root-config
# или вручную
npm install single-spa

root-config — регистрация приложений

// src/index.ts (root-config)
import { registerApplication, start } from 'single-spa'

registerApplication({
  name: '@company/navbar',
  app: () => System.import('@company/navbar'),
  activeWhen: () => true, // всегда активна
  customProps: {
    domElement: document.getElementById('navbar-container'),
  },
})

registerApplication({
  name: '@company/catalog',
  app: () => System.import('@company/catalog'),
  activeWhen: (location) => location.pathname.startsWith('/products'),
})

registerApplication({
  name: '@company/checkout',
  app: () => System.import('@company/checkout'),
  activeWhen: (location) => location.pathname.startsWith('/checkout'),
})

registerApplication({
  name: '@company/account',
  app: () => System.import('@company/account'),
  activeWhen: ['/account'],
})

start({
  urlRerouteOnly: true, // не вызывать перемонтирование при hash-change
})

index.html с import map

<!DOCTYPE html>
<html>
<head>
  <meta charset="UTF-8" />
  <meta name="importmap-type" content="systemjs-importmap" />
  <script type="systemjs-importmap">
    {
      "imports": {
        "single-spa": "https://cdn.jsdelivr.net/npm/single-spa@6/lib/es2015/esm/single-spa.min.js",
        "react": "https://cdn.jsdelivr.net/npm/react@18/umd/react.production.min.js",
        "react-dom": "https://cdn.jsdelivr.net/npm/react-dom@18/umd/react-dom.production.min.js",
        "@company/navbar": "https://cdn.example.com/navbar/latest/company-navbar.js",
        "@company/catalog": "https://cdn.example.com/catalog/latest/company-catalog.js",
        "@company/checkout": "https://cdn.example.com/checkout/latest/company-checkout.js",
        "@company/account": "https://cdn.example.com/account/latest/company-account.js"
      }
    }
  </script>
  <script src="https://cdn.jsdelivr.net/npm/systemjs@6/dist/extras/amd.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/systemjs@6/dist/system.min.js"></script>
</head>
<body>
  <div id="navbar-container"></div>
  <div id="single-spa-application:@company/catalog"></div>
  <div id="single-spa-application:@company/checkout"></div>
  <div id="single-spa-application:@company/account"></div>
  <script src="./src/index.js"></script>
</body>
</html>

React microfrontend

npx create-single-spa --moduleType app-parcel --framework react
// apps/catalog/src/index.tsx
import React from 'react'
import { createRoot, Root } from 'react-dom/client'
import singleSpaReact from 'single-spa-react'
import App from './App'

const lifecycles = singleSpaReact({
  React,
  ReactDOM: { createRoot: (el: Element) => createRoot(el) } as unknown,
  rootComponent: App,
  errorBoundary(err, info, props) {
    return <div>Catalog app crashed: {err.message}</div>
  },
})

export const { bootstrap, mount, unmount } = lifecycles
// apps/catalog/src/App.tsx
import React from 'react'
import { BrowserRouter, Routes, Route } from 'react-router-dom'

// Single-SPA передаёт customProps — используем для получения domElement, eventBus и т.д.
interface CatalogProps {
  eventBus?: EventBus
  basePath?: string
}

export default function App({ eventBus, basePath = '/products' }: CatalogProps) {
  return (
    <BrowserRouter basename={basePath}>
      <Routes>
        <Route path="/" element={<ProductListPage />} />
        <Route path="/:id" element={<ProductDetailPage />} />
        <Route path="/category/:slug" element={<CategoryPage />} />
      </Routes>
    </BrowserRouter>
  )
}

Vue microfrontend

npx create-single-spa --moduleType app-parcel --framework vue
npm install single-spa-vue
// apps/account/src/main.ts
import { createApp, App as VueApp, h } from 'vue'
import singleSpaVue from 'single-spa-vue'
import App from './App.vue'
import router from './router'

let app: VueApp | null = null

const vueLifecycles = singleSpaVue({
  createApp,
  appOptions: {
    render() {
      return h(App, {
        // single-spa props
        ...this.$props,
      })
    },
  },
  handleInstance(appInstance) {
    appInstance.use(router)
    app = appInstance
  },
})

export const bootstrap = vueLifecycles.bootstrap
export const mount = vueLifecycles.mount
export const unmount = vueLifecycles.unmount

Parcels — компоненты без привязки к роутам

Parcel — microfrontend без роут-условия. Используется для виджетов (корзина, уведомления, чат):

// apps/catalog/src/components/MiniCart.tsx
import { mountRootParcel } from 'single-spa'

function ProductPage() {
  const parcelRef = useRef<HTMLDivElement>(null)
  const parcelRef2 = useRef<ParcelObject | null>(null)

  useEffect(() => {
    if (!parcelRef.current) return

    const parcel = mountRootParcel(
      () => System.import('@company/cart'),
      {
        domElement: parcelRef.current,
        singleSpa: window.singleSpa,
      }
    )

    parcelRef2.current = parcel
    return () => parcel.unmount()
  }, [])

  return <div ref={parcelRef} />
}

Или через React-компонент:

import Parcel from 'single-spa-react/parcel'

function Layout() {
  return (
    <div>
      <Parcel
        config={() => System.import('@company/notifications')}
        mountParcel={mountRootParcel}
        wrapWith="div"
        wrapClassName="notifications-wrapper"
      />
    </div>
  )
}

Коммуникация между приложениями

Single-SPA рекомендует cross-microfrontend imports через import map:

// packages/shared-auth — отдельный npm-пакет в import map
// "@company/auth": "https://cdn.example.com/auth/auth.js"

// в catalog:
import { getUser, eventBus } from '@company/auth'

const user = getUser()
eventBus.on('auth:logout', () => clearLocalCart())

Или через CustomEvent на window — без прямой зависимости:

// catalog публикует
window.dispatchEvent(new CustomEvent('@company/cart:item-added', {
  detail: { productId: '123', quantity: 1 }
}))

// checkout слушает
window.addEventListener('@company/cart:item-added', (e: CustomEvent) => {
  checkoutStore.syncCartItem(e.detail)
})

Import map overrides — dev-режим

npm install import-map-overrides
<!-- в index.html -->
<script src="https://cdn.jsdelivr.net/npm/import-map-overrides@2/dist/import-map-overrides.js"></script>
<import-map-overrides-full show-when-local-storage="devtools"></import-map-overrides-full>

Разработчик catalog открывает браузер, нажимает на панель overrides и меняет URL @company/catalog на http://localhost:3001/catalog.js. Остальные microfrontends продолжают работать с CDN.

Обработка ошибок жизненного цикла

import { addErrorHandler, getAppStatus, SKIP_BECAUSE_BROKEN } from 'single-spa'

addErrorHandler((error) => {
  console.error('Single-SPA error:', error)
  monitoring.captureException(error, {
    tags: { appName: error.appOrParcelName },
  })

  // если приложение сломалось — не блокируем всё
  if (getAppStatus(error.appOrParcelName) === SKIP_BECAUSE_BROKEN) {
    return
  }
})

webpack.config.js для microfrontend

// systemjs совместимый output
module.exports = {
  output: {
    library: { type: 'system' },
    publicPath: '',
  },
  externals: ['react', 'react-dom', 'single-spa', 'react-router-dom'],
}

Ключевое — externals. Все shared-зависимости не включаются в бандл, загружаются из import map.

Мониторинг

import { addErrorHandler, getAppNames } from 'single-spa'

// время монтирования каждого приложения
const mountTimes: Record<string, number> = {}

window.addEventListener('single-spa:before-app-change', (e: CustomEvent) => {
  const { newAppStatuses } = e.detail
  Object.keys(newAppStatuses).forEach((name) => {
    if (newAppStatuses[name] === 'MOUNTED') {
      mountTimes[name] = performance.now()
    }
  })
})

window.addEventListener('single-spa:app-change', (e: CustomEvent) => {
  const { newAppStatuses } = e.detail
  Object.keys(newAppStatuses).forEach((name) => {
    if (newAppStatuses[name] === 'MOUNTED' && mountTimes[name]) {
      const duration = performance.now() - mountTimes[name]
      analytics.track('mf_mount_time', { app: name, duration })
    }
  })
})

Что делаем

Настраиваем root-config с SystemJS и import maps, создаём single-spa адаптеры для каждого microfrontend (React, Vue или Angular), настраиваем import-map-overrides для разработчиков, организуем коммуникацию через event bus или shared модули, выстраиваем CI/CD с версионированием через import map.

Срок: 8–15 дней — базовая архитектура, root-config, 2–3 microfrontend-приложения, dev-инструменты.