Объединение модулей Webpack 5 — перехваты в удаленном модуле — не работает

#javascript #reactjs #typescript #webpack

#javascript #reactjs #typescript #webpack

Вопрос:

Я пытаюсь создать динамическую систему во время выполнения с помощью объединения модулей (функция webpack 5). Все работает отлично, но когда я добавляю перехваты в модуль ‘producer’ (модуль, из которого хост-приложение динамически импортирует компонент) Я получаю массу ошибок «недопустимого правила перехватов».

 Warning: Do not call Hooks inside useEffect(...), useMemo(...), or other built-in Hooks. You can only call Hooks at the top level of your React function. For more information, see [LINK RULES OF HOOKS]
  
 Warning: React has detected a change in the order of Hooks called by PluginHolder. This will lead to bugs and errors if not fixed. For more information, read the Rules of Hooks: [LINK RULES OF HOOKS]
  

Я уже использовал поле externals и добавил тег script в html-файлы, я использовал опцию shared с добавлением одноэлементного поля: true и указанием версии react и react-dom
Каждый раз консоль выдает массу ошибок

Это мой метод прямо из документации для загрузки модуля

 const loadComponent = (scope: string, module: string) => async (): Promise<any> => {
    // @ts-ignore
    await __webpack_init_sharing__('default');
    // @ts-ignore
    const container = window[scope];
    // @ts-ignore
    await container.init(__webpack_share_scopes__.default);
    // @ts-ignore
    const factory = await window[scope].get(module);
    return factory();
};
  

Для загрузки remoteEntry.js файл Я использую makeAsyncScriptLoader HOC с react-async-script следующим образом:

 const withScript = (name: string, url: string) => {
    const LoadingElement = () => {
        return <div>Loading...</div>;
    };

    return () => {
        const [scriptLoaded, setScriptLoaded] = useState<boolean>(false);

        const AsyncScriptLoader = makeAsyncScriptLoader(url, {
            globalName: name,
        })(LoadingElement);

        if (scriptLoaded) {
            return <PluginHolder name={name}/>;
        }

        return (
            <AsyncScriptLoader
                asyncScriptOnLoad={() => {
                    setScriptLoaded(true);
                }}
            />
        );
    };
};
  

PluginHolder — это простой компонент, который переносит модуль загрузки из загруженного скрипта (загрузка выполняется фактически)

  useEffect((): void => {
        (async () => {
            const c = await loadComponent(name, './Plugin')();
            setComponent(c.default);
        })();
    }, []);

 return cloneElement(component);
  

И, кроме того, это стартер:

 const [plugins, setPlugins] = useState<PluginFunc[]>([]);

    useEffect((): void => {
        pluginNames.forEach(desc => {
            const loaded = withScript(desc.name, desc.url);
            setPlugins([...plugins, loaded]);
        });
    }, []);
  

Я не использую React.Ленивый, потому что я не могу использовать import(). Более того, в хост-приложении я установил поле eager: true в react и react-dom

My webpack.config.js (host) below:

 require('tslib');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { DefinePlugin } = require('webpack');
const { ModuleFederationPlugin } = require('webpack').container;
// @ts-ignore
const AutomaticVendorFederation = require('@module-federation/automatic-vendor-federation');
const packageJson = require('./package.json');
const exclude = ['babel', 'plugin', 'preset', 'webpack', 'loader', 'serve'];
const ignoreVersion = ['react', 'react-dom'];

const automaticVendorFederation = AutomaticVendorFederation({
    exclude,
    ignoreVersion,
    packageJson,
    shareFrom: ['dependencies', 'peerDependencies'],
    ignorePatchVersion: false,
});

module.exports = {
    mode: 'none',
    entry: {
        app: path.join(__dirname, 'src', 'index.tsx'),
    },
    target: 'web',
    resolve: {
        extensions: ['.ts', '.tsx', '.js'],
    },
    module: {
        rules: [
            {
                test: /.tsx?$/,
                exclude: '/node_modules/',
                use: 'ts-loader',
            },
            {
                test: /.(s[ac]|c)ss$/i,
                exclude: '/node_modules/',
                use: [
                    'style-loader',
                    'css-loader',
                    'sass-loader',
                ],
            },
        ],
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: path.join(__dirname, 'public', 'index.html'),
            favicon: path.join(__dirname, 'public', 'favicon.ico'),
        }),
        new DefinePlugin({
            'process.env.NODE_ENV': JSON.stringify('development'),
        }),
        new ModuleFederationPlugin({
            name: 'host',
            remotes: {},
            exposes: {},
            shared: {
                ...automaticVendorFederation,
                react: {
                    eager: true,
                    singleton: true,
                    requiredVersion: packageJson.dependencies.react,
                },
                'react-dom': {
                    eager: true,
                    singleton: true,
                    requiredVersion: packageJson.dependencies['react-dom'],
                },
            },
        }),
    ],
    output: {
        filename: '[name].js',
        path: path.resolve(__dirname, 'dist'),
        publicPath: 'http://localhost:3001/',
    },
    devServer: {
        contentBase: path.join(__dirname, 'dist'),
        port: 3001,
    },
};
  

А также мой webpack.config.js из второго модуля:

 require('tslib');
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const { DefinePlugin } = require('webpack');
const { ModuleFederationPlugin } = require('webpack').container;
// @ts-ignore
const AutomaticVendorFederation = require('@module-federation/automatic-vendor-federation');
const packageJson = require('./package.json');
const exclude = ['babel', 'plugin', 'preset', 'webpack', 'loader', 'serve'];
const ignoreVersion = ['react', 'react-dom'];

const automaticVendorFederation = AutomaticVendorFederation({
    exclude,
    ignoreVersion,
    packageJson,
    shareFrom: ['dependencies', 'peerDependencies'],
    ignorePatchVersion: false,
});

module.exports = (env, argv) => {

    const { mode } = argv;
    const isDev = mode !== 'production';

    return {
        mode,
        entry: {
            plugin: path.join(__dirname, 'src', 'index.tsx'),
        },
        target: 'web',
        resolve: {
            extensions: ['.ts', '.tsx', '.js'],
        },
        module: {
            rules: [
                {
                    test: /.tsx?$/,
                    exclude: '/node_modules/',
                    use: 'ts-loader',
                },
                {
                    test: /.(s[ac]|c)ss$/i,
                    exclude: '/node_modules/',
                    use: [
                        'style-loader',
                        'css-loader',
                        'sass-loader',
                    ],
                },
            ],
        },
        plugins: [
            new HtmlWebpackPlugin({
                template: path.join(__dirname, 'public', 'index.html'),
                favicon: path.join(__dirname, 'public', 'favicon.ico'),
            }),
            new DefinePlugin({
                'process.env.NODE_ENV': JSON.stringify('development'),
            }),
            new ModuleFederationPlugin({
                name: 'example',
                library: { type: 'var', name: 'example' },
                filename: 'remoteEntry.js',
                remotes: {},
                exposes: {
                    './Plugin': './src/Plugin',
                },
                shared: {
                    ...automaticVendorFederation,
                    react: {
                        eager: isDev,
                        singleton: true,
                        requiredVersion: packageJson.dependencies.react,
                    },
                    'react-dom': {
                        eager: isDev,
                        singleton: true,
                        requiredVersion: packageJson.dependencies['react-dom'],
                    },
                },
            }),
        ],
        output: {
            path: path.resolve(__dirname, 'dist'),
            publicPath: 'http://localhost:3002/',
        },
        devServer: {
            contentBase: path.join(__dirname, 'dist'),
            port: 3002,
        },
    };
};
  

Есть ли у вас какой-либо опыт работы с этим или какие-либо подсказки — я думаю, дело в том, что 2 приложения используют 2 экземпляра react, но это мое предположение.
Что-то не так с моей конфигурацией?

Комментарии:

1. Должен ли ваш список приложений хоста реагировать, реагировать-dom как общий ресурс? Разве это не должен быть тот, который загружает эти библиотеки?

2. В нем говорится, что не используйте перехваты в useEffect. withScript имеет перехват useState.

3. откуда вы импортировали функцию webpack_init_sharing?

Ответ №1:

Убедитесь, что вы добавляете общие зависимости в свой файл webpack.config.

См. Пример ниже:

   plugins: [
    new ModuleFederationPlugin(
      {
        name: 'MFE1',
        filename:
          'remoteEntry.js',

        exposes: {
          './Button':'./src/Button',
        },
        shared: { react: { singleton: true }, "react-dom": { singleton: true } },
      }
    ),
    new HtmlWebpackPlugin({
      template:
        './public/index.html',
    }),
  ],
};
  

У меня есть настройка как хоста, так и удаленного проекта с этим общим свойством. Исправлено для меня, когда перехваты нарушали работу моего хост-приложения. Это связано с тем, что существуют повторяющиеся зависимости react, независимо от того, одинакова ли версия, вы получите эту ошибку.

Комментарии:

1. К сожалению, у меня уже есть эта конфигурация (shared react и react-dom, singleton: true и т. Д. … убедился, что у меня одинаковые версии …), Однако это не устраняет проблему. Только удаление перехватов и переход на классы.