Lesson 05-Advanced Modular Programming

Dynamic Module Loading

import() Dynamic Import Syntax

Basic Usage of import():

// Basic dynamic import
import('./module.js')
  .then(module => {
    module.someFunction();
  })
  .catch(err => {
    console.error('Module loading failed', err);
  });

// Using async/await
async function loadModule() {
  try {
    const module = await import('./module.js');
    module.someFunction();
  } catch (err) {
    console.error('Module loading failed', err);
  }
}

Dynamic Import Features:

  1. Returns a Promise: import() returns a Promise object.
  2. Runtime Resolution: Module paths can be determined at runtime.
  3. Code Splitting: Tools like Webpack automatically split dynamically imported modules into separate chunks.
  4. Browser Support: Natively supported by modern browsers and Node.js (ESM).

Dynamic Path Example:

// Conditional dynamic import
const modulePath = isPremiumUser ? './premiumModule.js' : './freeModule.js';
import(modulePath).then(module => {
  module.init();
});

// Dynamic import based on user input
function loadPlugin(pluginName) {
  import(`./plugins/${pluginName}.js`)
    .then(plugin => plugin.init())
    .catch(err => console.error(`Plugin ${pluginName} loading failed`, err));
}

On-Demand Loading and Code Splitting

On-Demand Loading Patterns:

  1. Route-Level Code Splitting:
// React Router example
const Home = React.lazy(() => import('./routes/Home'));
const About = React.lazy(() => import('./routes/About'));

function App() {
  return (
    <Suspense fallback={<LoadingSpinner />}>
      <Routes>
        <Route path="/" element={<Home />} />
        <Route path="/about" element={<About />} />
      </Routes>
    </Suspense>
  );
}
  1. Component-Level Code Splitting:
// Dynamic loading of heavy components
const HeavyComponent = React.lazy(() => import('./HeavyComponent'));

function MyComponent() {
  const [showHeavy, setShowHeavy] = useState(false);

  return (
    <div>
      <button onClick={() => setShowHeavy(true)}>Load Heavy Component</button>
      {showHeavy && (
        <Suspense fallback={<div>Loading...</div>}>
          <HeavyComponent />
        </Suspense>
      )}
    </div>
  );
}
  1. Library-Level Code Splitting:
// On-demand loading of large libraries
async function loadChartLibrary() {
  if (window.Chart) return; // Skip if already loaded

  const chartLib = await import('chart.js');
  window.Chart = chartLib.default;
  initCharts();
}

// Call when needed
loadChartLibrary();

Code Splitting Strategies:

  1. Entry Splitting: Multiple independent entry files.
  2. Common Splitting: Extract common dependencies into separate chunks.
  3. Dynamic Splitting: Load modules on demand at runtime.

Performance Optimization for Dynamic Module Loading

Optimization Strategies:

  1. Preloading/Prefetching:
// Webpack magic comments
import(/* webpackPrefetch: true */ './future-module.js');
import(/* webpackPreload: true */ './critical-module.js');

// Manual prefetching
function prefetchModule() {
  const link = document.createElement('link');
  link.rel = 'prefetch';
  link.href = '/path/to/module.js';
  link.as = 'script';
  document.head.appendChild(link);
}
  1. Smart Loading Timing:
// Predictive loading based on user behavior
document.getElementById('feature-button').addEventListener('mouseover', () => {
  import('./feature-module.js');
});

// Loading during network idle time
if ('requestIdleCallback' in window) {
  requestIdleCallback(() => {
    import('./non-critical-module.js');
  });
} else {
  setTimeout(() => {
    import('./non-critical-module.js');
  }, 5000);
}
  1. Caching Optimization:
// Cache dynamically loaded modules with Service Worker
self.addEventListener('fetch', event => {
  if (event.request.url.includes('/dynamic-modules/')) {
    event.respondWith(
      caches.match(event.request)
        .then(response => response || fetchAndCache(event.request))
    );
  }
});

async function fetchAndCache(request) {
  const cache = await caches.open('dynamic-modules');
  const response = await fetch(request);
  cache.put(request, response.clone());
  return response;
}

Application Scenarios for Dynamic Module Loading

Route Lazy Loading:

// Vue Router example
const routes = [
  {
    path: '/dashboard',
    component: () => import(/* webpackChunkName: "dashboard" */ './views/Dashboard.vue')
  },
  {
    path: '/settings',
    component: () => import(/* webpackChunkName: "settings" */ './views/Settings.vue')
  }
];

Plugin System:

// Plugin loader implementation
class PluginManager {
  constructor() {
    this.plugins = {};
  }

  async loadPlugin(pluginName) {
    if (this.plugins[pluginName]) return this.plugins[pluginName];

    try {
      const pluginModule = await import(`./plugins/${pluginName}.js`);
      const plugin = pluginModule.default;
      this.plugins[pluginName] = plugin;
      return plugin;
    } catch (err) {
      console.error(`Plugin ${pluginName} loading failed`, err);
      throw err;
    }
  }

  async executePlugin(pluginName, ...args) {
    const plugin = await this.loadPlugin(pluginName);
    return plugin.execute(...args);
  }
}

// Usage example
const pluginManager = new PluginManager();
pluginManager.executePlugin('analytics', { trackPageView: true });

On-Demand Loading of Third-Party Libraries:

// On-demand loading of map library
let mapInstance = null;

async function initMap(containerId) {
  if (mapInstance) return mapInstance;

  const mapLib = await import('leaflet');
  const map = mapLib.map(containerId).setView([51.505, -0.09], 13);
  L.tileLayer('https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png').addTo(map);

  mapInstance = map;
  return map;
}

Error Handling and Fallback Mechanisms for Dynamic Module Loading

Robust Error Handling:

// Dynamic import with retry mechanism
async function loadModuleWithRetry(modulePath, retries = 3, delay = 1000) {
  try {
    const module = await import(modulePath);
    return module;
  } catch (err) {
    if (retries <= 0) {
      throw new Error(`Module ${modulePath} loading failed, retries exhausted`);
    }

    await new Promise(resolve => setTimeout(resolve, delay));
    return loadModuleWithRetry(modulePath, retries - 1, delay * 2);
  }
}

// Usage example
loadModuleWithRetry('./critical-module.js')
  .then(module => module.init())
  .catch(err => {
    console.error('Final loading failed:', err);
    loadFallbackModule();
  });

Fallback Mechanism Implementation:

// Load fallback module if primary module fails
async function loadModuleWithFallback(primaryPath, fallbackPath) {
  try {
    return await import(primaryPath);
  } catch (primaryError) {
    console.warn(`Primary module ${primaryPath} loading failed, attempting fallback`, primaryError);
    try {
      return await import(fallbackPath);
    } catch (fallbackError) {
      console.error(`Fallback module ${fallbackPath} also failed`, fallbackError);
      throw new Error('All module loading attempts failed');
    }
  }
}

// Usage example
loadModuleWithFallback(
  './modern-module.js',
  './legacy-module.js'
).then(module => {
  module.init();
});

Graceful Degradation Scheme:

// Feature degradation implementation
async function loadFeature() {
  try {
    const featureModule = await import('./advanced-feature.js');
    featureModule.enableAdvancedFeatures();
  } catch (err) {
    console.warn('Advanced features unavailable, enabling basic features', err);
    enableBasicFeatures();
  }
}

function enableBasicFeatures() {
  // Implement basic functionality
}

Module Federation

Concept and Core Principles of Module Federation

Core Concepts:

  1. Distributed Module System: Allows different applications to share modules.
  2. Remote Modules: Expose modules for use by other applications.
  3. Shared Dependencies: Coordinate dependency versions across applications.
  4. Runtime Integration: No build-time dependency relationships required.

Architectural Advantages:

  • Independent development and deployment.
  • Maximized code reuse.
  • Progressive upgrade capability.
  • Flexible architectural composition.

Webpack Module Federation Configuration and Implementation

Basic Configuration Example:

// App1 (Host Application)
new ModuleFederationPlugin({
  name: 'app1',
  remotes: {
    app2: 'app2@http://localhost:3002/remoteEntry.js'
  },
  shared: ['react', 'react-dom']
});

// App2 (Remote Application)
new ModuleFederationPlugin({
  name: 'app2',
  filename: 'remoteEntry.js',
  exposes: {
    './Button': './src/components/Button'
  },
  shared: ['react', 'react-dom']
});

Advanced Configuration:

// More complex shared configuration
shared: {
  react: {
    singleton: true,  // Ensure only one React instance
    requiredVersion: '^17.0.0'
  },
  'react-dom': {
    singleton: true,
    requiredVersion: '^17.0.0'
  },
  lodash: {
    eager: true  // Load immediately instead of on-demand
  }
}

Dynamic Remote Configuration:

// Dynamically configure remote application at runtime
const loadRemoteEntry = async (url) => {
  await new Promise((resolve, reject) => {
    const script = document.createElement('script');
    script.src = url;
    script.onload = resolve;
    script.onerror = reject;
    document.head.appendChild(script);
  });
};

const initApp = async () => {
  try {
    await loadRemoteEntry('http://localhost:3002/remoteEntry.js');
    const { getModule } = window.app2;
    const Button = await getModule('./Button');
    // Use Button component
  } catch (err) {
    console.error('Remote module loading failed', err);
    // Load local fallback component
    const { LocalButton } = await import('./LocalButton');
    // Use LocalButton component
  }
};

Cross-Application Module Sharing and Communication

Module Sharing Patterns:

  1. Component Sharing:
// Remote application exposes component
exposes: {
  './UserProfile': './src/components/UserProfile'
}

// Host application uses component
const UserProfile = await import('app2/UserProfile');
  1. Utility Function Sharing:
// Remote application exposes utility functions
exposes: {
  './utils': './src/utils'
}

// Host application uses utility functions
const { formatDate } = await import('app2/utils');
  1. State Management Sharing:
// Shared Redux store
shared: {
  'react-redux': {
    singleton: true
  },
  '@reduxjs/toolkit': {
    singleton: true
  }
}

Inter-Application Communication:

  1. Custom Events:
// Application A sends event
window.dispatchEvent(new CustomEvent('data-updated', {
  detail: { data: newData }
}));

// Application B listens for event
window.addEventListener('data-updated', (event) => {
  console.log('Received data update:', event.detail.data);
});
  1. Shared State:
// Shared Redux store example
// In shared store configuration
const store = configureStore({
  reducer: {
    shared: sharedReducer
  }
});

// Access same store in different applications
const dispatch = useDispatch();
const sharedData = useSelector(state => state.shared.data);

Security and Version Control in Module Federation

Security Measures:

  1. Content Security Policy (CSP):
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; script-src 'self' 'unsafe-inline' http://trusted-cdn.com">
  1. Subresource Integrity (SRI):
<script 
  src="http://localhost:3002/remoteEntry.js"
  integrity="sha384-...">
</script>
  1. Sandbox Isolation:
<iframe sandbox="allow-scripts allow-same-origin" src="http://remote-app"></iframe>

Version Control Strategies:

  1. Exact Version Locking:
shared: {
  react: {
    requiredVersion: '17.0.2',
    singleton: true
  }
}
  1. Version Range Control:
shared: {
  react: {
    requiredVersion: '^17.0.0',
    singleton: true
  }
}
  1. Version Conflict Resolution:
shared: {
  react: {
    requiredVersion: '^17.0.0',
    strictVersion: true  // Strict version matching
  }
}

Application Scenarios for Module Federation

Micro-Frontend Architecture:

// Main application configuration
new ModuleFederationPlugin({
  name: 'mainApp',
  remotes: {
    dashboard: 'dashboard@http://dashboard.example.com/remoteEntry.js',
    admin: 'admin@http://admin.example.com/remoteEntry.js'
  },
  shared: ['react', 'react-dom', 'api']
});

// Dynamic loading of micro-apps
const loadMicroApp = async (appName) => {
  const remote = window[appName];
  const component = await remote.get('./Widget');
  // Render component
};

Multi-Team Collaboration:

  1. Design System Sharing:
// Design system application exposes components
exposes: {
  './Button': './src/api/components/Button',
  './Input': './src/api/Input.js'
}

// Business application uses design system components
const Button = await import('design-system/Button');
  1. Feature Module Sharing:
// Authentication service application exposes functionality
exposes: {
  './AuthService': './src/api/services/AuthService'
}

// Other applications use authentication service
const AuthService = await import('auth-service/AuthService');

Modularization in Micro-Apps

Architecture Design for Micro-Frontends

Application Splitting Principles:

  1. Business Capability-Driven:
    • Divide application boundaries by business domain:
      • Each micro-app focuses on a specific business function:
  2. Technology Stack Independence:
    • Allow different micro-apps to use different tech stacks:
    • Achieve interoperability via Module Federation:
  3. Clear Data Ownership:
    • Each micro-app manages its own data state:
    • Communicate via APIs or shared state:

Modularization Practices in Micro-Frontends

Independent Development Workflow:

  1. Local Development Configuration:
// Development environment Webpack configuration
devServer: {
  port: 3001,
  headers: {
    'Access-Control-Allow-Origin': '*'
  }
},
plugins: [
  new ModuleFederationPlugin({
    name: 'app1',
    filename: 'remoteEntry.js',
    exposes: {
      './Button': './src/components/Button'
    },
    shared: ['react', 'react-dom']
  })
]
  1. Independent Build and Deployment:
// package.json
{
  "scripts": {
    "build": "webpack --config webpack.prod.js",
    "deploy": "aws s3 sync dist s3://my-bucket/app1"
  }
}

Independent Deployment Strategies:

  1. Blue-Green Deployment:
    • Keep old version running:
    • Deploy new version to a new environment:
    • Switch traffic via DNS:
  2. Canary Release:
    • Gradually shift traffic to the new version:
    • Monitor new version performance:
    • Quickly roll back if issues arise:

Micro-Frontend Frameworks’ Modularization Support

qiankun Framework Integration:

// Main application configuration
import { registerMicroApps, start } from 'qiankun';

registerMicroApps([
  {
    name: 'app1',
    entry: '//localhost:7100',
    container: '#app1-container',
    activeRule: '/app1',
  },
  {
    name: 'app2',
    entry: '//localhost:7101',
    container: '#app2-container',
    activeRule: '/app2',
  }
]);

start();

// Sub-application configuration
if (!window.__POWERED_BY_QIANKUN__) {
  // Entry for standalone execution
  render();
}

export async function bootstrap() {
  console.log('Sub-app bootstrap');
}

export async function mount(props) {
  console.log('Sub-app mount', props);
  render(props);
}

export async function unmount(props) {
  console.log('Sub-app unmount', props);
  ReactDOM.unmountComponentAtNode(props.container);
}

Single-SPA Configuration:

// Main application configuration
import { registerApplication, start } from 'single-spa';

registerApplication(
  'app1',
  () => System.import('http://localhost:7100/app1.js'),
  location => location.pathname.startsWith('/app1')
);

registerApplication(
  'app2',
  () => System.import('http://localhost:7101/app2.js'),
  location => location.pathname.startsWith('/app2')
);

start();

// Sub-application configuration (SystemJS)
SystemJS.config({
  map: {
    react: 'https://cdn.jsdelivr.net/npm/react@17.0.2/umd/react.production.min.js',
    'react-dom': 'https://cdn.jsdelivr.net/npm/react-dom@17.0.2/umd/react-dom.production.min.js'
  }
});

define(['react', 'react-dom'], (react, reactDom) => {
  const App = () => <div>Hello from App1</div>;

  const mount = (el) => reactDom.render(<App />, el);
  const unmount = (el) => reactDom.unmountComponentAtNode(el);

  return { mount, unmount };
});

Communication Mechanisms in Micro-Frontends

Event Bus Pattern:

// Main application creates event bus
const eventBus = {
  events: {},
  emit(event, ...args) {
    if (!this.events[event]) return;
    this.events[event].forEach(cb => cb(...args));
  },
  on(event, cb) {
    if (!this.events[event]) this.events[event] = [];
    this.events[event].push(cb);
  }
};

// Expose to sub-applications
window.microFrontendEventBus = eventBus;

// Sub-application uses event bus
window.microFrontendEventBus.on('user-logged-in', (userData) => {
  console.log('User login event:', userData);
  updateUI(userData);
});

// Trigger event
window.microFrontendEventBus.emit('cart-updated', { items: 5 });

Shared State Pattern:

// Shared Redux store example
// Main application configures store
const store = configureStore({
  reducer: {
    shared: sharedReducer,
    app1: app1Reducer,
    app2: app2Reducer
  }
});

// Expose store to sub-applications
window.sharedStore = store;

// Sub-application accesses shared state
const dispatch = useDispatch();
const sharedData = useSelector(state => state.shared.data);

// Update shared state
dispatch({ type: 'SHARED_DATA_UPDATE', payload: newData });

Custom Communication Protocol:

// Define communication protocol
const protocol = {
  commands: {
    getUser: {
      request: { type: 'GET_USER' },
      response: { type: 'USER_DATA', payload: {} }
    },
    updateCart: {
      request: { type: 'UPDATE_CART', payload: {} },
      response: { type: 'CART_UPDATED', payload: {} }
    }
  }
};

// Implement communication service
class MicroFrontendService {
  constructor() {
    this.handlers = {};
  }

  registerHandler(command, handler) {
    this.handlers[command] = handler;
  }

  async execute(command, payload) {
    if (!this.handlers[command]) {
      throw new Error(`Unknown command: ${command}`);
    }
    return this.handlers[command](payload);
  }
}

// Create global service instance
window.microFrontendService = new MicroFrontendService();

// Sub-application registers handler
window.microFrontendService.registerHandler('getUser', async () => {
  const response = await fetch('/api/user');
  return response.json();
});

Performance Optimization and Challenges in Micro-Frontends

Performance Optimization Strategies:

  1. Preloading Strategy:
// Preload potentially needed micro-apps
function prefetchMicroApps() {
  const likelyApps = ['app1', 'app2'];
  likelyApps.forEach(appName => {
    import(`./micro-apps/${appName}.js`);
  });
}

// Preload during user idle time
if ('requestIdleCallback' in window) {
  requestIdleCallback(prefetchMicroApps);
}
  1. Resource Caching:
// Service Worker caching strategy
self.addEventListener('fetch', event => {
  if (event.request.url.includes('/micro-apps/')) {
    event.respondWith(
      caches.match(event.request)
        .then(response => response || fetchAndCache(event.request))
    );
  }
});
  1. On-Demand Loading Optimization:
// Precise loading based on route
const loadApp = async (route) => {
  switch(route) {
    case '/app1':
      return import('./micro-apps/app1.js');
    case '/app2':
      return import('./micro-apps/app2.js');
    default:
      return Promise.resolve(null);
  }
};

Main Challenges and Solutions:

  1. Style Isolation:
    • Use Shadow DOM or CSS scoping techniques:
    • Implement namespace prefixes:
  2. JavaScript Sandboxing:
    • Use Proxy for JavaScript sandbox isolation:
    • Avoid global variable pollution:
  3. Communication Latency:
    • Implement local caching to reduce cross-app calls:
    • Use WebSocket for real-time communication:
  4. Version Compatibility:
    • Implement API version control:
    • Provide compatibility layers for version differences:
  5. Debugging Complexity:
    • Implement unified logging system:
    • Integrate development debugging tools:

Share your love