react+graphql起手和特性介绍(二)

1

紧接第一篇文章,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

你可能感兴趣的

载入中...