使用 Firebase 构建微前端

您正在阅读的帖子是从我的博客网站转发的。请参阅我在 andrewevans.dev 上的原始帖子。

最近我一直在使用 Microfrontends,想写一篇涵盖一些基本概念并包含示例项目的文章。我也是 Firebase 的粉丝,想介绍如何使用 Firebase 构建 Microfrontend。我在这个项目中使用了 React,但如果您想选择不同的前端库或框架,则应应用相同的基本原则。总的来说,这篇文章将介绍什么是 Microfrontend,并展示一个向您展示工作项目的示例项目。如果您想继续或直接跳到代码,请查看我的 GitHub repo firebase-mfe。

什么是微前端?

微前端(简称 MFE)已经存在好几年了,但其核心原则和模式始终如一。MFE 是一种将大型应用程序分解为可独立部署的较小部分的方法。MFE 的商业价值在于能够将大型应用程序分解为可以更快地开发和发布的较小团队。

以一个由 50 名开发人员维护的 Web 应用程序为例。假设此应用程序实际上由多个页面组成,这些页面是独立的部分(例如购物车页面、信息页面等)。开发人员每次进行更改,都可能影响同一共享应用程序的任何部分。这通常称为“整体”,其中所有内容都在同一代码库中运行,任何更改都会影响整个应用程序。

Monolith Visual

相反,考虑将拥有 50 名开发人员和多个页面的同一应用程序拆分成多个部分。想象一下让特定团队专注于应用程序的特定部分,而不必担心可能破坏或阻止其他团队的进度。相反,50 名开发人员的示例可以被视为一个拥有 5 个团队(每个团队 10 名开发人员)的项目。每个团队专注于前端应用程序的特定区域(或页面)。这是此模式的真正价值,因为它可以解决管理大型系统的许多痛点。

为了直观地展示这一点,还必须指出 MFE 与“主机”和“远程”一起工作。“主机”将应用程序作为主机进行管理,并使用“远程”应用程序。“远程”公开组件,然后由主机使用。修改早期的可视化效果以使用 MFE 进行显示:

MFE Host and Remote Visual

每个“远程”都应该能够独立开发和部署。“主机”还可以管理诸如令牌或共享设置之类的内容,这些内容可以由远程使用。“远程”可以从“主机”获取值作为组件内的 props。这非常强大,因为这意味着在企业环境中,团队可以管理诸如应用程序设置和身份验证之类的内容,然后远程团队都可以使用相同的值。这意味着每个团队不必经历设置自己的资源的过程。

从本质上讲,MFE 还利用了 Module Federation 和 Webpack。Module Federation 具有以下关键概念:

  • 通过公开或使用(而不是一次性构建所有内容)在应用程序的各个部分之间共享组件(或模块)
  • 在应用程序内共享依赖项(不必在多个区域复制相同的代码)
  • 按需加载依赖项(仅加载您需要的)
  • 独立部署和开发应用程序的各个部分
  • 通过模块联合,“主机”将“使用”远程设备,而远程设备将“公开”组件以供“主机”使用。

    如果您对此主题进行一些 Google 搜索,您会发现许多人对此实现的方式有不同的看法。有多种方法来管理这样的架构。总体概念仍然相同。在接下来的部分中,我将分享我的示例应用程序 firebase-mfe。并展示它如何使用 Firebase 实现 MFE 架构。

    使用 Firebase 构建 MFE

    开始之前,您需要转到 Firebase 控制台并创建一个应用程序。如果您是 Firebase 新手,我建议您查看其基础知识页面。Firebase 的初始设置大部分都是免费的。某些服务需要随着时间的推移付费,但就本项目而言,您不应产生任何费用。

    Firebase register app page

    一旦设置好项目,请确保添加一个应用程序,它将产生如下配置值:

    const firebaseConfig = {
      // Add your Firebase config here
      apiKey: "",
      authDomain: "",
      projectId: "",
      storageBucket: "",
      messagingSenderId: "",
      appId: "",
    };

    设置完这些值后,您还应该转到身份验证服务并添加测试用户。我的应用程序使用传递到远程服务器的 Auth 值,因此我们需要一个测试用户来验证所有内容。

    Firebase setup authentication page

    完成所有这些设置后,您现在可以转到应用程序进行设置。

    构建主机

    对于我的示例应用程序,我使用了 React,但您也可以轻松使用您选择的其他库或框架。最重要的是它必须能够使用 Webpack,这样您就可以使用 Module Federation。

    由于我有一个完整的工作项目要分享,因此我只想强调需要配置才能使一切正常运行的地方。如果有人愿意,这个示例项目的一般设置可以扩展到更大的项目。另一点是,我的示例项目都在同一个存储库中,但如果您愿意,您可以将您的项目完全放在单独的存储库中。MFE 最好的部分之一是您可以灵活地根据各种项目需求自定义设置。

    在我的示例应用程序中我有三个项目:

  • 主持人
  • 远程1
  • 远程2
  • `host` 是我存储 Firebase 配置的地方,您可以在 `host/src/firebase.ts` 中看到它:

    import { initializeApp } from "firebase/app";
    import { getAuth } from "firebase/auth";
    
    const firebaseConfig = {
      // Add your Firebase config here
      apiKey: "",
      authDomain: "",
      projectId: "",
      storageBucket: "",
      messagingSenderId: "",
      appId: "",
    };
    
    export const app = initializeApp(firebaseConfig);
    export const auth = getAuth(app);

    接下来,您会注意到,在示例应用程序中,我的“index.tsx”文件旁边有一个“bootstrap.tsx”文件。此文件由“index.tsx”读取以呈现我的应用程序。远程服务器也将有一个“bootstrap.tsx”文件,但我将在完成这部分后介绍它。

    // src/bootstrap.tsx
    import React from "react";
    import { createRoot } from "react-dom/client";
    import App from "./App";
    
    const root = document.getElementById("root");
    if (root) {
      createRoot(root).render();
    }

    现在如果你转到 `App.tsx` 文件,你会注意到顶部有 2 个导入,其中包含 2 个远程:

    const Remote1 = React.lazy(() => import("remote1/App"));
    const Remote2 = React.lazy(() => import("remote2/App"));

    您还会注意到,遥控器是在 `App.tsx` 组件的“视图”中定义的:

    
      
    {!authState.user ? ( ) : (

    Welcome {authState.user.email}

    )}

    还要注意的是,“用户”对象作为 prop 传递给两个遥控器。我们需要在下一节中解释“remote1”和“remote2”项目时考虑到这一点。

    如果我们跳转到 `host/config/webpack.config.js` 文件,你会在底部注意到如何使用 Module Federation 插件进行设置:

    config.plugins.push(
      new ModuleFederationPlugin({
        name: "host",
        remotes: {
          remote1: isEnvDevelopment
            ? "remote1@http://localhost:3001/remoteEntry.js"
            : "remote1@https://remote1-11182024.firebaseapp.com/remoteEntry.js",
          remote2: isEnvDevelopment
            ? "remote2@http://localhost:3002/remoteEntry.js"
            : "remote2@https://remote2-11182024.firebaseapp.com/remoteEntry.js",
        },
        shared: {
          react: {
            singleton: true,
            requiredVersion: require("../package.json").dependencies.react,
            eager: false, // Add this line
          },
          "react-dom": {
            singleton: true,
            requiredVersion: require("../package.json").dependencies["react-dom"],
            eager: false, // Add this line
          },
          firebase: {
            singleton: true,
            requiredVersion: require("../package.json").dependencies.firebase,
            eager: false, // Add this line
          },
        },
      }),
    );

    这里请注意以下几点:

  • remotes 块具有远程项目的定义,其前缀为 remote1 和 remote2。您会注意到,这与 App.tsx 文件中的导入相同。
  • 两个远程服务器均根据 isEnvDevelopment 标志是否设置而定义。如果在开发中,则从本地主机加载,否则指向已部署的站点。
  • 两个遥控器都记录了 remoteEntry.js 文件,这是由主机编译和读取的内容。
  • 还要注意,在共享部分,它根据远程内容指定 react、react-dom 和 firebase 的版本。
  • 请注意,此处显示的 React 应用已作为 `create-react-app` 项目的一部分“弹出”。有很多方法可以做到这一点,但我发现这是使用此类示例项目进行设置的最简单的方法。

    通过上述设置,您还需要运行“firebase init”并通过终端将项目设置为“firebase”项目。

    Firebase init terminal screen

    然后,您必须进入 Firebase 控制台,或运行命令以在项目中生成可以部署到的“站点”。以下是我运行的命令:

    firebase hosting:sites:create host-11182024
    firebase target:apply hosting host-11182024 -host-11182024

    请注意,我将主机命名为“host-11182024”,这只是我能记住的一个值。您可以将主机(和遥控器)命名为任何您想要的名称。

    我为主机项目取的名字是任意的。我还必须运行这些命令几次才能找出正确的设置。最终我得到了一个如下所示的“firebase.json”文件:

    {
      "hosting": {
        "target": "host-11182024",
        "public": "build",
        "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
        "rewrites": [
          {
            "source": "**",
            "destination": "/index.html"
          }
        ]
      }
    }

    我最终得到了一个如下所示的 `.firebaserc` 文件:

    {
      "projects": {
        "default": ""
      },
      "targets": {
        "": {
          "hosting": {
            "host-11182024": ["host-11182024"]
          }
        }
      },
      "etags": {}
    }

    最终,这些只是设置,因此当您部署时,您会将包发送到项目中的特定位置。在我的示例项目中,我使用了“host-11182024”,最终我使用了部署命令“firebase deploy --only hosting:host-11182024”。接下来我们将对远程项目进行类似的设置。

    构建遥控器

    示例项目中的远程项目位于 `remote1` 和 `remote2` 文件夹中。我将只介绍 `remote1`,您可以对 `remote2` 执行相同的过程。

    在remote1中,首先查看`bootstrap.tsx`文件:

    import React from "react";
    import { createRoot } from "react-dom/client";
    import App from "./App";
    
    const root = document.getElementById("root");
    if (root) {
      // Don't render anything in standalone mode for production
      if (process.env.NODE_ENV === "development") {
        import("./devBootstrap");
      } else {
        createRoot(root).render(
          
    This is a microfrontend that needs to be loaded from a host application.
    , ); } }

    请注意,我们考虑的是开发模式。`devBootstrap.tsx` 文件包含一个模拟用户。这是因为我们将 `user` 对象从主机传递到远程。有了模拟值,您就可以独立于主机运行远程以进行调试等。这是一个很好的例子,说明如何将令牌或其他值从 `host` 传递到 `remote`。仅供参考,继续解释,这就是 `devBootstrap.tsx` 文件的样子:

    import React from 'react'
    import { createRoot } from 'react-dom/client'
    import App from './App'
    import { User } from 'firebase/auth'
    
    const mockUser: User = {
        email: 'test@example.com',
        uid: 'test-uid',
        emailVerified: true,
        isAnonymous: false,
        metadata: {},
        providerData: [],
        refreshToken: '',
        tenantId: null,
        delete: async () => {},
        getIdToken: async () => '',
        getIdTokenResult: async () => ({
            token: '',
            authTime: '',
            issuedAtTime: '',
            expirationTime: '',
            signInProvider: null,
            claims: {},
            signInSecondFactor: null,
        }),
        reload: async () => {},
        toJSON: () => ({}),
        displayName: 'test user',
        phoneNumber: '123-123-1234',
        photoURL: '',
        providerId: '',
    }
    
    const root = document.getElementById('root')
    if (root) {
        createRoot(root).render()
    }

    在 `remote1` 项目中,我们接下来可以查看 `host/config/webpack.config.js` 文件,我们在其中定义了模块联合插件:

    new ModuleFederationPlugin({
      name: "remote1",
      filename: "remoteEntry.js",
      exposes: {
        "./App": "./src/App",
      },
      shared: {
        react: {
          singleton: true,
          requiredVersion: require("../package.json").dependencies.react,
        },
        "react-dom": {
          singleton: true,
          requiredVersion: require("../package.json").dependencies["react-dom"],
        },
        firebase: {
          singleton: true,
          requiredVersion: require("../package.json").dependencies.firebase,
        },
      },
    });

    请注意,名称“remote1”与“host”webpack 配置中的值名称一致。另请注意,“exposes”字段指向加载主应用程序的项目“App.tsx”文件。“shared”部分中的其他定义与我们在“host”配置文件中的同一位置定义值的方式类似。

    与我为 `host` 项目设置 Firebase “站点” 的方式类似,我们可以在这里使用类似的命令执行相同的操作:

    firebase hosting:sites:create remote1-11182024
    firebase target:apply hosting remote1-11182024 -remote1-11182024

    然后当我们想要部署的时候,我们调用 Firebase 命令“firebase deploy --only hosting:remote1-11182024”。

    通过 firebase 初始化设置,对于 `remote1` 项目,我们获得以下 `firebase.json` 值:

    {
      "hosting": {
        "target": "remote1-11182024",
        "public": "build",
        "ignore": ["firebase.json", "**/.*", "**/node_modules/**"],
        "rewrites": [
          {
            "source": "**",
            "destination": "/index.html"
          }
        ]
      }
    }

    类似地,我们还获得以下`.firebaserc`文件值:

    {
      "projects": {
        "default": ""
      },
      "targets": {
        "": {
          "hosting": {
            "remote1-11182024": ["remote1-11182024"]
          }
        }
      },
      "etags": {}
    }

    如果您查看“remote2”项目,您将看到基本相同的设置。唯一的区别是远程的名称是“remote2”,而我创建的 Firebase“站点”是“remote2-11182024”。

    观察一切实际行动

    如果您想查看我的示例项目在本地运行,您只需在自己的终端会话中运行每个项目即可。每个项目都在不同的端口上运行,如果您打开“端口 3000”处的“主机”页面,您将看到应用程序正在运行并拉入远程服务器:

    Host page loaded correctly

    我在 hello@gmail.com 创建了一个示例帐户,以便我可以登录并验证值是否确实正确传递。

    请注意,“主机”具有已登录用户的属性。这些属性被传递到远程,而项目无需配置身份验证设置。这只是“主机”和远程项目之间可以交换的内容的一个示例。能够像这样集中配置是 MFE 项目最强大的部分之一。

    如果你打开 Chrome DevTools,你会看到你所看到的似乎是一个常规的 React 应用程序:

    Host page in Chrome DevTools

    但是,如果你打开网络选项卡,你会看到拉入的内容来自不同的端点:

    Remote1 loaded in Chrome DevToolsRemote2 loaded in Chrome DevTools

    当你查看本地运行的各个页面时,你会看到 `devBootstrap` 在运行,因为用户的值被模拟:

    Remote1 loaded locallyRemote2 loaded locally

    经验教训

    到目前为止,我已经讨论了 MFE 背后的一般概念,并向您展示了如何从技术上构建自己的 MFE。我对 MFE 的一般经验是,设置过程往往需要进行一些实验才能正确完成。幸运的是,有相当多的 YouTube 视频和博客文章涵盖了 MFE 概念。我还能够利用 Claude AI 来帮助入门和完成部分实施。我建议使用所有这些资源(和 Claude)来帮助您进行初始设置。

    获取 Module Federation 插件公开的值可能会导致一些问题。在我参与的另一个 MFE 项目中,我为 `remoteEntry.js` 文件取了错误的名称,直到所有东西都部署完毕后我们才看到它,并且远程无法加载。

    此外,我遇到了一些关于急切消费的错误:

    Error: Shared module is not available for eager consumption: webpack/sharing/consume/default/react/react

    这就是为什么在我的 `host` webpack 配置的 Module Federation 部分中你会看到这样的 `eager: false`:

    shared: {
          react: {
            singleton: true,
            requiredVersion: require("../package.json").dependencies.react,
            eager: false, // Add this line
          },
        }

    在加载组件时能够正确处理错误也很重要。如果您在“主机”项目中注意到,我已经用“ErrorBoundary”包裹了“App.tsx”值的“视图”:

    
      
    {!authState.user ? ( ) : (

    Welcome {authState.user.email}

    )}

    ErrorBoundary 组件是 React 的错误边界 (Error Boundary) 的实现,可以在 host/src/components/ErrorBoundary.tsx 文件中看到:

    import React, { Suspense } from 'react'
    
    // Error Boundary Component
    class ErrorBoundary extends React.Component<
        { children: React.ReactNode },
        { hasError: boolean; error: Error | null }
    > {
        constructor(props: { children: React.ReactNode }) {
            super(props)
            this.state = { hasError: false, error: null }
        }
    
        static getDerivedStateFromError(error: Error) {
            return { hasError: true, error }
        }
    
        render() {
            if (this.state.hasError) {
                const styles = {
                    container: {
                        padding: '1rem',
                        backgroundColor: '#fef2f2',
                        border: '1px solid #fecaca',
                        borderRadius: '8px',
                    },
                    heading: {
                        color: '#dc2626',
                        fontWeight: 600,
                        marginBottom: '0.5rem',
                    },
                    message: {
                        color: '#ef4444',
                    },
                    button: {
                        marginTop: '0.75rem',
                        padding: '0.5rem 1rem',
                        backgroundColor: '#fee2e2',
                        color: '#dc2626',
                        border: 'none',
                        borderRadius: '4px',
                        cursor: 'pointer',
                    },
                }
    
                return (
                    

    Failed to Load Component

    {this.state.error?.message}

    ) } return this.props.children } } export default ErrorBoundary

    总结

    总的来说,我希望这篇文章向您展示了 MFE 的工作原理,并展示了如何使用 Firebase 构建一个 MFE。MFE 是一个强大的概念,团队可以使用它来管理更大的项目。MFE 还具有很大的灵活性,您可以微调您的项目以满足各种需求。

    在我的 Firebase 实现中,我希望您也注意到了 Firebase 的使用是多么简单。我是 Firebase 的长期粉丝,并且喜欢它只需进行一些基本配置即可轻松完成设置。我强烈推荐 Firebase 用于项目,尤其是像我为这篇文章创建的示例项目。

    我建议你看一下我的示例项目,以及查看其他帖子和 YouTube 视频,了解 MFE 的工作原理。Jack Herrington 在 YouTube 上有一些很棒的视频,比如这个,它介绍了 MFE 的一些高级概念和示例。Webpack 还提供了有关 Module Federation 插件的文档,你可以查看一下。

    希望我的博文能帮助您更好地了解 MFE 以及如何使用 Firebase 构建它们。感谢您阅读我的博文!