构建面向未来的前端架构

深入了解基于组件的前端架构如何随着复杂性而大规模崩溃,以及如何避免它。


构建高性能且易于更改的前端体系结构在规模上是很困难的。

在本指南中,我们将探讨复杂性在许多开发人员和团队所处理的前端项目中快速而无声地复合的主要方式。

我们还将研究有效的方法,以避免在这种复杂性中不堪重负。无论是在它成为问题之前,还是在之后,如果你发现自己在想“哦,糟糕,这怎么会变得如此复杂?”当你的任务是添加或更改一个功能时。

前端体系结构是一个涉及许多不同方面的广泛主题。本指南将特别关注组件代码结构,这些结构使弹性前端可以轻松适应更改。基本原则可以应用于任何基于组件的框架。

我们将从头开始。关于我们的代码结构是如何受到影响的,甚至在编写任何代码之前。

常见心智模型的影响

我们拥有的心智模型,我们如何看待事物,最终在很大程度上影响了我们的决定。

在大型代码库中,这是不断做出的许多决策的最终结果,这些决策导致了它的整体结构。

当我们作为一个团队构建东西时,重要的是要明确我们拥有的模型,并期望其他人拥有。因为每个人通常都有自己的隐含内容。

这就是为什么团队最终需要共享风格指南和更漂亮的工具。因此,作为一个群体,我们有一个共同的模型,即事情应该如何保持一致,事情是什么,以及事情应该去哪里。

这使生活变得更加轻松。它使我们能够避免随着时间的推移下降到一个不可维护的代码库,每个人都走上自己的道路。

如果您经历过许多渴望发布的开发人员正在快速开发的项目,那么您可能已经看到,如果没有适当的指导,事情会变得多么迅速。随着时间的推移,随着添加更多代码和运行时性能下降,前端会变得越来越慢。


在组件中思考

React 是最流行的基于组件的前端框架。

它列出了在构建前端应用程序时如何思考的关键心智模型“ React 方式”。这是一篇好文章,因为该建议也适用于任何基于组件的框架。

它列出的主要原则允许您在需要构建组件时提出以下问题。

此组件的一项责任是什么?好的组件API设计自然遵循单一职责原则,这对于组合模式很重要。很容易将简单的东西混为一谈。随着需求的进入和变化,保持简单通常非常困难,我们将在本指南的后面部分进行探讨。

其状态的绝对最小但完整的表示是什么?这个想法是,最好从你所在州最小但完整的事实来源开始,你可以从中推导出变化。这是灵活,简单,并避免了常见的数据同步错误,例如更新一段状态但不更新另一条状态。

状态应该放在哪里?状态管理是本指南范围之外的一个广泛主题。但一般来说,如果一个状态可以设为组件的本地状态,那么它应该是本地的。组件在内部依赖于全局状态的越多,它们的可重用性就越低。提出此问题对于确定哪些组件应依赖于哪种状态很有用。

”理想情况下,一个组件应该只做一件事。如果它最终增长,它应该分解成更小的子组件。“

这里概述的原则很简单,经过了实战检验,它们适用于驯服复杂性。它们构成了创建组件时最常见的心智模型的基础。

简单并不意味着容易。在实践中,在具有多个团队和开发人员的大型项目中,这说起来容易做起来难。

成功的项目往往来自于坚持基本原则,并且始终如一。不要犯太多代价高昂的错误。

这就引出了两个我们将要探讨的问题。

  1. 是什么情况阻碍了这些简单原则的应用?

  2. 我们如何才能尽可能最好地减轻这些情况?

下面我们将看到为什么随着时间的推移,保持简单性在实践中并不总是那么简单。

自上而下与自下而上

组件是像 React 这样的现代框架中抽象的核心单元。有两种主要方法可以考虑创建它们。以下是 React 中的思考必须表达的内容:

”您可以自上而下或自下而上地构建。也就是说,您可以从构建层次结构中更高级别的组件开始。在更简单的示例中,自上而下通常更容易,而在较大的项目中,在构建时自下而上并编写测试更容易。“

更可靠的建议。乍一看,这听起来很简单。就像读“单一责任是好的”一样,很容易点头继续前进。

但是,自上而下的心智模型和自下而上的心智模型之间的区别,比表面上看起来要重要得多。当大规模应用时,当一种思维模式被广泛分享作为构建组件的隐含方式时,这两种思维模式都会导致非常不同的结果。

自上而下建筑

上面的引文暗示了在通过对更简单的示例采用自上而下的方法轻松取得进展与对大型项目采用较慢的更可扩展的自下而上的方法之间的权衡。

自上而下通常是最直观、最直接的方法。根据我的经验,这是从事功能开发的开发人员在构建组件时最常见的心智模型。

自上而下的方法是什么样子的?当给出要构建的设计时,常见的建议是“在UI周围绘制框,这些框将成为您的组件”。

这构成了我们最终创建的顶级组件的基础。使用这种方法,我们通常会首先创建一个粗粒度的组件。有了看似正确的界限就可以开始了。

假设我们获得了需要构建的新管理员仪表板的设计。我们继续查看设计,看看我们需要制造哪些组件。

它在设计中有一个新的侧边栏导航。我们在侧边栏周围画一个框,并创建一个故事,告诉开发人员创建新组件。

按照这种自上而下的方法,我们可能会考虑它需要什么道具,以及它是如何渲染的。假设我们从后端 API 获取导航项的列表。按照我们的隐式自上而下的模型,看到一个类似于下面的伪代码的初始设计也就不足为奇了:

到目前为止,我们的自上而下的方法似乎相当直接和直观。我们的目的是使事情变得简单和可重用,消费者只需要传递他们想要渲染的物品,我们将为他们处理它。

在自上而下的方法中常见的一些注意事项:

  1. 我们开始在最初确定为所需组件的顶级边界上进行构建。我们从盒子里画了一下设计。

  2. 它是一个单一的抽象,处理与侧面导航栏相关的所有事情。

  3. 它的API通常是“自上而下的”,从某种意义上说,消费者传递它需要的数据通过顶部工作,并在引擎盖下处理所有内容。 通常,我们的组件直接从后端数据源呈现数据,因此这符合将数据“向下”传递到组件中以进行呈现的相同模型。

对于较小的项目,这种方法不一定有问题。对于许多开发人员试图快速交付的大型代码库,我们将看到自上而下的心智模型如何迅速大规模出现问题。

自上而下出错的地方

自上而下的思维模式倾向于将自己固定在一个特定的抽象上,以解决眼前的问题。

这是直观的。这通常感觉像是构建构件的最直接的方法。它还经常导致针对初始易用性进行优化的 API。

下面是一个比较常见的方案。你是一个团队,一个正在快速开发的项目。您已经绘制了框并创建了故事,现在您已经合并了新组件。出现了一个新的要求,要求您更新侧面导航组件。

这是事情开始快速变得毛茸茸的时候。这是一组常见的情况,可能导致创建大型整体组件。

开发人员选取故事进行更改。他们到达现场,准备编码。它们处于已经确定的抽象和API的上下文中。

他们是否:

答 - 考虑这是否是正确的抽象。如果没有,请在执行其故事中概述的工作之前,通过主动分解它来撤消它。

B - 添加其他属性。在检查该属性的简单条件后面添加新功能。写一些测试,通过新的道具。它的工作原理并经过测试。作为额外工作,它很快就完成了。

正如桑迪·梅茨所说:

”现有代码具有强大的影响力。它的存在本身就表明它既正确又必要。我们知道代码代表了所花费的努力,我们非常积极地保护这种努力的价值。不幸的是,可悲的事实是,代码越复杂和难以理解,即创建它的投资越深,我们就越感到保留它的压力(“沉没成本谬误”)。“

沉没成本谬误之所以存在,是因为我们自然而然地更敏锐地避免了损失。当你添加时间压力时,要么来自截止日期,要么只是简单地“故事点是1”。选择A的几率可能不利于你(或你的队友)。

在规模上,正是这些较小决策的快速高潮迅速累积起来,并开始加剧我们组件的复杂性。

不幸的是,我们现在失败了“在反应中思考”中概述的基本原则之一。容易做到的事情,通常不会带来简单。与替代方案相比,导致我们变得简单的事情并不容易做到。

警告

让我们将此常见方案应用于简单的导航侧边栏示例。

第一个设计变更随之而来。我们需要添加对导航项具有图标,不同大小的文本以及其中一些是链接而不是SPA页面过渡的要求。

在实践中,UI包含许多视觉状态。我们还希望有分隔符,在新选项卡中打开链接,一些选择默认状态之类的东西。

由于我们将导航项列表作为数组向下传递到侧边栏组件,因此对于这些新要求中的每一个,我们需要在这些对象上添加一些其他属性,以区分新类型的导航项及其各种状态。

因此,我们现在的类型可能看起来像与类型相对应的类型,无论它是链接还是常规导航项:等等。

然后在内部,我们必须检查并基于此呈现导航项。像这样的小变化已经开始有点味道了。

这里的问题是具有此类API的自上而下的组件,必须通过添加到API来响应需求的变化,并根据传入的内容在内部分叉逻辑。

”从小事做大事成长“

几周后,正在请求一项新功能,并且需要能够单击导航项并转换为该项目下的嵌套子导航,并使用后退按钮返回主导航列表。我们还希望管理员能够通过拖放对导航项重新排序。

我们现在需要有嵌套列表的概念,并将子列表与父列表相关联,以及某些项目是否存在。

一些需求发生了变化,你可以看到事情是如何开始变得复杂的。

最初作为一个具有简单API的相对简单的组件,在几次快速迭代中迅速发展成其他东西。假设我们的开发人员设法让事情及时完成。

此时,需要使用或调整此组件的下一个开发人员或团队正在处理需要复杂配置的整体式组件,也就是说(让我们真实一点)很可能记录得很差。如果有的话。

我们最初的意图是“只需传递列表,组件将负责其余的”在这一点上已经反弹,并且组件进行更改既慢又有风险。

此时的常见方案是考虑丢弃所有内容并从头开始重写组件。现在,我们已经了解了从第一轮迭代中需要解决的问题和用例。

整体组件的有机增长

”一切都应该自上而下地建立起来,除了第一次。“

正如我们所看到的,整体组件是试图做太多事情的组件。他们通过道具接收太多数据或配置选项,管理太多状态,输出太多UI。

它们通常从简单的组件开始,并且通过如上所述的复杂性的有机增长(更常见),最终会随着时间的推移做太多事情。

从简单组件开始的,在构建新功能的几次迭代中(即使在相同的冲刺 (sprint 中)就可以成为整体式组件。

当团队在快速开发下在同一代码库上工作时,当多个组件发生这种情况时,前端很快就会变得更难更改,并且最终用户的速度会变慢。

以下是整体组件可能导致事物悄无声息地内爆的其他一些方式。

  • 它们通过过早的抽象而产生。还有一个微妙的陷阱导致整体组件。与一些早期作为软件开发人员灌输的常见模型有关。特别是对DRY的坚持(不要重复自己)。 事实上,DRY很早就根深蒂固了,我们在组成组件的站点上看到了少量的重复。人们很容易认为“这被重复了很多,最好将其抽象成一个组件”,然后我们匆匆忙忙地进入一个过早的抽象。 一切都是权衡,但从没有抽象中恢复比从错误的抽象中恢复要容易得多。正如我们将在下面进一步讨论的那样,从自下而上的模型开始,使我们能够有机地达到这些抽象,从而避免过早地创建它们。

  • 它们会阻止跨团队重用代码。您经常会发现另一个团队已经实施或正在开发与您的团队需要的内容类似的东西。 在大多数情况下,它会做你想要的90%,但你想要一些轻微的变化。或者你只是想重用其功能的特定部分,而不必承担整个事情。 如果像我们这样是一个单一的“全有或全无”组件,那么利用现有工作将更加困难。而不是承担重构或分解他人包的风险。通常,只需重新实现并将其分叉到您自己的软件包中,通常会变得更容易。导致多个重复的组件,所有组件都有轻微的变化,并遭受相同的问题。

  • 它们使捆绑大小膨胀。我们如何只允许在正确的时间加载、解析和运行的代码? 有一些组件更重要,首先要向用户显示。大型应用程序的一个关键性能策略是基于优先级在“阶段”中协调异步加载的代码。 除了使组件能够选择加入和退出在服务器上呈现(因为理想情况下,我们只使用用户在第一次绘制时实际看到的组件尽可能快地执行服务器端呈现)。这里的想法是尽可能推迟。 整体式组件可以防止这些工作发生,因为您必须将所有内容作为一个大块组件加载。而不是拥有可以优化的独立组件,并且仅在用户真正需要时才加载。消费者只需支付他们实际使用的性能价格。

  • 它们会导致较差的运行时性能。像 React 这样的框架具有简单的状态 -> UI 功能模型,其效率非常高。但是,查看虚拟DOM中已更改内容的对帐过程在规模上是昂贵的。整体式组件使得很难确保在该状态更改时仅重新渲染最少量的内容。 在像 React 这样的框架中实现更好的渲染性能的最简单方法之一,就是将更改的组件与更改的组件分开。 因此,当状态发生变化时,您只会重新呈现严格必要的内容。如果使用像 Relay 这样的声明性数据提取框架,则此技术对于防止在数据更新发生时代价高昂地重新呈现子树变得越来越重要。 在整体式组件和一般的自上而下的方法中,找到这种分裂是困难的,容易出错,并且经常导致过度使用。

自下而上构建

与自上而下的方法相比,自下而上的方法通常不那么直观,并且最初可能较慢。它会导致多个较小的组件,其API是可重用的。而不是大厨房水槽式组件。

当您尝试快速发货时,这是一种不直观的方法,因为并非每个组件都需要在实践中重复使用。

但是,创建其API可以重用的组件,即使它们不是,通常会导致更具可读性,可测试性,可更改性和可删除的组件结构。

关于事情应该分解到什么程度,没有一个正确的答案。管理这一点的关键是使用单一责任原则作为一般准则。

自下而上的心智模型与自上而下的心智模型有何不同?

回到我们的例子。通过自下而上的方法,我们仍然有可能创建一个顶级水平,但正是我们如何建立它,使一切变得不同。

我们确定了顶层,但不同之处在于我们的工作不是从那里开始的。

它首先对构成整体功能的所有基础元素进行编目,并构造那些可以组合在一起的较小部分。通过这种方式,在开始时它稍微不那么直观。

总复杂性分布在许多较小的单责任组件中,而不是单个整体组件中。

自下而上的方法是什么样子的?

让我们回到侧面导航示例。下面是一个简单案例的示例:

在简单的情况下,没有什么了不起的。支持嵌套组的 API 会是什么样子?

自下而上方法的最终结果是直观的。这需要更多的前期工作,因为更简单的API的复杂性被封装在各个组件后面。但这就是使它成为一种更具消耗性和适应性的长期方法的原因。

与我们自上而下的方法相比,其优势很多:

  1. 使用该组件的不同团队只需为他们实际导入和使用的组件付费。

  2. 我们还可以很容易地对不是用户直接优先级的拆分和异步加载元素进行代码化。

  3. 渲染性能更好,更易于管理,因为只有由于更新而更改的子树需要重新渲染。

  4. 我们可以创建和优化在导航中具有特定责任的单个组件。从代码结构的角度来看,它也更具可扩展性,因为每个组件都可以单独处理和优化。

有什么问题?

自下而上最初较慢,但从长远来看更快,因为它更具适应性。您可以更轻松地避免仓促的抽象,而是随着时间的推移,在正确的抽象变得明显之前,顺应变化的浪潮。这是防止整体组件扩散的最佳方法。

如果它是跨代码库使用的共享组件,就像我们的侧边栏导航一样,则自下而上构建通常需要消费者方面稍微多一点的精力来组装这些部分。但正如我们所看到的,这是一个值得在具有许多共享组件的大型项目中进行的权衡。

自下而上方法的强大之处在于,您的模型始于“我可以组合哪些简单的基元来实现我想要的东西”为前提,而不是从已经想到的特定抽象开始。

”敏捷软件开发最重要的教训之一是迭代的价值;这适用于软件开发的各个层面,包括架构”

从长远来看,自下而上的方法使您可以更好地进行迭代。

未完待续......

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值