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