我们使用单个网页的日子已经一去不复返了。现代 Web 开发提供丰富的用户体验,涵盖用户流和交互的策略。构建、维护、部署和交付这些体验需要大规模的开发团队和复杂的部署系统。
现代 Web 应用程序最常用的模式是单页应用程序 (SPA)。SPA 的核心原则是构建交付给用户的单个 Web 应用程序。SPA 通过根据用户交互或数据更改重写页面内容来工作。SPA 通常包含一个路由器来处理页面导航和深度链接,并且可以由多个组件组成——例如购物篮或产品列表。
典型的 SPA 应用程序流程遵循标准步骤:
用户访问 Web 应用程序
浏览器请求 JavaScript 和 CSS
JavaScript 应用程序启动并将初始内容添加到浏览器文档中
用户与应用程序交互——例如单击导航链接或将产品添加到购物车
应用程序重写浏览器文档的部分内容以反映更改
在大多数情况下,使用 JavaScript 框架来实现上述目的。React、Vue 或 Angular 等框架具有帮助构建 SPA 的模式和最佳实践。例如,React 是一个非常直观的框架,它使用 JSX 来根据用户和数据变化来呈现内容。让我们看下面的一个基本示例:
//App.js
import React from "react";
import "./styles.css";
const App = () => {
return (
Hello I'm a SPA ????
);
}
export default App;
这是我们的基本应用。它呈现一个简单的视图:
import React from "react";
import ReactDOM from "react-dom";
import App from "./App";
const rootElement = document.getElementById("root");
ReactDOM.render(
,
rootElement
);
接下来,我们通过将 React 应用程序渲染到浏览器 DOM 中来启动应用程序。这只是SPA的基础。从这里,我们可以添加更多功能,例如路由和共享组件。
SPA 是现代开发的主要内容,但它们并不完美。SPA 有很多缺点。
其中之一是搜索引擎优化的损失,因为应用程序在用户在浏览器中查看之前不会呈现。Google 的网络爬虫会尝试呈现页面,但不会完全呈现应用程序,您将丢失许多提升搜索排名所需的关键字。
框架复杂性是另一个缺点。如前所述,有许多框架可以提供 SPA 体验并允许您构建可靠的 SPA,但是每个框架针对不同的需求,并且很难知道采用哪种框架。
浏览器性能也可能是一个问题。由于 SPA 负责用户交互的所有呈现和处理,因此它可以根据用户的配置产生连锁反应。并非所有用户都会通过高速连接在现代浏览器中运行您的应用程序。要获得流畅的用户体验,需要尽可能减小包大小并减少客户端上的处理。
以上所有导致最终问题,即规模。尝试构建可以满足所有用户需求的复杂应用程序需要多个开发人员。在 SPA 上工作可能会导致许多人在处理相同的代码时试图进行更改并导致冲突。
那么所有这些问题的解决方案是什么?微前端!
微前端是一种架构模式,用于构建可扩展的 Web 应用程序,该应用程序随着您的开发团队的发展而增长,并允许您扩展用户交互。我们可以将其与我们现有的 SPA 联系起来,说它是我们 SPA 的一个切片版本。这个版本对用户来说仍然看起来和感觉像一个 SPA,但在引擎盖下,它根据用户的流程动态加载应用程序的一部分。
为了进一步解释这一点,让我们以比萨店应用程序为例。核心功能包括选择比萨饼并能够将其添加到您的购物篮并结帐。下面是我们应用程序的 SPA 版本的模型。
让我们通过考虑可以分割的应用程序的不同部分将其转变为微前端。我们可以像分解创建应用程序所需的组件一样考虑这一点。
所有的微前端都从一个宿主容器开始。这是将所有部分结合在一起的主要应用程序。这将是访问应用程序时发送给用户的主要 JavaScript 文件。然后我们转到实际的微前端——产品列表和购物车前端。这些可以在本地与主主机分离并作为微前端交付。
让我们更深入地研究“本地与主主机分离”。当我们想到传统的 SPA 时,在大多数情况下,您会构建一个 JavaScript 文件并将其发送给用户。使用微前端,我们只将主机代码发送给用户,并且根据用户流,我们进行网络调用以获取应用程序其余部分的附加代码。代码可以存储在与起始主机不同的服务器上,并且可以随时更新。这会导致更高效的开发团队。
有多种方法可以构建微前端。对于这个例子,我们将使用 webpack。Webpack 5 发布了模块联合作为核心功能。这允许您将远程 webpack 构建导入您的应用程序,从而为微前端提供易于构建和维护的模式。
完整的 webpack 微前端应用程序可以在这里找到。
首先,我们需要创建一个容器作为应用程序的宿主。这可以是应用程序的一个非常基本的骨架,也可以是在用户与产品交互之前带有菜单组件和一些基本 UI 的容器。使用 webpack,我们可以导入ModuleFederation
插件并配置容器和任何微前端:
// packages/home/webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
...
plugins: [
new ModuleFederationPlugin({
name: "home",
library: { type: "var", name: "home" },
filename: "remoteEntry.js",
remotes: {
"mf-products": "products",
"mf-basket": "basket",
},
exposes: {},
shared: require("./package.json").dependencies,
}),
new HtmlWebPackPlugin({
template: "./src/index.html",
}),
],
};
注意:您可以webpack.config.js
在此处查看GitHub 上的文件。
在这里,我们将模块命名为“home”,因为这是包含所有前端的容器。然后我们提供库的详细信息,因为容器也可以是一个微前端,所以我们声明了关于它的详细信息——例如它的类型,在这种情况下是var
. 类型定义了它是哪种webpack 模块类型。var
声明该模块是一个 ES2015 编译模块。
然后我们将产品和篮子模块设置为遥控器。这些将在以后导入和使用组件时使用。将模块导入应用程序时将使用我们给模块命名的名称(“mf-products”和“mf-basket”)。
配置模块后,我们可以将脚本标记添加到 home 的主index.html文件中,该文件将指向托管模块。在我们的例子中,这一切都在 localhost 上运行,但在生产中,这可能是在 Web 服务器或 Amazon S3 存储桶上。
//product list
//basket
注意:您可以index.html
在此处查看GitHub 上的文件。
home 容器的最后一部分是导入和使用模块。对于我们的示例,模块是 React 组件,因此我们可以使用React.lazy导入它们并像使用任何React 组件一样使用它们。
通过使用React.lazy
我们可以导入组件,但只有在呈现组件时才会获取底层代码。这意味着我们可以导入组件,即使它们未被用户使用,并在事后有条件地呈现它们。让我们来看看我们如何在行动中使用这些组件:
// packages/home/src/src/App.jsx
const Products = React.lazy(() => import("mf-nav/Products"));
const Basket = React.lazy(() => import("mf-basket/Basket"));
注意:您可以App.jsx
在此处查看GitHub 上的文件。
这里与标准组件使用的主要区别是React.lazy。这是一个内置的 React 函数,用于处理代码的异步加载。正如我们过去常常在使用React.lazy
时获取代码一样,我们需要将组件包装在Suspense 组件中。这做了两件事:触发组件代码的获取,并呈现加载组件。除了 Suspense 组件和回退组件之外,我们可以像使用任何其他 React 组件一样使用我们的微前端模块。
配置完home容器后,我们需要设置product和basket模块。这些遵循与家庭容器类似的模式。首先,我们需要导入 webpackModuleFederation
插件,就像我们在 home 容器的 webpack 配置中所做的那样。然后我们配置模块设置:
// packages/basket/webpack.config.js
const ModuleFederationPlugin = require("webpack/lib/container/ModuleFederationPlugin");
module.exports = {
...
plugins: [
new ModuleFederationPlugin({
name: 'basket',
library: {
type: 'var', name: 'basket'
},
filename: 'remoteEntry.js',
exposes: {
'./Basket': './src/Basket'
},
shared: require('./package.json').dependencies
})
],
};
注意:您可以webpack.config.js
在此处查看GitHub 上的文件。
我们为模块提供了一个名称,即产品或购物篮以及图书馆的详细信息,然后是一个fileName
——在这种情况下是远程条目。这是 webpack 的标准,但它可以是你想要的任何东西——例如产品代码名称或模块名称。这将是 webpack 生成的文件,它将被托管以供主容器引用。使用文件名 remoteEntry,模块的完整 URL 将为http://myserver.com/remoteEntry.js.
然后我们定义了暴露选项。这定义了模块导出的内容。在我们的例子中,它只是 Basket 或 Products 文件,这是我们的组件。但是,这可能是多个组件或不同的资源。
最后,回到home容器中,您可以这样使用这些组件:
// packages/home/src/src/App.jsx
....loading product list
}>
{
selected.length > 0 &&
....loading basket
注意:您可以Product and Basket usage
在此处查看GitHub 上的文件。
我们还没有讨论依赖关系。如果您从上面的代码示例中注意到,每个 webpack 模块配置都有一个共享配置选项。这告诉 webpack 哪些 Node 模块应该在微前端之间共享。这对于减少最终应用程序的重复非常有用。例如,如果篮子和家庭容器都使用样式组件,我们不想加载两个版本的样式组件。
您可以通过两种方式配置共享选项。第一种方法是列出您知道要共享的已知共享 Node 模块。另一种选择是从它自己的包 JSON 文件中输入模块依赖项列表。这将共享所有依赖项,并且在运行时 webpack 将确定它需要哪个。例如,当 Basket 被导入时,webpack 将能够检查它需要什么,以及它的依赖项是否已共享。如果篮子使用了 Lodash,而 home 没有,它将从篮子模块中获取 Lodash 依赖项。如果 home 已经有 Lodash,它不会被加载。
这一切听起来都很棒——好得令人难以置信。在某些情况下,它是完美的解决方案。在其他情况下,它可能会导致超出其价值的开销。尽管微前端模式可以使团队更好地合作并快速推进应用程序的各个部分,而不会因繁琐的部署管道和杂乱的 Git 合并和代码审查而减慢速度,但仍有一些缺点:
重复的依赖逻辑。正如依赖部分中提到的,webpack 可以为我们处理共享的 Node 模块。但是当一个团队使用 Lodash 作为其功能逻辑而另一个团队使用 Ramda 时会发生什么?我们现在发布了两个函数式编程库来实现相同的结果。
设计、部署和测试的复杂性。既然我们的应用程序动态加载内容,就很难全面了解整个应用程序。确保跟踪所有微前端本身就是一项任务。部署可能会变得更具风险,因为您不能 100% 确定在运行时加载到应用程序中的内容。这导致更难的测试。每个前端都可以单独测试,但需要进行完整的、真实的用户测试,以确保应用程序适合最终用户。
标准。现在应用程序被分解成更小的部分,很难让所有开发人员都按照相同的标准工作。一些团队可能比其他团队进步更多,并提高或降低代码质量。让每个人都在同一页面上对于提供高质量的用户体验很重要。
成熟度:微前端并不是一个新概念,在使用 iframe 和自定义框架之前已经实现。然而,webpack 直到最近才将这个概念作为 webpack 5 的一部分引入。它对 webpack 捆绑的世界来说仍然是新的,并且有很多工作来构建标准并发现这种模式的错误。要使其成为一个强大的、生产就绪的模式,让使用 webpack 的团队可以轻松使用,还有很多工作要做。
因此,我们学习了如何使用 webpack 模块联合构建 React 应用程序,以及如何在微前端之间共享依赖项。与部署和发布过程缓慢的传统 SPA 应用程序相比,这种构建应用程序的模式非常适合团队将应用程序分解为更小的部分,以实现更快的增长和进步。显然,这不是适用于所有用例的灵丹妙药,但在构建下一个应用程序时需要考虑这一点。由于一切都还很新,我建议您尽早采用微前端以进入底层,因为从微前端模式转移到标准 SPA 比其他方式更容易。
如果你感觉写得不错,帮我点个
[在看、赞、关注]
吧让我们一起成为前端架构师!