译:不要将 DTO 传递给用户界面组件

原文: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 中的组件处于更低的抽象层次,因此组件的接口应该反映这一点。