架构师的交响曲:超越 React Query 和 SWR 的数据层构建

52 阅读6分钟

您站在应用程序摩天大楼的顶端,下方是熙熙攘攘的组件之城。多年来,您一直依赖值得信赖的管家——React Query,SWR——来管理这座大都市的命脉:数据。它们非常出色。它们以我们曾经梦寐以求的优雅处理着基础的获取、缓存和状态同步。

但你是一位高级工程师。你已经感受到了那些细微的裂痕。useQuery调用像五彩纸屑一样洒落在数百个组件上。无效化修改操作的尴尬舞步,触及了多个不同的查询。无声无息、不断增加的复杂性,将一个简单的功能请求变成了对层层缓存键的考古挖掘。

是时候超越现成的解决方案了。是时候停止仅仅使用数据层,开始构建一个数据层了。从租户转变为架构师。

这是构建强大、有意的异步数据层的艺术。

[]缺点画廊:为什么最好的工具还不够

我们钟爱的图书馆就像一幅幅大师级的印象派画作——远观美不胜收,但细节却略显模糊。对于大多数应用来说,它们堪称完美。但对于复杂的大型应用来说,我们开始看到其局限性:

  1. 共置难题:  “每个组件一个钩子”模型巧妙地将数据与 UI 共置。但它也分散了关于数据存在以及如何更改数据的知识,使其分散在整个应用程序中。您的领域逻辑没有单一的可信来源。
  2. 缓存键的狂欢: 我们将逻辑绑定到脆弱的字符串键上(['posts', 123])。应用程序某个部分发生变更时,必须知道另一个部分中键的确切含义才能使其失效。这种隐蔽的耦合最终会成为维护的噩梦。
  3. 黑匣子: 我们失去了细粒度的控制。缓存实际上是如何存储的?请求是如何去重的?我们如何实现自定义缓存策略,比如 IndexedDB 的直写缓存?我们经常被库的选择所束缚。
  4. Bundle Beast: 我们引入了一个强大的通用库来解决我们的具体问题。它包含一些我们可能永远不会用到的功能的代码,所有费用都由客户承担。

您的旅程始于这样的认识:我们不需要更智能的缓存;我们需要更智能的数据接口。

[]架构师的蓝图:设计数据层

想象一下,你的数据层不是一个缓存,而是一个中央的、有状态的管弦乐队指挥。它不仅记得谁演奏了什么;它知道整个乐谱、音乐家,以及如何指挥他们完美地和谐相处。

我们的创作将建立在四个核心乐章之上:

1. 统一 API 层:仪器库

首先,我们规范了与外界通信的方式。不再有fetch隐藏在钩子中的任意调用。

// api/todoApi.ts
// A pure, framework-agnostic instrument for playing the "Todo" score.
export const todoApi = {
  getTodos: (): Promise<Todo[]> => fetch('/api/todos').then(r => r.json()),
  getTodo: (id: string): Promise<Todo> => fetch(`/api/todos/${id}`).then(r => r.json()),
  updateTodo: (id: string, patch: Partial<Todo>): Promise<Todo> =>
    fetch(`/api/todos/${id}`, {
      method: 'PATCH',
      body: JSON.stringify(patch),
    }).then(r => r.json()),
  // ... more instruments
};

这是我们的基础。它没有状态,没有框架依赖。它只是一系列精心打造的工具的集合。

2. 数据存储:指挥乐谱

这是我们交响乐的核心。我们将创建一个中央存储(使用trx租赁 Zustand、Redux,甚至仅仅是 context + Reducer),用于保存我们应用程序的域状态——也就是我们获取的数据。

// stores/useTodoStore.ts
import { create } from 'zustand';
import { todoApi } from '../api/todoApi';

interface TodoStore {
  todos: Record<string, Todo>; // Normalized cache: { '1': {id: '1', ...}, '2': ... }
  loading: boolean;
  errors: Record<string, string>;

  // The conductor's actions
  actions: {
    fetchTodo: (id: string) => Promise<void>;
    fetchTodos: () => Promise<void>;
    updateTodo: (id: string, patch: Partial<Todo>) => Promise<void>;
  };
}

export const useTodoStore = create<TodoStore>((set, get) => ({
  todos: {},
  loading: false,
  errors: {},

  actions: {
    fetchTodo: async (id: string) => {
      try {
        const todo = await todoApi.getTodo(id);
        set((state) => ({
          todos: { ...state.todos, [todo.id]: todo },
        }));
      } catch (error) {
        set((state) => ({
          errors: { ...state.errors, [id]: error.message },
        }));
      }
    },
    // ... other actions implemented with a similar pattern
  },
}));

注意规范化。我们将项目存储在以 ID 为键的字典中。这使得更新可以即时进行,并消除了重复。

3. 定制钩子:首席小提琴手

现在,我们创建自己优雅的钩子函数来与 store 交互。在这里,我们注入自己的优化和逻辑。

// hooks/useTodos.ts
import { useTodoStore } from '../stores/useTodoStore';
import { useEffect } from 'react';

export const useTodos = () => {
  const { todos, loading, errors, actions } = useTodoStore();

  // Automatically fetch on mount? Maybe, maybe not. Your choice.
  useEffect(() => {
    actions.fetchTodos();
  }, []);

  // Derive data as needed
  const completedTodos = Object.values(todos).filter(todo => todo.completed);

  return {
    todos: Object.values(todos), // Return an array for the UI
    completedTodos,
    loading,
    errors,
    updateTodo: actions.updateTodo, // Expose the action
  };
};

这个钩子现在成了我们所有与 Todo 相关的控制接口。组件不再关心缓存键或库。

4. 进阶技巧:大师之作

这就是我们绘制杰作的地方。我们的自定义层为我们提供了一个实现强大模式的统一位置:

  • 请求重复数据删除: 将您的 API 调用包装在一个简单的Promise缓存函数中,以确保对相同数据的同时调用被重复数据删除。
  • 乐观更新: 在 中updateTodo,在请求解析之前立即更新存储,然后在发生错误时处理回滚。所有逻辑都集中在一个统一的地方。
  • 持久性: localStorage通过修改存储,轻松地将写入缓存分层到IndexedDB。
  • 离线支持: 当应用程序加载时,商店将成为您可以从持久层获取的唯一真实来源。

[]完成的交响曲:新的和声

在组件中,分散的钩子的混乱被宁静的意图所取代:

// Before: Scattered, coupled, and fragile
// const { data: todos } = useQuery(['todos'], fetchTodos);
// const { mutate: updateTodo } = useMutation(updateTodoApi, {
//   onSuccess: () => {
//     queryClient.invalidateQueries(['todos']);
//     queryClient.invalidateQueries(['todo', id]);
//   },
// });

// After: Intentional, decoupled, and robust
const { todos, loading, updateTodo } = useTodos();

const handleComplete = (id: string) => {
  updateTodo(id, { completed: true });
};

组件被释放了。它对缓存键、失效策略或底层库一无所知。它只是声明了自己的意图。数据层,也就是我们的指挥者,会精准地处理剩下的事情。

[]策展人笔记

这段旅程并不意味着要放弃 React Query 或 SWR。它们都是非常棒的工具。这段旅程关乎抽象意向性的艺术。它关乎何时认识到你的应用程序的需求已经超出了通用解决方案的范围,并需要一个专门为你的应用领域量身定制的系统。

你不再仅仅是工具的使用者。你是一位作曲家,一位架构师。你超越了缓存的局限,构建的不仅仅是一个功能,更是一个基础——一个健壮、可扩展且精湛的异步数据层。