阶段三:GraphQL
Schema是GraphQL的灵魂——它定义了整个API的类型系统、数据关系和操作入口。一个好的Schema让查询直觉自然,让类型安全贯穿前后端,让API演进无缝衔接。本课深入Schema设计原则、高级类型系统、Relay规范和Schema演进策略。
# 内置标量
Int # 32位整数
Float # 浮点数
String # UTF-8字符串
Boolean # 布尔值
ID # 唯一标识符(序列化为String)
# 自定义标量——扩展类型系统
scalar DateTime # ISO 8601日期时间
scalar Date # 日期
scalar Email # 邮箱(带验证)
scalar URL # URL(带验证)
scalar JSON # 自由格式JSON
scalar PositiveInt # 正整数
# 自定义标量的实现
const { GraphQLScalarType, Kind } = require('graphql');
const DateTimeScalar = new GraphQLScalarType({
name: 'DateTime',
description: 'ISO 8601格式的日期时间',
serialize(value) {
// 输出:Date → String
return value instanceof Date ? value.toISOString() : value;
},
parseValue(value) {
// 输入变量:String → Date
return new Date(value);
},
parseLiteral(ast) {
// 输入字面量:AST → Date
if (ast.kind === Kind.STRING) return new Date(ast.value);
return null;
},
});
const EmailScalar = new GraphQLScalarType({
name: 'Email',
description: '邮箱地址',
serialize: (v) => v,
parseValue: (v) => {
if (!/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(v))
throw new Error('无效的邮箱格式');
return v;
},
parseLiteral: (ast) => {
if (ast.kind === Kind.STRING && /^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(ast.value))
return ast.value;
throw new Error('无效的邮箱格式');
},
});
# 接口(Interface)—— 共享字段的抽象类型
interface Node {
id: ID!
createdAt: DateTime!
}
interface ContentItem implements Node {
id: ID!
title: String!
author: User!
createdAt: DateTime!
updatedAt: DateTime!
}
type Post implements ContentItem & Node {
id: ID!
title: String!
content: String!
author: User!
tags: [String!]!
createdAt: DateTime!
updatedAt: DateTime!
}
type Article implements ContentItem & Node {
id: ID!
title: String!
body: String!
summary: String
author: User!
category: String!
createdAt: DateTime!
updatedAt: DateTime!
}
# 联合类型(Union)—— 无共享字段的组合
union SearchResult = User | Post | Comment
type Query {
search(query: String!): [SearchResult!]!
}
# 查询联合类型需要内联片段
query Search {
search(query: "GraphQL") {
... on User { id name email }
... on Post { id title }
... on Comment { id body }
}
}
# 枚举——限定值的集合
enum Role {
ADMIN
EDITOR
VIEWER
}
enum PostStatus {
DRAFT # 草稿
PUBLISHED # 已发布
ARCHIVED # 已归档
}
enum SortOrder {
ASC
DESC
}
# 枚举输入
type Query {
posts(status: PostStatus, sort: SortOrder = DESC): [Post!]!
}
# 1. 一对多关联
type User {
posts: [Post!]! # 用户有多篇文章
}
type Post {
author: User! # 文章有一个作者
}
# 2. 多对多关联(通过中间类型)
type User {
groups: [Group!]! # 用户加入的组
}
type Group {
members: [User!]! # 组内成员
}
# 3. 自引用关联
type Comment {
replies: [Comment!]! # 评论的回复
parent: Comment # 父评论(可为空)
}
# 4. 多态关联
type Notification {
target: Notifiable! # 可以是Post/Comment/User
}
union Notifiable = Post | Comment | User
# 5. 分页关联(Relay规范)
type User {
posts(first: Int, after: String): PostConnection!
}
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
node: Post!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
// blog-schema.js - 完整博客平台Schema
const { buildSchema } = require('graphql');
const schema = buildSchema(`
# ===== 自定义标量 =====
scalar DateTime
# ===== 枚举 =====
enum Role { ADMIN EDITOR VIEWER }
enum PostStatus { DRAFT PUBLISHED ARCHIVED }
enum SortOrder { ASC DESC }
# ===== 接口 =====
interface Node {
id: ID!
}
# ===== 核心类型 =====
type User implements Node {
id: ID!
username: String!
displayName: String!
email: Email!
avatar: URL
bio: String
role: Role!
posts(status: PostStatus, first: Int, after: String): PostConnection!
postCount(status: PostStatus): Int!
followers(first: Int): UserConnection!
following(first: Int): UserConnection!
followerCount: Int!
followingCount: Int!
createdAt: DateTime!
}
type Post implements Node {
id: ID!
title: String!
slug: String!
content: String!
excerpt: String
author: User!
status: PostStatus!
tags: [Tag!]!
comments(first: Int, after: String): CommentConnection!
commentCount: Int!
likes: Int!
likedByMe: Boolean!
readingTime: Int!
createdAt: DateTime!
updatedAt: DateTime!
publishedAt: DateTime
}
type Comment implements Node {
id: ID!
body: String!
author: User!
post: Post!
parent: Comment
replies(first: Int): [Comment!]!
replyCount: Int!
createdAt: DateTime!
}
type Tag implements Node {
id: ID!
name: String!
slug: String!
postCount: Int!
posts(first: Int, after: String): PostConnection!
}
# ===== 连接类型(Relay分页)=====
type PostConnection {
edges: [PostEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type PostEdge {
node: Post!
cursor: String!
}
type CommentConnection {
edges: [CommentEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type CommentEdge {
node: Comment!
cursor: String!
}
type UserConnection {
edges: [UserEdge!]!
pageInfo: PageInfo!
totalCount: Int!
}
type UserEdge {
node: User!
cursor: String!
}
type PageInfo {
hasNextPage: Boolean!
hasPreviousPage: Boolean!
startCursor: String
endCursor: String
}
# ===== 输入类型 =====
input CreatePostInput {
title: String!
content: String!
tags: [String!]! = []
status: PostStatus = DRAFT
}
input UpdatePostInput {
title: String
content: String
tags: [String!]
status: PostStatus
}
input PostFilter {
status: PostStatus
authorId: ID
tag: String
search: String
}
input PostSort {
field: PostSortField! = CREATED_AT
order: SortOrder! = DESC
}
enum PostSortField {
CREATED_AT
UPDATED_AT
PUBLISHED_AT
LIKES
COMMENT_COUNT
}
# ===== 操作 =====
type Query {
# 节点查询(全局ID查询)
node(id: ID!): Node
# 用户
user(id: ID!): User
userByUsername(username: String!): User
users(role: Role, first: Int = 20, after: String): UserConnection!
# 文章
post(id: ID!): Post
postBySlug(slug: String!): Post
posts(filter: PostFilter, sort: PostSort, first: Int = 20, after: String): PostConnection!
# 标签
tag(slug: String!): Tag
tags(first: Int = 50): [Tag!]!
# 搜索
search(query: String!, first: Int = 10): [SearchResult!]!
}
union SearchResult = User | Post | Tag
type Mutation {
createPost(input: CreatePostInput!): Post!
updatePost(id: ID!, input: UpdatePostInput!): Post!
deletePost(id: ID!): Boolean!
publishPost(id: ID!): Post!
archivePost(id: ID!): Post!
addComment(postId: ID!, body: String!, parentId: ID): Comment!
deleteComment(id: ID!): Boolean!
toggleLike(postId: ID!): Post!
followUser(userId: ID!): User!
unfollowUser(userId: ID!): User!
}
`);
// Resolver实现(简化)
const data = {
users: [
{ id: '1', username: 'zhangsan', displayName: '张三', email: 'zhang@example.com', role: 'ADMIN', bio: 'API设计讲师', createdAt: '2025-01-01' },
{ id: '2', username: 'lisi', displayName: '李四', email: 'li@example.com', role: 'EDITOR', bio: '技术写作者', createdAt: '2025-01-02' },
],
posts: [
{ id: '1', title: 'GraphQL Schema设计', slug: 'graphql-schema', content: 'Schema是GraphQL的灵魂...', authorId: '1', status: 'PUBLISHED', tags: ['GraphQL'], likes: 42, createdAt: '2025-01-10' },
],
tags: [
{ id: '1', name: 'GraphQL', slug: 'graphql' },
{ id: '2', name: 'REST', slug: 'rest' },
],
};
const rootResolver = {
node: ({ id }) => {
// 全局ID查找
const user = data.users.find(u => u.id === id);
if (user) return user;
const post = data.posts.find(p => p.id === id);
if (post) return post;
return null;
},
user: ({ id }) => data.users.find(u => u.id === id),
users: ({ role, first, after }) => {
let result = [...data.users];
if (role) result = result.filter(u => u.role === role);
return { edges: result.slice(0, first).map(n => ({ node: n, cursor: n.id })), pageInfo: { hasNextPage: result.length > first, hasPreviousPage: false }, totalCount: result.length };
},
posts: ({ filter, sort, first, after }) => {
let result = [...data.posts];
if (filter?.status) result = result.filter(p => p.status === filter.status);
if (filter?.authorId) result = result.filter(p => p.authorId === filter.authorId);
return { edges: result.slice(0, first).map(n => ({ node: n, cursor: n.id })), pageInfo: { hasNextPage: false, hasPreviousPage: false }, totalCount: result.length };
},
createPost: ({ input }) => {
const id = String(data.posts.length + 1);
const post = { id, ...input, authorId: '1', slug: input.title.toLowerCase().replace(/\s+/g, '-'), likes: 0, createdAt: new Date().toISOString() };
data.posts.push(post);
return post;
},
User: {
posts: (user, { first }) => {
const userPosts = data.posts.filter(p => p.authorId === user.id);
return { edges: userPosts.slice(0, first || 20).map(n => ({ node: n, cursor: n.id })), pageInfo: { hasNextPage: false, hasPreviousPage: false }, totalCount: userPosts.length };
},
postCount: (user) => data.posts.filter(p => p.authorId === user.id).length,
},
Post: {
author: (post) => data.users.find(u => u.id === post.authorId),
tags: (post) => data.tags.filter(t => post.tags?.includes(t.name)),
commentCount: () => 0,
likes: (post) => post.likes || 0,
likedByMe: () => false,
readingTime: (post) => Math.ceil((post.content?.length || 0) / 500),
},
};
module.exports = { schema, rootResolver };
| 安全变更(可以加) | 破坏性变更(不能改/删) |
|---|---|
| 新增类型 | 删除类型 |
| 新增字段(可空) | 删除字段 |
| 新增枚举值 | 删除枚举值 |
| 新增查询/变更 | 删除查询/变更 |
| 将可空字段改为非空(谨慎) | 将非空字段改为可空 |
| 新增接口实现 | 修改接口字段签名 |
# Schema演进示例
# V1: 初始版本
type User {
id: ID!
name: String!
}
# V2: 安全演进
type User {
id: ID!
name: String! # 保留旧字段
displayName: String # 新增可空字段(向后兼容)
firstName: String # 新增
lastName: String # 新增
}
# 废弃字段标记
type User {
id: ID!
name: String! @deprecated(reason: "使用 displayName 代替")
displayName: String
firstName: String
lastName: String
}
设计一个社交媒体平台的Schema,包含:用户、帖子、评论、点赞、关注、消息、通知。使用Relay连接规范。
实现PhoneNumber自定义标量,验证中国手机号格式(1开头11位数字)。