【開源】Koa + GraphQL實戰(Typescript版)

寫在前面

前幾天看了一篇文章 GraphQL 搭配 Koa 最佳入門實踐,很是詳細地講述了koa集成graphql的實戰, 可是是js的版本,並且由於是兩年前的文章了,有的包如今也已經更新了使用方式。
因此心血來潮,基於原做者的版本更新了一個ts版本的。javascript

代碼戳這裏 https://github.com/sunxiuguo/Koa-GraphQL-Template,喜歡的話動動小手star一波❤~java

主要的變化

  1. graphql -> type-graphql
  2. mongoose -> typegoose
  3. graphql-server-koa -> apollo-server-koa
  4. 新增nodemon監聽代碼變化,熱更新

使用typegoose替換mongoose

考慮到後續type-graphql的使用, 咱們把實體的定義單獨拿出來,新建一個entities文件夾。node

  • typegoose中是用類的方式定義字段
src/entities/info.ts
export class Info {
    // 數組類型
    @arrayProp({ items: String })
    public hobby!: string[];

    @prop()
    public height!: string;

    @prop()
    public weight!: number;

    // 其餘文件定義的Meta類型
    @prop({ _id: false })
    public meta!: Meta; // 不生成_id
}
  • 定義好class後,咱們來生成model
export const InfoModal = getModelForClass(Info); // 是否是很簡單!
  • mongo hooks

原來的mongoose hooks,好比schema.pre('save', function(){}),改成了裝飾器的寫法git

@pre<Info>('save', function() {
    // 直接this.meta.xxx賦值會不生效
    this.meta = this.meta || {};
    if (this.isNew) {
        this.meta.createdAt = this.meta.updatedAt = Date.now();
    } else {
        this.meta.updatedAt = Date.now();
    }
})
export class Info {
    @arrayProp({ items: String })
    public hobby!: string[];

    @prop()
    public height!: string;

    @prop()
    public weight!: number;

    @prop({ _id: false })
    public meta!: Meta; // 不生成_id
}

export const InfoModal = getModelForClass(Info);
  • 一樣的,咱們來定義一下student的實體
src/entities/student.ts
import { prop, getModelForClass, Ref, pre } from '@typegoose/typegoose';
import { Info } from './info';
import { Meta } from './meta';

@pre<Student>('save', function() {
    // 直接this.meta.xxx賦值會不生效
    this.meta = this.meta || {};
    if (this.isNew) {
        this.meta.createdAt = this.meta.updatedAt = Date.now();
    } else {
        this.meta.updatedAt = Date.now();
    }
})
export class Student {
    @prop()
    public name!: string;

    @prop()
    public sex!: string;

    @prop()
    public age!: number;

    // info字段是和Info實體的id關聯的,在typegoose中,咱們經過Ref來定義
    @prop({ ref: Info })
    public info!: Ref<Info>;

    @prop({ _id: false })
    public meta!: Meta;
}

export const StudentModel = getModelForClass(Student);

使用type-graphql代替graphql

咱們上面已經定義了一遍student和info實體的類型,若是用type-graphql再定義一遍,豈不是麻煩到爆炸💥!
所幸type-graphql提供的也是class定義的方式,使用裝飾器來定義各類類型,咱們能夠直接在student.ts和info.ts里加幾行type-graphql的定義便可。github

  • 定義字段類型
src/entities/student.ts
import { Field, ObjectType } from 'type-graphql';

@ObjectType()
export class Student {
    @Field({ description: 'id' })
    public _id?: string;

    @Field({ description: '姓名' })
    @prop()
    public name!: string;

    @Field({ description: '性別' })
    @prop()
    public sex!: string;

    @Field({ description: '年齡' })
    @prop()
    public age!: number;

    @Field(() => Info, { description: 'infoid' })
    @prop({ ref: Info })
    public info!: Ref<Info>;

    @Field(() => Meta, { description: '時間' })
    @prop({ _id: false })
    public meta!: Meta;
}
  • 定義resolver

這裏分別定義了帶參的查詢,關聯查詢以及新增的三個方法。數組

src/graphql/resolvers/student.ts
import { Resolver, Query, Mutation, Arg, InputType, Field, Args, ArgsType } from 'type-graphql';
import { StudentModel, Student } from '../../entities/student';


@Resolver(Student)
export class StudentResolver {
    @Query(() => [Student], { nullable: true, description: '查詢學生列表' })
    async students(@Args() args: StudentArgs) {
        // TODO args have to be required?
        return await StudentModel.find(args);
    }

    @Query(() => [Student], { nullable: true, description: '查詢學生列表(帶Info)' })
    async studentsWithInfo() {
        return await StudentModel.find({})
            .populate('info', 'hobby height weight')
            .exec();
    }

    @Mutation(() => Student)
    async saveStudent(@Arg('data') newStudent: StudentInput) {
        const student = new StudentModel(newStudent);
        const res = await student.save();
        return res;
    }
}

type-graphql裏,對於query參數和mutation的參數須要分別用ArgsType()和InputType()定義。這裏由於個人兩種參數有區別,因此定義了兩份。app

@InputType()
class StudentInput {
    @Field({ description: '姓名', nullable: false })
    public name!: string;

    @Field({ description: '性別', nullable: false })
    public sex!: string; // 考慮用enum

    @Field({ description: '年齡', nullable: false })
    public age!: number;

    @Field({ description: 'infoid', nullable: false })
    public info!: string;
}

@ArgsType()
class StudentArgs {
    @Field({ description: '姓名', nullable: true })
    public name?: string;

    @Field({ description: '性別', nullable: true })
    public sex?: string;

    @Field({ description: '年齡', nullable: true })
    public age?: number;

    @Field({ description: 'infoid', nullable: true })
    public info?: string;
}

Koa服務集成graphql

src/graphql/index.ts

原文中的graphql-server-koa包已經更新爲apollo-server-koa,本文使用的是v2版本。koa

如今咱們已經定義好了schema,接下來要初始化apolloServer。
由於初始化時須要傳入schema參數,因此咱們先來獲取schema。async

  • 獲取全部已定義的schema
import { buildSchema } from 'type-graphql';
import path from 'path';

// 獲取匹配全部resolver的路徑
function getResolvers() {
    return [path.resolve(__dirname + '/resolvers/*.ts')];
}

// 經過buildSchema方法來生成graphql schema
async function getSchema() {
    return buildSchema({
        resolvers: getResolvers()
    });
}
  • 集成koa

由於我們是針對已有的koa服務,集成graphql,因此咱們寫一個單獨的函數來作這個。mongoose

export async function integrateGraphql(app: Koa<Koa.DefaultState, Koa.DefaultContext>) {
    const server = new ApolloServer({
        schema: await getSchema(),
        playground: {
            settings: {
                'request.credentials': 'include'
            }
        } as any,
        introspection: true,
        context: ({ ctx }) => ctx
    });
    server.applyMiddleware({ app });
    return server;
}

在初始化Koa時,就能夠直接調用這個函數支持graphql了

const app = new Koa();

integrateGraphql(app).then(server => {
    // 使用 bodyParser 和 KoaStatic 中間件
    app.use(bodyParser());
    app.use(KoaStatic(__dirname + '/public'));

    app.use(router.routes()).use(router.allowedMethods());

    app.listen(4000, () => {
        console.log(`server running success at ${server.graphqlPath}`);
    });
});

使用nodemon來監聽代碼變化,熱更新服務

這個很簡單,咱們安裝好nodemon後,直接添加一條命令就好.

nodemon --watch src -e ts --exec ts-node src/start.ts

這條命令表明的是,監聽src文件下的全部ts文件,而且執行src/start.ts文件。

大功告成,啓動!

yarn serve

訪問graphql查詢頁面

如今咱們就能夠訪問graphql的查詢頁面,開始查查查,改改改了!

http://localhost:4000/graphql

我們先來插入一條info

mutation {
  saveInfo(data: { hobby:["唱","跳","rap","籃球"], height:"165", weight: 100}){
    hobby
    height
    weight
  }
}

而後再來查詢一波

# Write your query or mutation here
query {
#   students(age:22){
#     sex
#     name
#     age
#   }
  
#   studentsWithInfo {
#     sex
#     name
#     age
#   }
  
   infos {
    _id
    height
    weight
    hobby
  }
}

妥了!!!

Github地址

代碼戳這裏 https://github.com/sunxiuguo/Koa-GraphQL-Template,喜歡的話動動小手star一波❤~

轉載請註明原文連接和做者。
相關文章
相關標籤/搜索