数据库的“问答”能力,其实一直是个老大难问题。以前我们写接口,总是按前端页面来“定制”,一个页面配一个接口,看起来挺省事,但项目一复杂,接口数量多到让人抓狂。前端想要两三个字段,后端就得写一整个新接口,或者把一大堆用不上的字段也一起返回,白花花的流量就这么被浪费了。今天我想跟你聊聊怎么用 Next.js 配 GraphQL 把这块理顺,让前端要数据跟点菜一样方便,让后端写接口不再像夹心饼干一样两头受气。
一、先搞清楚我们要解决什么麻烦
1.1 传统接口像“固定套餐”
想象一下,你去快餐店点餐。传统 REST 接口就好比是“固定套餐”,套餐里有什么你就得吃什么。比如一个用户信息的接口,里面可能包含了姓名、年龄、地址、订单记录、关注列表等等一大堆东西。可你的页面上只需要显示一个名字和一个头像,但剩下的数据还是会被传过来,白白占用了网络带宽。
这种“过度获取”在移动端上尤其心疼,用户每刷一次页面,可能在无形中多花了流量钱。更麻烦的是,假如下次页面调整了,想多要一个字段,后端就得跟着改一版,甚至新写一个接口。
1.2 前端要数据像“自助餐”
GraphQL 跟 REST 不一样,它更像吃自助餐。你想吃什么,自己拿盘子去夹。前端写一个查询语句,告诉后端“我要用户的名字、头像,还有最近三条订单的金额”。后端就只把这三个东西打包给你,一点多余的都没有。这就叫“按需取数”。
这一点放在 Next.js 里特别合适,因为 Next.js 本身就很擅长做前后端混合开发,既有静态页面,又有服务端渲染,搭配上 GraphQL 的灵活取数,能让整个数据交互变得特别顺手。
二、理解数据交互的新路子
2.1 一个查询,直达数据深处
GraphQL 最爽的地方就是能在一个请求里,拿到你想要的嵌套数据。在传统接口里,想拿到用户的文章,再拿到文章下的评论,可能得先请求用户接口,再循环请求文章接口,再循环请求评论接口。那种“循环请求”不仅代码难写,速度还慢。
GraphQL 查询天生就是树状的,你要什么结构,它就返回什么结构。比如看下面这个查询,一口气把“用户、文章、评论”三级关系全拿回来了。
2.2 变与不变,都在掌控里
GraphQL 里有 Query(查询)和 Mutation(修改)两种核心操作。简单的理解就是:Query 负责“查”,Mutation 负责“写”。数据查出来是什么样,基本是由前端的查询语句说了算,后端只要把“数据仓库”的入口定义好就行。
三、开始动工,搭一个能用的项目
3.1 技术栈说明
在正式开始之前,先明确一下这篇博客用的技术栈:
- 前端框架:Next.js(最新 App Router 版本)
- 数据层:Apollo Client
- 服务端:Next.js API 路由(模拟 GraphQL 服务端)
- 语言:JavaScript
我们接下来的所有代码示例,都会用这套全 JavaScript 技术栈,不会中途换别的语言。这样可以保证大家照着复制粘贴就能跑起来,不用在环境配置上踩坑。
3.2 初始化项目
我们先用终端命令创建一个 Next.js 项目,名字就叫 “next-graphql-demo”。我建议大家在练习的时候,选 JavaScript 而不是 TypeScript,这样省去类型配置的繁琐步骤,专注搞清楚数据交互的整个流程。
# 创建名为 next-graphql-demo 的 Next.js 项目
npx create-next-app@latest next-graphql-demo
# 进入项目目录
cd next-graphql-demo
# 安装 Apollo Client 和 graphql 依赖
npm install @apollo/client graphql
3.3 配置 GraphQL 客户端
在项目根目录下,新建一个文件夹叫 lib,在里面创建 apolloClient.js,专门用来生成 Apollo 客户端的实例。这就好比我们跟 GraphQL 服务端之间搭一根专用的“管道”,所有请求都走这根管道。
// lib/apolloClient.js
import { ApolloClient, InMemoryCache } from '@apollo/client';
// 创建一个全局的 Apollo Client 实例
const client = new ApolloClient({
// 指向我们自己的 Next.js API 路由地址
uri: 'http://localhost:3000/api/graphql',
// 使用内存缓存,减少重复请求
cache: new InMemoryCache(),
});
export default client;
3.4 编写一个极简的 GraphQL 服务端
这里我们不用真的去搭一个独立的 GraphQL 服务器,直接用 Next.js 的 API 路由就能顶着用。这样更简单,环境也更统一。下面这段代码放在 pages/api/graphql.js(如果项目是 App Router 结构,放在 app/api/graphql/route.js,不过为了兼容性我们示例用 Pages Router 写法,因为速度最快)。
// pages/api/graphql.js
// 引入官方提供的 Apollo Server 微服务适配器
import { ApolloServer } from '@apollo/server';
import { startServerAndCreateNextHandler } from '@as-integrations/next';
import { gql } from 'graphql-tag';
// 定义数据类型:User 代表用户,Article 代表文章
const typeDefs = gql`
type User {
id: ID!
name: String!
avatar: String
articles: [Article!]!
}
type Article {
id: ID!
title: String!
content: String
comments: [String!]!
}
# Query 类型是所有查询的入口
type Query {
user(id: ID!): User
hello: String!
}
`;
// 模拟数据库里的数据,实际项目一般会连 MySQL 或 PostgreSQL
const mockUsers = [
{
id: '1',
name: '张三',
avatar: 'https://example.com/avatar/zhangsan.png',
articles: [
{
id: 'a1',
title: '如何学习 Next.js',
content: '多写多练,不要怕报错。',
comments: ['写得不错!', '很实用。'],
},
{
id: 'a2',
title: 'GraphQL 快速入门',
content: '想象一下昂贵自助餐。',
comments: ['学到了。'],
},
],
},
{
id: '2',
name: '李四',
avatar: null,
articles: [],
},
];
// 定义具体的返回逻辑
const resolvers = {
Query: {
// 根据传入的 id 在 mock 数据里找到对应的用户
user: (parent, args) => mockUsers.find((u) => u.id === args.id),
hello: () => '你好,GraphQL!',
},
};
// 把 Apollo Server 实例和 Next.js 适配器结合起来
const server = new ApolloServer({
typeDefs,
resolvers,
});
// 导出处理函数
export default startServerAndCreateNextHandler(server);
到这一步,一个极简但五脏俱全的 GraphQL 服务端就算是搭完了。
四、在页面上实际操作取数据
4.1 客户端渲染下拉数据
我们在 app 目录下创建一个页面,叫做 client-demo.js。这里展示最常见的场景:用户打开页面,浏览器端发请求,然后拿到数据显示出来。
// app/client-demo.js
'use client'; // 明确这是一个客户端组件
import { useQuery, gql, ApolloProvider } from '@apollo/client';
import client from '../lib/apolloClient';
// 定义查询语句,注意和上面服务端的结构对应上
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
avatar
articles {
title
comments
}
}
}
`;
// 组件内部业务逻辑
function UserCard() {
// 这里指定我们要查询的用户 id 是 1
const { loading, error, data } = useQuery(GET_USER, {
variables: { id: '1' },
});
// 请求没结束时,给用户一个友好的加载提示
if (loading) return <div>正在加载用户信息...</div>;
// 请求出错了,给出具体的错误提示
if (error) return <div>数据加载失败:{error.message}</div>;
// 拿到数据之后,渲染到页面上
return (
<div>
<h1>{data.user.name}</h1>
<p>头像:{data.user.avatar || '暂无头像'}</p>
<h3>文章列表</h3>
<ul>
{data.user.articles.map((article, index) => (
<li key={index}>
<p>标题:{article.title}</p>
<p>评论数:{article.comments.length}</p>
</li>
))}
</ul>
</div>
);
}
// 导出的页面组件,记得包上 ApolloProvider
export default function ClientDemoPage() {
return (
<ApolloProvider client={client}>
<UserCard />
</ApolloProvider>
);
}
上面这段代码的核心,就是 useQuery 这个钩子。你只要把查询语句和变量丢给他,他就帮你把请求发出去,把数据拿回来。你完全不用手动写 fetch 再解析 JSON,省心得很。
4.2 服务端渲染的取数姿势
Next.js 最值得骄傲的其实是它的预渲染能力。我们可以把 GraphQL 查询放在服务端跑,用户拿到的 HTML 里已经包含了数据,不需要浏览器再等一个白屏过程。
修改一下页面代码,新增一个 server-demo.js。在 App Router 结构下,我们可以直接让组件变成异步的,在里面用 client.query() 去拿数据。
// app/server-demo.js
import client from '../lib/apolloClient';
import { gql } from '@apollo/client';
// 定义查询语句
const GET_USER_INFO = gql`
query GetUserInfo($id: ID!) {
user(id: $id) {
name
articles {
title
}
}
}
`;
// 这个组件默认是在服务端执行的
export default async function ServerDemoPage() {
// 直接使用 client.query 方法获取数据
const { data } = await client.query({
query: GET_USER_INFO,
variables: { id: '1' },
});
return (
<div>
<h2>服务端渲染的用户</h2>
<p>姓名:{data.user.name}</p>
<ul>
{data.user.articles.map((a, i) => (
<li key={i}>{a.title}</li>
))}
</ul>
</div>
);
}
看吧,是不是比客户端渲染还简单?它没有 loading、error 那些状态,因为服务端请求要么成功,要么直接报错,根本没有中间态。这种模式很适合对首屏速度要求很高的页面,比如文章的详情页、商品的活动页。
五、再深入一点点,动态参数玩起来
我们刚才的查询里用了一个固定 id,‘1’。但真实项目里,id 肯定是从路由参数里拿的。比如我们想访问 /user/2,页面就自动展示 id 为 2 的用户信息。
在 Next.js 的 App Router 下,我们可以这样写一个动态路由页面:
// app/user/[id]/page.js
import client from '../../../lib/apolloClient';
import { gql } from '@apollo/client';
// 查询某个用户的文章标题
const GET_USER_WITH_ARTICLES = gql`
query GetUserWithArticles($id: ID!) {
user(id: $id) {
name
articles {
id
title
content
}
}
}
`;
// 接收路由参数,params 里包含 id
export default async function UserDetailPage({ params }) {
const { id } = params;
// 把路由参数当成变量传给 GraphQL
const { data } = await client.query({
query: GET_USER_WITH_ARTICLES,
variables: { id },
});
return (
<div>
<h2>{data.user.name} 的主页</h2>
{data.user.articles.length === 0 ? (
<p>这个用户还没有发布任何文章。</p>
) : (
data.user.articles.map((article) => (
<div key={article.id}>
<h3>{article.title}</h3>
<p>{article.content}</p>
</div>
))
)}
</div>
);
}
动态参数这里关键点就一个:params 对象里的 id 是从文件名 [id] 来的。Next.js 会自动把 URL 里的那一段截取下来传给我们。我们再把 id 继续传给 GraphQL 查询变量,整条链路就通了。
六、结合什么场景去用最舒服
6.1 中后台管理页面
中后台有大量的列表和详情页,且表单项特别多。每个列表页要展示的列可能都不一样,不同用户偏好还不一样。用 GraphQL 就可以让前端按需自定义列,不必为每一种页面单独写接口。这就好比原本是“一个萝卜一个坑”,现在变成了“萝卜随你拔,坑还是原来那一个”。
6.2 内容聚合类网站
比如一个小型博客站、新闻站或论坛,首页要展示最新文章列表,每篇文章又有作者、标签、评论数。传统 REST 可能要连发三四个请求,或者后端硬拼一个大而全的数据结构。GraphQL 就可以一条查询把嵌套全都拿回来。用我们前面写过的用户查询例子,一次请求,三级数据全齐。
6.3 移动端极简流量场景
移动端对网络包体积最敏感。多传一个没有的字段,都是白花花的银子和电量。GraphQL 按需取数可以让前端只挑绝对用得到的字段,对手机用户友好度拉满。
七、技术优缺点,咱们摊开聊聊
7.1 GraphQL 的优点
第一个是 取数灵活,前端要什么就给什么,不多不少。第二个是 请求次数少,嵌套数据一轮搞定。第三个是 类型系统自带文档,后端定义好了类型,前端一眼就能看懂能查什么字段。第四个是 前后端并行开发,只要 Schema 定义好了,两边各干各的,不用互相等。
7.2 GraphQL 的缺点
第一个是 缓存逻辑复杂,不像 REST 可以用那种很成熟的 URL 缓存,GraphQL 查询语句一长串,做缓存需要对查询结构做处理。第二个是 后端性能压力变大,如果没有限制查询深度,用户可能写一个特别深的嵌套查询,直接把数据库拖垮。第三个是 学习曲线稍微陡,前端需要理解 Schema、Query、Mutation 的概念。
7.3 Next.js 搭配 GraphQL 在架构层面的优势
Next.js 的 SSR 和静态生成特别适合 GraphQL。我们可以把 GraphQL 查询放在服务端执行,直接把数据渲染进 HTML,这样就算是嵌套再深的数据,也不怕因为接口太慢导致页面白屏。
八、需要特别留神的地方
8.1 注意字段级权限控制
GraphQL 让你按需取数,这倒是方便了,不过也意味着前端可以尝试拿任何字段。比如有些字段存的是用户手机号、邮箱这种敏感信息,那就绝对不能放到 Schema 里,或者要加上严格的权限判断,不然别人一查就把所有用户手机号都拖走了。
8.2 注意 N+1 查询问题
GraphQL 的嵌套查询很爽,但这背后如果没有做数据加载优化,它很容易触发 N+1 查询。比如查 10 篇文章,每篇文章又查对应的作者,那数据库可能被执行 11 次查询。如果是生产环境,数据量庞大,结果可能很惨。
8.3 注意查询深度和复杂度
一定要加限制。不然用户故意写一个一千层的嵌套查询,后端会直接卡死。可以设置深度限制,比如最多查 5 层,超过就报错。
8.4 服务端渲染时不要复制一堆请求
在服务端请求 GraphQL,和客户端请求有一点不同,服务端没有浏览器缓存。如果多个页面同时触发同样的查询,可能会重复请求。我们可以借助 Apollo Client 的“查询缓存”机制,在服务端手动初始化一个缓存,把重复查询合并掉。
九、再补一刀:写入操作怎么玩
我们前面讲的主要是查询,但真实系统肯定也要改数据。GraphQL 的写入操作叫做 Mutation。我们假设实现一个“给文章添加评论”的功能。
前端传来两个参数,一个是文章 id,一个是评论内容。服务端把评论推入对应的数组,然后把更新后的文章对象返回给前端。
// 在 pages/api/graphql.js 中扩展 Mutation
// 注意看 gql 类型定义里新增的 Mutation 类型
// 在 typeDefs 里增加以下代码
// type Mutation {
// addComment(articleId: ID!, content: String!): Article!
// }
// 在 resolvers 里增加以下代码
// Mutation: {
// addComment: (parent, args) => {
// // 遍历所有用户,找到对应文章
// for (const user of mockUsers) {
// const article = user.articles.find((a) => a.id === args.articleId);
// if (article) {
// article.comments.push(args.content); // 把新评论加进去
// return article;
// }
// }
// throw new Error('文章不存在,请检查文章ID');
// },
// },
这样前端就能通过 Mutation 来更新数据了。虽然我们这里只是模拟内存数据,但真实项目中操作数据库也是同一个道理。
十、把整个数据交互链路串起来
到现在,我们其实已经完成了一个相当完整的数据交互闭环:前端页面拿到路由动态参数,通过 Apollo Client 把 GraphQL 查询发到 Next.js 的 API 路由,API 路由里的 Apollo Server 接收查询、解析类型、执行 resolver,然后从模拟数据库拿到数据。数据再一层层返回到页面组件里,最终渲染成 HTML。
这个架构的好处在于,前端、中间层、数据层各司其职,没有谁能一口气吃掉所有的逻辑,也没有谁被憋成被人频繁改的“工具人”。
如果后续项目越来越大,我们可以把 Next.js 的 API 路由替换成独立的 GraphQL 网关服务,底下再挂一堆微服务。数据查询的入口始终只有一个,前端代码几乎不用变动。
十一、总结一下这次旅行
Next.js 和 GraphQL 放一块儿,最迷人的地方就是“数据权利”往前端倾斜了。前端不再眼巴巴地等后端给一个“完全合身”的接口,而是自己动手拼自己要的东西。后端也能从繁琐的“页面需求变更”里解放出来,专注定义好数据结构、把好安全关。
当然你也不要指望这套东西零成本。它依然有缓存、安全、性能这些坑要填,但这些坑多数是我们能接受的,因为换来的是更清爽的架构和更快的迭代速度。
最后想提醒一句,不管是 Next.js 还是 GraphQL,都不是银弹。如果你的项目本身很小,接口谈不上浪费,那 REST 反而更简单。但如果你正被传统接口的“僵化”和“超额返回”折磨得头疼,那果断用我们今天讲的这套组合拳,大概率会开启一段非常舒服的开发体验。
许多团队在实践这套架构时,最开心的一件事就是前后端再也不用因为“这个字段加不加”来回扯皮了。前端自己掌控自己那点数据需求,不也挺香的吗?
评论
围绕“Next.js与GraphQL集成,构建高效的数据交互架构”参与讨论