将微前端的优势引入旧版 Web 应用程序
最近,我们介绍了一种新的片段架构,用于快速构建 Web 应用程序,这种方法成本效益高,可扩展到最大型的项目,还可实现快速迭代。该方法使用多个协作的 Cloudflare Worker 来渲染微前端,并注入一个比旧版客户端更快交互的应用程序中,从而改善用户体验,提高搜索引擎优化得分。
如果您正在开始一个新项目,或能够从头开始重写现有应用程序,这是一个不错的方法。但现实情况中,大多数项目都过于庞大,无法从头开始重新构建,只能逐步更改架构。
本文我们建议的方法是,使用服务器端渲染的片段进行替换,但仅替换客户端渲染的旧版应用程序中的所选部分。其结果是,应用程序最重要的视图交互时间更早,可以独立开发,并获得微前端方法的所有优势,同时无需大量重写旧版代码库。这种方法与框架无关;本文展示了使用 React、Qwik 和 SolidJS 构建的片段。
大型前端应用程序的痛点
现在开发的许多大型前端应用程序的用户体验并不好。这通常是因为需要下载、解析和执行大量 JavaScript 后,用户才能与应用程序互动。虽然通过延迟加载推迟了不重要的 JavaScript 代码,并使用了服务器端渲染,这些大型应用程序仍然需要太长时间才能交互,才能响应用户的输入。
此外,大型单体应用程序的构建和部署可能很复杂。可能由多个团队合作构建代码库,努力协调项目的测试和部署,因此,单项功能的开发、部署和迭代都会很困难。
正如上一篇文章所述,由 Cloudflare Workers 提供支持的微前端可以解决这些问题,但是将应用程序单体转换为微前端架构可能比较困难,而且成本高昂。可能需要数月甚至数年的开发时间,用户或开发人员才能感受到一些优势。
我们需要的方法是,可以逐步将应用程序中最具影响力的部分采用微前端,而不需要一次性重写整个应用程序。
片段营救
基于片段的架构旨在通过将应用程序分解为可在 Cloudflare Workers 中快速渲染(和缓存)的多个微前端,大幅降低大型 Web 应用程序的加载和交互延迟(如 Core Web Vitals 评估所示)。问题是如何将微前端片段集成到客户端渲染的旧版应用程序中,并将原始项目的成本降到最低。
利用我们提出的这项技术,可以转换旧版应用程序的用户界面中最有价值的部分,并与应用程序的其他部分隔离。
事实证明,在许多应用程序中,用户界面最有价值的部分往往嵌套在提供页眉、页脚和导航元素的应用程序“外壳”中。这类示例包括登录表单、电子商务应用程序中的产品详情面板、电子邮件客户端的收件箱等。
我们以登录表单为例。如果我们的应用程序需要花几秒钟才能显示登录表单,用户会害怕登录,我们可能就会失去用户。但我们可以将登录表单转换为服务器端渲染的片段,这样便可立即显示并交互,而旧版应用程序的其他部分则在后台启动。由于该片段会提前交互,用户甚至可以在旧版应用程序开始并渲染页面的其余部分之前提交凭证。
动画在主应用程序之前显示登录表单
与传统方法相比,利用这种方法,工程团队只需花费小部分时间和工程成本即可为用户提供有价值的改进。而传统方法要么无法改善用户体验,要么需要对整个应用程序进行冗长且高风险的重写。这种方法让团队在处理单体单页应用程序时逐步采用微前端架构,针对性地对应用程序中最有价值的部分进行改进,从而将投资回报前置。
将用户界面的部分组件转换为服务器端渲染的片段时,存在一个有趣的问题——在浏览器中显示后,我们希望旧版应用程序和替换片段感觉就像一个单一的应用程序。这些片段应该整齐地嵌入旧版应用程序外壳中,通过正确形成 DOM 层次结构来确保应用程序的可访问性,但我们也希望服务器端渲染的片段能够尽快——甚至在客户端渲染的旧版应用程序外壳出现之前显示出来并可交互。我们如何将用户界面片段嵌入到一个尚不存在的应用程序外壳中?为解决这一问题,我们设计了一项技术——我们称之为“片段穿透”。
片段穿透
片段穿透将服务器端渲染的微前端片段产生的 HTML/DOM 与客户端渲染的旧版应用程序产生的 HTML/DOM 结合到一起。
微前端片段直接渲染进入 HTML 响应的顶层,并设计为立即交互。在后台,旧版应用程序在客户端渲染成这些片段的兄弟元素。准备就绪后,这些片段将“穿透”到旧版应用程序中——每个片段的 DOM 将移动到旧版应用程序 DOM 中的适当位置,而不会造成任何视觉上的副作用,或客户端状态的损失,例如焦点、表单数据或文本选择。片段“穿透”后,就可以开始与旧版应用程序进行通信,有效集成到旧版应用程序中。
在穿透之前,您可以看到一个“登录”片段,DOM 顶层有一个空的旧版应用程序“根”元素。
<body>
<div id="root"></div>
<piercing-fragment-host fragment-id="login">
<login q:container...>...</login>
</piercing-fragment-host>
</body>
现在您可以看到,这个片段已经穿透到渲染的旧版应用程序中的“login-page”分区。
<body>
<div id="root">
<header>...</header>
<main>
<div class="login-page">
<piercing-fragment-outlet fragment-id="login">
<piercing-fragment-host fragment-id="login">
<login q:container...>...</login>
</piercing-fragment-host>
</piercing-fragment-outlet>
</div>
</main>
<footer>...</footer>
</div>
</body>
为了防止该片段在过渡期间移动并导致明显的布局偏移,我们应用 CSS 样式,在穿透前后以相同的方式定位该片段。
应用程序任何时候都可以显示任何数量的穿透片段,或者完全不显示。这种技术不局限于旧版应用程序的初始加载。片段也可以在任何时候添加到应用程序中或从中删除。因此可以渲染片段来响应用户互动和客户端路由。
通过片段穿透,您可以开始逐步采用微前端,一次采用一个片段。您决定片段的细粒度,以及将应用程序的哪些部分变成片段。这些片段不必都使用相同的 Web 框架,在切换堆栈时,或在收购后整合多个应用程序时,这可能会很有用。
“Productivity Suite”演示
为演示片段穿透和逐步采用,我们开发了一个“Productivity Suite”演示应用程序,让用户管理待办事项列表、阅读黑客新闻等。我们将这个应用程序的外壳实施为客户端渲染的 React 应用程序——企业应用程序中常见的技术选择。这是我们的“旧版应用程序”。该应用程序中有三条路由已经更新为使用微前端片段:
/login
:一个简单的带客户端验证的虚拟登录表单,在未验证用户身份时显示(在 Qwik 中实施)。/todos
:管理一个或多个待办事项列表,以两个协作片段的形式实施。/news
:HackerNews 试用应用程序的克隆(在 SolidJS 中实施)。
该演示展示了不同的独立技术既可用于旧版应用程序,也可用于每个片段。
将穿透到旧版应用程序中的片段可视化
该应用程序部署在 https://productivity-suite.web-experiments.workers.dev/。
如需试用,您必须先登录,使用自己喜欢的任何用户名即可(不需要密码)。用户的数据将保存在 cookie 中,因此您可以在退出后使用相同的用户名重新登录。登录后,使用应用程序顶部的导航栏浏览各个页面。特别注意看一下“待办事项列表”和“新闻”页面,检验穿透的实际效果。
您可随时尝试重新加载页面,看看旧版应用程序在后台缓慢加载时,片段是否会立即渲染。试着在旧版应用程序出现之前就与片段互动!
页面最上方有一些控件,可用来检验片段穿透的实际效果。
在旧版应用程序启动之前,使用“旧版应用程序引导延迟”滑块来设置模拟延迟。
切换“穿透已启用”,看看如果不使用片段,应用程序的用户体验如何。
切换“显示接缝”,查看每个片段在当前页面的位置。
工作方式
该应用程序由许多构建块组成。
Workers 协作和旧版应用程序主机概述
在我们的演示中,旧版应用程序主机提供定义客户端 React 应用程序的文件(HTML、JavaScript 和样式表)。使用其他技术栈构建的应用程序也可以一样运行。片段 Workers 托管微前端片段,我们在以前的片段架构文章中介绍过。网关 Worker 处理来自浏览器的请求,选择、获取并合并来自旧版应用程序和微前端片段的响应流。
这些部分全部部署完毕后,它们就会共同合作,处理来自浏览器的每个请求。我们来看看进入“/login”路由时会发生什么。
查看登录页面时的请求流
用户导航到应用程序,浏览器向网关 Worker 发出请求,以获取初始 HTML。网关 Worker 识别浏览器正在请求登录页面。然后,网关 Worker 发出两个并行的子请求——一个是获取旧版应用程序的 index.html,另一个是请求服务器端渲染的登录片段。然后,它将这两个响应合并为一个响应流,其中包含传递给浏览器的 HTML。
浏览器显示 HTML 响应,其中包含旧版应用程序空的根元素,以及用户可以立即互动的服务器端渲染的登录片段。
浏览器然后请求旧版应用程序的 JavaScript。这个请求由网关 Worker 代理给旧版应用程序主机。同样地,任何其他旧版应用程序或片段的资产都会通过网关 Worker 路由到旧版应用程序主机或适当的片段 Worker。
一旦下载并执行旧版应用程序的 JavaScript,同时渲染应用程序的外壳,片段穿透就会启动,将片段移动到旧版应用程序的适当位置,同时保留所有的用户界面状态。
虽然对片段穿透的解释集中在登录片段上,但这些解释也适用于在 /todos
和 /news
路由中实施的其他片段。
穿透库
虽然使用不同的 Web 框架来实施,但所有片段都使用来自“穿透库”的帮助程序,以相同方式集成到旧版应用程序中。这个库专为演示而开发,收集了服务器端和客户端实用程序,用于处理旧版应用程序与微前端片段的集成。该库的主要功能是 PiercingGateway
类、片段主机和片段出口自定义元素以及 MessageBus
类。
PiercingGateway
PiercingGateway
类可以用来将网关 Worker 实例化,处理对我们应用程序的 HTML、JavaScript 和其他资产的所有请求。PiercingGateway 将请求路由到适当的片段 Worker 或旧版应用程序的主机。它还将来自这些片段的 HTML 响应流与来自旧版应用程序的响应合并成一个单一的 HTML 流返回给浏览器。
使用穿透库实施网关 Worker 的过程非常简单。创建一个新的 PiercingGateway
网关实例,将旧版应用程序主机的 URL 和一个用于确定是否为给定请求启用了穿透功能的函数传递给它。从 Worker 脚本中默认导出 gateway
,以便 Workers 运行时可以连接其 fetch()
处理程序。
const gateway = new PiercingGateway<Env>({
// Configure the origin URL for the legacy application.
getLegacyAppBaseUrl: (env) => env.APP_BASE_URL,
shouldPiercingBeEnabled: (request) => ...,
});
...
export default gateway;
可以通过调用 registerFragment()
方法来注册片段,gateway
便可自动将对片段的 HTML 和资产的请求路由到其片段 Worker。注册登录片段示例如下:
gateway.registerFragment({
fragmentId: "login",
prePiercingStyles: "...",
shouldBeIncluded: async (request) => !(await isUserAuthenticated(request)),
});
片段主机和出口
在网关 Worker 中路由请求并合并 HTML 响应只是让穿透成为可能的部分原因。另一半原因是穿透需要在浏览器中进行,使用我们之前所述的技术将这些片段穿透到旧版应用程序中。
浏览器中的片段穿透由一对自定义元素来协助完成,即片段主机 (<piercing-fragment-host>
) 和片段出口 (<piercing-fragment-outlet>
)。
网关 Worker 将每个片段的 HTML 包裹在片段主机中。在浏览器中,片段主机管理片段的生命期,并用于将片段的 DOM 移动到旧版应用程序中的适当位置。
<piercing-fragment-host fragment-id="login">
<login q:container...>...</login>
</piercing-fragment-host>
在旧版应用程序中,开发人员通过添加一个片段出口来标记片段穿透时应该出现在哪个位置。演示应用程序的登录路由如下所示:
export function Login() {
…
return (
<div className="login-page" ref={ref}>
<piercing-fragment-outlet fragment-id="login" />
</div>
);
}
片段出口添加到 DOM 时,会在当前文档中搜索其关联的片段主机。如果找到了片段主机,则将片段主机及其内容移动到出口内。如果未找到片段主机,出口将向网关 Worker 请求片段 HTML,然后使用 writable-dom 库(由 MarkoJS 团队开发的一个强大的小型库)将其直接流式传输到片段出口。
这种后备机制支持客户端导航到包含新片段的路由。这样便可通过初始(硬)导航和客户端(软)导航在浏览器中渲染片段。
消息总线
除非应用程序中的片段已完全呈现或完全独立,否则它们也需要与旧版应用程序和其他片段通信。[MessageBus](https://github.com/cloudflare/workers-web-experiments/blob/df50b60cfff7bc299cf70ecfe8f7826ec9313b84/productivity-suite/piercing-library/src/message-bus/message-bus.ts#L18)
是一个简单的异步同构、与框架无关的通信总线,旧版应用程序和每个片段均可访问。
在我们的演示应用程序中,登录片段需要在验证用户身份后通知旧版应用程序。这个消息分派过程是在 Qwik LoginForm
组件中实施的,方式如下:
const dispatchLoginEvent = $(() => {
getBus(ref.value).dispatch("login", {
username: state.username,
password: state.password,
});
state.loading = true;
});
然后,旧版应用程序可以收听这些消息,方式如下:
useEffect(() => {
return getBus().listen<LoginMessage>("login", async (user) => {
setUser(user);
await addUserDataIfMissing(user.username);
await saveCurrentUser(user.username);
getBus().dispatch("authentication", user);
navigate("/", { replace: true, });
});
}, []);
我们决定采用这个消息总线实施,因为我们需要一个与框架无关的解决方案,而且在服务器和客户端都能良好运行。
不妨试试看!
利用片段、片段穿透和 Cloudflare Workers,您可以改善性能,缩短传统客户端渲染应用程序的开发周期。可以逐步采用这些更改,您甚至可以同时使用所选 Web 框架实施片段。
展示这些功能的“Productivity Suite应”用程序下载地址:https://productivity-suite.web-experiments.workers.dev/。
我们展示的所有代码均为开源代码,已发布在 Github 上:https://github.com/cloudflare/workers-web-experiments/tree/main/productivity-suite。
您可随意克隆该存储库。您可轻松在本地运行该库,甚至可以将您自己的版本(免费)部署到 Cloudflare。我们尽力让代码可重复。大部分核心逻辑都在穿透库中,您可以在自己的项目中尝试。如果能收到您的反馈和建议,或听说您想使用它构建应用程序,我们将会备感荣幸。加入我们的 GitHub 讨论,或在我们的 Discord 频道上联系我们。
我们相信,结合 Cloudflare Workers 与框架的最新理念,Web 应用程序的用户和开发人员的体验将会大幅提升。随着我们继续突破 Web 产品的界限,我们将推出更多演示和博客文章,并加强各项合作。如果您也想直接参与这一提升过程,请加入我们:我们正在招聘!