紧接第一篇文章,react+graphql起手和特性介绍(一),咱们接下来实现resolver,和自定义请求上下文,来完成建立用户,发帖,查看全部帖子的功能
首先,咱们进行自定义请求上下文,来模拟数据库和会话,保存咱们的用户数据,帖子数据,登陆状态。在server目录下建立context.js文件。前端
// server/context.js const store = new Map(); // 模拟数据库,保存注册用户,建立的帖子数据 const sessionStore = { // 模拟session会话,保存用户登陆状态数据 key: 0, }; module.exports = (context) => { const { ctx } = context; // 咱们是将graphql与koa整合在一块儿, // 这里的ctx就是koa提供的请求上下文 // 咱们为 graphql 的 context 添加 session 和 store context.session = { get() { const cookieValue = ctx.cookies.get('user_login'); return sessionStore[cookieValue]; }, set(value) { const cookieValue = ++sessionStore.key; ctx.cookies.set('user_login', cookieValue); sessionStore[cookieValue] = value; } }; context.store = store; return context; }
接下来咱们实现user的reslover和对应的schemareact
// server/resolver/user.js const idsKey = 'user_ids'; let idCount = 0; const genId = () => { idCount++; return 'user_id_' + idCount; }; module.exports = { Query: { // 查询登陆用户 user(root, query, ctx) { const { session } = ctx; return session.get(); }, // 查询全部用户 users(root, query, { store }) { const ids = store.get(idsKey) || []; const res = []; ids.forEach(id => { res.push(store.get(id)); }); return res; }, // 用户登陆 login(root, { id }, { store, session }) { const user = store.get(id); if (user) session.set(user); return user; } }, Gender: { MALE: 1, FEMALE: 2 }, // Mutation 是与Query同样的根节点,与Query没有什么区别,只有语义上的区分, // 对数据进行修改和新增的操做都放在 Mutation 中 Mutation: { // 建立用户 createUser(root, { data }, { session, store }) { data.id = genId(); let userIds = store.get(idsKey); if (!userIds) userIds = []; userIds.push(data.id); store.set(data.id, data); store.set(idsKey, userIds); session.set(data); return data; } } }
# server/schema/user.graphql ... extend type Query { user: User users: [User] login(id: ID!): User } # input 表明输入type,须要输入的类型须要用input进行定义。 # 好比建立用户的json数据,其结构须要用input定义,才能使用 input UserInput { name: String age: Int available: Boolean money: Float gender: Gender birthday: Date } extend type Mutation { # 使用 UserInput 做为输入结构类型,! 表示不能为空 createUser(data: UserInput!): User }
为使咱们返回的自定义类型数据生效,修改下对mock进行以下修改数据库
// server/mock.js module.exports = { Date(root, args, ctx, info) { // info表明解析信息,能够取到当前访问的字段名,咱们对返回数据root进行判断, // 若是为null,则建立新的对象,不然使用返回的数据 if (!root[info.fieldName]) return new Date(); return root[info.fieldName]; } }
好了,咱们的用户相关功能已经实现完成,如今启动服务,咱们建立一个用户,登陆,并查询
在mutation上咱们先定义$user变量,语法规定需以$开头,它的类型是UserInput!,对应咱们的schema定义,而后在createUser查询中使用此变量$user,它对应的schema解析变量是data,data就是咱们在reslover中访问请求参数的变量名。具体的请求数据,咱们经过query variables进行定义,它是json格式的数据,"user"对应咱们的$user变量,里面的结构与UserInput!一一对应,并输入值。建立完用户以后会将用户数据返回,并有对应的id值。
登陆用户
查询全部用户
根据咱们的定义,查询出来的是数组类型
让咱们继续完成帖子的功能json
// server/resolver/post.js const idsKey = 'post_ids'; let idCount = 0; const genId = () => { idCount++; return 'post_id_' + idCount; }; module.exports = { Query: { post(root, query, { store }) { return store.get(query.id) }, posts(root, query, { store }) { const ids = store.get(idsKey) || []; const res = []; ids.forEach(id => { res.push(store.get(id)); }); return res; } }, Post: { // 在返回post数据时有个user字段是User类型,咱们并不须要每次返回时都在post查询的 // resolver中查出对应的user数据,graphql的特性是,若是reslover返回的数据没有某个 // 定义了类型的字段值,就会找类型字段的具体定义reslover并执行,其root值就是上次查询 // 出来的对应类型值,而后将此reslover返回值拼接到原始对象中并返回。 // 在这里具体的执行流程会在下面示例中说明 user(root, query, { store }) { if (root && root.userId) { return store.get(root.userId) } } }, Mutation: { createPost(root, { data }, { store, session }) { // 若是用户没有登陆,将没法建立帖子 if (!session.get()) throw new Error("no permission"); data.id = genId(); data.userId = session.get().id; let ids = store.get(idsKey); if (!ids) ids = []; ids.push(data.id); store.set(data.id, data); store.set(idsKey, ids); return data; } } }
为了格式化错误,在建立服务时,自定义formatErrorsegmentfault
// server/index.js ... const server = new ApolloServer({ ... formatError: error => { // 删除 extensions 字段,删除异常的堆栈,不暴露服务器发生错误的文件 delete error.extensions; return error; }, }); ...
继续完善post schema后端
# server/schema/post.graphql ... extend type Query { post(id: ID!): Post @auth(role: ONE) posts: [Post] @auth(role: ALL) } input PostInput { title: String! content: String! } extend type Mutation { createPost(data: PostInput!): Post }
若是会话有异常,没有cookie信息,修改下graphql gui客户端的配置
修改 "request.credentials": "omit" 为 "request.credentials": "include"数组
下面咱们进行建立帖子和查询帖子的操做
能够看到,咱们在代码createPost和posts代码中并无查询user,这里也会返回user数据,是由于咱们定义了Post的user字段对应的reslover方法,在返回类型为Post时,posts/createPost返回的数据user字段为空,graphql就会自动调用user的reslover方法,而且以前posts/createPost返回的数据会做为user的reslover中root参数传入,这样咱们就能够从root数据中获取userId,而后对user数据的查询只用放在一个地方执行就能够。graphql很好地分化了类型数据的处理逻辑,使每一个resolver只关注处理此层对应的数据,剩下的数据拼接graphql会帮咱们处理好。服务器
最后咱们将用自定义指令,来实现服务端鉴权操做
建立文件directive.jscookie
// server/directive.js const { SchemaDirectiveVisitor } = require('apollo-server-koa'); class AuthDirective extends SchemaDirectiveVisitor { visitFieldDefinition(field) { // 对用户年龄进行校验 const age = this.args.age; // 指令的实现方式是将resolvoer进行hack,所以指令本质也是resolver const realResolve = field.resolve; field.resolve = async function (root, query, context, info) { const user = context.session.get(); if (user && user.age >= age) { return await realResolve.call(this, root, query, context, info); } else { throw Error('no permission'); } }; } } module.exports = { auth: AuthDirective }
在schema中定义指令session
# server/schema/schema.graphql ... # 使用directive关键子定义指令, auth 指令名,age为此指令接收的参数 directive @auth( age: Int, ) on FIELD_DEFINITION # FIELD_DEFINITION 表示此指令应用于字段定义
使用指令
# server/schema/post.graphql ... extend type Query { post(id: ID!): Post @auth(age: 18) posts: [Post] @auth(age: 20) } ...
如今咱们再进行查询,发现指令已经生效,用户age小于20是不能查出posts数据的,而post是能够查出数据的
到此,咱们graphql后端服务的搭建和特性就介绍完了,后面咱们会介绍前端react如何整合graphql