模块联合、React 和 Typescript 微前端简介

2025-06-10

模块联合、React 和 Typescript 微前端简介

微前端是目前互联网上最热门的话题之一。我们经常听到这个词,但什么是微前端呢?想象一下,一个网站包含许多组件,例如导航栏、页脚、主容器和侧边菜单。如果它们由不同的域名提供服务,会发生什么?是的,你猜对了,我们最终会得到一个微前端。现在,得益于微前​​端技术,我们可以分别处理这些应用程序。我们可以分别编写它们的单元测试、端到端测试,甚至可以使用不同的框架,例如 Angular、Vue 和 Svelte。

目前有两个主要参与者可以实现这些目标,其中一个是模块联合,另一个是单 SPA,我在这里介绍过:🔗使用单 SPA 将 CRA 迁移到微前端

与单 SPA不同模块联合的约束少得多。在模块联合中,您可以随心所欲地构建项目,而在单 SPA 中,您需要设置一个配置文件,并围绕该文件构建项目。
微前端只有一件事令人担忧,那就是配置。初始配置很容易让人望而却步,因为您需要整合很多组件,如果这是您的第一次尝试,在没有指导的情况下,很容易迷失方向。

工作示例

这是一个 POC(概念验证)项目,它可能看起来不太好,但这不是我们的重点。

🔗项目的 Github 地址

🔴实例

模块联合

模块联合实际上是 Webpack 配置的一部分。此配置使我们能够将 CRA 的不同部分公开或接收给另一个 CRA 项目。
这些独立的项目之间不应存在依赖关系,因此它们可以单独开发和部署。

让我们首先创建我们的Container项目,该项目导出其他两个应用程序APP-1APP-2

npx create-react-app container --template typescript
Enter fullscreen mode Exit fullscreen mode

容器应用

项目结构

container
├─ package.json
├─ public
│ ├─ index.dev.html
│ └─ index.prod.html
├─ src
│ ├─ App.tsx
│ ├─ bootstrap.tsx
│ └─ index.ts
├─ tsconfig.json
├─ webpack.config.js
├─ webpack.prod.js
└─ yarn.lock
Enter fullscreen mode Exit fullscreen mode

让我们添加依赖项

yarn add html-webpack-plugin serve ts-loader webpack webpack-cli webpack-dev-server
Enter fullscreen mode Exit fullscreen mode

我们需要做一些修改。创建一个名为 的文件bootstrap.tsx,并将其移入index.ts其中bootstrap.tsx

bootstrap.tsx

import App from './App';
import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(<App />, document.getElementById('root'));
Enter fullscreen mode Exit fullscreen mode

并将它们添加到index.ts

索引.ts

import('./bootstrap');
export {};
Enter fullscreen mode Exit fullscreen mode

最后,将这些添加到app.tsx以后使用。我们稍后再讨论。

应用程序.tsx

import React from 'react';
//@ts-ignore
import CounterAppTwo from 'app2/CounterAppTwo';
//@ts-ignore
import CounterAppOne from 'app1/CounterAppOne';

export default () => (
  <div style={{ margin: '20px' }}>
    <React.Suspense fallback="Loading header...">
      <div
        style={{
          border: '1px dashed black',
          height: '50vh',
          display: 'flex',
          justifyContent: 'space-around',
          alignItems: 'center',
          flexDirection: 'column',
        }}
      >
        <h1>CONTAINER</h1>
        <div
          style={{
            display: 'flex',
            flexDirection: 'row',
            justifyContent: 'space-around',
          }}
        >
          <div
            style={{
              marginRight: '2rem',
              padding: '2rem',
              border: '1px dashed black',
            }}
          >
            <h2>APP-1</h2>
            <CounterAppOne />
          </div>
          <div style={{ border: '1px dashed black', padding: '2rem' }}>
            <h2>APP-2</h2>
            <CounterAppTwo />
          </div>
        </div>
      </div>
    </React.Suspense>
  </div>
);
Enter fullscreen mode Exit fullscreen mode

我们已经完成了组件部分,接下来是关键部分。我们需要设置我们的容器应用 Webpack 来接收app-1app-2

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
const deps = require('./package.json').dependencies;

module.exports = {
  entry: './src/index.ts',
  mode: 'development',
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    port: 3000,
  },
  output: {
    publicPath: 'http://localhost:3000/',
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx|tsx|ts)$/,
        loader: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'container',
      library: { type: 'var', name: 'container' },
      remotes: {
        app1: 'app1',
        app2: 'app2',
      },
      shared: {
        ...deps,
        react: { singleton: true, eager: true, requiredVersion: deps.react },
        'react-dom': {
          singleton: true,
          eager: true,
          requiredVersion: deps['react-dom'],
        },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.dev.html',
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

按如下方式更新您的package.json脚本:

"scripts": {
    "start": "webpack serve --open",
    "build": "webpack --config webpack.prod.js",
    "serve": "serve dist -p 3002",
    "clean": "rm -rf dist"
}
Enter fullscreen mode Exit fullscreen mode

更新您的tsconfig如下内容:

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": false,
    "jsx": "react-jsx"
  },
  "include": ["src"]
}
Enter fullscreen mode Exit fullscreen mode

最重要的考虑因素是ModuleFederationPlugin。我们指定name模块,并remotes从项目外部接收数据。并设置共享依赖项以便立即使用。

不要弄乱远程名称。如果名称设置不正确,项目将无法编译。

最后一步是编辑index.html

<html>
  <head>
    <script src="http://localhost:3001/remoteEntry.js"></script>
    <script src="http://localhost:3002/remoteEntry.js"></script>
  </head>
  <body>
    <div id="root"></div>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

在这里,我们添加具有相应端口的遥控器。

现在我们的容器应用已经准备就绪,我们需要设置app-1app-2,并公开<Counter />组件。步骤基本相同,我们将设置bootstrap.tsxwebpack.config.js。Webpack
配置中只有细微的变化。

应用-1

项目结构

├─ package.json
├─ public
│  └─ index.html
├─ README.md
├─ src
│  ├─ App.tsx
│  ├─ bootstrap.tsx
│  ├─ components
│  │  └─ CounterAppOne.tsx
│  └─ index.ts
├─ tsconfig.json
├─ webpack.config.js
├─ webpack.prod.js
└─ yarn.lock
Enter fullscreen mode Exit fullscreen mode

让我们添加依赖项

npx create-react-app app-1 --template typescript
yarn add html-webpack-plugin serve ts-loader webpack webpack-cli webpack-dev-server
Enter fullscreen mode Exit fullscreen mode

就像我们在容器应用程序中所做的那样,我们将设置bootstrap.tsxindex.tsapp.tsx

bootstrap.tsx

import App from './App';
import React from 'react';
import ReactDOM from 'react-dom';

ReactDOM.render(<App />, document.getElementById('root'));
Enter fullscreen mode Exit fullscreen mode

并将它们添加到index.ts

索引.ts

import('./bootstrap');
export {};
Enter fullscreen mode Exit fullscreen mode

最后,将这些添加到app.tsx以后使用。我们稍后再讨论。

应用程序.tsx

import React from 'react';
import CounterAppOne from './components/CounterAppOne';

const App = () => (
  <div style={{ margin: '20px' }}>
    <div>APP-1 - S4 </div>
    <div>
      <CounterAppOne />
    </div>
  </div>
);

export default App;
Enter fullscreen mode Exit fullscreen mode

现在我们将创建<Counter />组件,稍后我们将在 webpack 配置中将其公开给容器。

组件 > CounterAppOne.tsx

import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>
        Add by one each click <strong>APP-1</strong>
      </p>
      <p>Your click count: {count} </p>
      <button onClick={() => setCount(count + 1)}>Click me</button>
    </div>
  );
};

export default Counter;
Enter fullscreen mode Exit fullscreen mode

我们基本上已经完成了,只需要添加 webpack 配置。

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
const deps = require('./package.json').dependencies;

module.exports = {
  entry: './src/index.ts',
  mode: 'development',
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    port: 3001,
  },
  output: {
    publicPath: 'http://localhost:3001/',
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx|tsx|ts)$/,
        loader: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'app1',
      library: { type: 'var', name: 'app1' },
      filename: 'remoteEntry.js',
      exposes: {
        // expose each component
        './CounterAppOne': './src/components/CounterAppOne',
      },
      shared: {
        ...deps,
        react: { singleton: true, eager: true, requiredVersion: deps.react },
        'react-dom': {
          singleton: true,
          eager: true,
          requiredVersion: deps['react-dom'],
        },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

按如下方式更新您的package.json脚本:

"scripts": {
    "start": "webpack serve --open",
    "build": "webpack --config webpack.prod.js",
    "serve": "serve dist -p 3001",
    "clean": "rm -rf dist"
}
Enter fullscreen mode Exit fullscreen mode

更新您的tsconfig如下内容:

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": false,
    "jsx": "react-jsx"
  },
  "include": ["src"]
}
Enter fullscreen mode Exit fullscreen mode

编辑index.html

<html>
  <head> </head>
  <body>
    <div id="root"></div>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

此配置有一些不同。我们设置了不同的端口,公开了我们的应用而不是远程访问,并且我们有一个叫做filenamewhere 的配置,用于将我们的
模块公开给不同的模块。记住,我们<script src="http://localhost:3001/remoteEntry.js"></script>在容器中添加了 index.html。这就是
container我们查找 的地方app-1

这里的重要事项:

  • 名称:“app1”
  • 文件名:'remoteEntry.js'
  • 暴露

暴露错误的路径很可能导致编译失败。设置错误的名称也会导致问题,因为如果找不到container,查找 就会失败。app-1

应用-2

项目结构

├─ package.json
├─ public
│  └─ index.html
├─ README.md
├─ src
│  ├─ App.tsx
│  ├─ bootstrap.tsx
│  ├─ components
│  │  └─ CounterAppTwo.tsx
│  └─ index.ts
├─ tsconfig.json
├─ webpack.config.js
├─ webpack.prod.js
└─ yarn.lock
Enter fullscreen mode Exit fullscreen mode

App-2 基本相同。创建一个新的 React 项目,执行上述所有操作,然后添加<CounterAppTwo />webpack配置即可。

组件 > CounterAppTwo

import React, { useState } from 'react';

const Counter = () => {
  const [count, setCount] = useState(1);

  return (
    <div>
      <p>
        Multiply by two each click <strong>APP-2</strong>
      </p>
      <p>Your click count: {count}</p>
      <button onClick={() => setCount((prevState) => prevState * 2)}>Click me</button>
    </div>
  );
};

export default Counter;
Enter fullscreen mode Exit fullscreen mode

webpack.config.js

const HtmlWebpackPlugin = require('html-webpack-plugin');
const { ModuleFederationPlugin } = require('webpack').container;
const path = require('path');
const deps = require('./package.json').dependencies;

module.exports = {
  entry: './src/index.ts',
  mode: 'development',
  devServer: {
    contentBase: path.join(__dirname, 'dist'),
    port: 3002,
  },
  output: {
    publicPath: 'http://localhost:3002/',
  },
  resolve: {
    extensions: ['.ts', '.tsx', '.js'],
  },
  module: {
    rules: [
      {
        test: /\.(js|jsx|tsx|ts)$/,
        loader: 'ts-loader',
        exclude: /node_modules/,
      },
    ],
  },
  plugins: [
    new ModuleFederationPlugin({
      name: 'app2',
      library: { type: 'var', name: 'app2' },
      filename: 'remoteEntry.js',
      exposes: {
        // expose each component
        './CounterAppTwo': './src/components/CounterAppTwo',
      },
      shared: {
        ...deps,
        react: { singleton: true, eager: true, requiredVersion: deps.react },
        'react-dom': {
          singleton: true,
          eager: true,
          requiredVersion: deps['react-dom'],
        },
      },
    }),
    new HtmlWebpackPlugin({
      template: './public/index.html',
    }),
  ],
};
Enter fullscreen mode Exit fullscreen mode

按如下方式更新您的package.json脚本:

"scripts": {
    "start": "webpack serve --open",
    "build": "webpack --config webpack.prod.js",
    "serve": "serve dist -p 3002",
    "clean": "rm -rf dist"
}
Enter fullscreen mode Exit fullscreen mode

更新您的tsconfig如下内容:

{
  "compilerOptions": {
    "target": "es5",
    "lib": ["dom", "dom.iterable", "esnext"],
    "allowJs": true,
    "skipLibCheck": true,
    "esModuleInterop": true,
    "allowSyntheticDefaultImports": true,
    "strict": true,
    "forceConsistentCasingInFileNames": true,
    "noFallthroughCasesInSwitch": true,
    "module": "esnext",
    "moduleResolution": "node",
    "resolveJsonModule": true,
    "isolatedModules": true,
    "noEmit": false,
    "jsx": "react-jsx"
  },
  "include": ["src"]
}
Enter fullscreen mode Exit fullscreen mode

编辑index.html

<html>
  <head> </head>
  <body>
    <div id="root"></div>
  </body>
</html>
Enter fullscreen mode Exit fullscreen mode

现在进入每个项目并运行,yarn start然后导航到 localhost:3000。如果你打开
开发者控制台的“源”选项卡,你会看到每个应用来自不同的端口。

来自不同端口的应用程序

围捕

优点

  • 更易于维护
  • 更容易测试
  • 独立部署
  • 提高团队的可扩展性

缺点

  • 需要大量配置
  • 如果其中一个项目崩溃也可能影响其他微前端
  • 在开发过程中,后台运行多个项目

本质上,它非常简单,将一堆应用整合到同一个网站,并由不同的服务器提供服务。如果你正在处理庞大的代码库,那么这绝对是一项非常棒的技术
。将庞大的组件解耦成小型应用,感觉轻而易举。我希望我鼓励你尝试一下微前端。

鏂囩珷鏉ユ簮锛�https://dev.to/ogzhanolguncu/introduction-to-micro-frontends-with-module-federation-react-and-typescript-nij
PREV
我正在创建一个新的 ✨JavaScript✨ 框架
NEXT
使用 PostgreSQL 持久化 Node API,无需借助 Sequelize 之类的 ORM。dev.to 上的 Node API 教程代码库 AwesomeNodePostgres