Micro-Frontends utilizando Single-SPA Framework

Francisco Izurieta

Tech Lead de Payments @Kushki

junio 08, 2020

La arquitectura de micro-frontends es un enfoque de diseño en el que una aplicación web se descompone en “micro partes” individuales, semi-independientes, que trabajan en conjunto. Esta arquitectura puede ser tan liberal como sea posible, donde cada aplicación puede ser implementada con diferentes frameworks. El concepto de micro-frontend está inspirado ampliamente en los microservicios.

En cuanto a los micro-frontends, es importante destacar que a pesar de que la aplicación se divide en varios proyectos, cada uno de ellos se integrará en una sola. Así, para el usuario final, todo parecería ser una única aplicación, en lugar de varias.

Aunque los micro-frontends han comenzado a recibir mucha atención últimamente, no existe una sola manera de implementarlos. De hecho, hay una variedad de enfoques en función de los objetivos y requisitos que se tenga en cada organización. Entre las implementaciones más conocidas en la actualidad tenemos:

  • Single-SPA Framework: Es un framework que permite la implementación de micro-frontends combinando otros frameworks (React, Angular, Vue, etc). Esta implementación es la que utilizamos en Kushki y la explicaremos a más detalle a continuación.
  • Iframes: Aisla las micro aplicaciones en IFrames usando las APIs de Window.postMessage para coordinar los diferentes frames. Los IFrames comparten APIs expuestas por la ventana padre.
  • Web Components: Las aplicaciones de front-end solo tratan con el enrutamiento y toman decisiones sobre el conjunto de componentes a mostrar y la orquestación de eventos entre los diferentes web components.

Monolito SPA versus Micro-Frontends

En el caso de Kushki, vamos a analizar el ejemplo de una aplicación construida con el propósito de que nuestros clientes puedan editar y ver diferentes configuraciones que mantienen con nuestro sistema de pagos, entre otras acciones. Esta aplicación en un principio fue construida como un monolito SPA hecho en Angular 5, la aplicación creció tanto que llegó a tener más de 200 mil líneas de código y alrededor de 13 componentes bastante complejos. Todo esto llevó a que la aplicación fuese cada vez más difícil de mantener y de trabajar en diferentes características al mismo tiempo.

Por lo cual, decidimos cambiarnos a micro-Frontends para continuar con el desarrollo de esta aplicación. Se analizaron varias alternativas, pero la alternativa más madura y con mejor documentación en ese momento, y en la actualidad, es el framework Single-SPA. En un principio se decidió realizar la migración de la aplicación manteniendo el framework con el cual estaba hecho el monolito SPA (Angular).

En una primera versión del nuevo micro-frontend se decidió que los componentes de la aplicación original se conviertan en uno micro-frontend cada uno. Pero, una de las dificultades que se tuvieron en esta primera versión fue la barra de navegación, que tenía que ser compartida por todas las aplicaciones. Por lo cual, se decidió crear una librería de Angular, la cual se instaló en cada micro-frontend y, de esta manera, se lanzó la primera versión de nuestra primera aplicación con micro-frontends. La cual tenía la siguiente estructura:

Liberia angular microfrontends

Con el tiempo nos dimos cuenta, que nuestra primera idea de que la barra de navegación sea una librería de Angular y tenga que instalarse en cada uno de nuestros micro-frontends, y por lo tanto actualizarse en cada aplicación cada vez que se realizaba un cambio en la navegación, no era nada práctico y nos restringía a utilizar sólo Angular para el desarrollo de nuestros micro-frontends.

El framework Single-SPA sugiere que se utilice un archivo index.html en donde se registren todos los micro-frontends que van a ser utilizados por la aplicación. El framework le llama a este archivo root config y por lo general consiste en un solo archivo HTML en donde se utilizan algunos métodos del framework para que todo funcione de manera correcta.

Entonces, para solucionar el problema de nuestra barra de navegación, en Kushki decidimos que nuestro root config del framework no sea un solo archivo HTML sino más bien un SPA hecho en Angular. A este migramos toda la lógica que teníamos en la librería y configuramos el index.html que genera Angular, para que funcione de manera correcta con las configuraciones que necesita el framework (todo esto será explicado con más detalle en un ejemplo más adelante).

Con este cambio se logró comenzar a desarrollar micro-frontends no solo en Angular sino también en React. Por lo cual la estructura original cambió un poco y quedo de la siguiente manera:

Libreria angular y micro frontends

Al dejar de lado nuestro monolito SPA y comenzar a utilizar micro-frontends en nuestra aplicación, se logró separar las diferentes funcionalidades que se tenían en un solo lugar y facilitar el ingreso de nuevas funcionalidades.

Sin embargo, esto no quiere decir que los micro-frontends son para todos los proyectos. Pues, para la gran mayoría de estos, un monolito SPA es probablemente más que suficiente. La arquitectura de micro-frontends es más adecuada para grandes aplicaciones que agrupan muchas funcionalidades. En Kushki la gran mayoría de nuestros SPAs siguen siendo monolitos debido a que no son tan grandes. Por lo cual, se debe considerar esta arquitectura solo si se cree que el proyecto va a tener problemas de escalabilidad a mediano o largo plazo.

Ejemplo de Micro-Frontends con Single-SPA Framework

Para este ejemplo, vamos a crear una aplicación root en React en donde se registrarán 2 aplicaciones creadas con Single-SPA framework, una con Angular y una con React.

Primera parte: Crear la aplicacion Root Config

Para la aplicación root solo se necesita una aplicación web creada con create-react-app. Para este ejemplo vamos utilizar npm como administrador de paquetes de javascript y Typescript, por sus múltiples beneficios y porque ahora es soportado por React.

npx create-react-app root-config-app --template typescript --use-npm

Una vez creada nuestra aplicación tenemos que agregar single-spa a nuestro proyecto para poder registrar nuestros micro-frontends y navegar entre ellos.

npm i single-spa

Lo siguiente que tenemos que hacer, es editar el archivo index.html del proyecto para agregar los scripts e imports necesarios para que nuestra aplicación funcione. Nuestro archivo deberá quedar de la siguiente manera:

<!DOCTYPE html>
<html lang="en">

  <head>
    <meta charset="utf-8" />
    <link rel="icon" href="%PUBLIC_URL%/favicon.ico" />
    <meta name="viewport" content="width=device-width, initial-scale=1" />
    <meta name="theme-color" content="#000000" />
    <meta name="description" content="Web site created using create-react-app" />
    <link rel="apple-touch-icon" href="%PUBLIC_URL%/logo192.png" />
    <!--
        manifest.json provides metadata used when your web app is installed on a
        user's mobile device or desktop. See https://developers.google.com/web/fundamentals/web-app-manifest/
      -->
    <link rel="manifest" href="%PUBLIC_URL%/manifest.json" />
    <!--
        Notice the use of %PUBLIC_URL% in the tags above.
        It will be replaced with the URL of the `public` folder during the build.
        Only files inside the `public` folder can be referenced from the HTML.

        Unlike "/favicon.ico" or "favicon.ico", "%PUBLIC_URL%/favicon.ico" will
        work correctly both with client-side routing and a non-root public URL.
        Learn how to configure a non-root public URL by running `npm run build`.
      -->
    <title>React App</title>
    <meta name="importmap-type" content="systemjs-importmap">
    <script type="systemjs-importmap">
        {
          "imports": {
            "@single-spa-test/app-angular": "http://localhost:4200/main.js",
            "@single-spa-test/app-react": "http://localhost:8080/app-react.js"
          }
        }
    </script>
    <script type="systemjs-importmap"
      src="https://storage.googleapis.com/react.microfrontends.app/importmap.json"></script>
    <script src="https://cdn.jsdelivr.net/npm/systemjs@6.2.5/dist/system.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/systemjs@6.2.5/dist/extras/amd.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/systemjs@6.2.5/dist/extras/named-exports.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/systemjs@6.2.5/dist/extras/named-register.min.js"></script>
    <script src="https://cdn.jsdelivr.net/npm/import-map-overrides/dist/import-map-overrides.js"></script>
    <script src="https://unpkg.com/zone.js"></script>
  </head>

  <body>
    <noscript>You need to enable JavaScript to run this app.</noscript>
    <div id="root"></div>
    <!--
        This HTML file is a template.
        If you open it directly in the browser, you will see an empty page.

        You can add webfonts, meta tags, or analytics to this file.
        The build step will place the bundled scripts into the <body> tag.

        To begin the development, run `npm start` or `yarn start`.
        To create a production bundle, use `npm run build` or `yarn build`.
      -->
  </body>

</html>

Para no tener que agregar CSS innecesarios a nuestro breve ejemplo vamos a agregar Material UI a nuestro proyecto y agregar un ejemplo de navegación rápida.

npm i @material-ui/core

Agregamos un nuevo componente Nav a nuestro proyecto en donde copiamos el ejemplo de navegación.

import React from 'react';
import { createStyles, Theme, makeStyles } from '@material-ui/core/styles';
import Drawer from '@material-ui/core/Drawer';
import AppBar from '@material-ui/core/AppBar';
import CssBaseline from '@material-ui/core/CssBaseline';
import Toolbar from '@material-ui/core/Toolbar';
import List from '@material-ui/core/List';
import Typography from '@material-ui/core/Typography';
import ListItem from '@material-ui/core/ListItem';
import ListItemText from '@material-ui/core/ListItemText';

const drawerWidth = 240;

const useStyles = makeStyles((theme: Theme) =>
  createStyles({
    root: {
      display: 'flex',
    },
    appBar: {
      zIndex: theme.zIndex.drawer + 1,
    },
    drawer: {
      width: drawerWidth,
      flexShrink: 0,
    },
    drawerPaper: {
      width: drawerWidth,
    },
    drawerContainer: {
      overflow: 'auto',
    },
    content: {
      flexGrow: 1,
      padding: theme.spacing(3),
    },
  }),
);

export default function ClippedDrawer() {
  const classes = useStyles();

  return (
    <div className={classes.root}>
      <CssBaseline />
      <AppBar position="fixed" className={classes.appBar}>
        <Toolbar>
          <Typography variant="h6" noWrap>
            Single-SPA Root Config
          </Typography>
        </Toolbar>
      </AppBar>
      <Drawer
        className={classes.drawer}
        variant="permanent"
        classes={{
          paper: classes.drawerPaper,
        }}
      >
        <Toolbar />
        <div className={classes.drawerContainer}>
          <List>
            <ListItem button>
              <ListItemText primary="App Angular" />
            </ListItem>
            <ListItem button>
              <ListItemText primary="App React" />
            </ListItem>
          </List>
        </div>
      </Drawer>
      <main className={classes.content}>
        <Toolbar />
        <div id="single-spa-application:app-angular"></div>
        <div id="single-spa-application:app-react"></div>
      </main>
    </div>
  );
}

Editamos el archivo App.tsx para incluir nuestro nuevo componente. El cual nos queda de la siguiente manera:

import React from 'react';
import Nav from './Nav';

function App() {
  return (
    <React.Fragment>
      <Nav />
    </React.Fragment>
  );
}

export default App;

Una vez hecho todo esto tenemos una aplicación como la siguiente:

Single SPA Root

Segunda parte: Crear micro-frontends Single-SPA

Angular

Para crear una aplicación Angular con single-spa, basta con crear un SPA con el CLI que proporciona el framework. Es importante crear la aplicación con Angular Router para que el micro-frontend funcione correctamente.

ng new app-angular

Una vez creada la aplicación tenemos que instalar el framework Single-SPA en nuestro proyecto. Para lo cual debemos seguir los pasos que nos encontramos en la documentación del framework.

ng add single-spa-angular

Lo que hace este comando es configurar automáticamente el framework en nuestra aplicación. También existe la posibilidad de instalar de forma manual (los detalles se pueden encontrar en la documentación antes mencionada).

Por último, crearemos un componente simple el cual vamos a mostrar en esta aplicación y configuraremos las rutas que va a usar este SPA.

ng g c angular

En el archivo de rutas se debe configurar una ruta principal, la cual debe coincidir con la ruta que se registre en single-spa del root config, también es importante configurar los providers del Routing Module. El archivo quedará configurado de la siguiente manera:

import { NgModule } from '@angular/core';
import { Routes, RouterModule } from '@angular/router';
import { APP_BASE_HREF } from "@angular/common";
import { AngularComponent } from './angular/angular.component';

const routes: Routes = [
  {
    path: "angular",
    component: AngularComponent,
  }
];

@NgModule({
  imports: [RouterModule.forRoot(routes)],
  exports: [RouterModule],
  providers: [{ provide: APP_BASE_HREF, useValue: "/" }]
})
export class AppRoutingModule { }

Eso es todo lo que debemos hacer al momento de crear un micro-frontend con este framework. Una vez realizado estos simples pasos, puedes editar tu aplicación Angular como desees.

React

Para crear un micro-frontend con React es necesario utilizar la herramienta con la que nos provee el framework Single-SPA (create-single-spa) en la cual deberemos contestar React cuando nos pregunte qué framework utilizar.

npx create-single-spa

Y con React es todo lo que debemos configurar.

Tercera parte: Registrar Micro-Frontends con Single-SPA

Una vez creados los micro-frontends tenemos que registrarlos en la aplicación root config que creamos en la primera parte. Para lo cual debemos editar el archivo index.tsx de la siguiente manera:

import React from 'react';
import ReactDOM from 'react-dom';
import App from './App';
import { LifeCycles, registerApplication, start } from "single-spa";

ReactDOM.render(
  <React.StrictMode>
    <App />
  </React.StrictMode>,
  document.getElementById('root')
);

registerApplication({
  name: "app-angular",
  app: (): Promise<LifeCycles> => (window as any).System.import("http://localhost:4200/main.js"),
  activeWhen: "/angular"
});

registerApplication({
  name: "app-react",
  app: (): Promise<LifeCycles> => (window as any).System.import("@single-spa-test/app-react"),
  activeWhen: "/react"
});

start();

Para poder ejecutar este ejemplo se deben correr los siguientes comandos:

npm start # root config app
npm run serve:single-spa # angular app
npm start # react app

Con todas las aplicaciones corriendo tendremos nuestro ejemplo de micro-frontends desarrollado en diferentes frameworks completamente funcional y en muy pocos pasos.

Single SPA Root config


El código completo del ejemplo explicado en este artículo lo puedes encontrar en el siguiente repositorio:

https://github.com/Kushki/micro-frontends-single-spa-example

¿Te gustaría mantenerte al tanto de nuestro contenido? Suscríbete a nuestra lista de correos.