Micro frontends em 2023, Parte II: Module Federation e NextJS

Esta é a segunda parte de uma série, leia também a parte I em: Micro frontends em 2023, parte I: Quando usar?

Opções de implementação

São muitas as implementações possíveis: pacotes npm compartilhados entre projetos; ESModules via tag script, iframe, importmaps, web components, url rewriting, NextJS Multi Zones, etc. Mas neste primeiro guia vou documentar uma delas em específico: webpack module federation.

Passo-a-passo com dois projetos NextJS

Vamos montar o seguinte exemplo, um site base principal (o hospedeiro, o host, o app shell, a container application) representada abaixo na cor laranja (app1), com um mini-app remoto (o módulo, o remote) representada em violeta (app2), embutido dentro, este módulo vem de outro site. Ambos os sites são projetos NextJS.

1. Criar os dois projetos NextJS

npx create-next-app app1  
npx create-next-app app2  

2. Instalar o plugin @module-federation/nextjs-mf e o bundler webpack 5 nos dois projetos

Existe um pacote que facilita um pouco a configuração de module federation em projetos Next, o @module-federation/nextjs-mf. Este plugin já foi proprietário e pago um dia, mas atualmente é livre e gratuito. Isto me confundiu muito pois varios resultados do Google e exemplos online ainda usam a referência dele no registry de pacotes fechados PrivJS.

cd app1  
yarn add @module-federation/nextjs-mf webpack  
cd ../app2  
yarn add @module-federation/nextjs-mf webpack  
cd ..  

3. Criar um módulo no projeto 2

cd app2  
mkdir src/components  
vim -p src/components/ModuleA.tsx src/pages/index.tsx  

app2/src/components/ModuleA.tsx

export default function ModuleA() {  
  return <h2 style={{ color: "darkviolet" }}>Módulo A from app2</h2>;
}

app2/src/pages/index.tsx

import ModuleA from '@/components/ModuleA'

export default function Home() {  
  return (
    <ModuleA/>
  )
}

Para testar suba o servidor de desenvolvimento na porta 3001:

yarn dev -p 3001  

e acesse http://localhost:3001/

4. Configurar a build do app2 para expor um remoteEntry contendo o módulo

vim next.config.js  

app2/next.config.js

const { NextFederationPlugin } = require("@module-federation/nextjs-mf");

/** @type {import('next').NextConfig} */
const nextConfig = {  
  reactStrictMode: true,
  webpack(config, options) {
    const { isServer } = options;
    config.plugins.push(
      new NextFederationPlugin({
        name: "app2",
        remotes: {},
        filename: "static/chunks/remoteEntry.js",
        exposes: {
          "./ModuleA": "./src/components/ModuleA",
        },
        shared: {},
      })
    );
    return config;
  },
};

module.exports = nextConfig;

Para testar se o arquivo remoteEntry.js está sendo exposto corretamente suba o servidor de desenvolvimento:

yarn dev -p 3001  

e acesse http://localhost:3001/_next/static/chunks/remoteEntry.js

Se aparecer um arquivo grande e enigmático, é porque deu certo, pode fazer o bundle de produção e manter este servidor no ar:

yarn build  
yarn start -p 3001  

5. Configurar a build do app1 para usar o módulo remoto do app2

Em outro terminal:

cd ../app1  
vim -p next.config.js src/pages/index.tsx  

app1/next.config.js

const { NextFederationPlugin } = require('@module-federation/nextjs-mf');

/** @type {import('next').NextConfig} */
const nextConfig = {  
  reactStrictMode: true,
  webpack(config, options) {
    const { isServer } = options;
    config.plugins.push(
      new NextFederationPlugin({
        name: 'app1',
        remotes: {
          app2: `app2@http://localhost:3001/_next/static/${isServer ? 'ssr' : 'chunks'}/remoteEntry.js`,
        },
      })
    );

    return config;
  },
}

module.exports = nextConfig  

src/pages/index.tsx

import dynamic from "next/dynamic";  
import styles from "@/styles/Home.module.css";

const ModuleA = dynamic(() => import("app2/ModuleA"), {  
  ssr: false,
});

export default function Home() {  
  return (
    <>
      <main className={styles.main}>
        <header>App1 Header</header>
        <nav>
          <h2>App1 Menu</h2>
        </nav>
        <ModuleA />
        <footer>App1 Footer</footer>
      </main>
    </>
  );
}

suba o servidor do app1 para testar se funcionou:

yarn dev -p 3002  

e acesse http://localhost:3002/

Repositório

O código desta demo se encontra em https://github.com/fczuardi/host-remote-nextjs-mf

Para correções dúvidas e discussões, tanto do código de exemplo quanto desta série de blog posts, use a página de issues lá.

Próximos passos

Este passo a passo foi só uma tentativa de demonstrar um conceito utilizando código, este exemplo foi um bem básico onde a Application Shell consome módulos de outro servidor, mas extender ele para que haja um app3, ou para que o app1 exponha alguns componentes para a app2 são outras possibilidades também. A app principal poderia por exemplo ser a responsável pelos widgets do Design System, ou um MFE só para isto poderia ser outra opção, um módulo para as strings de localização pode dar a chance de updates simples de texto não requererem uma nova build do site todo, agrupar os hooks de consumo de serviço em outro módulo, etc... As fronteiras e os contratos nos pontos de interface entre os módulos vão variar de caso a caso.

Ter uma separação de responsabilidades com espaço para donas diferentes cuidarem de diferentes produtos no mesmo site é um caminho arquitetural que eu sinto que alguns projetos maiores já estão trilhando, então é bom conhecer o conceito e ter no repertório :)

PS: Mas e os BFFs? Bom, este é um outro assunto para um outro momento, eu acho que na mesma filosofia de dar autonomia para times menores que queiram experimentar com diferentes stacks, trabalhar com unidades separadas pode ser um caminho também, algo como a figura abaixo.

Referências

Alguns outros textos / abas abertas sobre o tema para quem quiser ler mais:


Foto do cabeçalho Steve Harvey on Unsplash


se você gostou deste post e quiser receber novas atualizações deste blog por email, clique aqui

Fabricio Campos Zuardi

Read more posts by this author.