Практическое применение Nest.js и GraphQL в проекте

GraphQL NestJS

предисловие

В настоящее время работает над проектомBFF, nest.jsа такжеGraphQLЭти два стека технологий являются «новой» попыткой, хотяGraphQLсуществует15год вышел, но сnest.jsВ сочетании, благодаря хорошей авторской инкапсуляции, произошла замечательная химическая реакция

Конечно, это не гидрология, которая приклеивает официальные документы и потом учит, как ими пользоваться, а гидрология, основанная на опыте добычи полезных ископаемых.

плечи гигантов

  • type-graphqlБудуtypescriptопределение превращается вgraphqlизschema
  • @nestjs/graphqlавтор вapollo-serverНа основе 2 инкапсуляций
  • data-loaderАгрегация и кэширование данныхresolver (n+1)Эта проблема

Запись приложения

Здесь мы беремUserModuleНапример

в состоянии пройти

query UserList() {
  users {
    id
    name
  }
}

получать

{
  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генерация файла схемыgraphql.ts

Например

type User {
  id: ID!
  name: String
}

будет генерировать

export class User {
  id: string
  name?: string
}

Затем мы пишемresolverа такжеserviceможно использовать, когдаgraphql.tsСоздайте хорошее определение типа, но этот метод немного неудобен и немного несовместим с привычками программирования.

Если вы хотите написатьtypescriptопределение, генерироватьgraphqlизschemaфайл, затем используйтеtype-graphqlохватывать

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метафора схемы,@FieldКарты декоратораschemaвidтип

Class Userидентификатор описан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модуль по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когда соответствующий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

из-за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()
  }
}

Принцип, вероятно, состоит в том, чтобы поместить запрос текущего цикла событий вprocess.nextTickвыполнить

Развертывание докера

из-за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Отключить автоматическую генерацию в среде

Также укажитеtypePathsдляschema.gqlЭто можно решить

  • Способ 2:

...

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();

Вы можете копировать этот файл в него каждый раз, когда создаете образ.

АСД

существует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测试, документы все есть, я не буду тут носильщиком

наконец

Конечно, грусть от того, что я наступил на яму, - это гораздо больше, чем просто этот небольшой текст. На этот раз я также многое приобрел. Продолжайте в том же духе