Lesson 04-Micro Frontend Engineering Practices

Project Scaffolding Setup

Webpack-Based Micro Frontend Scaffolding

Basic Configuration

// webpack.config.js
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');

module.exports = {
  mode: 'development',
  entry: './src/index.js',
  output: {
    filename: '[name].[contenthash].js',
    path: path.resolve(__dirname, 'dist'),
    publicPath: 'auto',
  },
  module: {
    rules: [
      {
        test: /\.js$/,
        exclude: /node_modules/,
        use: 'babel-loader',
      },
      {
        test: /\.css$/,
        use: ['style-loader', 'css-loader'],
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      filename: 'remoteEntry.js',
      exposes: {
        './Button': './src/components/Button',
      },
      shared: {
        react: { singleton: true, requiredVersion: '^17.0.2' },
        'react-dom': { singleton: true, requiredVersion: '^17.0.2' },
      },
    }),
  ],
  devServer: {
    port: 3001,
    hot: true,
  },
};

Advanced Configuration

// webpack.config.js (Advanced Configuration)
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { CleanWebpackPlugin } = require('clean-webpack-plugin');

module.exports = {
  // ...Basic configuration
  plugins: [
    new CleanWebpackPlugin(),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
    new ModuleFederationPlugin({
      // ...Shared configuration
    }),
  ],
};

Vite-Based Micro Frontend Scaffolding

Basic Configuration

// vite.config.js
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  build: {
    rollupOptions: {
      output: {
        manualChunks: {
          'vendor': ['react', 'react-dom'],
        },
      },
    },
  },
});

Remote Application Configuration

// vite.config.js (Remote Application)
import { defineConfig } from 'vite';
import react from '@vitejs/plugin-react';

export default defineConfig({
  plugins: [react()],
  server: {
    port: 3001,
  },
  build: {
    rollupOptions: {
      output: {
        entryFileNames: '[name].[hash].js',
        chunkFileNames: '[name].[hash].js',
        assetFileNames: '[name].[hash].[ext]',
      },
    },
  },
});

Multi-Package Management Tool Configuration

Lerna + Yarn Workspaces

# Project Structure
micro-frontend/
├── packages/
│   ├── app1/
│   ├── app2/
│   └── shared/
├── lerna.json
└── package.json

lerna.json

{
  "packages": ["packages/*"],
  "version": "independent",
  "npmClient": "yarn",
  "useWorkspaces": true
}

package.json

{
  "private": true,
  "workspaces": ["packages/*"],
  "devDependencies": {
    "lerna": "^6.0.0"
  }
}

PNPM Workspaces

package.json

{
  "private": true,
  "workspaces": ["packages/*"],
  "devDependencies": {
    "pnpm": "^7.0.0"
  }
}

.npmrc

child-concurrency=10
reporter=silent

Dependency Management Strategies

Shared Dependency Extraction

Webpack Shared Configuration

new ModuleFederationPlugin({
  // ...
  shared: {
    react: {
      singleton: true,
      requiredVersion: '^17.0.2',
      eager: true, // Preload
    },
    'react-dom': {
      singleton: true,
      requiredVersion: '^17.0.2',
    },
  },
});

Vite Shared Configuration

// vite.config.js
import { defineConfig } from 'vite';

export default defineConfig({
  optimizeDeps: {
    include: ['react', 'react-dom'],
  },
});

Dependency Version Conflict Resolution

Version Alignment Strategies

  1. Strict Version Alignment: All applications use the exact same dependency versions.
  2. Compatible Version Range: Define compatible version ranges.
  3. Isolated Loading: Create separate contexts for different versions.
// webpack.config.js
new ModuleFederationPlugin({
  shared: {
    lodash: {
      requiredVersion: '^4.17.21',
      singleton: false, // Allow multiple versions
    },
  },
});

Dependency Resolution Priority

  1. Main Application Priority: Shared dependencies defined by the main application take precedence.
  2. Latest Compatible Version: Select the latest version compatible with all applications.
  3. Manual Override: Force a specific version through configuration.

Dependency Loading Optimization

Preloading Strategy

// webpack.config.js
new ModuleFederationPlugin({
  // ...
  shared: {
    react: {
      singleton: true,
      eager: true, // Preload
    },
  },
});

On-Demand Loading

// Dynamic import
import('app1/Button').then(({ default: Button }) => {
  // Use Button component
});

Code Splitting

// webpack.config.js
optimization: {
  splitChunks: {
    chunks: 'all',
    cacheGroups: {
      vendor: {
        test: /[\\/]node_modules[\\/]/,
        name: 'vendors',
        chunks: 'all',
      },
    },
  },
},

Build Optimization

Code Splitting and Lazy Loading

Webpack Implementation

// Dynamic import
const Button = React.lazy(() => import('app1/Button'));

function App() {
  return (
    <Suspense fallback={<div>Loading...</div>}>
      <Button />
    </Suspense>
  );
}

Vite Implementation

// Dynamic import
const Button = () => import('app1/Button');

function App() {
  const [Component, setComponent] = useState(null);
  
  useEffect(() => {
    Button().then(module => {
      setComponent(() => module.default);
    });
  }, []);
  
  return Component ? <Component /> : <div>Loading...</div>;
}

Tree Shaking Configuration

Webpack Configuration

// webpack.config.js
module.exports = {
  optimization: {
    usedExports: true,
    minimize: true,
  },
};

Babel Configuration

// .babelrc
{
  "presets": [
    ["@babel/preset-env", { "modules": false }],
    "@babel/preset-react"
  ]
}

Long-Term Caching Strategy

File Name Hashing

// webpack.config.js
output: {
  filename: '[name].[contenthash].js',
  chunkFilename: '[name].[contenthash].chunk.js',
},

Content Hashing

// webpack.config.js
output: {
  filename: '[name].[contenthash:8].js',
},

Cache Group Configuration

// webpack.config.js
optimization: {
  splitChunks: {
    cacheGroups: {
      vendor: {
        test: /[\\/]node_modules[\\/]/,
        name: 'vendors',
        chunks: 'all',
        priority: -10,
      },
    },
  },
},

Build Performance Optimization

Parallel Building

// webpack.config.js
const TerserPlugin = require('terser-webpack-plugin');
const ParallelUglifyPlugin = require('webpack-parallel-uglify-plugin');

module.exports = {
  optimization: {
    minimizer: [
      new ParallelUglifyPlugin({
        uglifyJS: {
          output: {
            comments: false,
          },
          compress: {
            warnings: false,
          },
        },
      }),
    ],
  },
};

Cache Optimization

// webpack.config.js
module.exports = {
  cache: {
    type: 'filesystem',
    buildDependencies: {
      config: [__filename],
    },
  },
};

DLL Precompilation

// webpack.dll.config.js
const path = require('path');
const webpack = require('webpack');

module.exports = {
  entry: {
    vendor: ['react', 'react-dom', 'lodash'],
  },
  output: {
    path: path.join(__dirname, 'dll'),
    filename: '[name].dll.js',
    library: '[name]_[hash]',
  },
  plugins: [
    new webpack.DllPlugin({
      path: path.join(__dirname, 'dll', '[name]-manifest.json'),
      name: '[name]_[hash]',
    }),
  ],
};

Testing Strategies

Unit Testing Framework Selection

Jest Configuration

// jest.config.js
module.exports = {
  testEnvironment: 'jsdom',
  moduleNameMapper: {
    '^@/(.*)$': '<rootDir>/src/$1',
  },
  transform: {
    '^.+\\.(js|jsx)$': 'babel-jest',
  },
  setupFilesAfterEnv: ['<rootDir>/jest.setup.js'],
};

React Testing Library Example

// Button.test.js
import { render, screen } from '@testing-library/react';
import Button from './Button';

test('renders button with text', () => {
  render(<Button>Click me</Button>);
  const buttonElement = screen.getByText(/click me/i);
  expect(buttonElement).toBeInTheDocument();
});

Integration Testing Solutions

Cypress Integration Testing

// cypress/integration/app_spec.js
describe('App Integration', () => {
  it('loads the app', () => {
    cy.visit('/');
    cy.contains('Welcome to Micro Frontend').should('be.visible');
  });
});

Micro Frontend-Specific Integration Testing

// cypress/integration/microfrontend_spec.js
describe('Microfrontend Integration', () => {
  it('loads remote app', () => {
    cy.visit('/');
    cy.get('#remote-app-container').should('exist');
    cy.get('#remote-app-container').contains('Remote App Content').should('be.visible');
  });
});

E2E Testing Solutions

Playwright Configuration

// playwright.config.js
module.exports = {
  use: {
    headless: true,
    viewport: { width: 1280, height: 720 },
    video: 'retain-on-failure',
  },
  projects: [
    { name: 'Chromium', use: { browserName: 'chromium' } },
    { name: 'Firefox', use: { browserName: 'firefox' } },
    { name: 'WebKit', use: { browserName: 'webkit' } },
  ],
};

Test Example

// e2e/specs/app.spec.js
const { test, expect } = require('@playwright/test');

test('loads the app', async ({ page }) => {
  await page.goto('http://localhost:3000');
  await expect(page.locator('text=Welcome')).toBeVisible();
});

Micro Frontend-Specific Test Scenarios

Loading State Testing

// Test remote application loading
cy.intercept('GET', 'http://localhost:3001/remoteEntry.js').as('loadRemote');
cy.visit('/');
cy.wait('@loadRemote');
cy.get('#remote-app').should('be.visible');

Communication Testing

// Test main and sub-application communication
cy.window().then((win) => {
  const event = new CustomEvent('test-event', { detail: 'test' });
  win.dispatchEvent(event);
});

cy.get('#event-handler').should('contain', 'test');

Sandbox Isolation Testing

// Test style isolation
cy.visit('/');
cy.get('#app1').should('have.css', 'color', 'rgb(255, 0, 0)'); // Red
cy.get('#app2').should('have.css', 'color', 'rgb(0, 0, 255)'); // Blue

Monitoring and Error Tracking

Performance Monitoring Metrics

Core Metrics

  1. FP (First Paint): Time to first paint.
  2. FCP (First Contentful Paint): Time to first contentful paint.
  3. LCP (Largest Contentful Paint): Time to largest contentful paint.
  4. FID (First Input Delay): Delay for first input.
  5. CLS (Cumulative Layout Shift): Cumulative layout shift.

Web Vitals Integration

// Import Web Vitals library
import { getCLS, getFID, getLCP } from 'web-vitals';

// Send metrics to analytics service
getCLS(console.log);
getFID(console.log);
getLCP(console.log);

Error Capture and Reporting

Global Error Handling

// Capture global JavaScript errors
window.addEventListener('error', (event) => {
  logErrorToService(event.error, event.filename, event.lineno);
});

// Capture unhandled Promise rejections
window.addEventListener('unhandledrejection', (event) => {
  logErrorToService(event.reason);
});

function logErrorToService(error, file, line) {
  // Report to Sentry or other error monitoring service
  Sentry.captureException(error, {
    extra: { file, line },
  });
}

React Error Boundaries

class ErrorBoundary extends React.Component {
  constructor(props) {
    super(props);
    this.state = { hasError: false };
  }

  static getDerivedStateFromError(error) {
    return { hasError: true };
  }

  componentDidCatch(error, errorInfo) {
    logErrorToService(error, errorInfo.componentStack);
  }

  render() {
    if (this.state.hasError) {
      return <h1>Something went wrong.</h1>;
    }

    return this.props.children; 
  }
}

// Use error boundary
<ErrorBoundary>
  <App />
</ErrorBoundary>

User Behavior Analysis

Basic Implementation

// Track page views
window.addEventListener('load', () => {
  trackPageView(window.location.pathname);
});

// Track click events
document.addEventListener('click', (event) => {
  trackEvent('click', {
    target: event.target.tagName,
    href: event.target.href,
  });
});

function trackPageView(path) {
  // Send to analytics service
  analytics.track('page_view', { path });
}

function trackEvent(name, data) {
  analytics.track(name, data);
}

Micro Frontend-Specific Analysis

// Track micro frontend loading
window.addEventListener('microfrontend-loaded', (event) => {
  trackEvent('microfrontend_load', {
    name: event.detail.name,
    loadTime: event.detail.loadTime,
  });
});

// Trigger after micro frontend loads
window.dispatchEvent(new CustomEvent('microfrontend-loaded', {
  detail: {
    name: 'app1',
    loadTime: performance.now() - startTime,
  },
}));

Performance Analysis

// Record performance metrics
const metrics = {
  tbt: performance.metrics.totalBlockingTime,
  fid: performance.getEntriesByName('first-input')[0]?.processingStart - performance.now(),
  cls: performance.metrics.layoutShift.total,
};

trackPerformance(metrics);

Advanced Monitoring Solutions

Custom Metrics

// Define custom metrics
const customMetric = new PerformanceObserver((list) => {
  for (const entry of list.getEntries()) {
    trackCustomMetric(entry.name, entry.value);
  }
});

customMetric.observe({ type: 'custom', buffered: true });

Distributed Tracing

// Generate trace ID
const traceId = generateTraceId();

// Pass trace ID in requests
fetch('/api/data', {
  headers: {
    'X-Trace-ID': traceId,
  },
});

// Record trace information
trackTrace(traceId, {
  startTime: performance.now(),
  duration: performance.now() - startTime,
});

Real-Time Monitoring

// WebSocket for real-time data sending
const socket = new WebSocket('wss://monitoring.example.com');

socket.onopen = () => {
  setInterval(() => {
    const metrics = collectCurrentMetrics();
    socket.send(JSON.stringify(metrics));
  }, 5000);
};

Monitoring System Integration

Sentry Integration

import * as Sentry from '@sentry/react';
import { Integrations } from '@sentry/tracing';

Sentry.init({
  dsn: 'https://examplePublicKey@o0.ingest.sentry.io/0',
  integrations: [new Integrations.BrowserTracing()],
  tracesSampleRate: 1.0,
});

// Use in React components
function MyComponent() {
  return (
    <Sentry.ErrorBoundary fallback={<p>An error occurred</p>}>
      <MyChildComponent />
    </Sentry.ErrorBoundary>
  );
}

Datadog Integration

import { datadogRum } from '@datadog/browser-rum';

datadogRum.init({
  applicationId: 'YOUR_APPLICATION_ID',
  clientToken: 'YOUR_CLIENT_TOKEN',
  site: 'datadoghq.com',
  service: 'micro-frontend-app',
  env: 'production',
  version: '1.0.0',
  sampleRate: 100,
  trackInteractions: true,
});

// Record custom event
datadogRum.addEvent({
  name: 'custom_event',
  service: 'micro-frontend',
  type: 'custom',
  tags: { feature: 'navigation' },
});

Monitoring Data Visualization

Grafana Dashboard

// Grafana Dashboard JSON Example
{
  "panels": [
    {
      "type": "graph",
      "title": "FP Metrics",
      "targets": [
        {
          "expr": "rate(microfrontend_fp_seconds_sum[1m])",
          "legendFormat": "{{app}}"
        }
      ]
    },
    {
      "type": "table",
      "title": "Error Rates",
      "targets": [
        {
          "expr": "sum(rate(microfrontend_errors_total[1m])) by (app)",
          "legendFormat": "{{app}}"
        }
      ]
    }
  ]
}

Kibana Visualization

// Kibana Visualization Configuration
{
  "title": "Microfrontend Performance",
  "visState": {
    "type": "line",
    "params": {
      "type": "line",
      "series": [
        {
          "id": 1,
          "data": {
            "id": "1",
            "label": "FCP",
            "color": "#7EB26D"
          },
          "valueAxes": "1",
          "drawLinesBetweenPoints": true,
          "showCircles": true
        }
      ],
      "addTooltip": true,
      "addLegend": true,
      "scale": "linear",
      "drawLinesBetweenPoints": true,
      "interpolate": "linear",
      "mode": "stacked"
    },
    "aggs": [
      {
        "id": "1",
        "enabled": true,
        "type": "avg",
        "schema": "metric",
        "params": {
          "field": "FCP"
        }
      },
      {
        "id": "2",
        "enabled": true,
        "type": "terms",
        "schema": "segment",
        "params": {
          "field": "app.keyword",
          "size": 5,
          "order": "desc",
          "orderBy": "_term"
        }
      },
      {
        "id": "3",
        "enabled": true,
        "type": "date_histogram",
        "schema": "bucket",
        "params": {
          "field": "@timestamp",
          "interval": "auto",
          "min_doc_count": 1,
          "extended_bounds": {}
        }
      }
    ]
  }
}

Summary

Micro frontend engineering practices encompass multiple aspects, from project scaffolding setup to dependency management, build optimization, testing strategies, and monitoring with error tracking. Each aspect requires careful design and optimization to ensure high performance, maintainability, and stability of micro frontend applications.

  1. Project Scaffolding Setup: Select appropriate toolchains (Webpack or Vite), configure multi-package management tools (Lerna or PNPM), and establish a unified development environment.
  2. Dependency Management Strategies: Extract and share dependencies effectively, resolve version conflicts, optimize dependency loading strategies, and ensure compatibility across sub-applications.
  3. Build Optimization: Implement code splitting and lazy loading, configure Tree Shaking, establish long-term caching strategies, improve build performance, and reduce build times.
  4. Testing Strategies: Choose appropriate unit testing frameworks (e.g., Jest), implement integration and E2E testing, cover micro frontend-specific test scenarios, and ensure application quality.
  5. Monitoring and Error Tracking: Define key performance metrics, capture and report errors, analyze user behavior, integrate monitoring systems (e.g., Sentry, Datadog), and enable real-time monitoring and visualization.

Through systematic engineering practices, micro frontend architecture can fully leverage its advantages, enabling modular development, independent deployment, and efficient collaboration for large-scale frontend applications.

Share your love