TypeScript + GraphQL = TypeGraphQL

核心提示背景近一段时间我们团队的项目大部分都开始使用 TypeScript 作为开发语言。TypeScript 在项目维护和重构的时候可以带来非常明显的好处。之前一个项目中我们使用了 GraphQL 替代了传统的 REST API。所以在最新的一个

背景近一段时间我们团队的项目大部分都开始使用 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安装依赖

  1. 安装依赖
yarn add type-graphql 2. 安装 reflect-metadata

yarn

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.ts

import

{

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.ts

import

"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。实现一个 AuthChecker :

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

}

配合 @Authorized 使用

@ObjectType

class

MyObject

{

@Field

publicField

:

string

;

@Authorized

@Field

authorizedField

:

string

;

@Authorized

@Field

adminField

:

string

;

}

ValidationTypeGraphQL 默认集成了 class-validator 来做数据校验。

import

{

MaxLength

,

Length

}

from

"class-validator"

;

@InputType

export

class

RecipeInput

{

@Field

@MaxLength

title

:

string

;

@Field

@Length

description

:

string

;

}

MiddlewaresTypeGraphQL 提供了类似于 koa.js 的中间件处理。

export

const

ResolveTime

:

MiddlewareFn

=

async

=>

{

const

start

=

Date

.

now

;

await

next

;

const

resolveTime

=

Date

.

now

-

start

;

console

.

log

;

};

Global middlewares全局中间件会拦截所有的 query、mutation、subscription、field resolver。

可以来做一些全局相关的事情,比如异常拦截,请求跟踪(数据量大小,深度控制)等

const

schema

=

await

buildSchema

;

Attaching middlewares配合 @UseMiddleware 对单个字段做拦截

@Resolver

export

class

RecipeResolver

{

@Query

@UseMiddleware

randomValue

:

number

{

return

Math

.

random

;

}

}

Query complexityTypeGraphQL 默认提供了查询复杂度控制,来防止一些恶意或者无意的过度复杂查询消耗大量的服务端资源,比如数据库连接等。详细使用方法参考文档总结虽然 TypeGraphQL 还没有正式 1.0.release ,但是目前的版本已经是 MVP 。我们在正式使用中目前也没有遇到大的问题,该项目目前也比较活跃,很多新的特性也在开发中,建议可以做一些尝试。
 
友情链接
鄂ICP备19019357号-22