紧接第一篇文章,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和对应的schema
// 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值。
登录用户
查询所有用户
根据我们的定义,查询出来的是数组类型
让我们继续完成帖子的功能
// 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;
}
}
}
为了格式化错误,在创建服务时,自定义formatError
// 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.js
// 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中定义指令
# 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
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。