文章目录
在现代Web开发中,前后端分离的趋势越来越明显。GraphQL作为一种数据查询和操作语言,以其强大的类型系统和高效的查询方式受到了广泛欢迎。而TypeScript作为JavaScript的一个超集,通过添加静态类型检查,提高了代码质量和可维护性。将这两者结合使用,可以极大地提升开发效率和应用质量。
基础概念介绍
GraphQL简介
- 定义:GraphQL是一种用于API的查询语言,也是一个执行这些查询所需的服务。
- 特点:
- 类型安全:定义清晰的数据结构。
- 客户端驱动:客户端可以精确地请求所需的数据。
- 高效:减少网络传输量,提高加载速度。
TypeScript简介
- 定义:TypeScript是JavaScript的一个超集,增加了静态类型系统。
- 优势:
- 类型安全:帮助开发者在编码阶段发现错误。
- 工具支持:IDE支持良好,如自动补全、重构等。
- 面向对象:支持类、接口等特性。
TypeScript与GraphQL结合的优势
- 类型一致性:TypeScript的类型系统可以直接映射到GraphQL的schema上,确保前端请求的数据类型与后端提供的数据类型一致。
- 开发体验:IDE可以提供基于GraphQL schema的自动补全功能,提高开发效率。
- 错误预防:静态类型检查可以在编译阶段捕获类型不匹配的问题。
实践步骤
安装必要的依赖
npm install graphql apollo-server graphql-tools typescript @types/node ts-node
初始化项目
创建一个新的TypeScript项目,并设置基本的文件结构:
mkdir graphql-ts && cd graphql-ts
npm init -y
npx tsc --init
创建GraphQL服务
定义schema
// schema.graphql
type Query {
hello: String
}
type Mutation {
sayHello(name: String!): String
}
编写resolver
// resolvers.ts
const resolvers = {
Query: {
hello: () => 'Hello, world!',
},
Mutation: {
sayHello: (_, { name }) => `Hello, ${name}!`,
},
};
设置Apollo Server
// index.ts
import { ApolloServer } from 'apollo-server';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { resolvers } from './resolvers';
const typeDefs = `
type Query {
hello: String
}
type Mutation {
sayHello(name: String!): String
}
`;
const schema = makeExecutableSchema({ typeDefs, resolvers });
const server = new ApolloServer({ schema });
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
使用TypeScript定义类型
为了更好地利用TypeScript的类型优势,我们可以在resolvers.ts
中定义类型:
interface Context {}
type HelloQuery = {
hello: string;
};
type SayHelloMutationArgs = {
name: string;
};
type SayHelloMutation = {
sayHello: string;
};
运行服务器
启动项目并测试GraphQL API:
npx ts-node index.ts
访问http://localhost:4000/graphql
来查看GraphQL Playground。
复杂类型与接口
定义复杂类型
复杂类型是指包含多个字段的对象类型。例如,一个用户对象可能包含姓名、邮箱和地址等信息。
// schema.graphql
type User {
id: ID!
name: String!
email: String!
address: Address
}
type Address {
street: String!
city: String!
state: String!
zip: String!
}
使用复杂类型
在resolver中使用这些复杂类型:
// resolvers.ts
const users = [
{
id: "1",
name: "Alice",
email: "alice@example.com",
address: {
street: "123 Main St",
city: "New York",
state: "NY",
zip: "10001"
}
},
{
id: "2",
name: "Bob",
email: "bob@example.com",
address: {
street: "456 Elm St",
city: "San Francisco",
state: "CA",
zip: "94107"
}
}
];
const resolvers = {
Query: {
user: (_: any, { id }: { id: string }) => users.find(user => user.id === id),
users: () => users
},
User: {
address: (parent: any) => parent.address
}
};
枚举类型
枚举类型用于表示一组固定的值。例如,用户的性别可以用枚举类型表示。
定义枚举类型
// schema.graphql
enum Gender {
MALE
FEMALE
OTHER
}
type User {
id: ID!
name: String!
gender: Gender!
}
使用枚举类型
在resolver
中使用枚举类型:
// resolvers.ts
const resolvers = {
Query: {
user: (_: any, { id }: { id: string }) => users.find(user => user.id === id),
users: () => users
},
User: {
gender: (parent: any) => parent.gender
}
};
输入对象
输入对象用于表示客户端发送给服务器的数据结构。例如,创建用户时需要输入的信息。
定义输入对象
// schema.graphql
input CreateUserInput {
name: String!
email: String!
password: String!
}
type Mutation {
createUser(input: CreateUserInput!): User!
}
使用输入对象
在resolver
中使用输入对象:
// resolvers.ts
const resolvers = {
Mutation: {
createUser: (_: any, { input }: { input: CreateUserInput }) => {
const newUser = {
id: "3",
name: input.name,
email: input.email,
password: input.password // In a real application, you should hash the password
};
users.push(newUser);
return newUser;
}
}
};
自定义标量类型
自定义标量类型允许你定义新的数据类型,例如日期或货币。
定义自定义标量类型
// schema.graphql
scalar Date
type Post {
id: ID!
title: String!
content: String!
createdAt: Date!
}
使用自定义标量类型
在resolver中使用自定义标量类型:
// resolvers.ts
import { GraphQLDate } from 'graphql-custom-scalars';
const resolvers = {
Date: GraphQLDate,
Query: {
post: (_: any, { id }: { id: string }) => posts.find(post => post.id === id),
posts: () => posts
}
};
错误处理与中间件
在GraphQL中,错误处理非常重要,可以使用中间件来统一处理各种错误情况。
定义错误处理中间件
// middleware.ts
import { ApolloServerPlugin } from 'apollo-server-plugin-base';
const errorMiddleware: ApolloServerPlugin = {
requestDidStart() {
return {
willSendResponse({ response }) {
if (response.errors) {
console.error(response.errors);
response.errors = response.errors.map(error => ({
message: error.message,
code: 'INTERNAL_SERVER_ERROR'
}));
}
}
};
}
};
export default errorMiddleware;
应用中间件
在Apollo Server配置中应用中间件:
// index.ts
import { ApolloServer } from 'apollo-server';
import { makeExecutableSchema } from '@graphql-tools/schema';
import { resolvers } from './resolvers';
import { errorMiddleware } from './middleware';
const typeDefs = `
type Query {
hello: String
}
type Mutation {
sayHello(name: String!): String
}
`;
const schema = makeExecutableSchema({ typeDefs, resolvers });
const server = new ApolloServer({
schema,
plugins: [errorMiddleware]
});
server.listen().then(({ url }) => {
console.log(`🚀 Server ready at ${url}`);
});
高级类型系统
联结类型
联结类型用于表示多个类型的联合。例如,一个搜索结果可能包含用户或帖子。
// schema.graphql
union SearchResult = User | Post
type Query {
search(query: String!): [SearchResult!]!
}
使用联结类型
在resolver中使用联结类型:
// resolvers.ts
const resolvers = {
Query: {
search: (_: any, { query }: { query: string }) => {
const results = [...users.filter(user => user.name.includes(query)), ...posts.filter(post => post.title.includes(query))];
return results;
}
},
SearchResult: {
__resolveType(obj: any) {
if (obj.id && obj.name) {
return 'User';
} else if (obj.id && obj.title) {
return 'Post';
}
return null;
}
}
};
类型安全的客户端查询
定义类型安全的客户端查询
在客户端使用TypeScript定义类型安全的查询:
// client.ts
import { gql } from '@apollo/client';
const GET_USERS = gql`
query GetUsers {
users {
id
name
email
address {
street
city
state
zip
}
}
}
`;
const GET_USER = gql`
query GetUser($id: ID!) {
user(id: $id) {
id
name
email
address {
street
city
state
zip
}
}
}
`;
使用类型安全的查询
在客户端使用这些查询:
// client.ts
import { useQuery } from '@apollo/client';
import { GET_USERS, GET_USER } from './queries';
function App() {
const { loading, error, data } = useQuery(GET_USERS);
if (loading) return <p>Loading...</p>;
if (error) return <p>Error: {error.message}</p>;
return (
<div>
{data.users.map(user => (
<div key={user.id}>
<h2>{user.name}</h2>
<p>Email: {user.email}</p>
<p>Address: {`${user.address.street}, ${user.address.city}, ${user.address.state} ${user.address.zip}`}</p>
</div>
))}
</div>
);
}