写在前面
前几天看了一篇文章 GraphQL 搭配 Koa 最佳入门实践,非常详细地讲述了koa集成graphql的实战, 但是是js的版本,而且因为是两年前的文章了,有的包现在也已经更新了使用方式。
所以心血来潮,基于原作者的版本更新了一个ts版本的。
代码戳这里 https://github.com/sunxiuguo/Koa-GraphQL-Template,喜欢的话动动小手star一波❤~
主要的变化
- graphql -> type-graphql
- mongoose -> typegoose
- graphql-server-koa -> apollo-server-koa
- 新增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一波❤~
转载请注明原文链接和作者。
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。