Saltar a contenido

Microfrontends en RSBuild

Una de las principales ventajas de rsbuild es que tiene soporte nativo con module federation por lo que la configuración es nativa, y se adapta bien anuestra snecesidades.

Module (remoto)

Una de las principales diferencias es la configuración base que se necesita para que un microfront pueda ser ejecutado con rsbuild.

Toma en consideración que tienes en el archivo .env declarado el puerto en el que se va a correr el microfront

.env
# ... el resto de las variables
PORT=3001
rsbuild.config.ts
import { mfeConfig } from "@architecture-it/rsbuild/mfeConfig";

import { dependencies } from "./package.json";

export default mfeConfig({
  dependencies,
  moduleFederation: {
    options: {
      name: "module_example",
      exposes: {
        "./App": "./src/App",
      },
      shared: {
        "@mui/x-data-grid": {
          requiredVersion: dependencies["@mui/x-data-grid"],
        },
      },
    },
  },
});

Modulo exportando subrutas

Si el consumo de microfrontend contiene n rutas en una subpágina (estrategia adaptada por la mayoría de nuestras aplicaciones) se debe configurar el startUrl con la ruta de la subpágina en este caso por el ejemplo se usa indicadores, y se debe configurar el basename en el BrowserRouter de la aplicación para garantizar que el comportamiento sea el esperado.

bootstrap.tsx
// ... resto del código
<BrowserRouter basename="indicadores">

</BrowserRouter>
rsbuild.config.ts
import { mfeConfig } from "@architecture-it/rsbuild/mfeConfig";

import { dependencies } from "./package.json";

export default mfeConfig({
  dependencies,
  moduleFederation: {
    options: {
      name: "module_example",
      exposes: {
        "./App": "./src/App",
      },
      shared: {
        "@mui/x-data-grid": {
          requiredVersion: dependencies["@mui/x-data-grid"],
        },
      },
    },
  },
  dev: {
    // Definir el puerto en que debe estar el microfront en .env. Ejemplo: PORT=3001
    startUrl: `http://localhost:${process.env.PORT}/indicadores`,
  },
});

Modulo funcionando standalone

Si se desea que el microfrontend funcione de manera standalone, se debe quitar en el index el condicional

index.tsx
// if (process.env.NODE_ENV === "development") {
import("./bootstrap");
// }
export default true;

Uso de Redux en Módulos

Recomendamos siempre utilizar un contexto único en los módulos debido a como funciona redux internamente (como singleton) por lo que el uso adecuado debe ser el siguiente:

store/hooks
import React from "react";
import {
  createDispatchHook,
  createSelectorHook,
  ReactReduxContextValue,
  type TypedUseSelectorHook,
} from "react-redux";

import type { RootState, AppDispatch } from "./store";

// Esta es la diferencia con el uso de redux normal, se específica el contexto
export const ModuleContext = React.createContext<ReactReduxContextValue<RootState>>(null as any);

const useDispatch = createDispatchHook(ModuleContext);
const useSelector = createSelectorHook(ModuleContext);

// y se crean dispatch y selectores tipados desde el contexto
export const useAppDispatch: () => AppDispatch = useDispatch;
export const useAppSelector: TypedUseSelectorHook<RootState> = useSelector;
src/App.tsx
import { Provider } from "react-redux";
import { ModuleContext } from "store/hooks";
import { store } from "store/store";

interface IAppProps {}

const App = ({}: IAppProps) => (
    <Provider context={ModuleContext} store={store}>
      <AppRoutes user={user} />
    </Provider>
);

export default App;

Nota: Se debe tener en cuenta que el contexto de redux para este caso tiene que estar exportado en el componente que se comparte.

Shell (host)

Para el shell, en cambio se necesita la configuración básica de rsbuild y las dependencias de module fedderation runtime @module-federation/runtime.

rsbuild.config.ts
import { defaultConfig } from "@architecture-it/rsbuild";

export default defaultConfig({});

Se debe agregar la inicializacion en el bootstrap.tsx, el nombre es el nombre único del shell, y se debe agregar la configuración de las dependencias que se van a compartir.

Si el shell usa react-router-dom se debe agregar la configuración de la librería en el initializeMicrofronts para que se pueda compartir entre los microfronts.

bootstrap.tsx
// resto de los imports
import * as ReactRouterDom from "react-router-dom";

import { dependencies } from "../package.json";

initializeMicrofronts("shell", {
  dependencies,
  shared: {
    "react-router-dom": {
      version: dependencies["react-router-dom"],
      scope: "default",
      // por default toma esta instancia de la librería
      lib: () => ReactRouterDom,
      shareConfig: {
        singleton: true,
        requiredVersion: dependencies["react-router-dom"],
      },
    },
  },
});
//... resto de la aplicación

Y para finalizar se debe cargar el microfrontend siempre envuelto por un <Suspense /> para garantizar el llamado asincronico del componente. El template base ahora lo tiene si no lo tienes puedes agregarlo. La principal diferencia es que siempre tiene como fallback el Fallback component del Stylesystem

src/routes/RouteWrapperRemote.tsx
import { Skeleton } from "@mui/material";
import { Suspense } from "react";
import { Outlet } from "react-router-dom";

const RouteWrapperRemote = () => {
  return (
    <Suspense fallback={<Skeleton />}>
      <Outlet />
    </Suspense>
  );
};

export default RouteWrapperRemote;
routes/index.tsx
import { lazy, Suspense } from "react";
import { dynamicImport } from "@architecture-it/rsbuild/dynamicImport";
import RouteWrapperRemote from "./RouteWrapperRemote";

interface AppRemoteProps {
    name: string;
}

const AppRemote = lazy(() => dynamicImport<AppRemoteProps>("remote/App"));


<Route element={<RouteWrapperRemote />} path="module/*">
    <Route element={<AppRemote name="exampleremote" />} path="*" />
</Route>

Para ver más casos de compatibilidad en un repo con cuatro alternativas, revisar el siguiente repositorio