阿里云主机折上折
  • 微信号
Current Site:Index > Application of Design Patterns in Micro-Frontend Architecture

Application of Design Patterns in Micro-Frontend Architecture

Author:Chuan Chen 阅读数:20596人阅读 分类: JavaScript

Application of Design Patterns in Micro-Frontend Architecture

Micro-frontend architecture breaks down monolithic frontend applications into multiple independent, loosely coupled sub-applications, each of which can be developed, deployed, and run independently. In this distributed architecture, the application of design patterns is particularly important, as they help address common challenges such as cross-application communication, state management, and style isolation.

Singleton Pattern in Global State Management

In micro-frontend architecture, multiple sub-applications may need to share global state. The Singleton pattern ensures the global state is unique and easily accessible:

class GlobalState {
  constructor() {
    if (GlobalState.instance) {
      return GlobalState.instance;
    }
    this.state = {};
    GlobalState.instance = this;
  }

  set(key, value) {
    this.state[key] = value;
  }

  get(key) {
    return this.state[key];
  }
}

// Sub-application A
const stateA = new GlobalState();
stateA.set('user', { name: 'Alice' });

// Sub-application B
const stateB = new GlobalState();
console.log(stateB.get('user')); // { name: 'Alice' }

This implementation ensures all sub-applications access the same state instance, avoiding state inconsistency issues.

Pub-Sub Pattern for Cross-Application Communication

Communication between sub-applications is a key challenge in micro-frontends. The Publish-Subscribe pattern provides a loosely coupled solution:

class EventBus {
  constructor() {
    this.events = {};
  }

  subscribe(event, callback) {
    if (!this.events[event]) {
      this.events[event] = [];
    }
    this.events[event].push(callback);
  }

  publish(event, data) {
    if (this.events[event]) {
      this.events[event].forEach(callback => callback(data));
    }
  }
}

// Main application initializes the event bus
window.eventBus = new EventBus();

// Sub-application A publishes an event
window.eventBus.publish('cart-updated', { items: 5 });

// Sub-application B subscribes to the event
window.eventBus.subscribe('cart-updated', data => {
  console.log('Cart updated:', data);
});

This pattern allows sub-applications to communicate without direct dependencies on each other.

Strategy Pattern for Multi-Environment Configuration

Different sub-applications may require different environment configuration strategies:

class ConfigStrategy {
  constructor(strategy) {
    this.strategy = strategy;
  }

  setStrategy(strategy) {
    this.strategy = strategy;
  }

  getConfig() {
    return this.strategy.getConfig();
  }
}

class DevConfig {
  getConfig() {
    return {
      apiBase: 'https://dev.api.example.com',
      debug: true
    };
  }
}

class ProdConfig {
  getConfig() {
    return {
      apiBase: 'https://api.example.com',
      debug: false
    };
  }
}

// Usage example
const config = new ConfigStrategy(new DevConfig());
console.log(config.getConfig());

// Switch strategy at runtime
config.setStrategy(new ProdConfig());

Facade Pattern to Simplify Micro-Frontend API

Provides a unified and simplified interface for sub-applications:

class MicroFrontendFacade {
  constructor() {
    this.router = new RouterService();
    this.state = new GlobalState();
    this.events = new EventBus();
  }

  navigateTo(app, path) {
    this.router.navigate(app, path);
  }

  getSharedState(key) {
    return this.state.get(key);
  }

  subscribeToEvent(event, callback) {
    this.events.subscribe(event, callback);
  }
}

// Usage in sub-application
const mfe = new MicroFrontendFacade();
mfe.navigateTo('checkout', '/cart');
mfe.subscribeToEvent('user-logged-in', user => {
  console.log('User logged in:', user);
});

Proxy Pattern for Lazy Loading

Optimizes sub-application loading performance:

class MicroAppProxy {
  constructor(name, loader) {
    this.name = name;
    this.loader = loader;
    this.app = null;
  }

  async mount(container) {
    if (!this.app) {
      this.app = await this.loader();
    }
    return this.app.mount(container);
  }

  unmount() {
    if (this.app) {
      return this.app.unmount();
    }
  }
}

// Usage example
const checkoutApp = new MicroAppProxy('checkout', () => 
  import('https://example.com/checkout-app.js')
);

document.getElementById('checkout-btn').addEventListener('click', () => {
  checkoutApp.mount(document.getElementById('app-container'));
});

Decorator Pattern to Enhance Sub-Application Features

Extends functionality without modifying original code:

function withAnalytics(App) {
  return class extends App {
    mount(container) {
      console.log(`[Analytics] Starting app load: ${this.name}`);
      const startTime = performance.now();
      
      const result = super.mount(container);
      
      const loadTime = performance.now() - startTime;
      console.log(`[Analytics] App loaded, time taken: ${loadTime}ms`);
      
      return result;
    }
  };
}

// Original app class
class CheckoutApp {
  constructor() {
    this.name = 'Checkout';
  }
  
  mount(container) {
    console.log('Mounting checkout app');
    // Actual mounting logic
  }
}

// Enhanced app
const EnhancedCheckoutApp = withAnalytics(CheckoutApp);
new EnhancedCheckoutApp().mount(document.body);

Composite Pattern for Building Application Trees

Manages complex nested relationships between sub-applications:

class MicroAppComponent {
  constructor(name) {
    this.name = name;
    this.children = [];
  }

  add(child) {
    this.children.push(child);
  }

  remove(child) {
    const index = this.children.indexOf(child);
    if (index !== -1) {
      this.children.splice(index, 1);
    }
  }

  async mount(container) {
    console.log(`Mounting ${this.name}`);
    const appContainer = document.createElement('div');
    appContainer.id = `app-${this.name}`;
    container.appendChild(appContainer);
    
    for (const child of this.children) {
      await child.mount(appContainer);
    }
  }
}

// Usage example
const dashboard = new MicroAppComponent('dashboard');
const header = new MicroAppComponent('header');
const sidebar = new MicroAppComponent('sidebar');
const mainContent = new MicroAppComponent('main');

dashboard.add(header);
dashboard.add(sidebar);
dashboard.add(mainContent);

// Add more sub-applications to the main area
const productList = new MicroAppComponent('product-list');
const recommendations = new MicroAppComponent('recommendations');
mainContent.add(productList);
mainContent.add(recommendations);

dashboard.mount(document.body);

Observer Pattern for Style Isolation

class StyleObserver {
  constructor() {
    this.observers = [];
    this.currentApp = null;
  }

  register(app) {
    this.observers.push(app);
  }

  setActiveApp(appName) {
    this.currentApp = appName;
    this.notifyAll();
  }

  notifyAll() {
    this.observers.forEach(app => {
      app.updateStyles(this.currentApp === app.name);
    });
  }
}

class MicroApp {
  constructor(name, styleObserver) {
    this.name = name;
    this.styleObserver = styleObserver;
    this.styleObserver.register(this);
  }

  updateStyles(isActive) {
    if (isActive) {
      document.body.classList.add(`app-${this.name}-active`);
    } else {
      document.body.classList.remove(`app-${this.name}-active`);
    }
  }
}

// Usage example
const styleObserver = new StyleObserver();
const app1 = new MicroApp('checkout', styleObserver);
const app2 = new MicroApp('dashboard', styleObserver);

// When switching applications
styleObserver.setActiveApp('checkout');

Factory Pattern for Creating Sub-Application Instances

class MicroAppFactory {
  static createApp(type) {
    switch (type) {
      case 'react':
        return new ReactMicroApp();
      case 'vue':
        return new VueMicroApp();
      case 'angular':
        return new AngularMicroApp();
      default:
        throw new Error('Unknown application type');
    }
  }
}

class ReactMicroApp {
  mount(container) {
    console.log('Mounting React app');
    // React app mounting logic
  }
}

class VueMicroApp {
  mount(container) {
    console.log('Mounting Vue app');
    // Vue app mounting logic
  }
}

// Usage example
const app1 = MicroAppFactory.createApp('react');
const app2 = MicroAppFactory.createApp('vue');

app1.mount(document.getElementById('app1'));
app2.mount(document.getElementById('app2'));

Adapter Pattern for Integrating Different Framework Sub-Applications

class FrameworkAdapter {
  constructor(app) {
    this.app = app;
  }

  mount(container) {
    // Unified mount interface
    if (this.app.mount) {
      return this.app.mount(container);
    } else if (this.app.init) {
      return this.app.init(container);
    } else if (this.app.bootstrap) {
      return this.app.bootstrap().then(() => {
        this.app.mountComponent(container);
      });
    }
    throw new Error('Unadaptable application interface');
  }

  unmount() {
    // Unified unmount interface
    if (this.app.unmount) {
      return this.app.unmount();
    } else if (this.app.destroy) {
      return this.app.destroy();
    }
    return Promise.resolve();
  }
}

// Sub-applications using different frameworks
const reactApp = new ReactMicroApp();
const vueApp = new VueMicroApp();

// Unified interface
const adaptedReactApp = new FrameworkAdapter(reactApp);
const adaptedVueApp = new FrameworkAdapter(vueApp);

// Now different framework apps can be operated the same way
adaptedReactApp.mount(document.getElementById('react-app'));
adaptedVueApp.mount(document.getElementById('vue-app'));

State Pattern for Managing Sub-Application Lifecycle

class MicroAppState {
  constructor(app) {
    this.app = app;
  }

  mount() {
    throw new Error('mount method must be implemented');
  }

  unmount() {
    throw new Error('unmount method must be implemented');
  }
}

class NotLoadedState extends MicroAppState {
  mount() {
    console.log(`Loading application: ${this.app.name}`);
    return this.app.load().then(() => {
      this.app.setState(new LoadedState(this.app));
      return this.app.mount();
    });
  }

  unmount() {
    console.log('Application not loaded, no need to unmount');
    return Promise.resolve();
  }
}

class LoadedState extends MicroAppState {
  mount() {
    console.log(`Mounting loaded application: ${this.app.name}`);
    // Actual mounting logic
    return Promise.resolve();
  }

  unmount() {
    console.log(`Unmounting application: ${this.app.name}`);
    // Actual unmounting logic
    return Promise.resolve();
  }
}

class MicroApp {
  constructor(name) {
    this.name = name;
    this.state = new NotLoadedState(this);
  }

  setState(state) {
    this.state = state;
  }

  mount() {
    return this.state.mount();
  }

  unmount() {
    return this.state.unmount();
  }

  load() {
    console.log(`Simulating loading application: ${this.name}`);
    return new Promise(resolve => setTimeout(resolve, 1000));
  }
}

// Usage example
const app = new MicroApp('checkout');
app.mount(); // First load and mount
app.unmount(); // Unmount
app.mount(); // Mount again (already loaded)

Memento Pattern for Application State Snapshots

class AppMemento {
  constructor(state) {
    this.state = JSON.parse(JSON.stringify(state));
  }

  getState() {
    return this.state;
  }
}

class MicroApp {
  constructor() {
    this.state = {
      user: null,
      preferences: {},
      lastRoute: '/'
    };
  }

  save() {
    return new AppMemento(this.state);
  }

  restore(memento) {
    this.state = memento.getState();
  }

  updateState(newState) {
    this.state = { ...this.state, ...newState };
  }
}

// Usage example
const app = new MicroApp();
app.updateState({ user: { name: 'Alice' } });

// Create snapshot
const snapshot1 = app.save();

app.updateState({ lastRoute: '/checkout' });

// Restore state
app.restore(snapshot1);
console.log(app.state); // { user: { name: 'Alice' }, preferences: {}, lastRoute: '/' }

Chain of Responsibility Pattern for Handling Sub-Application Errors

class ErrorHandler {
  constructor(successor = null) {
    this.successor = successor;
  }

  handle(error, app) {
    if (this.canHandle(error)) {
      return this.process(error, app);
    } else if (this.successor) {
      return this.successor.handle(error, app);
    }
    throw error;
  }

  canHandle() {
    throw new Error('canHandle method must be implemented');
  }

  process() {
    throw new Error('process method must be implemented');
  }
}

class NetworkErrorHandler extends ErrorHandler {
  canHandle(error) {
    return error.message.includes('Network');
  }

  process(error, app) {
    console.log('Handling network error:', error.message);
    return app.retry();
  }
}

class AuthErrorHandler extends ErrorHandler {
  canHandle(error) {
    return error.message.includes('Auth');
  }

  process(error) {
    console.log('Handling authentication error:', error.message);
    window.location.href = '/login';
    return Promise.resolve();
  }
}

class DefaultErrorHandler extends ErrorHandler {
  canHandle() {
    return true;
  }

  process(error) {
    console.error('Unhandled error:', error);
    return Promise.reject(error);
  }
}

// Build the chain of responsibility
const errorHandler = new NetworkErrorHandler(
  new AuthErrorHandler(
    new DefaultErrorHandler()
  )
);

// Usage example
class MicroApp {
  constructor() {
    this.retryCount = 0;
  }

  async mount() {
    try {
      // Simulate mounting operation that might fail
      if (Math.random() > 0.5) {
        throw new Error('Network error: Failed to fetch');
      }
      console.log('Application mounted successfully');
    } catch (error) {
      return errorHandler.handle(error, this);
    }
  }

  retry() {
    this.retryCount++;
    console.log(`Retry attempt ${this.retryCount}`);
    return this.mount();
  }
}

const app = new MicroApp();
app.mount();

本站部分内容来自互联网,一切版权均归源网站或源作者所有。

如果侵犯了你的权益请来信告知我们删除。邮箱:cc@cccx.cn

Front End Chuan

Front End Chuan, Chen Chuan's Code Teahouse 🍵, specializing in exorcising all kinds of stubborn bugs 💻. Daily serving baldness-warning-level development insights 🛠️, with a bonus of one-liners that'll make you laugh for ten years 🐟. Occasionally drops pixel-perfect romance brewed in a coffee cup ☕.