前言
GraphQL 在我们之前的项目中的使用情况非常不错,后端可以只需要专注于合理的 Schema 设计与开发,并不需要太关心界面上的功能交互,在前端我们用 Apollo GraphQL 替代了 Redux 结合 React 也获得了很好的开发体验 (还在用 Redux,要不要试试 GraphQL 和 Apollo?) 。
我们在准备使用 TypeScript 来写 GraphQL 的时候,我们会有面临一个最大的问题
GraphQL Schema Type DSL 和数据 Modal 需要写两份么?
TypeGraphQL ( type-graphql ) 是我们今天介绍的重点,它通过一些 decorator 帮我们解决了这个问题。下面我会先介绍如何构建一个基于 egg.js 的 TypeScript GraphQL 工程,然后会介绍一些 TypeGraphQL 常见用法。
构建
初始化工程
egg.js 对 TypeScript 现在已经有了比较好的支持 (参考),下面我们先创建一个基于 TypeScript 的 egg.js 工程。
代码语言:javascript复制npx egg-init --type=ts type-graphql-demo
cd type-graphql-demo
yarn && yarn dev
通过 egg.js 提供的脚手架生成后,可以得到下面的一个工程目录结构
代码语言:javascript复制├── app
│ ├── controller
│ │ └── home.ts
│ ├── service
│ │ └── news.ts
│ └── router.ts
├── config
│ ├── config.default.ts
│ ├── config.local.ts
│ ├── config.prod.ts
│ └── plugin.ts
├── test
│ └── **/*.test.ts
├── typings
│ └── **/*.d.ts
├── README.md
├── package.json
├── tsconfig.json
└── tslint.json
安装依赖
- 安装依赖
yarn add type-graphql
2. 安装 reflect-metadata
代码语言:javascript复制yarn add reflect-metadata
3. reflect-metadata 需要在入口或者使用 type-graphql 之前被引入,建议在 app.ts 中引入
代码语言:javascript复制// ~/app.ts
import "reflect-metadata";
4. 安装 apollo-server-koa , 处理请求路由( egg.js 是基于 koa )
代码语言:javascript复制yarn add apollo-server-koa
集成中间件路由
代码语言:javascript复制// ~/app/graphql/index.ts
import * as path from "path";
import { ApolloServer } from "apollo-server-koa";
import { Application } from "egg";
import { GraphQLSchema } from "graphql";
import { buildSchema } from "type-graphql";
export interface GraphQLConfig {
router: string;
graphiql: boolean;
}
export default class GraphQL {
private readonly app: Application;
private graphqlSchema: GraphQLSchema;
private config: GraphQLConfig;
constructor(app: Application) {
this.app = app;
this.config = app.config.graphql;
}
getResolvers() {
const isLocal = this.app.env === "local";
return [path.resolve(this.app.baseDir, `app/graphql/schema/**/*.${isLocal ? "ts" : "js"}`)];
}
async init() {
this.graphqlSchema = await buildSchema({
resolvers: this.getResolvers(),
dateScalarMode: "timestamp"
});
const server = new ApolloServer({
schema: this.graphqlSchema,
tracing: false,
context: ({ ctx }) => ctx, // 将 egg 的 context 作为 Resolver 传递的上下文
playground: {
settings: {
"request.credentials": "include"
}
} as any,
introspection: true
});
server.applyMiddleware({
app: this.app,
path: this.config.router,
cors: false
});
this.app.logger.info("graphql server init");
}
// async query({query, var})
get schema(): GraphQLSchema {
return this.graphqlSchema;
}
}
~/app/extend/application.ts
代码语言:javascript复制import { Application } from "egg";
import GraphQL from "../graphql";
const TYPE_GRAPHQL_SYMBOL = Symbol("Application#TypeGraphql");
export default {
get graphql(this: Application): GraphQL {
if (!this[TYPE_GRAPHQL_SYMBOL]) {
this[TYPE_GRAPHQL_SYMBOL] = new GraphQL(this);
}
return this[TYPE_GRAPHQL_SYMBOL];
}
};
~/app.ts
代码语言:javascript复制import "reflect-metadata";
import { Application } from "egg";
export default async (app: Application) => {
await app.graphql.init();
app.logger.info("started");
}
使用 TypeGraphQL 创建 Schema
下面简单介绍一下 TypeGraphQL 的一些基本的用法。详细文档链接
定义 Schema
TypeGraphQL 提供了一些 decorator 来帮助我们通过 class 类来声明 graphql DSL。
ObjectType & InputType
- @ObjectType 创建 GraphQLObjectType
- @InputType 创建 GraphQLInputType
- @Field 声明对象的哪些字段作为 GraphQL 的字段,复杂类型的字段需要通过
type => Rate
声明
@ObjectType({ description: "The recipe model" })
class Recipe {
@Field(type => ID)
id: string;
@Field({ description: "The title of the recipe" })
title: string;
@Field(type => [Rate])
ratings: Rate[];
@Field({ nullable: true })
averageRating?: number;
}
@InputType({ description: "New recipe data" })
class AddRecipeInput implements Partial<Recipe> {
@Field()
title: string;
@Field({ nullable: true })
description?: string;
}
接口与继承
TypeScript 的接口只是在编译时存在,所以对于 GraphQL 的 interface,我们需要借助于抽象类来声明。
代码语言:javascript复制abstract class IPerson {
@Field(type => ID)
id: string;
@Field()
name: string;
@Field(type => Int)
age: number;
}
@ObjectType({ implements: IPerson })
class Person implements IPerson {
id: string;
name: string;
age: number;
}
对于继承,子类和父类必须有相同的 ObjectType 或者 InputType。
代码语言:javascript复制@ObjectType()
class Person {
@Field()
age: number;
}
@ObjectType()
class Student extends Person {
@Field()
universityName: string;
}
⚠️注意:在使用继承后,Resolver 里面返回 Plain Object 作为结果的时候会报错,这个 Bug (#160) 还未修复。
Resolvers
对于 Resolver 的处理,TypeGraphQL 提供了一些列的 decorator 来声明和处理数据。通过 Resolver 类的方法来声明 Query 和 Mutation,以及动态字段的处理 FieldResolver。
- @Resolver:来声明当前类是数据处理的
- @Query:声明改方法是一个 Query 查询操作
- @Mutation:声明改方法是一个 Mutation 修改操作
- @FieldResovler:对
@Resolver(of => Recipe)
返回的对象添加一个字段处理
方法参数:
- @Root:获取当前查询对象
- @Ctx:获取当前上下文,这里可以拿到 egg 的 Context (见上面中间件集成中的处理)
- @Arg:定义 input 参数
@Resolver(of => Recipe)
class RecipeResolver {
// ...
@Query(returns => [Recipe])
async recipes(
@Arg("title", { nullable: true }) title?: string,
@Arg("servings", { defaultValue: 2 }) servings: number,
): Promise<Recipe[]> {
// ...
}
@FieldResolver()
averageRating(@Root() recipe: Recipe, @Ctx() ctx: Context) {
// handle with egg context
}
@Mutation()
addRecipe(
@Arg("data") newRecipeData: AddRecipeInput,
@Ctx() ctx: Context,
): Recipe {
// handle with egg context
}
}
Scalars & Enums & Unions
GraphQL 的其他特性比如 scalar、enum、union、subscriptions 等,TypeGraphQL 都做了很好的支持,在使用 TypeScript 编写的时候更加方便。
Scalars
默认提供了 3 个基本类型的别名
- Int –> GraphQLInt;
- Float –> GraphQLFloat;
- ID –> GraphQLID;
默认提供了日期类型 Date
的 scalar 处理,它支持两种时间格式:
- timestamp based (
"timestamp"
) –1518037458374
- ISO format (
"isoDate"
) –"2018-02-07T21:04:39.573Z"
import { buildSchema } from "type-graphql";
const schema = await buildSchema({
resolvers,
dateScalarMode: "timestamp", // "timestamp" or "isoDate"
});
Enums
TypeScript 支持 enum 类型,可以和 GraphQL 的 enum 进行复用。
代码语言:javascript复制enum Direction {
Up = "UP",
Down = "DOWN",
Left = "LEFT",
Right = "RIGHT",
}
import { registerEnumType } from "type-graphql";
registerEnumType(Direction, {
name: "Direction", // this one is mandatory
description: "The basic directions", // this one is optional
});
Unions
TypeGraphQL 提供了 createUnionType
方法来创建一个 union 类型。
@ObjectType()
class Movie {
...fields
}
@ObjectType()
class Actor {
...fields
}
import { createUnionType } from "type-graphql";
const SearchResultUnion = createUnionType({
name: "SearchResult", // the name of the GraphQL union
types: [Movie, Actor], // array of object types classes
});
其他新特性
Authorization
TypeGraphQL 默认提供了拦截器 AuthChecker
和注解 @Authorized()
来进行权限校验。如果校验不通过,则会返回 null 或者报错,取决于当前字段或者操作是否支持 nullable。
实现一个 AuthChecker
:
export const customAuthChecker: AuthChecker<ContextType> =
({ root, args, context, info }, roles) => {
// here you can read user from context
// and check his permission in db against `roles` argument
// that comes from `@Authorized`, eg. ["ADMIN", "MODERATOR"]
return true; // or false if access denied
}
配合 @Authorized
使用
@ObjectType()
class MyObject {
@Field()
publicField: string;
@Authorized()
@Field()
authorizedField: string;
@Authorized("ADMIN")
@Field()
adminField: string;
}
Validation
TypeGraphQL 默认集成了 class-validator
来做数据校验。
import { MaxLength, Length } from "class-validator";
@InputType()
export class RecipeInput {
@Field()
@MaxLength(30)
title: string;
@Field({ nullable: true })
@Length(30, 255)
description?: string;
}
Middlewares
TypeGraphQL 提供了类似于 koa.js
的中间件处理。
export const ResolveTime: MiddlewareFn = async ({ info }, next) => {
const start = Date.now();
await next();
const resolveTime = Date.now() - start;
console.log(`${info.parentType.name}.${info.fieldName} [${resolveTime} ms]`);
};
Global middlewares
全局中间件会拦截所有的 query、mutation、subscription、field resolver。可以来做一些全局相关的事情,比如异常拦截,请求跟踪(数据量大小,深度控制)等
代码语言:javascript复制const schema = await buildSchema({
resolvers: [RecipeResolver],
globalMiddlewares: [ErrorInterceptor, ResolveTime],
});
Attaching middlewares
配合 @UseMiddleware()
对单个字段做拦截
@Resolver()
export class RecipeResolver {
@Query()
@UseMiddleware(ResolveTime, LogAccess)
randomValue(): number {
return Math.random();
}
}
Query complexity
TypeGraphQL 默认提供了查询复杂度控制,来防止一些恶意或者无意的过度复杂查询消耗大量的服务端资源,比如数据库连接等。
详细使用方法参考文档
总结
虽然 TypeGraphQL 还没有正式 1.0.release
,但是目前的版本已经是 MVP (Minimum Viable Product)。我们在正式使用中目前也没有遇到大的问题,该项目目前也比较活跃,很多新的特性也在开发中,建议可以做一些尝试。