W

Webpack 5 和模块联合——微前端革命

2025-05-25

Webpack 5 和模块联合——微前端革命

想象一下:你拥有一个非常酷炫的组件,它可不是普通的组件,而是那种似乎在每个页面上都存在的经典组件……你知道页眉,或者页眉中的身份验证状态,以及主页上的 CTA(行动号召按钮)……你明白了。到目前为止,你可能只是简单地将该组件作为 npm 包共享,然后在其中独立构建和部署每个应用程序,从而实现代码共享。这看似合理,但总觉得哪里不太对劲。

如果你和我一样,你一定经历过设计师要求你修改某个炫酷组件的边框或背景颜色时的痛苦。你害怕部署时不得不重新构建每一个应用。也许你会很幸运,一切顺利,但也可能并非如此。你可能会影响正常运行时间,或者你正在静态生成代码,现在你的后端会因为急于快速构建 25 万个排列组合以完成这项更改而承受巨大压力(我个人就经历过这种情况)。

引入模块联邦!🎉

模块联合旨在解决分布式系统中模块共享的问题,通过将这些关键的共享模块以宏或微的形式传输,以满足您的需求。它通过将模块从构建管道和应用程序中拉出来实现这一点。

为了实现这一点,需要理解两个主要概念:主机(Host ) 和远程 (Remote )。

主持人

宿主应用是一种可以冷加载的组件。通常,它通常由事件初始化。宿主应用包含 SPA 或 SSR 应用的所有典型功能。它会加载所有初始代码块,启动应用并渲染用户首先看到的内容。这里的主要区别在于,宿主应用不是将那个臭名昭著的超级共享组件远程捆绑,而是被引用。为什么?因为该组件是Remotewindow.onload的一部分

您会看到,这种方法的优点在于,您可以拥有加载第一个应用程序所需的关键 JavaScript,并且只需要它;这符合微前端 (MFE) 的理念。

示例配置:

const ModuleReferencePlugin = require("webpack/lib/container/ContainerReferencePlugin");

new ModuleReferencePlugin({
    remoteType: 'global',
    remotes: ['app_one', 'app_two'],
    overrides: {
        react: 'react',
    }
});
Enter fullscreen mode Exit fullscreen mode

偏僻的

远程设备既可以是主机,也可以是严格意义上的远程设备。远程设备的作用是提供(或者更确切地说是提供)expose可供其他主机远程设备使用的模块。

您还可以选择让此远程共享其部分(或全部)依赖项;如果主机已经有react,只需将其发送到此运行时,从而允许远程不必下载其自己的 react 副本。

示例配置:

const ModuleContainerPlugin = require("webpack/lib/container/ContainerPlugin");

new ModuleContainerPlugin({
    name: 'app_one',
    library: { type: 'global', name: 'app_a' },
    shared: {
        react: 'react',
    },
    exposes: {
        Title: './src/components/Title'
    }
});
Enter fullscreen mode Exit fullscreen mode

工作原理

从上图可以看出,a、b、c 都是来自不同应用的公开组件。B 和 C 来自我们的 app_three 容器,而 A 来自我们的 app_two 容器。这三个组件组合在一起构成了我们的绿色组件,您也可以将其公开!

为了使事情变得更简单、更统一;我们有:

联邦插件🕺

重要的一点!

但大多数时候,您会希望您的应用程序能够公开和/或使用联合模块。

为此,我们有一个插件来统治它们!

const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");

new ModuleFederationPlugin({
    name: 'app_two',
    library: { type: 'global', name: 'app_a' },
    remotes: {
      app_one: 'app_one',
      app_three: 'app_three'
    },
    exposes: {
       AppContainer: './src/App'
    },
    shared: ['react', 'react-dom', 'relay-runtime']
}),
Enter fullscreen mode Exit fullscreen mode

上面你看到的是一个应用程序,它可以托管自己的 副本reactreact-dom并且relay暴露自己的副本AppContainer——但它能够Title从 app_one 导入 ,并让主机共享依赖项reactreact-dom甚至可能relay-runtime与远程服务器共享。这意味着在远程服务器中加载时只会下载驱动该组件所需的代码,而不会下载任何共享模块。

在实践中,这将允许您让每个 MFE 公开其路线图,通常是您要提供的组件片段react-router

// AboutApp
// routes.jsx

export default () => (
    <Routes>
        <Route path="/about" component={About} />
    </Routes>
)

// AboutUserApp
// routes.jsx

export default () => (
    <Routes>
        <Route path="/about/:slug" component={AboutUser} />
    </Routes>
)
Enter fullscreen mode Exit fullscreen mode

将 routes.jsx 文件标记为 AboutApp 和 AboutUserApp 在其各自的 webpack 配置中的导出成员。

// HomepageApp
// routes.jsx

import { lazy } from 'react';

const AboutAppRoutes = lazy(() => import('AboutApp/routes'));
const AboutUserAppRoutes = lazy(() => import('AboutUserApp/routes'));

// assuming you have suspense higher up in your tree 🤞
export default () => (
    <Routes>
        <Route path="/" component={Homepage} />
        <AboutAppRoutes />
        <AboutUserAppRoutes />
    </Routes>
)
Enter fullscreen mode Exit fullscreen mode

瞧!您已经拥有了一个惰性联合应用程序!

魔法

由此,关于应用程序和关于用户应用程序都是从各自的捆绑包中加载的 - 但表现得就像它们一开始就捆绑在一起一样!

这还不是全部,如果您现在还可以将路由器包装在 中AppContainer,您通常会在其中共享页眉和页脚!

// AppContainerApp
// container.jsx

export default ({ title, children }) => (
    <>
        <Helmet>
            <title>{title}</title>
        </Helmet>
        <Header/>
        <main>
            {children}
        </main>
        <Footer/>
    </>
)
// Please don't actually do the Helmet part, re-renders are bad!

// HomepageApp
// App.jsx

import * as React from 'react';
import { render } from 'react-dom';
import { HashRouter } from 'react-router-dom';

import AppContainer from 'AppContainerApp/Container';
import RouterConfig from './routes';

const App = () => (
    <HashRouter>
        <Suspense fallback={'loading...'}>
            <AppContainer title="I'm the Homepage App">
                <RouterConfig />
            </AppContainer>
        </Suspense>
    </HashRouter>
);

render(App, document.getElementById('app'));

// AboutApp
// App.jsx

import * as React from 'react';
import { render } from 'react-dom';
import { HashRouter } from 'react-router-dom';

import AppContainer from 'AppContainerApp/Container';
import RouterConfig from './routes';

const App = () => (
    <HashRouter>
        <Suspense fallback={'loading...'}>
            <AppContainer title="I'm the About app">
                <RouterConfig />
            </AppContainer>
        </Suspense>
    </HashRouter>
);

render(App, document.getElementById('app'));
Enter fullscreen mode Exit fullscreen mode

太棒了!您有了一个应用程序;

  1. 有一个主页 MFE,可以独立于我们的关于应用程序构建、部署和运行。
  2. 具有关于 MFE 的信息,也可以单独构建、部署和 100% 运行。
  3. 两个应用程序共享相同的页眉和页脚。
  4. 关于路由已经懒惰地放到主页应用中了,所以……等等!你可以在两个应用之间进行 SPA 转换!而且只需下载这两个应用之间的差异部分。react等等react-router都是共享的,所以不需要重新下载!

哇喔

想想这些可能性:你可以分享你的设计系统,这样你就可以更改我们之前提到的组件的背景颜色,并有效地让所有东西在整个系统中保持常青!你可以分享每篇文章底部的行动号召 (CTA)。那么你想在结账和产品页面上放置的交叉销售组件呢?实际上,无限可能。

注意事项

听起来很神奇吧?但也有一些缺点。

  1. 这实际上是前端的微服务。所以版本问题肯定会出现。“为什么要引入一个重大变更”……对此,我建议使用Contract API 快照 Jest 测试。
  2. 如果您正在使用relay,则无法在包装潜在联合模块的查询上展开片段。因为片段可能已更改。为此,我建议使用QueryRenderer 组件
  3. 依赖于 React 上下文的模块,其提供程序永远不会暴露。诸如此类。
  4. 在这个阶段,加载正确的初始远程块相当繁琐。这需要提前知道块文件名并手动注入。不过我们有一些想法。
  5. 本地开发活动。目前还没有找到一种简洁好用的方法,不用一次性运行所有应用,不过我个人目前一直在使用 webpack 别名,将这些应用引用指向我的 mono-repo 中的文件夹。
  6. ...就是这样,在我所有的试验中,这个解决方案都没有出现任何初始问题。

社区

社区的反响非常热烈,Zack和我(Marais)非常感谢大家对我们的帮助,并揭示了许多潜在的极端情况,以及我们在发展这项技术时正在展开调查的用例!

Liquid 错误:内部

特别感谢:

SystemJS 的作者Joel Denning —— 引导我们探索 SystemJS 的世界,并让我们了解 importmaps 的奥秘,以及进一步探究依赖 URL 解析(目前这部分工作基本依靠手动完成)。感谢
Tobias Koopers的 Webpack,他为我们提供了如此出色的基础,并最终帮助我们编写出将这一概念变为现实所需的代码。感谢
AutoGuru给予我创作和实验的空间!

照片由 Daniel Fazio 在 Unsplash 上拍摄

文章来源:https://dev.to/marais/webpack-5-and-module-federation-4j1i
PREV
“编码占工作的30%”
NEXT
我如何获得多个面试电话和推荐?