Tabela de Conteúdos
- Introdução
- Termos utilizados
- Criar o monorepo do projeto
- Criar o host com o Next.js
- Configurar Typescript no Next.js
- Criar projeto remote com React, Typescript e Webpack 5
- Configurar o Module Federation
- Conclusão
- Repositório no Github
- Referências
Introdução
Certamente você, se é da área tech, já ouviu falar sobre Micro Frontends, e provavelmente também ouviu a respeito do Module Federation.
Um Micro Frontend é basicamente a extensão do conceito de micro serviços para o Frontend. Já o Module Federation é um recurso do Webpack 5 que eleva a construção de Micro Frontends para um novo patamar. Pretendo abordar mais conceitualmente sobre estes temas em outro post.
Esse tem como objetivo partir para a prática e mostrar como criar do zero a estrutura simples para um projeto de Micro Frontend utilizando React, Next.js, Typescript e Module Federation.
Vamos lá!
Termos utilizados
Primeiro, vamos explicar alguns termos que utilizaremos ao longo do post:
HOST: Trata-se da aplicação central (shell) que será responsável por carregar os componentes remotos federados. Vamos utilizar o Next.js aqui.
REMOTE: É a aplicação que vai compartilhar componentes com o HOST
. Será construída com o React, sem utilizar o CRA.
Vamos para nosso passo a passo:
Criar o monorepo do projeto
Agora é a hora de abrir o terminal e vamos codar
!
Começamos criando a pasta do projeto:
mkdir next-react-typescript-mfe
cd next-react-typescript-mfe
Vamos iniciar nosso projeto:
yarn init -y -p
git init # opcional caso queira realizar o controle de versão com o Git
Por enquanto as únicas dependências que vamos instalar é o Typescript, o Concurrently e algumas tipagens:
yarn add -D typescript @types/react @types/react-dom \
@types/node concurrently
Estas dependências vão ficar compartilhadas com os projetos que teremos dentro do nosso monorepo. Para gerenciar o monorepo vamos usar o Yarn Workspaces.
Podemos também adicionar um arquivo .gitignore
com o seguinte conteúdo (opcional):
.gitignore
node_modules
Criar o host com o Next.js
Para criar nosso projeto HOST, vamos digitar o seguinte comando:
npx create-next-app host
Ao final do processo teremos nossa pasta host
com a instalação do Next.js prontinha.
Finalizado o processo anterior, podemos adicionar o projeto host
nas configurações do workspace, dentro do package.json
na raiz do projeto:
package.json:
{
// ...
"workspaces": ["host"], // Adicionar aqui
// ...
}
Configurar Typescript no Next.js
Para configurar o Typescript é bem simples, basta criar o arquivo tsconfig.json dentro da pasta host e realizar alguns pequenos ajustes.
touch host/tsconfig.json
Na próxima vez que iniciarmos o projeto será criado o next-env.d.ts
e serão populadas configurações no tsconfig.json
.
Para iniciar podemos rodar o comando:
yarn workspace host dev
O projeto host está configurado, é hora de renomear nossos arquivos para que fiquem com a extensão ts
ou tsx
. Para isso você pode usar sua IDE (VS Code por exemplo), gerenciador de arquivos ou via linha de comando:
mv host/pages/_app.js host/pages/_app.tsx
mv host/pages/index.js host/pages/index.tsx
Criar projeto remote com React, Typescript e Webpack 5
Momento de criarmos nossa aplicação remota. Vamos ter um pouco mais de trabalho aqui, pois não vamos usar o create-react-app
para que tenhamos maior controle das configurações.
Começamos criando a pasta do projeto e iniciando o projeto:
mkdir remote
cd remote
yarn init -y -p
Podemos voltar para a raiz do projeto:
cd ..
Precisamos adicionar o projeto ao workspace, da mesma forma que fizemos com o host:
package.json:
{
// ...
"workspaces": [
"host",
"remote" // Adicionar aqui
],
// ...
}
Vamos adicionar o react
e o react-dom
ao projeto:
yarn workspace remote add react react-dom
E mais a algumas dependências de desenvolvimento:
yarn workspace remote add -D webpack webpack-cli \
webpack-dev-server html-webpack-plugin css-loader \
source-map-loader style-loader ts-loader
Agora necessitamos criar as pastas dentro do projeto:
cd remote
mkdir src
mkdir public
cd ..
E também os arquivos App.tsx
, index.tsx
e index.html
:
touch remote/src/App.tsx
remote/src/App.tsx:
import React from "react";
const App = (): JSX.Element => {
return (
<>
<div>React Remote</div>
</>
);
};
export default App;
touch remote/src/index.tsx
remote/src/index.tsx:
import React from "react";
import { createRoot } from "react-dom/client";
import App from "./App";
const container = document.getElementById("root");
const root = createRoot(container!);
root.render(<App />);
touch remote/public/index.html
remote/public/index.html:
<!DOCTYPE html>
<html lang="en">
<head> </head>
<body>
<div id="root"></div>
</body>
</html>
Neste momento precisamos adicionar os arquivos de configuração do webpack
e do typescript
:
touch remote/tsconfig.json
remote/tsconfig.json:
{
"compilerOptions": {
"outDir": "./dist/",
"noImplicitAny": true,
"module": "commonjs",
"target": "es5",
"jsx": "react",
"allowJs": true,
"moduleResolution": "node",
"allowSyntheticDefaultImports": true,
"esModuleInterop": true
}
}
touch remote/webpack.config.js
remote/webpack.config.js:
const HtmlWebpackPlugin = require("html-webpack-plugin");
module.exports = {
entry: "./src/index",
target: "web",
mode: "development",
devtool: "source-map",
resolve: {
extensions: [".jsx", ".js", ".tsx", ".ts", ".json"],
},
module: {
rules: [
{
enforce: "pre",
test: /\.js$/,
loader: "source-map-loader",
},
{
test: /\.(ts|tsx)$/,
use: "ts-loader",
exclude: /node_modules/,
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
],
};
Precisamos também adicionar scripts nos arquivos package.json
da raiz e do remote:
remote/package.json:
{
// ...
"scripts": {
"start": "webpack-dev-server --port 3001"
},
// ...
}
package.json:
{
// ...
"scripts": {
"start": "concurrently \"yarn workspace host dev\" \"yarn workspace remote start\""
},
// ...
}
E para finalizar, rodamos o install para atualizar as dependências:
yarn
Neste momento sua IDE (no caso do print, o VS Code) pode estar acusando o seguinte erro no arquivo host/tsconfig.json
:
Para resolver, basta adicionar o item moduleResolution
:
host/tsconfig.json:
{
"compilerOptions": {
// ...
"moduleResolution": "node",
"resolveJsonModule": true,
// ...
},
// ...
}
Configurar o Module Federation
Nesta etapa é que a mágica vai acontecer!
Vamos começar pelo REMOTE
, nosso projeto React, criando um simples componente para ser federado e consumido pelo host:
remote/src/components/Button.tsx:
import React from "react";
const Button = (): JSX.Element => {
return (
<>
<button>Remote Button</button>
</>
);
};
export default Button;
Também precisamos adicionar algumas configurações do Webpack:
remote/webpack.config.js:
const HtmlWebpackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin =
require("webpack").container.ModuleFederationPlugin;
module.exports = {
entry: "./src/index",
target: "web",
mode: "development",
devtool: "source-map",
resolve: {
extensions: [".jsx", ".js", ".tsx", ".ts", ".json"],
},
module: {
rules: [
{
enforce: "pre",
test: /\.js$/,
loader: "source-map-loader",
},
{
test: /\.(ts|tsx)$/,
use: "ts-loader",
exclude: /node_modules/,
},
{
test: /\.css$/i,
use: ["style-loader", "css-loader"],
},
],
},
plugins: [
new HtmlWebpackPlugin({
template: "./public/index.html",
}),
new ModuleFederationPlugin({
name: "remote",
filename: "remoteEntry.js",
exposes: {
"./Button": "./src/components/Button",
},
shared: {
react: {
requiredVersion: false,
singleton: true,
},
},
}),
],
};
Depois vamos configurar nosso projeto HOST
com Next.js. Para ele vamos precisar instalar um plugin:
yarn workspace host add @module-federation/nextjs-mf@2.3.1
Também temos algumas alterações no next.config.js
:
host/next.config.js:
/** @type {import('next').NextConfig} */
const nextConfig = {
webpack5: true,
reactStrictMode: true,
webpack(config, options) {
const { webpack, isServer } = options;
config.experiments = { topLevelAwait: true };
config.module.rules.push({
test: /_app.js/,
loader: "@module-federation/nextjs-mf/lib/federation-loader.js",
});
config.plugins.push(
new webpack.container.ModuleFederationPlugin({
remotes: {
remote: "remote@http://localhost:3001/remoteEntry.js",
},
shared: {
react: {
singleton: true,
eager: true,
requiredVersion: false,
},
},
})
);
return config;
},
}
module.exports = nextConfig
E finalmente vamos importar o Button
exposto pelo REMOTE
em nosso index:
host/pages/index.tsx:
import Head from 'next/head'
import Image from 'next/image'
import styles from '../styles/Home.module.css'
import dynamic from 'next/dynamic'; // new
// new
const RemoteButton = dynamic(() => import('remote/Button'), {
ssr: false,
});
export default function Home() {
return (
<div className={styles.container}>
<Head>
<title>Create Next App</title>
<meta name="description" content="Generated by create next app" />
<link rel="icon" href="/favicon.ico" />
</Head>
{/** new */}
<RemoteButton />
<main className={styles.main}>
<h1 className={styles.title}>
Welcome to <a href="https://nextjs.org">Next.js!</a>
</h1>
<p className={styles.description}>
Get started by editing{' '}
<code className={styles.code}>pages/index.js</code>
</p>
<div className={styles.grid}>
<a href="https://nextjs.org/docs" className={styles.card}>
<h2>Documentation →</h2>
<p>Find in-depth information about Next.js features and API.</p>
</a>
<a href="https://nextjs.org/learn" className={styles.card}>
<h2>Learn →</h2>
<p>Learn about Next.js in an interactive course with quizzes!</p>
</a>
<a
href="https://github.com/vercel/next.js/tree/canary/examples"
className={styles.card}
>
<h2>Examples →</h2>
<p>Discover and deploy boilerplate example Next.js projects.</p>
</a>
<a
href="https://vercel.com/new?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
className={styles.card}
>
<h2>Deploy →</h2>
<p>
Instantly deploy your Next.js site to a public URL with Vercel.
</p>
</a>
</div>
</main>
<footer className={styles.footer}>
<a
href="https://vercel.com?utm_source=create-next-app&utm_medium=default-template&utm_campaign=create-next-app"
target="_blank"
rel="noopener noreferrer"
>
Powered by{' '}
<span className={styles.logo}>
<Image src="/vercel.svg" alt="Vercel Logo" width={72} height={16} />
</span>
</a>
</footer>
</div>
)
}
Neste momento a análise estática do typescript estará alertando pelo tipo desconhecido do Button. Para solucionar, basta criarmos o type definition:
host/remote.d.ts:
/// <reference types="react" />
declare module "remote/Button" {
const Button: React.ComponentType;
export default Button;
}
Tudo pronto! É so rodar o projeto...
yarn start
... e acessar o HOST
pelo endereço http://localhost:3000
e veremos o botão do REMOTE
sendo exibido.
Conclusão
Pronto! Se deu tudo certo na execução dos passos acima, você tem seu projeto de Micro Frontend utilizando Module Federation rodando em sua máquina. Legal, né? E também é mais simples do que parece, não é verdade?
Se gostou do post, se ele foi útil para você, deixe sua reação ao post e também aproveite para seguir meu perfil aqui no dev.to. Em breve farei novas postagens sobre o assunto.
Repositório no Github
https://github.com/patrickcoutinho/next-react-typescript-mfe
Top comments (4)
Faz sentido usar module federation pra servir 2 apps distintos? Com logo diferente, cores diferentes, mas que por ventura possam compartilhar certas features?
Olá, Lucas. Faz sentido sim, seus componentes vão herdar essas características, como cores, do host, principalmente se você estiver utilizando algum tipo de tema.
Caso as características estejam hard coded no componente, aí basta você abstrair elas para o tema, ou passar por props, caso do logotipo por exemplo.
Show, nesse caso eu poderia usar variáveis de ambiente pra determinar qual tema aplicar, ou existe alguma forma mais interessante pra fazer essa diferenciação?
Vou usar como exemplo o tailwind css:
Digamos que vc tem a seguinte estrutura:
No teu Components você criaria uma nomenclatura para cores, por exemplo primary, secondary, tertiary... no
tailwind.config.js
vc teria a configuração dessas cores para desenvolvimento.Teus App1 e App2 teriam seus
tailwind.config.js
cada um com suas respectivas cores. Então quando vc utilizasse o componente federalizado ele assumiria as cores do App host.No
tailwind.config.js
você pode usar css variables, então na prática essas cores poderiam vir até de um banco de dados e serem gerenciadas por um CMS.