How to prepare microfronts in Webpack 5

Hello everyone, my name is Ivan and I am a front-end developer.





On my commentary about microfronts, there were as many as three likes, so I decided to write an article describing all the bumps that our stream has filled and fills as a result of the introduction of microfronts.





Let's start with the fact that the guys from Habr (@ artemu78, @dfuse, @Katsuba) have already written about the Module Federation, so my article is not something unique and breakthrough. Rather, these are bumps, crutches and bicycles that are useful for those who are going to use this technology.





Cause

, , - , , - . , Webpack 5 Module Federation. , -. , , . , , Webpack, -, ... .





, , Webpack 5?





, , Webpack , Module Federation .





shell-

, , , . Webpack 4.4 5 . , .





Webpack Webpack- :





const webpack = require('webpack');

// ...

const { ModuleFederationPlugin } = webpack.container;

const deps = require('./package.json').dependencies;

module.exports = {
  // ...
  output: {
    // ...
    publicPath: 'auto', // !    publicPath,  auto
  },
  module: {
    // ...
  },
  plugins: [
    // ...
    new ModuleFederationPlugin({
      name: 'shell',
      filename: 'shell.js',
      shared: {
        react: { requiredVersion: deps.react },
        'react-dom': { requiredVersion: deps['react-dom'] },
        'react-query': {
          requiredVersion: deps['react-query'],
        },
      },
      remotes: {
        widgets: `widgets@http://localhost:3002/widgets.js`,
      },
    }),
  ],
  devServer: {
    // ...
  },
};

      
      



, , bootstrap.tsx index.tsx





// bootstrap.tsx

import React from 'react';
import { render } from 'react-dom';

import { App } from './App';
import { config } from './config';

import './index.scss';

config.init().then(() => {
  render(<App />, document.getElementById('root'));
});
      
      



index.tsx bootstrap





import('./bootstrap');
      
      



, - remotes <name>@< >/<filename>. , , .





import React from 'react';

// ...

import Todo from 'widgets/Todo';

// ...

const queryClient = new QueryClient();

export const App = () => {
  // ...

  return (
    <QueryClientProvider client={queryClient} contextSharing>
      <Router>
        <Layout sidebarContent={<Navigation />}>
          <Switch>
            {/* ... */}

            <Route exact path='/'>
              <Todo />
            </Route>

            {/* ... */}
          </Switch>
        </Layout>
      </Router>
    </QueryClientProvider>
  );
};
      
      



, , , , , React, React- LazyService:





// LazyService.tsx

import React, { lazy, ReactNode, Suspense } from 'react';

import { useDynamicScript } from './useDynamicScript';
import { loadComponent } from './loadComponent';
import { Microservice } from './types';
import { ErrorBoundary } from '../ErrorBoundary/ErrorBoundary';

interface ILazyServiceProps<T = Record<string, unknown>> {
  microservice: Microservice<T>;
  loadingMessage?: ReactNode;
  errorMessage?: ReactNode;
}

export function LazyService<T = Record<string, unknown>>({
  microservice,
  loadingMessage,
  errorMessage,
}: ILazyServiceProps<T>): JSX.Element {
  const { ready, failed } = useDynamicScript(microservice.url);

  const errorNode = errorMessage || <span>Failed to load dynamic script: {microservice.url}</span>;

  if (failed) {
    return <>{errorNode}</>;
  }

  const loadingNode = loadingMessage || <span>Loading dynamic script: {microservice.url}</span>;

  if (!ready) {
    return <>{loadingNode}</>;
  }

  const Component = lazy(loadComponent(microservice.scope, microservice.module));

  return (
    <ErrorBoundary>
      <Suspense fallback={loadingNode}>
        <Component {...(microservice.props || {})} />
      </Suspense>
    </ErrorBoundary>
  );
}
      
      



useDynamicScript , html-.





// useDynamicScript.ts
  
import { useEffect, useState } from 'react';

export const useDynamicScript = (url?: string): { ready: boolean; failed: boolean } => {
  const [ready, setReady] = useState(false);
  const [failed, setFailed] = useState(false);

  useEffect(() => {
    if (!url) {
      return;
    }

    const script = document.createElement('script');

    script.src = url;
    script.type = 'text/javascript';
    script.async = true;

    setReady(false);
    setFailed(false);

    script.onload = (): void => {
      console.log(`Dynamic Script Loaded: ${url}`);
      setReady(true);
    };

    script.onerror = (): void => {
      console.error(`Dynamic Script Error: ${url}`);
      setReady(false);
      setFailed(true);
    };

    document.head.appendChild(script);

    return (): void => {
      console.log(`Dynamic Script Removed: ${url}`);
      document.head.removeChild(script);
    };
  }, [url]);

  return {
    ready,
    failed,
  };
};
      
      



loadComponent Webpack-, - .





// loadComponent.ts

export function loadComponent(scope, module) {
  return async () => {
    // Initializes the share scope. This fills it with known provided modules from this build and all remotes
    await __webpack_init_sharing__('default');

    const container = window[scope]; // or get the container somewhere else
    // Initialize the container, it may provide shared modules
    await container.init(__webpack_share_scopes__.default);
    const factory = await window[scope].get(module);
    const Module = factory();
    return Module;
  };
}
      
      



, , .





// types.ts

export type Microservice<T = Record<string, unknown>> = {
  url: string;
  scope: string;
  module: string;
  props?: T;
};
      
      



  • url - + (, http://localhost:3002/widgets.js),





  • scope - name, ModuleFederationPlugin





  • module - ,





  • props - , ,





LazyService :





import React, { FC, useState } from 'react';

import { LazyService } from '../../components/LazyService';
import { Microservice } from '../../components/LazyService/types';
import { Loader } from '../../components/Loader';
import { Toggle } from '../../components/Toggle';
import { config } from '../../config';

import styles from './styles.module.scss';

export const Video: FC = () => {
  const [microservice, setMicroservice] = useState<Microservice>({
    url: config.microservices.widgets.url,
    scope: 'widgets',
    module: './Zack',
  });

  const toggleMicroservice = () => {
    if (microservice.module === './Zack') {
      setMicroservice({ ...microservice, module: './Jack' });
    }

    if (microservice.module === './Jack') {
      setMicroservice({ ...microservice, module: './Zack' });
    }
  };

  return (
    <>
      <div className={styles.ToggleContainer}>
        <Toggle onClick={toggleMicroservice} />
      </div>
      <LazyService microservice={microservice} loadingMessage={<Loader />} />
    </>
  );
};
      
      



, , , url , , .





, shell- , - .





shell- , Webpack => 5





ModuleFederationPlugin, , .





// ...

new ModuleFederationPlugin({
      name: 'widgets',
      filename: 'widgets.js',
      shared: {
        react: { requiredVersion: deps.react },
        'react-dom': { requiredVersion: deps['react-dom'] },
        'react-query': {
          requiredVersion: deps['react-query'],
        },
      },
      exposes: {
        './Todo': './src/App',
        './Gallery': './src/pages/Gallery/Gallery',
        './Zack': './src/pages/Zack/Zack',
        './Jack': './src/pages/Jack/Jack',
      },
    }),

// ...
      
      



exposes , , . , LazyService .





, .





, . , , , , . , , React JavaScript, , Webpack, , , . CDN, . .





, , . , , . , , .





, , , . , shell- , Module Federation . , , , .





, , , , , , .





React-

react-router, , useLocation, , .





Error when trying to access the context of a shell application from the microfront
shell-

Apollo, , ApolloClient shell-. useQuery, useLocation.





, , npm- , shell-, .





UI- shell-

, , shell- . , :





  1. UI- npm- shared-





  2. "" ModuleFederationPlugin





, , , . Module Federation , npm.





TypeScript, , Module Federation , . - , . , .d.ts , - .





emp-tune-dts-plugin, , .





, Webpack 5 Module Federation , , - . , , , .





, , . - , .





, , , , , Module Federation.









Module Federation Documentation in Webpack 5 Docks





Examples of using Module Federation





Module Federation YouTube playlist








All Articles