2

写在前面

前几天看了一篇文章 GraphQL 搭配 Koa 最佳入门实践,非常详细地讲述了koa集成graphql的实战, 但是是js的版本,而且因为是两年前的文章了,有的包现在也已经更新了使用方式。
所以心血来潮,基于原作者的版本更新了一个ts版本的。

代码戳这里 https://github.com/sunxiuguo/Koa-GraphQL-Template,喜欢的话动动小手star一波❤~

主要的变化

  1. graphql -> type-graphql
  2. mongoose -> typegoose
  3. graphql-server-koa -> apollo-server-koa
  4. 新增nodemon监听代码变化,热更新

使用typegoose替换mongoose

考虑到后续type-graphql的使用, 我们把实体的定义单独拿出来,新建一个entities文件夹。

  • 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(){}),改为了装饰器的写法

@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的定义即可。

  • 定义字段类型
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()定义。这里因为我的两种参数有区别,所以定义了两份。


@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版本。

现在我们已经定义好了schema,接下来要初始化apolloServer。
因为初始化时需要传入schema参数,所以我们先来获取schema。

  • 获取所有已定义的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,所以我们写一个单独的函数来做这个。

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一波❤~

转载请注明原文链接和作者。

路从今夜白
219 声望2 粉丝