手动创建一个基于react + webpack module federation的微前端项目

270 阅读5分钟

我所在的团队使用 React 和 Webpack Module Federation 构建了微前端架构。由于对微前端概念的不理解,我踩了不少坑,甚至导致了一些线上 bug,结果被同事们“diss”了一番。

我困惑于为什么在开发 A 代码库时会影响到 B 代码库。微前端的每个模块都是独立部署的,尽管在本地开发时六个模块都在同一个文件夹中,但却不能使用相对路径 ../../xxx 来引用微前端模块。因为一旦部署到服务器上,一个服务器上可能只有一个微前端模块,这样相对路径就会失效。

为了加深理解,决定手动创建一个基于 React 和 Webpack Module Federation 的微前端项目,从零开始,一步一步搭建,了解各种配置项,不仅为日常需求的顺利进行打基础,也为项目优化、技术项目积累经验。

一、手动启动一个react项目

1.创建一个你项目的文件夹

mkdir main-react

2.进入新创建的目录并初始化一个新的Node.js项目

cd main-react npm init -y

3.安装React和ReactDOM库

npm install react react-dom

4.安装 Webpack 和 Babel 相关依赖。

Webpack 是一个用于打包 JavaScript 应用程序的模块捆绑器,而 Babel 是一个 JavaScript 编译器,用于将 ES6 和 JSX 转换为兼容更多浏览器的 JavaScript 代码。

npm install --save-dev webpack webpack-cli webpack-dev-server babel-loader @babel/core @babel/preset-env @babel/preset-react html-webpack-plugin style-loader css-loader

5.在项目根目录下创建一个 webpack.config.js 文件,并添加以下代码

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /.(js|jsx)$/,
                exclude: /node_modules/,
                use: ['babel-loader']
            },
            {
                test: /\.css$/, // 匹配所有 .css 文件
                use: [
                  'style-loader', // 将 CSS 注入到 DOM
                  'css-loader'    // 处理 CSS 文件
                ],
            }
        ]
    },
    resolve: {
        extensions: ['*', '.js', '.jsx']
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, 'public', 'index.html'),
        })
    ],
    devServer: {
        static: {
            directory: path.resolve(__dirname, 'public'),
        },
        hot: true,
    }
};

6.创建 .babelrc 文件,并添加以下代码

{
    "presets": ["@babel/preset-env", "@babel/preset-react"]
}

7.在 package.json 文件的 "scripts" 部分添加以下代码

"scripts": {
    "start": "webpack serve",
    "build": "webpack"
}

直接替代script部分即可

image.png

8.在项目的根目录下创建一个新的 src 目录,并在其中创建 index.jsstyle.css 文件。并添加以下代码

注意,我安装的React的版本是最新的19.0.0的

import React from 'react';
import { createRoot } from 'react-dom/client';
import './style.css';

function App() {
    return (
        <div className='main-react-app'>
            MAIN-REACT-APP
        </div>
    );
}

const root = document.getElementById('root');
createRoot(root).render(<App />);

style.css:

.main-react-app {
    background-color: pink;
}

9.在 public 文件夹下创建 index.html 文件,内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>React App</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>

image.png

10.启动

现在,你的项目就已经搭建完成了,就可以使用 npm start 来运行你的 React 应用,或者使用 npm run build 来生成生产环境的构建包。

项目启动成功:

image.png

一、在刚刚的文件夹内部,再启动一个react项目

名称为remote1,步骤跟(一)中一样,但是改改文案 和 样式,方便区分

1.创建一个你项目的文件夹

mkdir remote1

2.进入新创建的目录并初始化一个新的Node.js项目

cd remote1 npm init -y

3.安装React和ReactDOM库

npm install react react-dom

4.安装 Webpack 和 Babel 相关依赖。

Webpack 是一个用于打包 JavaScript 应用程序的模块捆绑器,而 Babel 是一个 JavaScript 编译器,用于将 ES6 和 JSX 转换为兼容更多浏览器的 JavaScript 代码。

npm install --save-dev webpack webpack-cli webpack-dev-server babel-loader @babel/core @babel/preset-env @babel/preset-react html-webpack-plugin style-loader css-loader

5.在项目根目录下创建一个 webpack.config.js 文件,并添加以下代码

const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /.(js|jsx)$/,
                exclude: /node_modules/,
                use: ['babel-loader']
            },
            {
                test: /\.css$/, // 匹配所有 .css 文件
                use: [
                  'style-loader', // 将 CSS 注入到 DOM
                  'css-loader'    // 处理 CSS 文件
                ],
            }
        ]
    },
    resolve: {
        extensions: ['*', '.js', '.jsx']
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, 'public', 'index.html'),
        })
    ],
    devServer: {
        static: {
            directory: path.resolve(__dirname, 'public'),
        },
        hot: true,
    }
};

6.创建 .babelrc 文件,并添加以下代码

{
    "presets": ["@babel/preset-env", "@babel/preset-react"]
}

7.在 package.json 文件的 "scripts" 部分添加以下代码

"scripts": {
    "start": "webpack serve",
    "build": "webpack"
}

8.在项目的根目录下创建一个新的 src 目录,并在其中创建 index.jsstyle.css 文件。并添加以下代码

注意,我安装的React的版本是最新的19.0.0的

index.js:

import React from 'react';
import { createRoot } from 'react-dom/client';
import './style.css';

function App() {
    return (
        <div className='remote-1'>
            REMOTE1
        </div>
    );
}

const root = document.getElementById('root');
createRoot(root).render(<App />);
export default App; // 确保使用 default 导出

style.css:

.remote-1 {
    background-color: palegoldenrod;
}

9.在 public 文件夹下创建 index.html 文件,内容如下:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>React App</title>
</head>
<body>
    <div id="root"></div>
</body>
</html>

10.启动

项目启动成功:

image.png

三、配置module federation

main-react/webpack.config.jsmain-react/remote1/webpack.config.js新加了一些配置项,以下是替换代码:

main-react/webpack.config.js:

const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /.(js|jsx)$/,
                exclude: /node_modules/,
                use: ['babel-loader']
            },
            {
                test: /\.css$/, // 匹配所有 .css 文件
                use: [
                  'style-loader', // 将 CSS 注入到 DOM
                  'css-loader'    // 处理 CSS 文件
                ],
            }
        ]
    },
    resolve: {
        extensions: ['*', '.js', '.jsx']
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, 'public', 'index.html'),
        }),
        new ModuleFederationPlugin({
            name: 'main-react',
            remotes: {
                remote1: 'remote1@http://172.26.95.140:3001/remoteEntry.js', // 指定远程模块的 URL
            },
            shared: {
                react: { singleton: true, eager: true, requiredVersion: '19.x' },
                'react-dom': { singleton: true, eager: true, requiredVersion: '19.x' },
            },
        }),
    ],
    devServer: {
        static: {
            directory: path.resolve(__dirname, 'public'),
        },
        hot: true,
        port: 3002,
        open: true,
    }
};

main-react/remote1/webpack.config.js:

const path = require('path');
const { ModuleFederationPlugin } = require('webpack').container;
const HtmlWebpackPlugin = require('html-webpack-plugin');

module.exports = {
    mode: 'development',
    entry: './src/index.js',
    output: {
        path: path.resolve(__dirname, 'dist'),
        filename: 'bundle.js'
    },
    module: {
        rules: [
            {
                test: /.(js|jsx)$/,
                exclude: /node_modules/,
                use: ['babel-loader']
            },
            {
                test: /\.css$/, // 匹配所有 .css 文件
                use: [
                  'style-loader', // 将 CSS 注入到 DOM
                  'css-loader'    // 处理 CSS 文件
                ],
            }
        ]
    },
    resolve: {
        extensions: ['*', '.js', '.jsx']
    },
    plugins: [
        new HtmlWebpackPlugin({
            template: path.resolve(__dirname, 'public', 'index.html'),
        }),
        new ModuleFederationPlugin({
            name: 'remote1',
            filename: 'remoteEntry.js',
            exposes: {
                './App': './src/index.js',
            },
            shared: {
                react: { singleton: true, eager: true, requiredVersion: '19.x' },  // 设置为 singleton
                'react-dom': { singleton: true, eager: true, requiredVersion: '19.x' },
            },
        })
    ],
    devServer: {
        static: {
            directory: path.resolve(__dirname, 'public'),
        },
        hot: true,
        port: 3001,
        open: true,
    }
};

另外main-react/src/index.js中的内容也有所改动:

import React from 'react';
import { createRoot } from 'react-dom/client';
import './style.css';
const loadRemote1 = () => {
    return import('remote1/App').then((module) => module.default);
};

// 使用动态加载的组件
loadRemote1().then((Remote1) => {
    const App = () => (
        <div className='main-react-app'>
            MAIN-REACT-APP
            <Remote1 />
        </div>
    );
    const root = document.getElementById('root');
    createRoot(root).render(<App />);
});

启动

1.先启动remote1中的代码:

cd ./remote1
npm run start

image.png

1.再启动main-react中的代码:

注意:此时要保持remote1处于启动状态,另启一个终端来启动main-react中的代码

在main-react目录下

npm run start

启动成功!可以看到main-react项目中 使用module federation引用到了remote1项目中的组件

image.png