Webpack Module Federation ve ReactJS kullanarak Mikro Frontend Örnek Uygulama Geliştirme

Emre Sert
KoçSistem
Published in
6 min readApr 16, 2024

Bu yazımızda bir e-ticaret uygulaması örneği ile hem Mikro Frontend’ler arası iletişimi ele alacağız hem de Mikro uygulamaları bir bütün olarak nasıl component olarak uygulamaya dahil edeceğimize değineceğiz.

Öncelikle Header-App ve List-App adında 2 ayrı mikro proje oluşturalım. Proje oluşturma aşamalarını bir önceki yazımda anlatmıştım. İsterseniz buraya tıklayarak yazımızı tekrar gözden geçirip proje oluşturma aşamalarını inceleyebilirsiniz.

Dipnot: Proje oluştururken projelerin aynı source tree içerisinde yer almasına ve Header-App için 3000, List-App için 3001 portu kullanımına dikkat edelim.

Source Tree

Projelerimizi ayrı ayrı oluşturduktan sonra ilk olarak kendimize Header-App/src/components/Header.tsx altında diğer projemizden component olarak çağırılmak üzere bir Header component’i geliştirelim.

Example Header-App Source Tree

Header.tsx :

import React, { useEffect, useState } from 'react'

const Header = ({ selectedProducts }) => {

// modal true olduğunda selectedProducts prop'u içindeki item'ler listelenir.
const [showModal, setShowModal] = useState(false)

return (
<>
<div style={{
width: "100%",
height: "80px",
display: "flex",
alignItems: "center",
justifyContent: "space-between",
backgroundColor: "#222831",
color: "red"
}}>

<div className='logo-part' style={{ marginLeft: "25px" }}>
<img src="https://www.shutterstock.com/image-vector/shopping-logo-ecommerce-logotype-shooping-260nw-1978607771.jpg" width={120} height={30} style={{ borderRadius: "5px" }} alt="" />
</div>

<div style={{ color: "white", fontWeight: "bold" }}>
Header-App MF
</div>

<div className='right-part' style={{ position: "relative", marginRight: "25px" }}>
{
selectedProducts.length > 0 && (
<div
style={{ position: "absolute", right: "-10px", top: "-10px", height: "30px", width: "30px", display: "flex", alignItems: "center", justifyContent: "center", backgroundColor: "red", color: "white", borderRadius: "100px",cursor:"pointer" }}
onClick={() => {

// butona basıldığında modal state'i güncelleme
setShowModal(!showModal)

}}
>
{selectedProducts.length}
</div>
)}
<img src="https://images.pngnice.com/download/2007/E-Commerce-PNG-Transparent.png" width={50} height={30} style={{ borderRadius: "100px" }} alt="" />
</div>

</div>

{
showModal && (
<div style={{ width: "400px", margin: "auto", backgroundColor: "gainsboro", borderRadius: "8px", border: "2px solid gray", padding: "10px", marginTop: "10px" }}>
{
// seçili ürünlerin listelenmesi
selectedProducts.map((sItem) => {
return (
<div>{sItem.name}</div>
)
})
}
</div>
)
}

</>
)
}

export default Header

Ardından geliştirdiğimiz bu Header’ı List-App gibi diğer uygulamaların erişimine açmak için bazı tanımlamaları yapmamız gerekli. Webpack konfigurasyonlarını kullanarak bu işlemi yapabiliriz. Webpack içerisinde bulunan plugin’ler sayesinde diğer projeler bu componentimizi dışarıdan exposes kısmını kullanarak import edebilir. Fakat erişim sağlayabilmek için bize birde build dosyası içerisinde yer alan remoteEntry.js dosyası lazım. Bu remoteEntry.js dosyası içerisinde dışarıya expose edilen componentlerin JavaScript komut satırlarını içerisinde barındırır. İlk olarak Webpack konfigurasyonlarımızı yapalım.

Header-App/webpack.config.js

const HtmlWebPackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const Dotenv = require('dotenv-webpack');
const deps = require("./package.json").dependencies;
module.exports = (_, argv) => ({
output: {
publicPath: "http://localhost:3000/",
},

resolve: {
extensions: [".tsx", ".ts", ".jsx", ".js", ".json"],
},

devServer: {
port: 3000,
historyApiFallback: true,
},

module: {
rules: [
{
test: /\.m?js/,
type: "javascript/auto",
resolve: {
fullySpecified: false,
},
},
{
test: /\.(css|s[ac]ss)$/i,
use: ["style-loader", "css-loader", "postcss-loader"],
},
{
test: /\.(ts|tsx|js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
],
},

plugins: [
new ModuleFederationPlugin({
name: "Header_App",
filename: "remoteEntry.js",
remotes: {

},
// Dışarıdan erişime açma konfigurasyonları
exposes: {
// Header Component'i alt satırdaki tanımlama ile beraber dışarıdan erişime açılır.
"./Header" : "./src/components/Header.tsx"
},
shared: {
...deps,
react: {
singleton: true,
requiredVersion: deps.react,
},
"react-dom": {
singleton: true,
requiredVersion: deps["react-dom"],
},
},
}),
new HtmlWebPackPlugin({
template: "./src/index.html",
}),
new Dotenv()
],
});

Daha sonra terminalimizde npm run build komutuyla build dosyamızı oluşturup içerisinde remoteEntry.js dosyamızın oluşmasını sağlayalım.

Dipnot: npm run build komutunu Webpack konfigurasyonlarını yaptıktan sonra uygulamaya dikkat edelim.

Showing remoteEntry.js after npm run build

Artık remote dosyamızda oluştuğuna göre sırada List-App içerisinde bu component’i import etme aşamasına geldik. Öncelikle farklı bir projeden erişim sağlayacağımız için componentin tanımlamalarını yine Webpack plugin yardımıyla plugin içerisindeki remotes kısmından List-App projemize dahil edelim.

List-App/webpack.config.js

const HtmlWebPackPlugin = require("html-webpack-plugin");
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
const Dotenv = require('dotenv-webpack');
const deps = require("./package.json").dependencies;
module.exports = (_, argv) => ({
output: {
publicPath: "http://localhost:3001/",
},

resolve: {
extensions: [".tsx", ".ts", ".jsx", ".js", ".json"],
},

devServer: {
port: 3001,
historyApiFallback: true,
},

module: {
rules: [
{
test: /\.m?js/,
type: "javascript/auto",
resolve: {
fullySpecified: false,
},
},
{
test: /\.(css|s[ac]ss)$/i,
use: ["style-loader", "css-loader", "postcss-loader"],
},
{
test: /\.(ts|tsx|js|jsx)$/,
exclude: /node_modules/,
use: {
loader: "babel-loader",
},
},
],
},

plugins: [
new ModuleFederationPlugin({
name: "List_App",
filename: "remoteEntry.js",
remotes: {

// Header-App uygulamasındaki Header componentimize uzaktan erişim için
// remoteEntry.js'e istek atarak componentimizi List-App uygulamnamıza import ederiz
// import Header from "headerApp/Header";
headerApp : "Header_App@http://localhost:3000/remoteEntry.js"

},
exposes: {},
shared: {
...deps,
react: {
singleton: true,
requiredVersion: deps.react,
},
"react-dom": {
singleton: true,
requiredVersion: deps["react-dom"],
},
},
}),
new HtmlWebPackPlugin({
template: "./src/index.html",
}),
new Dotenv()
],
});

Header-App projemizdeki Header component’imiz List-App projemizde kullanıma hazır. Şimdi List-App/src/App.jsx içerisindeki dosyamıza artık bu component’i import edebiliriz.

Example List-App Source Tree

App.jsx :

import React, { useState } from "react";
import ReactDOM from "react-dom";

// Header component'i headerApp source'unden import edildi
import Header from "headerApp/Header";

const App = () => {

// seçili ürünleri tutan ve prop olarak geçilecek olan state
const [selectedProducts, setSelectedProducts] = useState([])

// dummy ürün listesi
const products = [{
id: "11", name: "Product 1", image: "https://picsum.photos/300/300"
}, {
id: "22", name: "Product 2", image: "https://picsum.photos/200/301"
}, {
id: "33", name: "Product 3", image: "https://picsum.photos/200/302"
}, {
id: "44", name: "Product 4", image: "https://picsum.photos/200/303"
}, {
id: "55", name: "Product 5", image: "https://picsum.photos/200/304"
}]

return (
<div className="mt-10 text-3xl mx-auto max-w-6xl">

{ /* Header componenti kullanımı */}
<Header
selectedProducts={selectedProducts}
/>

<div className="list-wrapper" style={{ margin: "25px" }}>

{ /* ürün listeleme */}
{

products.map((pItem, index) => {
const checkData = selectedProducts.find((el) => el.id == pItem.id)
return (
<div key={index} className="item-wrapper" style={{ display: "flex", alignItems: "center", margin: "5px" }}>

<div style={{ marginRight: "25px", maxHeight: "50px" }}>
<img src={pItem.image} style={{ maxHeight: "50px", borderRadius: "5px", border: "1px solid black" }} width={50} height={50} alt="item" />
</div>

<div style={{ marginRight: "25px" }}>{pItem.name}</div>

<div>

<button style={{ backgroundColor: "dodgerblue", padding: "5px", borderRadius: "5px", minWidth: "60px", color: "white", fontSize: "13px", cursor: "pointer" }} onClick={() => {

// seç butonuna basıldığında alınacak aksiyon
if (!checkData) {
let arr = []
selectedProducts.map((item) => {
arr.push(item)
})
arr.push(pItem)
setSelectedProducts(arr)
}

}}>

{checkData ? "Seçildi" : "Seç"}

</button>

</div>
</div>
)
})
}

</div>
</div>
)
}

ReactDOM.render(<App />, document.getElementById("app"));

Son olarak npm run start komutuyla sırasıyla önce Header-App daha sonrada List-App uygulamalarmızı ayağa kaldıralım.

Sample Split Terminal

Sonuç olarak 2 ayrı source’den Header ve List uygulamalarının Webpack remotes/exposes aracılığıyla bir arada kullanmını ve ReactJS kullanarak state/prop mantığıylada 2 ayrı projedeki component’lerin iletişimini oluşturduğumuz bu Mikro Frontend projede ele aldık.

Uygulama Video :

Github :

https://github.com/emresert/Micro-FE-Example

--

--