Webpack5でマイクロフロントを準備する方法

みなさん、こんにちは。私の名前はIvanで、フロントエンド開発者です。





マイクロフロントについての私の解説では、3つものいいねがあったので、マイクロフロントの導入の結果として私たちのストリームが満たされ、満たされるすべてのビッグウィッグを説明する記事を書くことにしました。





Habr(@ artemu78、@ dfuse、@ Katsuba)の人たちがすでにモジュールフェデレーションについて書いているという事実から始めましょう。したがって、私の記事はユニークで画期的なものではありません。むしろ、これらは、このテクノロジーを使用しようとしている人に役立つバンプ、松葉杖、自転車です。





原因

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





マイクロフロントからシェルアプリケーションのコンテキストにアクセスしようとしたときにエラーが発生しました
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.









Webpack5ドックのモジュールフェデレーションドキュメント





モジュールフェデレーションの使用例





モジュールフェデレーションYouTubeプレイリスト








All Articles