Реалізація 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 не прив'язаний до фреймворку — один мікрофронтенд може бути на React, інший на Vue, третій на Angular.

Кожна програма експортує три функції: bootstrap, mount, unmount. Single-SPA викликає їх за графіком на основі URL.

Що входить до роботи

Налаштування root config (оркестратор), реєстрація мікрофронтенд-програм, адаптери для 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 мікрофронтенд

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 мікрофронтенд

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 — мікрофронтенд без маршрутної умови. Використовується для віджетів (кошик, повідомлення, чат):

// 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-мікрофронтенд імпорти через 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 — режим розробки

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>

Розробник відкриває каталог в браузері, натискає на панель overrides і змінює URL @company/catalog на http://localhost:3001/catalog.js. Інші мікрофронтенди продовжують працювати з 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 для мікрофронтенду

// systemjs сумісний output
module.exports = {
  output: {
    library: { type: 'system' },
    publicPath: '',
  },
  externals: ['react', 'react-dom', 'single-spa', 'react-router-dom'],
}

Ключовим є externals. Все спільні залежності не включаються в bundle, завантажуються з 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 адаптери для кожного мікрофронтенду (React, Vue або Angular), налаштовуємо import-map-overrides для розробників, організовуємо комунікацію через event bus або спільні модулі, будуємо CI/CD з версіонуванням через import map.

Терміни: 8–15 днів — базова архітектура, root-config, 2–3 мікрофронтенд-програми, dev-інструменти.