最近項目中在作一個 BFF
, nest.js
和 GraphQL
這兩個技術棧 是一個"新"的嘗試, 雖然 GraphQL
在 15
年 就出來了, 可是在與 nest.js
結合, 得益於做者良好的封裝, 發生了奇妙的化學反應前端
固然這不是一篇 粘貼官方文檔 而後教你如何使用的 水文, 而是採坑心得的 水文node
type-graphql
將 typescript
的定義轉成 graphql
的 schema
@nestjs/graphql
是 做者 在 apollo-server 的基礎上進行了2 次封裝data-loader
數據的聚合與緩存 解決 resolver (n+1)
的問題在這裏咱們以一個 UserModule
爲例git
能夠經過github
query UserList() {
users {
id
name
}
}
複製代碼
獲得sql
{
data: {
users: [{
id: "1",
name: '名字'
}]
}
}
複製代碼
import { Module } from '@nestjs/common'
import { GraphQLModule } from '@nestjs/graphql'
import { UserModule } from './user.module'
@Module({
imports: [
GraphQLModule.forRoot({
path: '/graphql',
typePaths: ['./**/*.graphql'],
definitions: {
path: join(process.cwd(), 'src/graphql.ts'),
outputAs: 'class',
},
}),
UserModule,
]
})
export class AppModule 複製代碼
在這裏 每次 啓動應用的時候 會遍歷全部的 graphql
schema 文件 生成 graphql.ts
docker
例如typescript
type User {
id: ID!
name: String
}
複製代碼
會生成express
export class User {
id: string
name?: string
}
複製代碼
而後咱們寫 resolver
和 service
的時候 就能夠用 graphql.ts
生成好的類型定義, 可是這種方式有一點不方便, 有點不符合編程習慣編程
若是想要先寫 typescript
的定義, 生成 graphql
的 schema
文件, 那麼就要用到 type-graphql
了bootstrap
import { Module } from '@nestjs/common'
import { GraphQLModule } from '@nestjs/graphql'
import { UserModule } from './user.module'
import { resolve } from 'path'
const schema = resolve(__dirname, 'schema.gql')
@Module({
imports: [
GraphQLModule.forRoot({
path: '/graphql',
autoSchemaFile: schema,
typePaths: [schema],
}),
UserModule,
]
})
export class AppModule 複製代碼
最後 只須要寫對應的 model
便可
import { Field, ID } from 'type-graphql'
export class User {
@Field(() => ID, { nullable: false })
id: string
@Field({ nullable: false })
name?: string
}
複製代碼
這裏能夠理解 是對 graphql
schema 的一個隱射 , @Field
裝飾器映射的是 schema
裏面 id
的類型
Class User
的 id 描述的 ts
的類型
值得注意的是 string
| boolean
等 基礎 類型 @Field
能夠省略, 可是 number
默認會轉成 float
, 因此須要顯示聲明,這點比較坑
另一點是若是是枚舉,須要使用 registerEnumType
註冊一次
import { registerEnumType } from 'type-graphql'
export enum Enum {
a,
b
}
registerEnumType(Enum, {
name: 'RolesEnum'
})
// 使用
export class User {
@Field(() => Enum, { nullable: false })
name?: Enum
}
複製代碼
在 nest.js
裏 一個 Graphql
模塊 由 resolver
和 service
組成
import { Module } from '@nestjs/common'
import { UserResolver } from './user.resolver'
import { UserService } from './user.service'
@Module({
providers: [
UserResolver,
UserService,
]
})
export class UserModule {}
複製代碼
import { Args, Resolver, Query } from '@nestjs/graphql'
import { UserService } from './user.service'
@Resolver()
export class UserResolver {
constructor(private readonly userService: UserService)
@Query(() => User[], {
name: 'users'
})
public async users(): Promise<User[]> {
this.userService.xxxxx()
}
}
複製代碼
每一個 @Query
裝飾器 對應一個 方法 默認會將函數的名字 當成 query 的名字 , 使用 name 能夠顯示的指定,
這樣當發起一個 Query
時, 對應的 Resolver
會調用對應的 service
處理邏輯, 便可
query users {
id
name
}
複製代碼
若是想查詢第三個字段 age
可是 age
又不在 User
的數據裏, 好比要調另一個接口查詢, 這時候 能夠 用到 @ResolveProperty
import { Args, Resolver, ResolveProperty } from '@nestjs/graphql'
...
@Resolver()
export class UserResolver {
constructor(private readonly userService: UserService)
@ResolveProperty(() => number)
public async age(): Promise<number> {
this.userService.getAge()
}
}
複製代碼
可是別忘了 在 model
裏面加上 age
字段
import { Field, ID } from 'type-graphql'
export class User {
@Field(() => ID, { nullable: false })
id: string
@Field({ nullable: false })
name?: string
@Field(()=> Number, { nullable: false })
age?: number
}
複製代碼
這樣查詢的時候 Resolver
會幫你合併在一塊兒
query users {
id
name
age
}
複製代碼
{
id: '1',
name: 'xx',
age: 18
}
複製代碼
因爲 Resolver
的 N+1 查詢問題
像上面 this.userService.getAge()
, 會執行屢次, 若是是 執行一些 sql
可能會有性能問題,和資源浪費, 可是問題不大,
咱們用 dataloader
來解決這個問題
import DataLoader from 'dataloader'
@Injectable()
export class UserService {
loader = new DataLoader(()=>{
return 一些查詢操做
})
getAge() {
this.loader.load()
// 查詢多個 this.loader.loadMany()
}
}
複製代碼
原理大概就是 把當前 event loop 的 請求 放在 process.nextTick
去執行
因爲 docker
裏面沒有寫入文件的權限, 這樣會帶來一個問題, 因爲啓動應用的時候
...
RUN node dist/index.js 複製代碼
會自動生成 schema
文件, 也就是 fs.writeFile
這樣會致使 docker
啓動不了, 因此須要小小修改下 GraphqlModule
的配置
import { Module } from '@nestjs/common'
import { GraphQLModule } from '@nestjs/graphql'
import { UserModule } from './user.module'
const schema = resolve(__dirname, 'schema.gql')
@Module({
imports: [
GraphQLModule.forRoot({
path: '/graphql',
autoSchemaFile: process.env.NODE_ENV === 'development' ? schema : false,
typePaths: [schema],
}),
UserModule,
]
})
export class AppModule
複製代碼
在development
的時候 會生成 schema.gql
, 在 production
環境下 關閉自動生成
同時指定 typePaths
爲 schema.gql
這樣既可解決
...
COPY schema.gql /dist RUN node dist/index.js 複製代碼
首先 使用 type-graphql
提供的 buildSchema
事實上 nest.js
的 GraphqlModule
也是使用的這個方法幫你自動生成的
import { buildSchema } from "type-graphql";
async function bootstrap() {
const schema = await buildSchema({
resolvers: [__dirname + "/**/*.resolver.ts"],
});
// other initialization code, like creating http server
}
bootstrap();
複製代碼
在每次 構建鏡像的時候 將這個文件 copy 進去既可
在 express
中 能夠經過 中間鍵 攔截 request
來作權限驗證, 在 nest.js
中 能夠很方便的 使用 Guards
實現
import { Args, Resolver, ResolveProperty } from '@nestjs/graphql'
import { AuthGuard } from './auth.guard'
...
@Resolver()
@UseGuards(AuthGuard)
export class UserResolver {
constructor(private readonly userService: UserService)
@ResolveProperty(() => number)
public async age(): Promise<number> {
this.userService.getAge()
}
}
複製代碼
因爲 Graphql
有一個 context
的概念 能夠經過 context
拿到 當前的 request
// auth.guard.ts
import { Injectable, CanActivate, ExecutionContext } from '@nestjs/common'
import { GqlExecutionContext } from '@nestjs/graphql'
@Injectable()
export class AuthGuard implements CanActivate {
canActivate(context: ExecutionContext) {
const ctx = GqlExecutionContext.create(context).getContext()
const request = context.switchToHttp().getRequest()
// 作一些權限驗證
// jwt 驗證
// request.headers.authorization
}
}
複製代碼
因爲使用的事 apollo-server
, 在每次 Query
或 Mutation
報錯時, 發送到前端的 錯誤
層級會很深,
若是想自定義可使用 formatError
和 formatResponse
, 但因爲 這兩個字段 nest.js
並無提供 相應詳細的定義
可能要去看下 apollo-server
的文檔才行, 儘管 TMD 文檔只有幾行
import { Module } from '@nestjs/common'
import { GraphQLModule } from '@nestjs/graphql'
import { UserModule } from './user.module'
const schema = resolve(__dirname, 'schema.gql')
@Module({
imports: [
GraphQLModule.forRoot({
path: '/graphql',
autoSchemaFile: process.env.NODE_ENV === 'development' ? schema : false,
typePaths: [schema],
context(ctx) {
// 在 context 裏面 添加一些 東西 ctx.req
ctx.xx = 1
return ctx
}
formatError(error) {
return error
},
formatResponse(response, {context}){
// 這裏進行重寫
// data, errors 是 graphql 的規範 沒法覆蓋
return {
errors: {}
}
// ❌ 這樣是不行的
return {
name: 1,
age: 18
}
// ✅
return {
data: {
name: 1,
age: 18
}
}
}
}),
UserModule,
]
})
export class AppModule
複製代碼
你可能想寫一點 單元測試
或者 e2e測試
, 文檔都有, 這裏就不當搬運工了
固然, 踩坑的心酸 遠不止 這一點點文字, 這一次也是收穫頗多, 繼續加油