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