原文:https://darios.blog/posts/do-not-pass-dtos-to-ui-components
作者:Dario
译者:ChatGPT 4 Turbo
编者注:1)DTO 即来自后端的 data transfer objects,2)直接传给 UI 层会导致可维护性、可重用性和关注点分离方面的问题,增加了耦合,当后端接口变更时,重构和修改会变得困难,3)解法是增加「数据访问层」,做减法和添加派生字段。
作为前端开发人员,我们经常处理来自后端 API 或服务的数据传输对象(DTOs)。这些 DTOs 代表了用于网络传输的原始数据结构。然而,在 UI 组件中直接使用 DTOs 可能会导致可维护性、可重用性和关注点分离方面的问题。
DTO 反模式
将 DTOs 直接作为 props 传递给 UI 组件会使你的 UI 组件与后端的数据传输结构紧密耦合。当后端数据模型变化时,这可能会使修改或重构组件接口变得困难,并且在开发的早期阶段,它们可能会发生显著变化。在组件中直接使用 DTOs 还违反了最少权限原则,因为它为组件提供了它们不需要的更多数据。最后,直接在组件中使用传输数据模糊了数据访问和 UI 渲染角色之间的界限。
我们不应该将原始的 DTOs 传递给组件,而应该引入一个数据访问层,作为我们的 UI 和后端服务之间的抽象边界。
数据访问层
把数据访问层看作是一个映射层,它将后端的 DTOs 转换为专门为你的应用程序 UI 的需要制定的简化对象模型。这可能意味着扁平化嵌套对象,选择一部分属性,派生计算字段,或者其他必要的数据转换。
数据访问层本质上隔离了传输数据模型,防止它们泄漏到你的 UI 组件的域中并污染它。组件只需要知道为其特定职责塑形的对象模型,而不是数据在幕后传输的各种细节。
例如,假设你有一个后端 DTO 用于博客文章,看起来像这样:
{
id: "abc123",
authorId: 42,
title: "New Blog Post",
content: "This is my new blog post...",
metadata: {
createdAt: "2022-01-15T08:25:00Z",
updatedAt: "2022-01-15T08:25:00Z",
tags: ["react", "javascript"]
}
}
您的数据访问层可以将此 DTO 映射为一个为 UI 渲染而设计的简化的 Post
对象:
{
id: "abc123",
author: "Jane Doe",
title: "New Blog Post",
content: "This is my new blog post...",
formattedDate: "January 15, 2022",
tags: ["react", "javascript"]
}
注意后一个对象省略了像 authorId
这样 UI 不需要的额外属性。它还映射和派生了新的字段,如 author 和 formattedDate
,这些字段是显示目的更有用的抽象。
遵循抽象边界
有了数据访问层来将 DTOs 转换为适合 UI 的视图模型,我们也可以在适当的抽象级别设计我们的 UI 组件属性和接口。接近您的组件树根部的容器组件可以处理代表整个页面、屏幕或复杂特性的高级抽象。
当我们深入到组件层次结构中时,我们可以引入更细粒度的抽象,这些抽象拥有更精简的接口,专注于它们专门的 UI 关注点所需的数据。这允许我们遵循最小权限原则 – 只提供组件所需的精确数据,而不更多。
例如,一个顶级的 BlogPost
组件可能需要整个帖子数据模型来协调渲染标题、内容、元数据等子组件:
// BlogPost.jsx
const BlogPost = ({ post }) => {
return (
<article>
<BlogPostHeader
title={post.title}
formattedDate={post.formattedDate}
tags={post.tags}
/>
<BlogPostContent content={post.content} />
</article>
)
}
而 BlogPostHeader
组件只需要该数据的子集:
// BlogPostHeader.js
const BlogPostHeader = ({ title, formattedDate, tags }) => {
return (
<header>
<h1>{title}</h1>
<time>{formattedDate}</time>
<BlogPostTags tags={tags} />
</header>
)
}
而 BlogPostTags
组件需要的更少:
// BlogPostTags.js
const BlogPostTags = ({ tags }) => {
return (
<ul>
{tags.map(tag => <li key={tag}>{tag}</li>)}
</ul>
)
}
通过保留抽象边界和有意识地建模组件接口,我们可以构建一个更加模块化和可维护的 UI 架构。数据访问层从后端数据模型的变化中保护了组件,而简化的 props 促进了 UI 元素更好的重用和组合。
因此,当你下次在开发前端应用时,试着思考每个组件的接口是否独立。它真的需要你提供的大量数据吗?或者它能用更少的数据工作吗?看看你是如何使用来自 API 的数据。通过网络传输的对象比你 UI 中的组件处于更低的抽象层次,因此组件的接口应该反映这一点。