三年多一点以前,我在LogRocket的一篇文章《驯服前端单体》中提出了通过某种发现机制形成微前端的想法。这种发现机制,即所谓的feed服务,是我所参与的一个解决方案的核心。
这个解决方案就是Piral,半年后我们在O'Reilly的软件架构会议上正式公布了它。
今天,Piral是微前端领域中最常用和最知名的解决方案之一。仅仅这一点就可以证明另一篇博文的合理性--但随后,我们也看到了微型前端的日益普及,以及对可扩展性的普遍需求。
闲话少说,让我们来看看什么是微前端,为什么松散耦合对它们如此重要,以及Piral如何解决这个问题(和其他问题)以实现巨大的可扩展性。我们将在下面的章节中介绍这些:
什么是微型前台?
近年来,微型前端变得越来越流行。其原因之一是对大型网络应用的需求增加。今天,强大的门户,如AWS和Azure门户,以及丰富的用户体验,如Netflix或DAZN,已经不是例外,而是常态了。如何构建这样的大型应用?如何扩大它们的规模?
这些问题的一个答案可以是使用微前端。一个微前端是一个业务子域的技术代表。其基本思想是孤立地开发用户界面的一个部分。这一块不需要在屏幕上表现为一个区域;它实际上可以由多个片段组成,即一个菜单项和该菜单项所链接的页面。唯一的限制是,这块应该与一个业务子域相关。
一个微型前端由不同的组件组成,但这些不是经典的UI组件,如下拉菜单或富文本字段框。相反,这些组件是特定领域的组件,包含一些业务逻辑,例如需要提出哪些API请求。
在这种情况下,即使是像菜单项这样简单的东西也是一个领域组件,因为它已经知道通往该页面的链接来自同一个业务领域。一旦该组件有了一些领域逻辑,它就是一个领域组件--因此可以成为微前端的一部分。
为了实现微前端,有一整套的方法和实践。它们可以在构建时,在服务器端和客户端汇集在一起。
在这篇文章中,我们将看看在客户端的组合,但同样的故事也可以写在服务器上。那么,微前端究竟是如何扩展的呢?
其他微前端框架的可扩展性问题
许多微前端框架在现实世界的背景下面临着可扩展性问题。看看其他文章,技术一开始似乎很健全;例如,如果你读了《Creating micro-frontends apps with single-spa》或《Building Svelte micro-frontends with Podium》,它们很好地介绍了技术和用例。另一个例子可以在Build progressive micro-frontends with Fronts中看到。
问题是,这些框架通常试图在视觉上分割UI。然而,在现实中,你永远不会把你的前端分成 "导航"、"页眉"、"内容 "和 "页脚 "等部分。这是为什么呢?
一个真正的应用程序是由不同的部分组成的,正如上一节所解释的,这些部分来自不同的子域,这些子域聚集在一起形成了完整的应用程序领域。虽然这些子域在纸面上可以完全分开,但它们通常出现在终端用户的同一个布局元素中。
想一想像一个网络商店一样的东西。如果你有一个子域处理产品细节,另一个子域处理以前的订单,那么你不会希望作为一个用户在你的订单历史中只看到毫无意义的产品的ID。相反,你会希望在订单历史中至少显示产品名称和一些细节。所以,这些子域在视觉上向终端用户交错排列。
同样地,几乎每一个子域都有一些东西可以贡献给共享的UI布局元素,比如导航、页眉或页脚。因此,拥有专门处理导航区域的微型前端在实践中并没有多大意义,因为这个微型前端会收到来自其他团队的大量请求--并成为一个瓶颈。这样做会导致一个隐藏的单片机。
现在,有人可能会争辩说,不在微观前台设置导航会导致同样的变化需求,但这次是对应用程序外壳所有者的要求。这甚至会更糟。
那么,解决方案是什么呢?很明显,我们需要把这些东西解耦。因此,不要使用类似的东西。
import MyMenuItem1 from 'my-micro-frontend1';
import MyMenuItem1 from 'my-micro-frontend2';
import MyMenuItemN from 'my-micro-frontendN';
const MyMenu = () => (
<>
<MyMenuItem1 />
<MyMenuItem2 />
<MyMenuItemN />
</>
);
我们需要注册每一个必要的部分,例如来自微前端本身的导航项目。这样一来,我们最终可以得到这样一个结构。
const MyMenu = () => {
const items = useRegisteredMenuItems();
return (
<>
{items.map(({ id, Component }) => <Component key={id} />)}
</>
);
};
为了避免需要知道微型前端的名称和位置,需要一种发现。这只是一个JSON文档,可以从一个已知的位置,如后台服务中检索到。
现在我们知道我们需要扩展什么,是时候开始实施了。幸运的是,有一个框架让我们在这方面已经有了一个开始。Piral。
Piral有什么不同?
Piral是一个使用微前端创建超可扩展网络应用的框架。在许多方面,它的特点是:
- 一个集成的微前端发现机制
- 广泛的开发者体验,端到端提供你所需要的一切
- 具有跨框架支持的松散耦合的组件
这样一来,各个团队可以专注于他们特定的领域问题,而不需要对齐和联合发布。一个Piral应用程序由三部分组成:
- 应用外壳。这可以是一个简单的HTML文件,包含一个对微前端发现服务的引用,称为feed
- 微前端的feed(也可以是一个静态的JSON,但通常你会希望有更强大的功能)。
- 不同的模块(微前端),被称为pilets。
整个设置可以勾勒出如下内容:

模块开发者可以使用命令行工具piral-cli 来构建(即用一些模板来创建)和发布新的pilets,调试和更新现有的模块,或者执行一些提示和验证。真正的用户不会把解决方案看作是不同的部分--他们实际上是在一个应用程序中消费应用程序外壳和桩子。从技术上讲,这些堆积物是从一个feed服务中获取的。
很多时候,微型前端的开发经验并不理想。要么需要检查和启动多个东西,要么整个过程归结为开发-提交-尝试-失败-重新启动的循环。Piral在这方面有所不同--它试图做到离线优先。微前端直接在被称为模拟器的应用程序外壳的一个特殊版本中开发。

仿真器只是一个npm包,可以安装在任何npm项目中。当piral-cli 用于调试时,它实际上将使用模拟器中的内容作为显示的页面。然后,该厕所将通过内部的API提供服务,而不是到远程feed服务或类似的东西。
尽管如此,在开发过程中,加载现有的微前端可能仍然是有意义的。在这种情况下,来自现有feed的pilets仍然可以被集成。
让我们看看这一切在实践中是如何运作的。
用Piral开发你的应用程序外壳
有多种方法可以用Piral创建一个应用程序的外壳:
- 移植现有项目
- 手动添加软件包到一个新项目
- 使用
piral-cli来创建一个新的项目
在这篇文章中,我们要做的是后者。
在命令行中,我们运行:
npm init piral-instance --bundler esbuild --target my-app-shell --defaults
这将在my-app-shell 目录中创建一个新的应用程序外壳。该项目将使用npm、TypeScript和esbuild 工具作为我们的捆绑器(尽管我们实际上可以选择任何种类的捆绑器,如webpack、Parcel或Vite等)。在许多情况下,选择esbuild 应该是足够的,并提供了最快的安装时间的好处。
现在,我们可以开始调试该项目了。进入新的目录(例如:cd my-app-shell )并开始调试会话。
npm start
前往 [http://localhost:1234](http://localhost:1234)应该可以看到标准模板。

改变模板
我们现在可以在所有可能的方面改变模板。例如,我们可以改变所提供的布局,使其没有任何固定的内容瓦片;只需编辑src/layout.tsx 文件并删除defaultTiles 和defaultMenuItems 。确保不仅删除它们的初始化,而且还删除对它们的引用。
为了更加详细,我们可以将DashboardContainer 从:
DashboardContainer: ({ children }) => (
<div>
<h1>Hello, world!</h1>
<p>Welcome to your new microfrontend app shell, built with:</p>
<div className="tiles">
{defaultTiles}
{children}
</div>
</div>
),
到:
DashboardContainer: ({ children }) => (
<div>
<h1>Hello, world!</h1>
<p>Welcome to your new microfrontend app shell, built with:</p>
<div className="tiles">
{children}
</div>
</div>
),
在这里可以看到的所有组件都有不同的用途。虽然它们中的许多来自于可选的插件,但有些--如ErrorInfo 或Layout --已经通过驱动Piral的核心库定义了。
在上面的例子中,我们为Piral的仪表板插件定义了仪表板容器。仪表盘插件为我们提供了一个仪表盘,它--默认情况下--位于我们页面的主页(/)。我们可以改变这里的一切,包括它的外观和它的位置(当然,还有我们是否想拥有一个仪表盘)。
仪表盘对于门户应用来说是很好的,因为它们在一个屏幕上收集了大量的信息。对于微型前端来说,仪表盘也是很好的--特别是作为一个展示窗口。在这个页面上,可能大多数(如果不是全部)的微型前端都想展示一些东西。
从仪表盘容器中移除默认的磁贴后,现在的Web应用看起来应该更空旷一些。

我们的Web应用程序空虚的主要原因是,我们没有集成任何渲染组件的微前端的地方。在这一点上,脚手架机制将我们的新应用外壳与Piral本身拥有的一个特殊的feed相连接:空的feed。
空馈送是一个馈送,顾名思义,将永远保持空。我们可以改变应用程序外壳的代码来代替其他的feed。
改变Feeds以显示不同的堆积物
为此,你需要打开src/index.tsx 文件。在那里,你会看到带有要使用的feed的URL的变量。
const feedUrl = 'https://feed.piral.cloud/api/v1/pilet/sample';
在另一个应用程序外壳中使用的样本馈送,我们实际上可以看到,如果由微型前端正确填充,外壳会是什么样子。仪表板现在应该看起来像:

事实上,我们已经可以在我们的新应用程序外壳中显示来自另一个feed的pilets,这实际上是相当酷的。这预示着这些桩子真的是独立的,与它们的外壳没有很大关系。
然而,请记住,这种顺利的整合可能并不总是可能的。桩子总是可以以一种相当容易集成到其他Piral实例中的方式来开发。同样地,以排除不同应用外壳的方式开发一个桩也是可能的。
创建应用仿真器
在我们为这个app shell开发一些微前端之前,我们需要创建它的仿真器。停止调试进程并运行以下命令:
npm run build
这将在当前项目上运行piral build 。其结果是,在dist 中有两个子目录:
dist/releasedist/emulator
虽然前者可以用来实际部署我们的网络应用,但后者包含一个.tgz 文件,可以上传到像这样的注册中心。
npm publish dist/emulator/my-app-shell-1.0.0.tgz
你可能需要npm凭证来发布软件包,但即使你已经登录了npm,你也可能不想发布它,而是将它保持在私有状态,或者发布在不同的注册表上。
为了测试使用自定义注册表的发布过程,你可以使用Verdaccio。在一个新的外壳中,启动:
npx verdaccio
这将安装并运行Verdaccio的本地版本。你应该看到屏幕上印有类似下面的内容。
warn --- http address - http://localhost:4873/ - verdaccio/5.13.1
转到这个地址,看看说明。它们应该是这样的:

运行登录命令(npm adduser --registry [http://localhost:4873/](http://localhost:4873/))并填写数据。对于Username 和Password ,你可以直接使用test 。任何东西都会被采纳;Email ,可以简单到foo@bar.com 。
发布到自定义注册表现在可以通过。
npm publish dist/emulator/my-app-shell-1.0.0.tgz --registry http://localhost:4873/
一旦完成,我们就可以为这个shell创建微型前端了!
推广微前端
就像我们为应用程序外壳所做的那样,我们可以使用piral-cli ,为我们提供一个项目的支架。现在的命令是使用pilet ,而不是piral-instance 。 我们运行:
npm init pilet --source my-app-shell --registry http://localhost:4873/ --bundler esbuild --target my-pilet --defaults
这将创建一个名为my-pilet 的新目录,其中包含一个新的微前端的代码。工具设置为esbuild (像以前一样,我们使用esbuild,因为它的安装速度非常快,但你也可以选择不同的东西,如webpack)。
上面的重要部分是指定--source ,它表示开发时使用的模拟器。现在一切都准备就绪,我们可以cd my-pilet ,然后运行:
npm start
像之前一样,开发服务器托管在 [http://localhost:1234](http://localhost:1234).去那里的结果是一个页面,如下图所示:

几乎和我们使用empty 饲料一样空。然而,在这种情况下,新桩子的模板已经注册了一个瓦片和一个菜单项。让我们看看如何改变这一点。
改动桩子
打开src/index.tsx 文件,看一下代码。
import * as React from 'react';
import type { PiletApi } from 'my-app-shell';
export function setup(api: PiletApi) {
api.showNotification('Hello from Piral!', {
autoClose: 2000,
});
api.registerMenu(() =>
<a href="https://docs.piral.io" target="_blank">Documentation</a>
);
api.registerTile(() => <div>Welcome to Piral!</div>, {
initialColumns: 2,
initialRows: 1,
});
}
简单地说,一个pilet只是一个JavaScript库;重要的部分是这个库导出了什么。
一个pilet输出一个setup 函数(准确地说,还可以选择一个teardown 函数)。这个函数在微型前端连接后使用,并接收一个参数,api ,这个参数是由app shell定义和创建的。
app shell的API(通常被称为Pilet API)是Pilets可以在应用程序中注册其部分的地方。让我们添加一个页面,并稍微改变一下瓷砖。
我们从瓦片开始。我们可以给它一些类,比如teaser ,以便真正有一点背景。此外,我们要为仪表板容器添加一点元数据。我们可以使用initialColumns 和initialRows 属性来传达所需的尺寸。
app.registerTile(() => <div className="teaser">Hello LogRocket!</div>, {
initialColumns: 2,
initialRows: 2,
})
一旦保存,瓦片将有一点不同的外观。让我们删除不再需要的showNotification ,并引入一个新的页面。
api.registerPage('/foo', () =>
<p>This is my page</p>
);
为了链接到这个页面,我们可以改变注册的菜单项。为了执行SPA导航,我们可以使用一个熟悉的React工具,react-router-dom 。
api.registerMenu(() =>
<Link to="/foo">Foo</Link>
);
很好!然而,像页面这样的片段并不总是需要的,只有在应该渲染的时候才应该被加载。这种懒惰的加载可以通过将代码放在一个专门的文件中来实现,即Page.tsx ,并将注册改为。
const Page = React.lazy(() => import('./Page'));
api.registerPage('/foo', Page);
Page.tsx 的内容可以简单明了。
import * as React from 'react';
export default () => {
return (
<>
<h1>Page Title</h1>
<p>Lorem ipsum dolor sit ...</p>
<p>Lorem ipsum dolor sit ...</p>
</>
);
};
注册了该页面后,你现在可以点击导航栏中的 "Foo",看到该页面:

构建并发布我们的代码
现在我们的代码已经写好了,我们可以真正地建立和发布它。在这一点上,我们还没有创建我们自己的feed,也没有在任何地方发布应用程序的外壳,所以最后一部分实际上是有点理论上的。
要构建pilet,你可以运行:
npm run build
一旦创建了,你就可以用npx pilet pack 来打包这个pilet。这将与运行npm pack 相当类似。结果是另一个.tgz 文件--这次不是一个模拟器,而是实际的pilet。这个tarball是可以上传到一个专门的服务,比如feed服务,提供feed给app shell使用。
在piral.cloud上可以找到非商业性和商业性服务的例子。
在我们结束教程之前,让我们看看我们如何能够整合一个常见的功能--在这种情况下,用SWR执行HTTP请求。
集成SWR来执行HTTP请求
有多种方法可以整合像SWR这样的共同关注点。一旦你把swr(或其他坐在它上面的库)添加到app shell中并在那里配置它,你有三种选择:
- 将它作为一个共享库公开
- 通过Pilet API公开它
- 不公开它(不过,在我们的例子中,SWR是行不通的,因为我们需要一个对Hook的引用)。
- 将使用
swr的决定留给桩子:它们可以将swr作为分布式依赖来共享(也就是说,只有在其他桩子还没有加载它的时候才加载)。
集成SWT的最简单和最可靠的方法是使用第一个选项。为此,我们回到app shell。
在app shell的目录下运行:
npm install swr
现在,让我们修改一下package.json. 保留几乎所有的东西,但是像这样修改externals 数组的pilets 部分。
{
"name": "my-app-shell",
"version": "1.1.0",
// ...
"pilets": {
"externals": ["swr"],
// ...
},
// ...
}
注意,我还改变了版本号。因为我们将对我们的模拟器进行更新,我们需要有一个新的版本。这将指示Piral实际上与所有的桩子共享swr 依赖关系。
为了实际测试这一点,让我们编写npm run build ,并再次发布。
npm run build
npm publish dist/emulator/my-app-shell-1.1.0.tgz --registry http://localhost:4873/
有了更新后的shell,让我们进入pilet的目录,升级应用程序的shell。
npx pilet upgrade
厕所的package.json 文件应该已经改变。它现在应该包含对1.1.0 版本中的my-app-shell 的引用,而不是1.0.0 。此外,你应该看到swr 列在devDependencies 和peerDependencies 。
让我们修改页面以使用swr 。
import * as React from 'react';
import LogRocket from 'swr';
// note: fetcher could have also been globally configured in the app shell
// however, in general the pilet's don't know about this and so they may want to
// reconfigure or redefine it like here
const fetcher = (resource, init) => fetch(resource, init).then(res => res.json());
export default () => {
const { data, error } = useSWR('https://jsonplaceholder.typicode.com/users/1', fetcher);
if (error) {
return <div>failed to load</div>;
}
if (!data) {
return <div>loading...</div>;
}
return (
<>
Hello {data.name}!
</>
);
};
现在我们已经完成了!不仅在我们的应用程序中成功地设置了SWR,我们还可以在所有的微型前端使用它。这既节省了加载SWR的带宽,也节省了SWR的内部缓存,给所有的微前端带来了良好的性能优势。
总结
在这篇文章中,你已经看到了开始使用Piral是多么容易。Piral为你提供了将你的网络应用分发到不同的仓库,甚至是不同的团队的选项。
在这篇文章中,我们只探讨了最基本的设置,但你可以用Piral做的事情还有很多。探索Piral的最好方法是通过官方文档。
Piral之所以比其他大多数解决方案具有更好的扩展性,是因为Piral鼓励松散耦合。这样一来,你就很难把两个东西融合在一起,这有助于你避免功能重叠和隐藏的单体。
无论你打算做什么,确保已经想好了哪些依赖要共享,哪些依赖要留给桩子。我们已经看到一个例子,提供swr 作为共享的依赖,实际上在几秒钟内就设置好了。编码愉快!