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

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

相關文章
相關標籤/搜索