Comment préparer les micro-fronts dans Webpack 5

Bonjour à tous, je m'appelle Ivan et je suis développeur front-end.





Sur mon commentaire sur les micro-fronts, il y avait jusqu'à trois likes, j'ai donc décidé d'écrire un article décrivant tous les bigwigs que notre stream a remplis et remplis à la suite de l'introduction des micro-fronts.





Commençons par le fait que les gars de Habr (@ artemu78, @dfuse, @Katsuba) ont déjà écrit sur la Module Federation, donc mon article n'est pas quelque chose d'unique et de révolutionnaire. Ce sont plutôt des bosses, des béquilles et des vélos qui sont utiles pour ceux qui vont utiliser cette technologie.





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, , .





Erreur lors de la tentative d'accès au contexte d'une application shell depuis le micro-front
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.









Documentation de fédération de modules dans Webpack 5 Docks





Exemples d'utilisation de la fédération de modules





Liste de lecture YouTube Module Federation








All Articles