Nest.js 與 GraphQL 在項目中的實際應用

前言

最近項目中在作一個 BFF, nest.jsGraphQL 這兩個技術棧 是一個"新"的嘗試, 雖然 GraphQL15 年 就出來了, 可是在與 nest.js 結合, 得益於做者良好的封裝, 發生了奇妙的化學反應前端

固然這不是一篇 粘貼官方文檔 而後教你如何使用的 水文, 而是採坑心得的 水文node

巨人的肩膀

  • type-graphqltypescript 的定義轉成 graphqlschema
  • @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.tsdocker

例如typescript

type User {
  id: ID!
  name: String
}
複製代碼

會生成express

export class User {
  id: string
  name?: string
}
複製代碼

而後咱們寫 resolverservice 的時候 就能夠用 graphql.ts 生成好的類型定義, 可是這種方式有一點不方便, 有點不符合編程習慣編程

若是想要先寫 typescript 的定義, 生成 graphqlschema 文件, 那麼就要用到 type-graphqlbootstrap

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
}
複製代碼

Resolver

nest.js 裏 一個 Graphql 模塊 由 resolverservice 組成

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
 }
複製代碼

DateLoader

因爲 ResolverN+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 部署

因爲 docker 裏面沒有寫入文件的權限, 這樣會帶來一個問題, 因爲啓動應用的時候

...

RUN node dist/index.js 複製代碼

會自動生成 schema 文件, 也就是 fs.writeFile 這樣會致使 docker 啓動不了, 因此須要小小修改下 GraphqlModule 的配置

  • 方法 1 :
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 環境下 關閉自動生成

同時指定 typePathsschema.gql 這樣既可解決

  • 方法 2 :
...

COPY schema.gql /dist RUN node dist/index.js 複製代碼

首先 使用 type-graphql 提供的 buildSchema 事實上 nest.jsGraphqlModule 也是使用的這個方法幫你自動生成的

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
  }
}
複製代碼

轉換 error response

因爲使用的事 apollo-server, 在每次 QueryMutation 報錯時, 發送到前端的 錯誤 層級會很深,

若是想自定義可使用 formatErrorformatResponse, 但因爲 這兩個字段 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測試 , 文檔都有, 這裏就不當搬運工了

最後

固然, 踩坑的心酸 遠不止 這一點點文字, 這一次也是收穫頗多, 繼續加油

相關文章
相關標籤/搜索