最近項目中在作一個 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測試
, 文檔都有, 這裏就不當搬運工了
固然, 踩坑的心酸 遠不止 這一點點文字, 這一次也是收穫頗多, 繼續加油