GraphQL 在前端的落地实践:从 REST 迁移到 Apollo Client 全指南

By | 2026年10月8日

为什么前端需要 GraphQL?

如果你经历过 REST API 的“接口爆炸”和“过度获取”,一定深有体会:一个页面需要调用 3~5 个接口,返回的数据字段可能只用了一半,或者为了拿一个字段不得不请求整个对象。GraphQL 让前端可以精确声明需要的数据,一次请求获取所有资源,极大提升了开发效率和性能。

但 GraphQL 在前端落地并非只是换个请求库那么简单。本文将通过一个 Todo 应用,手把手教你如何设计 Schema、集成 Apollo Client、处理缓存与错误,并分享我在生产环境中总结的最佳实践。

技术栈选择

  • 前端:React 18 + TypeScript
  • GraphQL 客户端:Apollo Client 3
  • 后端:Node.js + Apollo Server(示例用内存数据,方便运行)
  • 构建工具:Vite

> 注意:本文假设你已了解 GraphQL 基础语法(Query、Mutation、Subscription)。如果不熟悉,建议先阅读官方文档。

第 1 步:定义 Schema 与搭建服务端

首先,我们需要一个 GraphQL 服务端。创建 server.ts:


import { ApolloServer } from '@apollo/server';
import { startStandaloneServer } from '@apollo/server/standalone';

// 内存数据
let todos = [
  { id: '1', text: '学习 GraphQL', completed: false },
  { id: '2', text: '写一篇教程', completed: true },
];

const typeDefs = `#graphql
  type Todo {
    id: ID!
    text: String!
    completed: Boolean!
  }

  type Query {
    todos: [Todo!]!
    todo(id: ID!): Todo
  }

  type Mutation {
    addTodo(text: String!): Todo!
    toggleTodo(id: ID!): Todo!
    deleteTodo(id: ID!): Boolean!
  }
`;

const resolvers = {
  Query: {
    todos: () => todos,
    todo: (_, { id }) => todos.find(t => t.id === id),
  },
  Mutation: {
    addTodo: (_, { text }) => {
      const newTodo = { id: String(Date.now()), text, completed: false };
      todos.push(newTodo);
      return newTodo;
    },
    toggleTodo: (_, { id }) => {
      const todo = todos.find(t => t.id === id);
      if (!todo) throw new Error('Todo not found');
      todo.completed = !todo.completed;
      return todo;
    },
    deleteTodo: (_, { id }) => {
      const index = todos.findIndex(t => t.id === id);
      if (index === -1) return false;
      todos.splice(index, 1);
      return true;
    },
  },
};

const server = new ApolloServer({ typeDefs, resolvers });

startStandaloneServer(server, { listen: { port: 4000 } }).then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

运行 node server.js(需先编译或使用 ts-node),服务端就启动了。

第 2 步:前端集成 Apollo Client

安装依赖:


npm install @apollo/client graphql

创建 apolloClient.ts:


import { ApolloClient, InMemoryCache, HttpLink } from '@apollo/client';

const client = new ApolloClient({
  link: new HttpLink({ uri: 'http://localhost:4000/graphql' }),
  cache: new InMemoryCache({
    typePolicies: {
      Query: {
        fields: {
          todos: {
            merge(existing, incoming) {
              return incoming; // 简单覆盖,后续可优化
            },
          },
        },
      },
    },
  }),
});

export default client;

在 main.tsx 中包裹 ApolloProvider:


import React from 'react';
import ReactDOM from 'react-dom/client';
import { ApolloProvider } from '@apollo/client';
import client from './apolloClient';
import App from './App';

ReactDOM.createRoot(document.getElementById('root')!).render(
  <React.StrictMode>
    <ApolloProvider client={client}>
      <App />
    </ApolloProvider>
  </React.StrictMode>
);

第 3 步:编写 Query 和 Mutation

使用 gql 模板字符串定义操作。创建 graphql/operations.ts:


import { gql } from '@apollo/client';

export const GET_TODOS = gql`
  query GetTodos {
    todos {
      id
      text
      completed
    }
  }
`;

export const ADD_TODO = gql`
  mutation AddTodo($text: String!) {
    addTodo(text: $text) {
      id
      text
      completed
    }
  }
`;

export const TOGGLE_TODO = gql`
  mutation ToggleTodo($id: ID!) {
    toggleTodo(id: $id) {
      id
      completed
    }
  }
`;

export const DELETE_TODO = gql`
  mutation DeleteTodo($id: ID!) {
    deleteTodo(id: $id)
  }
`;

第 4 步:在组件中使用

创建 TodoList.tsx:


import { useQuery, useMutation } from '@apollo/client';
import { GET_TODOS, ADD_TODO, TOGGLE_TODO, DELETE_TODO } from './graphql/operations';
import { useState } from 'react';

interface Todo {
  id: string;
  text: string;
  completed: boolean;
}

export default function TodoList() {
  const { loading, error, data } = useQuery<{ todos: Todo[] }>(GET_TODOS);
  const [addTodo] = useMutation(ADD_TODO, {
    refetchQueries: [{ query: GET_TODOS }], // 简单粗暴,后续用缓存更新替代
  });
  const [toggleTodo] = useMutation(TOGGLE_TODO);
  const [deleteTodo] = useMutation(DELETE_TODO, {
    refetchQueries: [{ query: GET_TODOS }],
  });
  const [input, setInput] = useState('');

  if (loading) return <p>加载中...</p>;
  if (error) return <p>错误: {error.message}</p>;

  const handleAdd = () => {
    if (!input.trim()) return;
    addTodo({ variables: { text: input } });
    setInput('');
  };

  return (
    <div>
      <input value={input} onChange={e => setInput(e.target.value)} />
      <button onClick={handleAdd}>添加</button>
      <ul>
        {data?.todos.map(todo => (
          <li key={todo.id}>
            <input
              type="checkbox"
              checked={todo.completed}
              onChange={() => toggleTodo({ variables: { id: todo.id } })}
            />
            <span style={{ textDecoration: todo.completed ? 'line-through' : 'none' }}>
              {todo.text}
            </span>
            <button onClick={() => deleteTodo({ variables: { id: todo.id } })}>删除</button>
          </li>
        ))}
      </ul>
    </div>
  );
}

运行应用,你应该能看到完整的 Todo 增删改查功能。

最佳实践与踩坑指南

1. 用缓存更新代替 refetchQueries

上面的 refetchQueries 会重新请求整个列表,性能较差。更优雅的方式是利用 Apollo 的缓存自动更新:


const [addTodo] = useMutation(ADD_TODO, {
  update(cache, { data }) {
    const newTodo = data?.addTodo;
    if (!newTodo) return;
    cache.modify({
      fields: {
        todos(existing = []) {
          const newTodoRef = cache.writeFragment({
            data: newTodo,
            fragment: gql`
              fragment NewTodo on Todo {
                id
                text
                completed
              }
            `,
          });
          return [...existing, newTodoRef];
        },
      },
    });
  },
});

这样列表会自动更新,无需额外网络请求。

2. 处理错误与加载状态

Apollo 的 error 对象包含网络错误和 GraphQL 错误。建议使用 errorPolicy: 'all' 来同时获取部分数据和错误:


const { data, error } = useQuery(GET_TODOS, { errorPolicy: 'all' });

3. 分页与游标

对于列表数据,建议使用 fetchMore 实现无限滚动:


const { data, fetchMore } = useQuery(GET_TODOS, {
  variables: { offset: 0, limit: 10 },
});

const loadMore = () => {
  fetchMore({
    variables: { offset: data.todos.length, limit: 10 },
    updateQuery: (prev, { fetchMoreResult }) => {
      if (!fetchMoreResult) return prev;
      return { todos: [...prev.todos, ...fetchMoreResult.todos] };
    },
  });
};

4. 避免 N+1 查询

GraphQL 服务端容易产生 N+1 问题。例如,查询用户及其文章时,每个用户都会单独查一次文章。解决方案是使用 DataLoader 批量加载。


import DataLoader from 'dataloader';

const postLoader = new DataLoader(async (userIds) => {
  const posts = await db.posts.find({ userId: { $in: userIds } });
  return userIds.map(id => posts.filter(p => p.userId === id));
});

5. 安全与性能

  • 限制查询深度和复杂度,防止恶意查询。
  • 使用持久化查询(Persisted Queries)减少请求体积。
  • 生产环境启用 CDN 缓存 GET 请求。

总结

GraphQL 在前端落地需要关注 Schema 设计、客户端缓存、错误处理和性能优化。通过 Apollo Client,我们可以快速构建类型安全、高效的数据层。本文的示例代码已覆盖核心用法,你可以在此基础上扩展订阅、文件上传等高级功能。

下一步建议:

  • 学习 GraphQL Code Generator 自动生成 TypeScript 类型。
  • 探索 urql 或 Relay 等替代客户端。
  • 在服务端实施查询成本分析。

希望这篇教程能帮助你顺利将 GraphQL 引入生产项目!