# Panduan Lengkap Membangun Micro Frontend dengan ...

Created on: May 8, 2025

Answered using O3 by Chat01

Question

Panduan Lengkap Membangun Micro Frontend dengan Module Federation: Next.js vs Nuxt.js

Berikut adalah panduan langkah demi langkah untuk membuat aplikasi micro frontend yang membandingkan performa antara Next.js dan Nuxt.js menggunakan Module Federation, dengan Tailwind CSS untuk styling.

Langkah 1: Persiapan Proyek

bash
# Buat direktori utama mkdir micro-frontend-comparison cd micro-frontend-comparison # Buat subdirektori untuk setiap aplikasi dan aset mkdir host-shell remote-nex remote-nux shared mkdir -p shared/assets/logos

Langkah 2: Menyiapkan Host Shell (React + TypeScript)

bash
# Masuk ke direktori host shell cd host-shell # Inisialisasi proyek React dengan TypeScript npx create-react-app . --template typescript # Install dependensi webpack dan modul federasi npm install webpack webpack-cli webpack-dev-server html-webpack-plugin @pmmmwh/react-refresh-webpack-plugin webpack-merge -D # Install babel dependencies npm install @babel/core @babel/preset-react @babel/preset-typescript babel-loader -D # Install UI dan utility libraries npm install tailwindcss postcss autoprefixer -D npm install framer-motion react-router-dom nats.ws mitt

Langkah 2.1: Konfigurasi Tailwind CSS

bash
# Inisialisasi Tailwind CSS npx tailwindcss init -p

Edit file tailwind.config.js:
host-shell/tailwind.config.js

js
/** @type {import('tailwindcss').Config} */ module.exports = { content: ["./src/**/*.{js,jsx,ts,tsx}"], theme: { extend: { colors: { primary: "#002B46", }, }, }, plugins: [], };

Pastikan file postcss.config.js berisi:

js
module.exports = { plugins: { tailwindcss: {}, autoprefixer: {}, }, };

Langkah 2.2: Modifikasi CSS Base

Edit file host-shell/src/index.css:

css
@tailwind utilities; body { margin: 0; font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; -webkit-font-smoothing: antialiased; -moz-osx-font-smoothing: grayscale; }

Langkah 2.3: Konfigurasi Webpack untuk Module Federation

Buat file webpack.config.js di root direktori host-shell:
host-shell/webpack.config.js

js
const HtmlWebpackPlugin = require("html-webpack-plugin"); const { ModuleFederationPlugin } = require("webpack").container; const path = require("path"); const deps = require("./package.json").dependencies; module.exports = { entry: "./src/index", mode: "development", devServer: { static: { directory: path.join(__dirname, "dist"), }, port: 3000, historyApiFallback: true, }, output: { publicPath: "auto", }, resolve: { extensions: [".ts", ".tsx", ".js", ".jsx"], }, module: { rules: [ { test: /\.tsx?$/, loader: "babel-loader", exclude: /node_modules/, options: { presets: ["@babel/preset-react", "@babel/preset-typescript"], }, }, { test: /\.css$/i, include: path.resolve(__dirname, "src"), use: ["style-loader", "css-loader", "postcss-loader"], }, { test: /\.(png|svg|jpg|jpeg|gif)$/i, type: "asset/resource", }, ], }, plugins: [ new ModuleFederationPlugin({ name: "host", remotes: { remoteNex: "remoteNex@http://localhost:3001/remoteEntry.js", remoteNux: "remoteNux@http://localhost:3002/remoteEntry.js", }, shared: { ...deps, react: { singleton: true, requiredVersion: deps.react, }, "react-dom": { singleton: true, requiredVersion: deps["react-dom"], }, }, }), new HtmlWebpackPlugin({ template: "./public/index.html", }), ], };

Langkah 2.4: Buat Event Bus

Buat direktori utils:

bash
mkdir -p src/utils

Buat file src/utils/eventBus.ts:

typescript
// host-shell/src/utils/eventBus.ts import mitt from 'mitt'; import * as nats from 'nats.ws'; type Events = { 'component:loaded': { name: string, timestamp: number }; }; // Create mitt instance as fallback const emitter = mitt<Events>(); // NATS connection let nc: nats.NatsConnection | null = null; // Connect to NATS async function connectToNats() { try { nc = await nats.connect({ servers: 'ws://localhost:8080' }); console.log('Connected to NATS'); // Subscribe to all events const sub = nc.subscribe('events.>'); (async () => { for await (const msg of sub) { const subject = msg.subject.replace('events.', ''); const data = JSON.parse(new TextDecoder().decode(msg.data)); emitter.emit(subject as keyof Events, data); } })(); return true; } catch (err) { console.error('Failed to connect to NATS:', err); return false; } } // Publish event async function publish<K extends keyof Events>(event: K, data: Events[K]) { // Try NATS first if (nc) { try { const encodedData = new TextEncoder().encode(JSON.stringify(data)); await nc.publish(`events.${event}`, encodedData); return; } catch (err) { console.warn('NATS publish failed, falling back to mitt', err); } } // Fallback to mitt emitter.emit(event, data); } // Subscribe to event function subscribe<K extends keyof Events>(event: K, callback: (data: Events[K]) => void) { emitter.on(event, callback as any); return () => emitter.off(event, callback as any); } // Initialize connectToNats().catch(console.error); export const eventBus = { publish, subscribe, };

Langkah 2.5: Buat Utility untuk Performance Metrics

Buat file src/utils/performanceMetrics.ts:

typescript
// src/utils/performanceMetrics.ts type FrameworkType = 'next' | 'nuxt'; interface PerformanceMetric { fcp: number; // First Contentful Paint lcp: number; // Largest Contentful Paint fid: number; // First Input Delay cls: number; // Cumulative Layout Shift loadTime: number; // Component Load Time } interface PerformanceData { next?: PerformanceMetric; nuxt?: PerformanceMetric; } // Extend PerformanceEntry types for Web Vitals interface PerformanceEntryWithFID extends PerformanceEntry { processingStart: number; } interface LayoutShiftEntry extends PerformanceEntry { hadRecentInput: boolean; value: number; } // Save metrics to localStorage export const saveMetrics = (framework: FrameworkType, metrics: PerformanceMetric) => { try { const existingData = localStorage.getItem('performanceMetrics'); const data: PerformanceData = existingData ? JSON.parse(existingData) : {}; data[framework] = metrics; localStorage.setItem('performanceMetrics', JSON.stringify(data)); return true; } catch (err) { console.error('Failed to save metrics:', err); return false; } }; // Get metrics from localStorage export const getMetrics = (): PerformanceData => { try { const data = localStorage.getItem('performanceMetrics'); return data ? JSON.parse(data) : {}; } catch (err) { console.error('Failed to get metrics:', err); return {}; } }; // Collect metrics for a framework export const collectMetrics = async (framework: FrameworkType): Promise<PerformanceMetric> => { return new Promise((resolve) => { // Use Performance Observer to collect metrics const metrics: Partial<PerformanceMetric> = { loadTime: performance.now(), }; // Use PerformanceObserver to collect web vitals const fcpObserver = new PerformanceObserver((entryList) => { const entries = entryList.getEntries(); if (entries.length > 0) { metrics.fcp = entries[0].startTime; fcpObserver.disconnect(); } }); const lcpObserver = new PerformanceObserver((entryList) => { const entries = entryList.getEntries(); if (entries.length > 0) { metrics.lcp = entries[entries.length - 1].startTime; lcpObserver.disconnect(); } }); const fidObserver = new PerformanceObserver((entryList) => { const entries = entryList.getEntries(); if (entries.length > 0) { // Use type assertion to access FID-specific properties const fidEntry = entries[0] as PerformanceEntryWithFID; metrics.fid = fidEntry.processingStart - fidEntry.startTime; fidObserver.disconnect(); } }); const clsObserver = new PerformanceObserver((entryList) => { let clsValue = 0; const entries = entryList.getEntries(); entries.forEach((entry) => { // Use type assertion to access Layout Shift specific properties const layoutShiftEntry = entry as LayoutShiftEntry; if (!layoutShiftEntry.hadRecentInput) { clsValue += layoutShiftEntry.value; } }); metrics.cls = clsValue; }); // Start observing fcpObserver.observe({ type: 'paint', buffered: true }); lcpObserver.observe({ type: 'largest-contentful-paint', buffered: true }); fidObserver.observe({ type: 'first-input', buffered: true }); clsObserver.observe({ type: 'layout-shift', buffered: true }); // Resolve after 5 seconds to ensure we've collected most metrics setTimeout(() => { clsObserver.disconnect(); resolve({ fcp: metrics.fcp || 0, lcp: metrics.lcp || 0, fid: metrics.fid || 0, cls: metrics.cls || 0, loadTime: metrics.loadTime || 0, }); }, 5000); }); };

Langkah 2.6: Buat Vue Component Wrapper

Buat file src/utils/vueComponentWrapper.tsx:

tsx
// host-shell/src/utils/vueComponentWrapper.tsx import React, { useRef } from 'react'; // A simple wrapper that doesn't actually use Vue directly export function wrapVueComponent(VueComponent: any) { return function VueWrapper(props: any) { const containerRef = useRef<HTMLDivElement>(null); // In a real implementation, this would use Vue to mount the component // But for now, we'll just render a placeholder return ( <div ref={containerRef} className="vue-component-container" style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', padding: '20px', background: '#002B46', color: 'white', height: '100vh' }} > <div> <h2 style={{ fontSize: '24px', marginBottom: '10px' }}>Vue Component Placeholder</h2> <p>This is a placeholder for a Vue component that would be mounted here.</p> <pre style={{ marginTop: '20px', padding: '10px', background: 'rgba(255,255,255,0.1)' }}> {JSON.stringify(props, null, 2)} </pre> </div> </div> ); }; }

Langkah 2.7: Buat Struktur Komponen

Buat direktori komponen:

bash
mkdir -p src/components src/pages

Langkah 2.8: Buat Remote Loader Component

Buat file src/components/RemoteLoader.tsx:

tsx
// host-shell/src/components/RemoteLoader.tsx import React, { useEffect, useState, useRef } from 'react'; import { motion } from 'framer-motion'; import { eventBus } from '../utils/eventBus'; import { collectMetrics, saveMetrics } from '../utils/performanceMetrics'; // Define a type for Vue component interface VueComponent { setup?: Function; render?: Function; template?: string; [key: string]: any; } interface RemoteLoaderProps { framework: 'next' | 'nuxt'; fallback?: React.ReactNode; } const RemoteLoader: React.FC<RemoteLoaderProps> = ({ framework, fallback }) => { const [loading, setLoading] = useState(true); const [error, setError] = useState<Error | null>(null); const [Component, setComponent] = useState<React.ComponentType | null>(null); const startTimeRef = useRef(performance.now()); useEffect(() => { const loadComponent = async () => { try { startTimeRef.current = performance.now(); setLoading(true); setError(null); let RemoteComponent; if (framework === 'next') { // Load Next.js component // @ts-ignore - Ignore TypeScript error for dynamic import const remoteNex = await import('remoteNex/LandingPage'); RemoteComponent = remoteNex.default; } else { // Load Nuxt.js component - requires wrapper for Vue components // @ts-ignore - Ignore TypeScript error for dynamic import const remoteNux = await import('remoteNux/LandingPage'); // Use type assertion to safely check Vue component properties const component = remoteNux.default as any; // Check if it's a React component (has __esModule property) if (component.__esModule) { RemoteComponent = component; } else { // For Vue components, use the provided wrapper const { wrapVueComponent } = await import('../utils/vueComponentWrapper'); RemoteComponent = wrapVueComponent(component); } } setComponent(() => RemoteComponent); // Collect performance metrics const metrics = await collectMetrics(framework); saveMetrics(framework, metrics); // Publish event that component is loaded eventBus.publish('component:loaded', { name: framework, timestamp: performance.now() }); setLoading(false); } catch (err) { console.error(`Error loading ${framework} component:`, err); setError(err instanceof Error ? err : new Error(String(err))); setLoading(false); } }; loadComponent(); }, [framework]); if (loading) { return ( <div className="flex items-center justify-center h-screen bg-primary"> <motion.div animate={{ rotate: 360 }} transition={{ repeat: Infinity, duration: 1, ease: "linear" }} className="w-16 h-16 border-t-4 border-blue-500 rounded-full" /> </div> ); } if (error) { return ( <div className="flex flex-col items-center justify-center h-screen bg-primary text-white"> <h2 className="text-2xl font-bold mb-4">Error Loading Component</h2> <p>{error.message}</p> {fallback} </div> ); } if (!Component) { return fallback || null; } return <Component />; }; export default RemoteLoader;

Langkah 2.9: Buat Navbar Component

Buat file src/components/Navbar.tsx:

tsx
// host-shell/src/components/Navbar.tsx import React from 'react'; import { Link, useLocation } from 'react-router-dom'; import { motion } from 'framer-motion'; const Navbar: React.FC = () => { const location = useLocation(); return ( <nav className="fixed top-0 w-full bg-white shadow-md z-50"> <div className="container mx-auto px-6 py-3"> <div className="flex justify-between items-center"> <Link to="/" className="font-bold text-xl text-primary"> Micro Frontend Comparison </Link> <div className="flex space-x-6"> <NavLink to="/" current={location.pathname === "/"}> Home </NavLink> <NavLink to="/comparison" current={location.pathname === "/comparison"}> Comparison </NavLink> </div> </div> </div> </nav> ); }; interface NavLinkProps { to: string; current: boolean; children: React.ReactNode; } const NavLink: React.FC<NavLinkProps> = ({ to, current, children }) => { return ( <Link to={to} className="relative"> <span className={`text-lg ${current ? 'text-blue-600' : 'text-gray-600'}`}> {children} </span> {current && ( <motion.div layoutId="underline" className="absolute bottom-0 left-0 right-0 h-0.5 bg-blue-600" initial={false} /> )} </Link> ); }; export default Navbar;

Langkah 2.10: Buat Footer Component

Buat file src/components/Footer.tsx:

tsx
// host-shell/src/components/Footer.tsx import React from 'react'; const Footer: React.FC = () => { return ( <footer className="bg-primary text-white py-8"> <div className="container mx-auto px-6"> <div className="flex flex-col md:flex-row justify-between items-center"> <div className="mb-4 md:mb-0"> <h3 className="text-xl font-bold">Micro Frontend Comparison</h3> <p className="mt-2">Next.js vs Nuxt.js Performance Analysis</p> </div> <div className="flex space-x-4"> <a href="https://github.com" target="_blank" rel="noopener noreferrer" className="hover:text-blue-300"> GitHub </a> <a href="https://nextjs.org" target="_blank" rel="noopener noreferrer" className="hover:text-blue-300"> Next.js </a> <a href="https://nuxtjs.org" target="_blank" rel="noopener noreferrer" className="hover:text-blue-300"> Nuxt.js </a> </div> </div> <div className="mt-8 text-center text-sm"> <p>© {new Date().getFullYear()} Micro Frontend Demo. All rights reserved.</p> </div> </div> </footer> ); }; export default Footer;

Langkah 2.11: Buat Home Page

Buat file src/pages/HomePage.tsx:

tsx
// host-shell/src/pages/HomePage.tsx import React, { useState } from 'react'; import { motion } from 'framer-motion'; import RemoteLoader from '../components/RemoteLoader'; const HomePage: React.FC = () => { const [activeFramework, setActiveFramework] = useState<'next' | 'nuxt'>('next'); return ( <div className="min-h-screen bg-gray-50"> <div className="container mx-auto px-6 py-24"> <div className="flex justify-center mb-10"> <div className="flex p-1 bg-gray-200 rounded-lg"> <FrameworkToggle framework="next" active={activeFramework === 'next'} onClick={() => setActiveFramework('next')} /> <FrameworkToggle framework="nuxt" active={activeFramework === 'nuxt'} onClick={() => setActiveFramework('nuxt')} /> </div> </div> <div className="bg-white rounded-lg shadow-lg overflow-hidden"> <RemoteLoader framework={activeFramework} fallback={ <div className="flex flex-col items-center justify-center h-screen bg-primary text-white"> <h2 className="text-2xl font-bold mb-4">Loading {activeFramework.toUpperCase()}</h2> <p>Please wait while we load the component...</p> </div> } /> </div> </div> </div> ); }; interface FrameworkToggleProps { framework: 'next' | 'nuxt'; active: boolean; onClick: () => void; } const FrameworkToggle: React.FC<FrameworkToggleProps> = ({ framework, active, onClick }) => { return ( <button onClick={onClick} className={`relative px-6 py-2 rounded-lg ${active ? 'text-white' : 'text-gray-700'}`} > {active && ( <motion.div layoutId="activeTab" className="absolute inset-0 bg-blue-600 rounded-lg" initial={false} transition={{ type: 'spring', duration: 0.6 }} /> )} <span className="relative z-10"> {framework === 'next' ? 'Next.js' : 'Nuxt.js'} </span> </button> ); }; export default HomePage;

Langkah 2.12: Buat Comparison Page

Buat file src/pages/ComparisonPage.tsx:

tsx
// host-shell/src/pages/ComparisonPage.tsx import React, { useEffect, useState } from 'react'; import { motion } from 'framer-motion'; import { getMetrics } from '../utils/performanceMetrics'; interface PerformanceMetric { fcp: number; lcp: number; fid: number; cls: number; loadTime: number; } interface PerformanceData { next?: PerformanceMetric; nuxt?: PerformanceMetric; } const ComparisonPage: React.FC = () => { const [metrics, setMetrics] = useState<PerformanceData>({}); const [loading, setLoading] = useState(true); useEffect(() => { // Get metrics from localStorage const data = getMetrics(); setMetrics(data); setLoading(false); }, []); if (loading) { return ( <div className="flex items-center justify-center min-h-screen bg-gray-50"> <motion.div animate={{ rotate: 360 }} transition={{ repeat: Infinity, duration: 1, ease: "linear" }} className="w-16 h-16 border-t-4 border-blue-500 rounded-full" /> </div> ); } const hasData = metrics.next || metrics.nuxt; if (!hasData) { return ( <div className="container mx-auto px-6 py-24 text-center"> <h1 className="text-3xl font-bold mb-6">No Performance Data Available</h1> <p className="text-xl mb-8"> Please visit the Home page and load both Next.js and Nuxt.js components to collect performance data. </p> <a href="/" className="inline-block px-6 py-3 bg-blue-600 text-white rounded-lg hover:bg-blue-700 transition-colors" > Go to Home Page </a> </div> ); } return ( <div className="container mx-auto px-6 py-24"> <h1 className="text-3xl font-bold mb-10 text-center"> Performance Comparison: Next.js vs Nuxt.js </h1> <div className="grid grid-cols-1 md:grid-cols-2 gap-10"> <MetricCard title="First Contentful Paint (FCP)" nextValue={metrics.next?.fcp} nuxtValue={metrics.nuxt?.fcp} unit="ms" description="Time until the first content is painted on screen" lowerIsBetter /> <MetricCard title="Largest Contentful Paint (LCP)" nextValue={metrics.next?.lcp} nuxtValue={metrics.nuxt?.lcp} unit="ms" description="Time until the largest content element is rendered" lowerIsBetter /> <MetricCard title="First Input Delay (FID)" nextValue={metrics.next?.fid} nuxtValue={metrics.nuxt?.fid} unit="ms" description="Time from first user interaction to response" lowerIsBetter /> <MetricCard title="Cumulative Layout Shift (CLS)" nextValue={metrics.next?.cls} nuxtValue={metrics.nuxt?.cls} unit="" description="Measure of visual stability (lower is better)" lowerIsBetter /> <MetricCard title="Component Load Time" nextValue={metrics.next?.loadTime} nuxtValue={metrics.nuxt?.loadTime} unit="ms" description="Time taken to load and render the component" lowerIsBetter className="md:col-span-2" /> </div> <div className="mt-12 p-6 bg-gray-100 rounded-lg"> <h2 className="text-2xl font-bold mb-4">Interpretation</h2> <p className="mb-4"> The metrics above show the performance characteristics of Next.js and Nuxt.js components when loaded via Module Federation. Lower values are better for all metrics. </p> <ul className="list-disc pl-6 space-y-2"> <li><strong>FCP:</strong> First visual feedback to the user</li> <li><strong>LCP:</strong> When the main content is likely loaded</li> <li><strong>FID:</strong> Responsiveness to user interactions</li> <li><strong>CLS:</strong> Visual stability during page load (0-1 scale)</li> <li><strong>Load Time:</strong> Total time to load the remote component</li> </ul> </div> </div> ); }; interface MetricCardProps { title: string; nextValue?: number; nuxtValue?: number; unit: string; description: string; lowerIsBetter?: boolean; className?: string; } const MetricCard: React.FC<MetricCardProps> = ({ title, nextValue, nuxtValue, unit, description, lowerIsBetter = true, className = "" }) => { // Determine which framework performed better let nextIsBetter = false; let nuxtIsBetter = false; if (nextValue !== undefined && nuxtValue !== undefined) { if (lowerIsBetter) { nextIsBetter = nextValue < nuxtValue; nuxtIsBetter = nuxtValue < nextValue; } else { nextIsBetter = nextValue > nuxtValue; nuxtIsBetter = nuxtValue > nextValue; } } return ( <div className={`bg-white p-6 rounded-lg shadow-md ${className}`}> <h3 className="text-xl font-bold mb-2">{title}</h3> <p className="text-gray-600 mb-6">{description}</p> <div className="grid grid-cols-2 gap-4"> <div className={`p-4 rounded-lg ${nextIsBetter ? 'bg-green-100' : 'bg-gray-100'}`}> <h4 className="font-bold mb-2">Next.js</h4> <p className="text-2xl font-mono"> {nextValue !== undefined ? `${nextValue.toFixed(2)}${unit}` : 'N/A'} </p> {nextIsBetter && <span className="text-green-600 text-sm">Better</span>} </div> <div className={`p-4 rounded-lg ${nuxtIsBetter ? 'bg-green-100' : 'bg-gray-100'}`}> <h4 className="font-bold mb-2">Nuxt.js</h4> <p className="text-2xl font-mono"> {nuxtValue !== undefined ? `${nuxtValue.toFixed(2)}${unit}` : 'N/A'} </p> {nuxtIsBetter && <span className="text-green-600 text-sm">Better</span>} </div> </div> </div> ); }; export default ComparisonPage;

Langkah 2.13: Buat App Component

Modifikasi file src/App.tsx:

tsx
// host-shell/src/App.tsx import React from 'react'; import { BrowserRouter as Router, Routes, Route } from 'react-router-dom'; import Navbar from './components/Navbar'; import Footer from './components/Footer'; import HomePage from './pages/HomePage'; import ComparisonPage from './pages/ComparisonPage'; import './index.css'; const App: React.FC = () => { return ( <Router> <div className="flex flex-col min-h-screen"> <Navbar /> <main className="flex-grow pt-16"> <Routes> <Route path="/" element={<HomePage />} /> <Route path="/comparison" element={<ComparisonPage />} /> </Routes> </main> <Footer /> </div> </Router> ); }; export default App;

Langkah 2.14: Update package.json

Modifikasi package.json untuk menambahkan script:

json
"scripts": { "start": "webpack serve", "build": "webpack --mode production", "test": "react-scripts test", "eject": "react-scripts eject" }

Langkah 3: Menyiapkan Remote Next.js (remote-nex)

bash
# Masuk ke direktori remote-nex cd ../remote-nex # Inisialisasi proyek Next.js npx create-next-app . --typescript # Install dependensi yang diperlukan npm install @module-federation/nextjs-mf npm install -D tailwindcss postcss autoprefixer npm install nats.ws mitt

Langkah 3.1: Konfigurasi Tailwind CSS

bash
# Inisialisasi Tailwind CSS npx tailwindcss init -p

Edit file tailwind.config.js:
remote-nex/tailwind.config.js

js
/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './pages/**/*.{js,ts,jsx,tsx}', './components/**/*.{js,ts,jsx,tsx}', ], theme: { extend: { colors: { primary: '#002B46', }, }, }, plugins: [], };

Modifikasi file styles/globals.css:
remote-nex/styles/globals.css

css
@import "tailwindcss"; @tailwind utilities; :root { --background: #ffffff; --foreground: #171717; } @theme inline { --color-background: var(--background); --color-foreground: var(--foreground); --font-sans: var(--font-geist-sans); --font-mono: var(--font-geist-mono); } @media (prefers-color-scheme: dark) { :root { --background: #0a0a0a; --foreground: #ededed; } } body { background: var(--background); color: var(--foreground); font-family: Arial, Helvetica, sans-serif; }

Langkah 3.2: Konfigurasi Next.js untuk Module Federation

Buat file next.config.Ts:

ts
// remote-nex/next.config.ts import { NextConfig } from 'next'; import { NextFederationPlugin } from '@module-federation/nextjs-mf'; import webpack from 'webpack'; const nextConfig: NextConfig = { reactStrictMode: true, webpack(config: webpack.Configuration) { // Destructuring is commented out as isServer is not used // const { isServer } = options; config.plugins = config.plugins || []; config.plugins.push( new NextFederationPlugin({ name: 'remoteNex', filename: 'static/chunks/remoteEntry.js', exposes: { './LandingPage': './components/LandingPage.tsx', }, shared: { react: { singleton: true, requiredVersion: false, }, 'react-dom': { singleton: true, requiredVersion: false, }, }, extraOptions: { exposePages: false, }, }) ); return config; }, }; export default nextConfig;

Langkah 3.3: Buat Event Bus

Buat direktori utils:

bash
mkdir -p utils

Buat file utils/eventBus.ts dengan konten yang sama seperti di host-shell.

Langkah 3.4: Buat Direktori Komponen

bash
mkdir -p components

Langkah 3.5: Buat Landing Page Component

Buat file components/LandingPage.tsx:

tsx
// remote-nex/components/LandingPage.tsx import React, { useState, useEffect } from 'react'; import { eventBus } from '../utils/eventBus'; interface Logo { src: string; alt: string; } const logos: Logo[] = [ { src: '/logos/docker.svg', alt: 'Docker' }, { src: '/logos/kubernetes.svg', alt: 'Kubernetes' }, { src: '/logos/terraform.svg', alt: 'Terraform' }, { src: '/logos/ansible.svg', alt: 'Ansible' }, { src: '/logos/aws.svg', alt: 'AWS' }, { src: '/logos/gcp.svg', alt: 'Google Cloud' }, { src: '/logos/azure.svg', alt: 'Microsoft Azure' }, { src: '/logos/prometheus.svg', alt: 'Prometheus' }, { src: '/logos/grafana.svg', alt: 'Grafana' }, { src: '/logos/elastic.svg', alt: 'Elastic' }, { src: '/logos/nginx.svg', alt: 'NGINX' }, { src: '/logos/redis.svg', alt: 'Redis' }, { src: '/logos/mongodb.svg', alt: 'MongoDB' }, { src: '/logos/postgresql.svg', alt: 'PostgreSQL' }, { src: '/logos/mysql.svg', alt: 'MySQL' }, { src: '/logos/jenkins.svg', alt: 'Jenkins' }, { src: '/logos/github.svg', alt: 'GitHub' }, { src: '/logos/gitlab.svg', alt: 'GitLab' }, ]; const LandingPage: React.FC = () => { const [isMobile, setIsMobile] = useState(false); const [isTablet, setIsTablet] = useState(false); useEffect(() => { // Notify that component is loaded via event bus eventBus.publish('component:loaded', { name: 'next', timestamp: performance.now() }); // Handle responsive design const checkSize = () => { setIsMobile(window.innerWidth < 768); setIsTablet(window.innerWidth >= 768 && window.innerWidth < 1024); }; checkSize(); window.addEventListener('resize', checkSize); return () => window.removeEventListener('resize', checkSize); }, []); // Filter logos based on screen size const displayedLogos = isMobile ? logos.slice(0, 4) : isTablet ? logos.slice(0, 6) : logos; return ( <section className="flex flex-col md:flex-row w-full h-screen items-center px-6 md:px-16 lg:px-24 pt-40 md:pt-0 bg-[#002B46]"> <div className="w-full md:w-1/2 flex flex-col justify-center text-center md:text-left"> <h1 className="text-white text-4xl font-bold leading-tight"> Empowering IT with <br /> Open-Source Excellence </h1> <p className="text-white mt-4 text-lg leading-relaxed"> Building secure, scalable, and high-performance infrastructure with <br /> industry-leading open-source technologies. </p> <span className="text-white mt-6 text-lg"> 11 years of delivering the right technology for business </span> </div> <div className={`w-full md:w-1/2 grid p-6 items-stretch ${ isMobile ? "grid-cols-2 gap-6" : isTablet ? "grid-cols-3 gap-6" : "grid-cols-6 grid-rows-7 gap-4" }`} > {displayedLogos.map((logo, index) => ( <div key={index} className="flex items-center justify-center"> <img src={logo.src} alt={logo.alt} className="w-32 md:w-full h-auto max-h-full" loading="lazy" /> </div> ))} </div> </section> ); }; export default LandingPage;

Langkah 3.6: Update Home Page

Modifikasi file pages/index.tsx:

tsx
// remote-nex/pages/index.tsx import type { NextPage } from 'next'; import LandingPage from '../components/LandingPage'; const Home: NextPage = () => { return <LandingPage />; }; export default Home;

Langkah 4: Menyiapkan Remote Nuxt.js (remote-nux)

bash
# Masuk ke direktori remote-nux cd ../remote-nux # Inisialisasi proyek Nuxt.js npx nuxi init . # Install dependensi yang diperlukan npm install -D tailwindcss postcss autoprefixer npm install nats.ws mitt @module-federation/node @originjs/vite-plugin-federation

Langkah 4.1: Konfigurasi Tailwind CSS

bash
# Inisialisasi Tailwind CSS npx tailwindcss init -p

Edit file tailwind.config.js:
remote-nux\tailwind.config.js

js
/** @type {import('tailwindcss').Config} */ module.exports = { content: [ './components/**/*.{vue,js}', './layouts/**/*.vue', './pages/**/*.vue', './plugins/**/*.{js,ts}' ], theme: { extend: { colors: { primary: '#002B46', }, }, }, plugins: [], };

Langkah 4.2: Buat CSS File

Buat direktori assets:

bash
mkdir -p assets/css

Buat file assets/css/tailwind.css:
remote-nux/assets/css/tailwind.css

css
@tailwind base; @tailwind components; @tailwind utilities;

Langkah 4.3: Konfigurasi Nuxt.js untuk Module Federation

Modifikasi file nuxt.config.ts:

ts
// remote-nux/nuxt.config.ts import { defineNuxtConfig } from 'nuxt/config'; import federation from '@originjs/vite-plugin-federation'; export default defineNuxtConfig({ ssr: false, css: ['~/assets/css/tailwind.css'], postcss: { plugins: { tailwindcss: {}, autoprefixer: {}, }, }, vite: { plugins: [ federation({ name: 'remoteNux', filename: 'remoteEntry.js', exposes: { './LandingPage': './components/LandingPage.vue' }, shared: ['vue'] }) ], build: { target: 'esnext', minify: false, cssCodeSplit: false } }, app: { baseURL: process.env.NODE_ENV === 'production' ? '/remote-nux/' : '/', buildAssetsDir: '/static/' }, devServer: { port: 3002 } });

Langkah 4.4: Buat Event Bus

Buat direktori utils:

bash
mkdir -p utils

Buat file utils/eventBus.ts dengan konten yang sama seperti di host-shell.

Langkah 4.5: Buat Direktori Komponen

bash
mkdir -p components pages

Langkah 4.6: Buat Landing Page Component

Buat file components/LandingPage.vue:

vue
<!-- remote-nux\components\LandingPage.vue --> <template> <section class="flex flex-col md:flex-row w-full h-screen items-center px-6 md:px-16 lg:px-24 pt-40 md:pt-0 bg-[#002B46]"> <div class="w-full md:w-1/2 flex flex-col justify-center text-center md:text-left"> <h1 class="text-white text-4xl font-bold leading-tight"> Empowering IT with <br /> Open-Source Excellence </h1> <p class="text-white mt-4 text-lg leading-relaxed"> Building secure, scalable, and high-performance infrastructure with <br /> industry-leading open-source technologies. </p> <span class="text-white mt-6 text-lg"> 11 years of delivering the right technology for business </span> </div> <div :class="[isMobile ? 'grid-cols-2 gap-6' : isTablet ? 'grid-cols-3 gap-6' : 'grid-cols-6 grid-rows-7 gap-4']" class="w-full md:w-1/2 grid p-6 items-stretch" > <div v-for="(logo, index) in displayedLogos" :key="index" class="flex items-center justify-center"> <img :src="logo.src" :alt="logo.alt" class="w-32 md:w-full h-auto max-h-full" loading="lazy" /> </div> </div> </section> </template> <script setup> import { ref, onMounted, onUnmounted, computed } from 'vue'; import { eventBus } from '../utils/eventBus'; const isMobile = ref(false); const isTablet = ref(false); const logos = [ { src: '/logos/docker.svg', alt: 'Docker' }, { src: '/logos/kubernetes.svg', alt: 'Kubernetes' }, { src: '/logos/terraform.svg', alt: 'Terraform' }, { src: '/logos/ansible.svg', alt: 'Ansible' }, { src: '/logos/aws.svg', alt: 'AWS' }, { src: '/logos/gcp.svg', alt: 'Google Cloud' }, { src: '/logos/azure.svg', alt: 'Microsoft Azure' }, { src: '/logos/prometheus.svg', alt: 'Prometheus' }, { src: '/logos/grafana.svg', alt: 'Grafana' }, { src: '/logos/elastic.svg', alt: 'Elastic' }, { src: '/logos/nginx.svg', alt: 'NGINX' }, { src: '/logos/redis.svg', alt: 'Redis' }, { src: '/logos/mongodb.svg', alt: 'MongoDB' }, { src: '/logos/postgresql.svg', alt: 'PostgreSQL' }, { src: '/logos/mysql.svg', alt: 'MySQL' }, { src: '/logos/jenkins.svg', alt: 'Jenkins' }, { src: '/logos/github.svg', alt: 'GitHub' }, { src: '/logos/gitlab.svg', alt: 'GitLab' }, ]; const displayedLogos = computed(() => { if (isMobile.value) return logos.slice(0, 4); if (isTablet.value) return logos.slice(0, 6); return logos; }); const checkSize = () => { isMobile.value = window.innerWidth < 768; isTablet.value = window.innerWidth >= 768 && window.innerWidth < 1024; }; onMounted(() => { // Notify that component is loaded via event bus eventBus.publish('component:loaded', { name: 'nuxt', timestamp: performance.now() }); checkSize(); window.addEventListener('resize', checkSize); }); onUnmounted(() => { window.removeEventListener('resize', checkSize); }); </script>

Langkah 4.7: Buat App Component

Buat file app.vue:
remote-nux/app.vue

vue
<template> <div> <NuxtPage /> </div> </template>

Langkah 4.8: Buat Home Page

Buat file pages/index.vue:

vue
<!-- remote-nux/pages/index.vue --> <template> <LandingPage /> </template> <script setup> import LandingPage from '../components/LandingPage.vue'; </script>

Langkah 5: Menyiapkan Aset Bersama

Langkah 5.1: Menyiapkan Logo-Logo

Buat direktori logos di public folder untuk kedua remote:

bash
# Buat direktori public/logos untuk Next.js mkdir -p ../remote-nex/public/logos # Buat direktori public/logos untuk Nuxt.js mkdir -p ../remote-nux/public/logos

Langkah 5.2: Download Logo-Logo

Download beberapa logo open-source ke direktori shared/assets/logos:

bash
cd ../shared/assets/logos # Gunakan curl, wget, atau download manual logo-logo berikut: # - docker.svg # - kubernetes.svg # - terraform.svg # - ansible.svg # - aws.svg # - gcp.svg # - azure.svg # - prometheus.svg # - grafana.svg # - elastic.svg # - nginx.svg # - redis.svg # - mongodb.svg # - postgresql.svg # - mysql.svg # - jenkins.svg # - github.svg # - gitlab.svg

Langkah 5.3: Salin Logo ke Remote Next.js dan Nuxt.js

bash
# Salin logo ke Next.js cp -r * ../../../remote-nex/public/logos/ # Salin logo ke Nuxt.js cp -r * ../../../remote-nux/public/logos/

Langkah 6: Menyiapkan Docker

Langkah 6.1: Buat Dockerfile untuk Host Shell

Buat file ../host-shell/Dockerfile:

Dockerfile
FROM node:16-alpine WORKDIR /app COPY package*.json ./ RUN npm install COPY . . EXPOSE 3000 CMD ["npm", "start"]

Langkah 6.2: Buat Dockerfile untuk Remote Next.js

Buat file ../remote-nex/Dockerfile:

Dockerfile
FROM node:16-alpine WORKDIR /app COPY package*.json ./ RUN npm install COPY . . EXPOSE 3001 CMD ["npm", "run", "dev"]

Langkah 6.3: Buat Dockerfile untuk Remote Nuxt.js

Buat file ../remote-nux/Dockerfile:

Dockerfile
FROM node:16-alpine WORKDIR /app COPY package*.json ./ RUN npm install COPY . . EXPOSE 3002 CMD ["npm", "run", "dev"]

Langkah 6.4: Buat Docker Compose

Buat file ../docker-compose.yml:

yaml
version: '3' services: nats: image: nats:latest ports: - "4222:4222" - "8222:8222" - "8080:8080" command: -js -m 8222 --websocket_port 8080 host-shell: build: context: ./host-shell ports: - "3000:3000" depends_on: - nats environment: - NATS_URL=ws://nats:8080 remote-nex: build: context: ./remote-nex ports: - "3001:3001" depends_on: - nats environment: - NATS_URL=ws://nats:8080 remote-nux: build: context: ./remote-nux ports: - "3002:3002" depends_on: - nats environment: - NATS_URL=ws://nats:8080 asset-server: image: nginx:alpine ports: - "8000:80" volumes: - ./shared/assets:/usr/share/nginx/html/assets - ./shared/assets/logos:/usr/share/nginx/html/logos

Langkah 7: Menjalankan Aplikasi

Langkah 7.1: Menjalankan dengan Docker Compose (Opsional)

bash
# Kembali ke direktori root cd ../.. # Jalankan semua layanan docker-compose up -d

Langkah 7.2: Menjalankan Secara Manual

Terminal 1: Jalankan NATS Server

bash
# Jalankan NATS Server menggunakan Docker docker run -p 4222:4222 -p 8222:8222 -p 8080:8080 nats:latest -js -m 8222 --websocket_port 8080

Terminal 2: Jalankan Remote Next.js

bash
cd remote-nex npm run dev

Terminal 3: Jalankan Remote Nuxt.js

bash
cd remote-nux npm run dev

Terminal 4: Jalankan Host Shell

bash
cd host-shell npm start

Langkah 8: Melakukan Pengujian Performa

  1. Buka browser dan akses http://localhost:3000
  2. Pada halaman Home, toggle antara Next.js dan Nuxt.js untuk mengumpulkan data performa
  3. Kunjungi halaman Comparison di http://localhost:3000/comparison untuk melihat perbandingan performa

Langkah 8.1: Reset Metrik Performa

Jika ingin mereset metrik performa, buka konsol browser dan jalankan:

javascript
localStorage.removeItem('performanceMetrics');

Langkah 8.2: Pengujian Manual dengan DevTools

  1. Buka Chrome DevTools (F12 atau klik kanan -> Inspect)
  2. Buka tab Performance
  3. Klik "Record" dan reload halaman atau toggle antar komponen
  4. Hentikan perekaman dan analisis timeline
  5. Cari metrik kunci seperti FCP, LCP dalam timeline yang direkam

Langkah 9: Troubleshooting

Masalah Umum dan Solusinya

Masalah: Error "Module not found" saat mengakses remote component

Solusi:

  1. Pastikan semua aplikasi (host, remote-nex, remote-nux) berjalan
  2. Verifikasi port yang digunakan (3000, 3001, 3002) tidak digunakan oleh aplikasi lain
  3. Periksa konfigurasi webpack di host-shell dan pastikan URL remotes benar

Masalah: Tailwind CSS tidak berfungsi

Solusi:

  1. Pastikan file CSS base (@tailwind base; @tailwind components; @tailwind utilities;) diimpor di entrypoint
  2. Verifikasi konfigurasi tailwind.config.js menentukan jalur konten yang benar
  3. Pastikan PostCSS dikonfigurasi dengan benar

Masalah: Logo-logo tidak muncul

Solusi:

  1. Verifikasi logo-logo telah disalin ke direktori public yang benar
  2. Pastikan path URL di komponen benar
  3. Periksa konsol browser untuk error 404

Masalah: CORS Issues

Solusi:

  1. Pastikan semua layanan berjalan di domain/port yang diizinkan
  2. Tambahkan header CORS yang diperlukan jika menggunakan server terpisah
  3. Gunakan proxy di development server jika diperlukan

Masalah: Event Bus tidak berfungsi

Solusi:

  1. Verifikasi NATS server berjalan dan dapat diakses
  2. Periksa koneksi websocket ke NATS (port 8080)
  3. Pastikan fallback ke mitt berfungsi jika NATS tidak tersedia

Selesai!

Sekarang Anda memiliki aplikasi micro frontend yang membandingkan performa Next.js dan Nuxt.js menggunakan Module Federation. Aplikasi ini menampilkan metrik performa penting seperti FCP, LCP, FID, CLS, dan Load Time untuk kedua framework.

Pertanyaan kenapa docker compose up tidak ada port yang menampilkan page

Thought

Investigating Docker issue

The user is facing an issue where running docker-compose up doesn't display the page in the browser. It could be because ports weren't exposed properly or the services aren't binding to the correct network, like 0.0.0.0. Another possibility is that the remoteNext or remoteNux services aren’t running the dev script. Also, there might be mismatch with the container's internal port and the exposed ports (3000, 3001, 3002) for the host. Time to troubleshoot the Docker setup carefully!

Investigating Docker port mismatch

I think the issue is that the Docker containers have some port mismatches. The containers expose ports 3001 and 3002 for remote-nex and remote-nux, but the processes inside those containers are listening on port 3000 by default. So when Docker mapping tries to use port 3001 for remote-nex (or 3002 for remote-nux), nothing is actually being served because the server is still listening on port 3000 internally. I believe updating the commands to specify port and host should fix the issue.

Thought for 33 seconds

Answer

Intinya: kontainer‑kontainernya memang jalan, tetapi server dev‑nya tidak “kelihatan” di luar kontainer karena:

  1. webpack‑dev‑server pada host‑shell hanya mendengarkan 127.0.0.1 di dalam kontainer.
  2. Next.js di remote‑nex tetap memakai port 3000 (bawaan) padahal Anda mem‑publish 3001.
  3. Nuxt di remote‑nux sudah memakai port 3002, tetapi—sama seperti poin 1—hanya mendengarkan 127.0.0.1.

Berikut patch tercepat agar docker compose up langsung bisa di‑akses dari host OS:


1 host-shell/webpack.config.js

diff
devServer: { static: { directory: path.join(__dirname, 'dist'), }, port: 3000, + host: '0.0.0.0', // <— Buka ke semua interface + allowedHosts: 'all', // (opsional) hindari “Invalid Host header” historyApiFallback: true, },

2 remote‑nex (Next.js)

package.json

diff
"scripts": { - "dev": "next dev", + "dev": "next dev -H 0.0.0.0 -p 3001", "build": "next build", "start": "next start" }

Kenapa? Port 3001 yang Anda publish di docker-compose.yml harus sama dengan port di dalam kontainer.


3 remote‑nux (Nuxt)

nuxt.config.ts

diff
devServer: { - port: 3002 + port: 3002, + host: '0.0.0.0' }

(Port 3002 sudah cocok; kita hanya membuka ke semua interface.)


4 (Opsional) atur lewat docker-compose.yml

Jika Anda lebih suka tanpa menyentuh kode:

yaml
services: host-shell: environment: - HOST=0.0.0.0 # untuk webpack-dev-server &ge; 5.0 remote-nex: command: npm run dev -- -H 0.0.0.0 -p 3001 remote-nux: command: nuxi dev -H 0.0.0.0 -p 3002

Cara cek cepat

bash
docker compose down -v # matikan & buang volume lama docker compose up --build
  1. Logs
    docker compose logs -f host-shell
    Harus muncul Project running at: http://0.0.0.0:3000.

  2. Port mapping
    docker compose ps
    Kolom “Ports” harus menampilkan:

    text
    0.0.0.0:3000->3000/tcp 0.0.0.0:3001->3001/tcp 0.0.0.0:3002->3002/tcp
  3. Akses di browser

Jika masih kosong, periksa firewall/antivirus host atau pesan error di docker compose logs.


Ringkasan

  • Bukalah semua dev‑server pada 0.0.0.0 di dalam kontainer.
  • Pastikan port dalam kontainer sama dengan port yang Anda publish.
  • Gunakan docker compose ps & docker compose logs untuk memverifikasi.

Sesudah tiga perubahan di atas, halaman akan muncul begitu docker compose up selesai membangun layanan. Selamat mencoba!

Share this Q&A