小部件驱动开发
GenAI LIVE! | 2025年6月4日
前言
在开发 UI 应用时,我们通常使用组件来构建应用。每个 UI 组件本质上都是标记、作用域样式和一些 UI 逻辑的组合。数据管理通常不受组件控制,导致架构复杂,数据流错综复杂。
在本文中,我将演示如何将组件转换为自主隔离的小部件,并完全控制数据逻辑和 UI。
组件的历史
在我看来,Widget 是组件的自然继承者。为了理解这一点,我建议回顾一下我们构建 UI 的方法是如何随着时间演变的。
许多人还记得,所有应用程序样式都定义在一个全局 CSS 文件中的时代。样式定义使用了各种CSS 选择器的复杂组合。应用程序中经常发生样式冲突。这些样式的大小和复杂性有时甚至会影响网站的性能。
BEM诞生于 2009 年。BEM 提供了一套用于定义样式和命名类的指南。这些规则旨在解决样式冲突和选择器效率低下的问题。BEM 鼓励从块、元素和修饰符的角度来思考 UI。
2013-2015 年标志着组件化方法的兴起。React 简化了 UI 拆分,将标记(HTML)和 UI 逻辑(JavaScript)组合成组件的过程。这彻底改变了应用程序开发的游戏规则。其他框架也很快效仿,采用了基于组件的方法。
随着构建工具、CSS 预处理器以及 CSS-in-JS 和CSS 模块等技术的兴起,将样式作为组件的一部分变得可行。
诸如 Storybook 之类的组件游乐场应运而生,旨在帮助开发者在隔离的环境中构建组件,并确保适当的样式范围。它们鼓励开发者将 UI 视为状态函数:组件的 props 值定义了组件的外观和行为。
可重复使用的高质量组件的集合已成为现实。
尚未解决的障碍
组件驱动的方法有助于将 UI 分解为独立的可重用部分,并能够使用预先构建的组件集合来构建大型应用程序。
但缺少的是一种向 UI 组件提供数据的方法。
数据管理成为前端工程中最困难的任务之一,也是 UI 应用程序复杂性的主要原因。
我们学会了将组件分为两种类型:
- 展示组件,负责 UI 呈现,通常无状态且无副作用
- 容器组件,处理与数据相关的逻辑并将数据传递给展示组件。
剩下的就是定义容器组件如何处理数据。
朴素方法
简单的方法是让每个容器组件简单地获取底层展示组件所需的数据。
由于相同的数据通常被多个不同的组件需要,因此在实践中实现这种方法会带来一系列问题:
- 重复请求和数据过度获取。导致 UI 速度缓慢、服务器过载。
- 当对同一端点的请求导致不同的数据时,组件之间可能存在数据不一致
- 复杂的数据失效(想象一下后端数据发生变化的情况,你需要确保每个依赖组件重新获取数据)
共同父母方法
我们学会了通过将数据获取(和变异)功能上移到公共父组件来解决这个问题,然后将数据传递给所有底层组件。
我们解决了请求重复和数据失效的问题,但也面临着新的挑战:
- 整个应用程序逻辑变得更加复杂和耦合
- 我们被迫通过多个组件向下传递数据。这个问题变得臭名昭著,并被命名为“Prop Drilling”。
国家管理方法
为了规避 Prop Drilling 问题,我们学习了状态管理库和技术:我们不再将数据向下传播到底层组件,而是将数据放置在某个 Store 中,所有组件都可以访问该 Store,让它们直接从 Store 获取数据。组件订阅 Store 中的更改,以确保数据始终保持最新。
螺旋桨钻孔问题已得到解决,但不是免费的:
-
我们现在必须处理一个全新的概念,即 Store,并关心一堆新事物,例如设计和维护 Store 结构、适当更新 Store 中的数据、数据规范化、可变与不可变、单个 store 与多个 store 等等。
-
状态管理库要求我们学习新的词汇:Actions、Action Creators、Reducers、Middlewares、Thunks等等。
-
引入的复杂性和缺乏清晰度迫使开发人员创建有关如何使用商店、做什么和避免什么的样式指南。
-
结果,我们的应用程序变得非常混乱和耦合。沮丧的开发人员试图通过发明具有不同语法的新状态管理库来缓解这些问题。
重新构想的朴素方法
我们能做得更好吗?有没有更简单的数据管理方法?我们能让数据流透明易懂吗?我们能理清我们的应用程序并提升正交性吗?我们能像控制标记、样式和 UI 逻辑一样,将数据逻辑置于组件的控制之下吗?
我们肯定是陷入了困境,只见树木不见森林。让我们回到起点,回到朴素方法,看看能否用不同的方法解决它的问题。
主要问题是请求重复和数据不一致。
如果我们可以在组件和后端之间有一个中间层,比如 API 包装器或拦截器,来解决所有这些问题,会怎么样呢?
- 删除所有请求的重复数据
- 确保数据一致性:所有组件在使用相同请求时应始终具有相同的数据
- 提供数据失效能力:如果组件更改服务器上的数据,则依赖该数据的其他组件应该接收新数据
- 对组件透明,并且不会以任何方式影响其逻辑(使组件认为它们直接与后端通信)
好消息是我们可以拥有它,并且已经有图书馆提供这样的解决方案:
- 一些 GraphQL 客户端,例如Relay
- React-Query、SWR、Redux Toolkit Query、适用于 RESTful API 的Vue Query
我们基本上需要做的就是用这样的 API 包装器来包装每个 API 调用。剩下的事情就交给我们自动处理了。
这种方法的巨大好处是,我们最终可以解开应用程序的数据逻辑,将数据逻辑置于组件的控制之下,并通过将所有部分组合在一起实现更好的正交性。
小部件驱动开发
在我的团队中,我们开始将上述 Naive 方法与 React Query 结合使用,我们非常喜欢它。它使我们能够以不同的方式构建应用程序。我称之为“Widget 驱动开发”。
我们的想法是将每个页面分成所谓的小部件,这些小部件可以自主运行并且自成一体。
每个小部件负责:
- 获取并提供所有需要的数据给 UI
- 如果需要,改变服务器上的相关数据
- UI 中的数据表示
- 加载状态的 UI
- (可选)错误状态的 UI
说到代码组织,我们将所有与小部件相关的文件放在一起:
通常,多个小部件会使用相同的 API 端点。因此,我们决定将它们全部保存在一个单独的共享文件夹中。
我们使用 React Query 库,queries/
文件夹中的每个文件都公开了包装在 React Query 中的获取和变异方法。
所有容器组件都有类似的代码结构。
import { useParams } from 'react-router-dom';
import { useBookQuery } from 'queries/useBookQuery';
import { useAuthorQuery } from 'queries/useAuthorQuery';
import Presentation from './Presentation';
import Loading from './Loading';
import Error from './Error';
export default BookDetailsContainer() {
const { bookId } = useParams();
const { data: book, isError: isBookError } = useBookQuery(bookId);
const { data: author, isError: isAuthorError } = useAuthorQuery(book?.author);
if (book && author) {
return <Presentation book={book} author={author} />
}
if (isBookError || isAuthorError) {
return <Error />
}
return <Loading />
}
注意,依赖查询的处理方式是多么简单,并且声明式地。此外,我们的小部件的唯一依赖项是bookId
URL 中的 。
我们的大多数小部件的容器组件都没有道具,并且除了 URL 数据之外不依赖任何外部状态。
这种方法使我们的小部件所依赖的 API 查询变得透明。这种透明性加上几乎零的外部依赖性,使得小部件的测试变得简单,并增强了我们对代码的信心。
通常,对小部件的更改仅限于修改该小部件文件夹下的文件。这极大地降低了破坏应用程序其他部分的风险。
添加新的小部件也非常简单:为小部件创建一个包含所有必需文件的新文件夹,并在必要时在该/queries
文件夹中创建一个新的查询。同样,破坏应用程序其他部分的风险非常有限。
由于对上下文的依赖性较低,每个小部件也可以轻松地在不同页面上复用。我们通常只需要确保这些页面的 URL 包含小部件所需的数据标识符即可。
结论
组件方法使得创建可重用的独立 UI 变得简单直接。
但它并不能解决所有问题,前端应用程序常常受到数据管理复杂度的困扰。
有一些库可以采用不同的方式进行数据管理,并显著降低应用程序的复杂性。
利用这些库,我们可以将数据逻辑置于组件的控制之下,并将应用程序转换为一组可重用的独立小部件。这使得数据流透明、架构灵活、代码弹性高且易于测试。
鏂囩珷鏉ユ簮锛�https://dev.to/aantipov/widget-driven-development-1n5m