什么是Web极简架构

摘要:本文为运行或构建跨多个团队的 Web 应用程序的中小型公司提供了蓝图。本文的目的不是描述严格基于“照本宣科”模式的理论技术架构,也不是创建关于如何构建 IT 组织的高级管理指南。相反,本文的目的是提供实用的、基于经验的指南,演示如何通过简单、强大的架构以及实现

极其简单Web架构(radically simple web)是一个面向初创企业和小型企业的 Web 应用程序蓝图:何使用 模块化单体Modular Monoliths、SSR、微前端Micro Frontends、HTMX 和 Tailwind CSS 跨多个领域团队构建 Web 应用程序。

本文为运行或构建跨多个团队的 Web 应用程序的中小型公司提供了蓝图。本文的目的不是描述严格基于“照本宣科”模式的理论技术架构,也不是创建关于如何构建 IT 组织的高级管理指南。相反,本文的目的是提供实用的、基于经验的指南,演示如何通过简单、强大的架构以及实现这一目标的团队结构将您的 HTML 从 A 点传输到 B 点。

在软件开发中,有一句流行的话:“过早优化是万恶之源。”然而,我经常发现自己所处的项目非常复杂,有许多可变部分,但核心业务逻辑却很少。这引发了一个问题——为什么?在网站上显示数据库中的一些数据怎么会成为如此复杂的任务?

问题在于我所说的“心理默认”。通常,当一个项目开始时,人们会不加思索地采用最​常见的架构模式和框架——仅仅是因为它们被广泛使用。这有点像 70 年代的 IBM 电脑;如果你需要购买办公设备,它们是最安全的选择。这种心态可以用一句话来概括:“没有人因为购买 IBM 而被解雇。”

今天,你可以说“没有人因为构建微服务和 SPA 而被解雇”,尽管这通常是针对简单问题的过于复杂的解决方案。

我推荐一篇名为“彻底简化”的文章,它鼓励使用更少的框架、库和工具来回归更简单的解决方案。这降低了整体复杂性,释放了开发时间,专注于创造商业价值。

什么是彻底简单?
彻底简单意味着尽可能少地使用组件和移动部件。将技术重复用于不同的目的,而不是为每个目的使用新的移动部件。

不要使用 Postgres作为数据库、DrUId 作为事件存储、Redis 作为缓存、Rabbit MQ 作为消息队列和 Elastic 作为全文搜索,而是使用托管的 Postgres 作为数据库,用于全文搜索、html 缓存、发布/订阅和带有 TimescaleDB 的事件存储。


这使我们能够拥有更深入的知识,行动更快,让新开发人员更快地入职,减少可能出现故障的东西,无需为数十个框架和组件制定升级计划,并让开发人员更加快乐。

开发的核心是进入流程。开发人员在流程内部比在流程外部更有效率。您需要阅读有关打破流程的数据库组件、升级和边缘情况。

在此蓝图中,我们将扩展并应用“彻底简单”原则,以跨多个领域团队的 Web 应用程序实施和架构,随着您的需求发展,提供从简单架构到更复杂架构的途径。这种方法使您的架构能够随着您的业务逐步发展 — — 我们称之为“彻底简单的架构”。

组织蓝图
在深入研究技术解决方案之前,让我们先介绍一下组织结构——如何组建团队并为每个角色确定合适的人员。

想象一下,您要构建一个高度定制的网上商店或 SaaS 应用程序,需要两个小型开发团队来完成工作量。首先出现的问题是如何在这两个团队之间分配工作。

一种选择是按技术层(水平)分配工作,例如后端和前端——这是过去的常见模式。然而,这种设置导致几乎每个功能的团队间依赖性,同一领域的知识分散在两个团队中。如果你将来需要添加更多团队,这种结构也无法很好地扩展。

就独立性、开发速度和集中知识而言,更好的方法是按业务领域(垂直)分组。在这种方法中,只有一个团队需要完全了解新功能,并且能够处理完整的功能开发,包括后端和前端工作。领域驱动设计(DDD) 是一种众所周知的识别和处理领域的方法。

例如,在网店中,域可能包括:导航、搜索、产品详情、结账和账户。识别域及其边界并不总是那么简单,因此对于此示例,我们假设我们有一个合理的设置。在项目开始时,详细考虑潜在域,然后对它们进行分组可能会有所帮助。例如,在网店中,您可以将导航、搜索和产品分组到“discovery”域中,将结账作为单独的域。

对于 SaaS 应用程序,可能只有两个域:帐户和调度,假设它是一个团队调度服务。

随着业务及其需求的发展,您可以随时进一步细分领域、软件和团队。然而,在对领域进行分组之前,广泛发现潜在领域至关重要。

除了特定领域的逻辑之外,您还会遇到跨领域关注点,例如基础架构和集成。两者都是技术层面上将“领域团队”“粘合”在一起所必需的。我们稍后会介绍技术方面,但从高层次来看,这些部分可以被视为协助面向业务的领域团队的“支持团队”。

领域团队
领域团队负责为最终用户设计、实施和操作功能。这些功能以独立的方式构建,从前端到托管应用程序的数据库和服务器,称为端到端或垂直功能。

例如,负责“帐户”域的域团队将构建完整的注册页面,包括呈现页面、输入验证、跟踪和其他前端任务。他们还会处理后端工作,例如存储用户数据和触发电子邮件。

根据您的整体产品的规模和业务领域的数量,您可能有多个团队独立工作来完成端到端的任务。

理想情况下,每个团队都应该规模小、持续时间长,这样他们就可以专注于工作,而不是流程和内部协调。由 4 名全栈开发人员和一名产品经理/所有者组成的团队效果很好。产品经理负责收集功能需求并将其传达给团队,同时处理范围、时间和预算方面的考虑。

团队中拥有全栈开发人员有利于在内部分配工作,因为没有人会局限于一个领域,例如后端或前端。虽然看起来全栈开发人员可以处理任何任务,但事实上他们通常仍然有偏好或专业领域。因此,在团队中平衡前端和后端技能组合非常重要。

UI/UX 对于完整的领域功能至关重要,因此团队通常共享一位 UI/UX 设计师,该设计师提供初步设计并通过与开发人员密切合作来支持开发。

支持团队
支持团队不直接实现最终用户功能。相反,他们提供有效运作所需的服务和基础设施领域团队。

在此示例中,我们将有一个平台团队,通过提供一个空的云帐户或可能只是一个 PaaS(例如 Heroku)帐户(具体取决于您的要求),使领域团队能够在云中部署和运行他们的应用程序。平台帐户还将提供用于监控和日志收集的解决方案。

在堆栈的另一端,我们需要一个集成团队将领域团队的工作“粘合”到一个有凝聚力的网站,使其在用户看来就像一个网站,即使不同的团队贡献了独立的页面。该团队将处理 DNS 设置、路由和页面组装,以及创建领域团队使用的模式库,以实现一致的 UI/UX。页面的实现不属于集成团队。

支持团队通常提供领域团队可以根据需要利用和集成的“护栏”和服务。

在较大的组织中,这些支持团队可能由专门负责这些系统组件的开发人员组成。

在像我们的例子这样的较小规模的设置中,支持团队可以是由每个领域团队的代表(一到两名)组成的虚拟团队。他们会定期开会讨论与其支持领域相关的任务,然后在领域团队内完成实际工作。

通常,支持工作集中在项目的初始阶段。一旦这些支持部分到位,功能开发将成为领域团队的主要关注点。

技术蓝图
在定义了组织设置和团队角色后,让我们来详细探讨一下可以采用哪些技术以及整体架构是什么样子的。
最终的架构将满足以下要求:

该网站将由团队贡献的单独 HTML 页面组成(例如产品页面、帐户页面)。团队提供的较小的 HTML 片段也可以被其他团队纳入(例如,页眉和页脚)。页面和片段应该无缝合并,为用户创造统一的体验。团队必须能够独立工作、部署和扩展。该架构必须能够支持不断发展的组织。必须应用“简单”原则:当有两个选项时,我们会选择较简单的选项,直到情况的复杂性需要更高级的选项。


为了满足这些技术和组织需求,我们将使用独立系统、服务器端渲染和微前端的组合作为我们架构的核心。
我们将从用户访问应用程序的角度开始,然后深入研究领域团队的实施,并构建完整的大局。

集成层
集成层是一组工具和资源,可将领域团队提供的页面组合成一个有凝聚力的应用程序。我们将使用一种称为“微前端”的模式,它有助于模块化应用程序,同时为最终用户创造统一的体验。

集成团队将设置这些工具和资源,但不会自行构建页面。这些工作由领域团队完成。

为了了解前端的结构,我们将跟踪用户的请求,从浏览器到应用程序。

路由
我们的网站将托管在一个顶级域名下,例如example.com。当用户输入该域名时,浏览器会向我们的系统发送请求并等待 HTML 响应。由于多个团队提供不同的页面,因此我们会根据 URL 路由请求。例如:

/ → discovery团队/product/id→ discovery团队/account→ 收银团队/cart→ 收银团队


然后,每条路由都会返回由浏览器呈现的 HTML 页面。

这同样适用于 HTML 片段。片段是包含在页面中的一小段标记,而不是完整的 HTML 页面。

/navigation/fragment/v1/header→ discovery团队/account/fragment/v1/user-icon→ 收银团队


向片段 URL 添加版本被认为是最佳做法。如果您的片段中有重大更改(例如完全重新设计的布局),则此方法必不可少。然后,包含页面可以选择要加载哪个版本。

页面组装
使用微前端需要一种方法来将来自不同团队的 HTML 页面和片段合并为一个统一的 HTML 响应。有多种方法可以包含和替换片段,具体取决于您使用的技术堆栈和前端框架。

在这个例子中,我们将使用服务器端渲染并避免使用 JavaScript 前端框架,而是专注于纯 HTML 请求以实现快速高效的渲染。

回到我们的例子:调用example.com/cart可能会返回来自结帐团队的完整 HTML 页面。购物车页面还可能包含由discovery团队托管的共享页眉和页脚。

在 HTML 结构中,我们将使用虚拟包含来包含页眉和页脚元素:

... 购物车页面内容...


我们的页面组装解决方案将把页面和片段的 HTML 合并为最终的 HTML 页面。

NGINX是一款 Web 服务器,是完成此项工作的理想工具,它内置了使用指定 URL 的 HTML 替换包含内容的支持。此外,它还可以处理 URL 到团队的路由,使其成为每个 Web 请求的入口点。

完整的请求周期如下:

浏览器请求example.com/cart。NGINX 调用/cart结帐收银域。结帐收银域返回带有include语句的 HTML。NGINX 解析包含的 HTML。NGINX 调用/navigation/fragment ,discovery域上的端点。discovery域返回页眉和页脚 HTML。NGINX 用片段 HTML 替换包含部分。浏览器从 NGINX 接收完整的 HTML 并呈现页面。

页面组装
到目前为止,我们已经介绍了如何将域团队返回的 HTML 合并为完全有效的 HTML 页面。在组装好的 HTML 代码中,您将不再看到团队界限。布局中也必须实现同样的集成水平。每个页面都需要给人一种有凝聚力、统一的体验。

模式库的技术深度差异很大。从简单的角度来看,模式库可能是 PDF 样式指南或 Figma 文件,其中显示按钮、表单、颜色等,每个领域团队会将其转换为自己的 HTML 和 CSS。从技术角度来看,模式库可能是每个团队代码中包含的代码库,其中包含可节省开发时间但限制团队对前端堆栈选择的组件。

中间立场包括具有可重用代码组件的 CSS 框架,通过使用纯 HTML、CSS 和最少的 JavaScript,可以减少团队工作量,同时使前端堆栈选择更加灵活。

例如,Bootstrap框架改变了游戏规则,使开发人员无需从头开始构建即可创建美观的 UI。Bootstrap 易于使用,尽管自定义功能有些有限。因此,出现了更灵活的 CSS 实用程序框架,如Tailwind CSS,它们保留了复制粘贴代码的便利性,同时允许在组件设计方面具有更大的灵活性。

上面的代码展示了使用 Tailwind CSS 构建的下载按钮。您可以将该代码复制并粘贴到任何前端堆栈中,包括它tailwind.css,它的外观将始终相同 - 这是一个非常强大的功能。

我们将在这里使用的模式库正是:由 Tailwind 组件和模板构建的组件库。可以将其视为专门针对此项目的高度定制的 Bootstrap。模式库将提供:

列出复制粘贴代码组件的托管网页。此库使团队能够浏览和查找组件。它可以是一个简单的静态网站,也可以是像 Storybook 这样的解决方案。托管myproject-tailwind.css文件,包括组件的所有实用程序类。在深入研究性能优化之前,您可以使用公共 CDN 进行基本的tailwind.css。托管的 alpine.js 文件,使用 AlpineJS 为需要一些交互性的组件(如手风琴或飞出模式)提供动力。 该文件也可从公共 CDN 加载。HTMX的托管htmx.js文件或 CDN 版本。它允许团队使用属性直接在 HTML 中访问 AJAX、CSS Transitions、WebSockets 和服务器发送事件,这样他们就可以使用超文本的简单性和强大功能构建现代用户界面。


每个页面都将包含Tailwind CSS、HTMX和AlpineJS源,从而允许每个领域团队假定这些资源可用于呈现其服务器端 HTML 页面和片段。


领域层
我们架构的领域层将包含多个应用程序,处理领域业务逻辑并为集成层合并的页面提供服务。

独立系统
对于应用程序,我们希望遵循自包含系统架构。有一篇关于 SCS 细节和优势的文章,但为了简化,您可以将 SCS 视为大型 Web 应用程序的一个小的垂直部分,它独立运行,包含用户界面、业务逻辑和特定域(如“discovery”)的持久性。


自包含服务模式
对于团队拥有的每个域,它将创建一个 SCS。如我们的示例中所述,我们希望在discovery团队中对多个域进行分组。对于这个团队,我们将为导航、搜索和产品创建一个 SCS。

目前,我们尚未讨论代码组织本身或稍后将讨论的部署单元。我们希望专注于实现技术堆栈每个级别上每个域的独立性。


每个域一个 SCS,每个团队 N 个域
代码结构和部署单元
如前所述,独立性是我们微架构的一个关键特征。因此,您可能会认为将每个 SCS 放入其自己的微服务和可部署单元是一种理想的解决方案——这已成为近年来的默认方法。

微服务提供了明确的关注点分离,并强制执行域之间的严格界限。然而,由于其分布式特性,这种设置使更改更具挑战性,需要版本控制和迁移。这可能不是启动新项目时的最佳解决方案。

模块化单体也强制执行严格的边界,但提供的实现更能适应变化,因为它只有一个代码库和一个可部署单元。保持较低的“变更成本”对于新项目尤其重要,因为随着业务的增长和需求的变化,领域边界可能会发生变化。

在我们的示例中,这意味着将每个域及其 SCS 移至单独的模块。大多数编程语言和构建工具都支持将一个代码库与另一个代码库隔离的模块。


每个域一个模块的整体式结构
有了通用的代码结构后,我们现在开始讨论自包含服务的各个层。

UI 渲染
遵循简单性原则,我们还将在堆栈的渲染层采用这种方法,选择“服务器端渲染优先”(SSR)。

如今,单页应用程序 (SPA) 方法通常是默认选择。但除非您要构建类似于桌面应用程序的复杂 UI,如 Miro、Gmail 等,否则由 SSR 驱动的多页应用程序通常是更好的起点。如果您的用例主要包括从后端渲染静态数据、提供输入表单、显示模式和类似的常见 Web 功能,则 SSR 是正确的选择。为了复制 SPA 的快速、动态感觉,可以使用 HTMX 等工具让您的 MPA 感觉像 SPA。

从 SSR 开始的一个主要原因是简化技术堆栈并提高开发速度——想想“彻底简单”中概述的原则。SSR 需要更少的代码来维护、更新和理解。

因此,我们的每个 SCS 都将返回使用模板引擎在服务上呈现的 HTML。为了进一步降低复杂性,我们将在所有 UI 上共享相同的前端库和 CSS 框架。这些将由 CDN 提供,这样就无需在 SCS 构建中进行额外的前端构建步骤。

HTMX 与 AlpineJS 和 Tailwind CSS 相结合,应该可以满足大部分功能和布局要求。如果 SCS 仍需要额外的 JavaScript 或自定义 CSS,则可以根据需要添加自己的文件。

业务逻辑
业务逻辑是领域的核心。这是功能的实际逻辑实现之处 — 处理、验证、存储或从持久性中检索 UI 数据。

对于堆栈的这一层,请选择适合您的团队和组织的编程语言和 Web 框架。只需确保它与 SSR 顺利集成,并包含必要的 Web 功能,如路由、表单处理/验证、模板和身份验证(如果需要)。在每个模块中,您可以自由选择最适合的微架构,无论是六边形架构、三层架构还是任何其他模式,以保持 UI、业务逻辑和持久性之间的清晰分离。

持久性和数据交换
为了保持独立性,每个 SCS 都必须管理呈现其 UI 和执行特定于域的操作所需的数据。

例如,在搜索 SCS 中,这意味着处理搜索输入的呈现、将产品存储在其自己的搜索索引中以执行搜索以及呈现搜索结果页面。但是,由于产品域(及其对应的 SCS)是所有产品数据的主要来源,因此必须将这些数据复制到搜索域。

对于复制,我们有三个主要选项:

数据提取:搜索 SCS 对产品 SCS 中的 JSON 端点执行同步提取,仅加载呈现搜索结果所需的特定产品。数据馈送:搜索 SCS 定期通过 JSON 端点从产品 SCS 提取完整的产品数据集,并使用异步作业将其存储在自己的数据库中。这可以作为完整或增量更新发生。呈现搜索结果时,产品数据将直接从此本地数据库加载。数据事件:产品 SCS 将产品数据推送到事件总线,搜索 SCS 会使用该事件总线并将其存储在自己的数据库中。ProductUpdateEvents从头开始读取所有内容即可进行完整更新。呈现搜索结果时,将从本地数据库检索产品数据。

在所有情况下,建立标准化模式和实施约定都很重要,以确保在域间所有数据交换(例如产品、用户)中一致使用相同的机制。

如果此调用是在同一模块化整体内的两个 SCS 之间进行的,则从数据提取开始通常是一个好主意,因为 API 调用将在内存中进行,从而绕过网络。

如果 SCS 部署在不同的环境中,出于性能原因,需要进行真正的数据复制。在这种情况下,Data Feeds 通常是一种有效的方法,因为它们提供了一种强大且相对易于实施的解决方案。

调整架构
我们决定每个功能团队使用一个模块化整体,包含多个域。每个域都是一个作为自包含服务实现的代码模块,从而为我们的项目启动提供了非常紧凑和灵活的配置。

通常,您的 Web 应用程序的流量会随着时间的推移而增长。作为初始响应,垂直扩展(更多 CPU 和内存)和水平扩展(添加更多实例)应该可以满足不断增长的需求。

但是,随着领域和功能的增长,代码库变得越来越大,某些 SCS 比其他 SCS 拥有更高的网络流量,或者如果您需要针对特定​​领域提供 SPA,会发生什么情况?

在项目的这个阶段 — — 也只有到那时 — — 我们才开始拆分我们的整体,并使用我们之前建立的边界和层。

水平拆分
如果一个域(SCS)需要自己的可部署单元,则可能需要进行水平分割。

如果一个域名的网络流量明显增加,或者代码库和用例增长过多以至于需要由新创建的团队来管理该域名,则可能会出现这种情况。
模块化单体本质上是大型架构的缩小版,其中逻辑边界和通信模式保持一致。将一个模块转换为独立应用程序将取代与其物理对应模块的内存通信。

模块 → 自己的应用程序服务调用 → HTTP API 调用内存事件总线 → 托管事件总线1 可部​​署 → n 可部署

垂直分割
当您需要分离域内的 SCS 的一层时,就会发生垂直分割。

一个常见的原因是需要将用户界面与后端(业务逻辑和持久性)分离,随着用户界面复杂性的增加,从 SSR 方法切换到 SPA 和 API 设置。
API 仍将保留在整体模块中,但 SPA 将是一个单独的应用程序并可部署。在这种情况下,之前返回服务器端呈现的 HTML 的端点将改为返回 JSON。

UI 层 → 自己的 UI 应用程序服务调用 → HTTP API 调用HTML 路由 → API 路由1 可部​​署 → 1 + SPA 可部署

平台层
平台层允许团队部署、操作和监控他们的应用程序。

应用程序托管
同样,在托管应用程序时,存在各种各样的技术解决方案,每种解决方案都有其优点和缺点。

托管应用程序的选项多种多样,每种都有各自的优缺点。AWS AppRunner、Google Cloud Run 或 Heroku 等简单选项可让您在数小时内运行 Docker 容器并链接域。

或者,您可以从一个空的云帐户开始,给予团队完全的自由,但需要更大的初始设置。

无论您选择哪种解决方案,每个团队都能够独立工作至关重要。

鉴于我们的组织规模较小,建议从简单的解决方案开始,然后随着需求超出基本设置而进行扩展。

监控和记录
平台层提供的另一个基本功能是日志记录和监控解决方案。领域团队不需要设置自己的解决方案。

Datadog 等工具非常适合这项任务,它使团队能够发送事件和应用程序日志,然后 Datadog 会存储和索引这些日志。团队可以根据需要构建自定义仪表板、设置警报或查询日志。

概览
最后,我们来看一下完整的情况,包括组织结构、技术设计及其实施。

“极简架构”模型是一种灵活的方法,可用于为初创公司和中小型公司构建和扩展 Web 应用程序。该模型会逐步发展,随着业务的增长而进行调整,并通过专注于简单性并仅在需要时增加复杂性来避免过早优化和过度设计。

最终,它平衡了简单性和可扩展性,确保技术决策支持业务增长并有效地提供价值。

关于作者
Alex 是 Inaudi Tech 的创始人、架构师和全栈开发人员。过去 20 多年来,他一直从事应用程序开发工作,帮助小型初创公司和大型电子商务公司找到适合其任务的技术堆栈

来源:解道Jdon

相关推荐