原本这篇文章准备51假期期间就发出来的,可是由于本身的笔记本电脑出了一点问题,因此拖到了如今😂。为了你们更好的学习GraphQL,我写一个先后端的GraphQL的Demo,包含了登录,增长数据,获取数据一些常见的操做。前端使用了Vue和TypeScript,后端使用的是Koa和GraphQL。前端
这个是预览的地址: GraphQLDeom 默认用户root,密码rootgit
这个是源码的地址: learn-graphqlgithub
按照官方文档中给出的定义, "GraphQL 既是一种用于 API 的查询语言也是一个知足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端可以准确地得到它须要的数据,并且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具"。可是我在使用以后发现,gql须要后端作的太多了,类型系统对于前端很美好,可是对于后端来讲可能意味着屡次的数据库查询。虽然gql实现了http请求上的优化,可是后端io的性能也应当是咱们所考虑的。web
GraphQL中操做类型主要分为查询和变动(还有subscription订阅),分别对应query,mutation关键字。query,mutation的操做名称operation name是能够省略的。可是添加操做名称能够避免歧义。操做能够传递不一样的参数,例如getHomeInfo中分页参数,AddNote中笔记的属性参数。下文中,咱们主要对query和mutation进行展开。redis
query getHomeInfo { users(pagestart: ${pagestart}, pagesize: ${pagesize}) { data { id name createDate } } } mutation AddNote { addNote(note: { title: "${title}", detail: "${detail}", uId: "${uId}" }) { code } }
全称Schema Definition Language。GraphQL实现了一种可读的模式语法,SDL和JavaScript相似,这种语法必须存储为String格式。咱们须要区分GraphQL Schema和Mongoose Schema的区别。GraphQL Schema声明了返回的数据和结构。Mongoose Schema则声明了数据存储结构。数据库
GraphQL提供了一些默认的标量类型, Int, Float, String, Boolean, ID。GraphQL支持自定义标量类型,咱们会在后面介绍到。npm
对象类型是Schema中最多见的类型,容许嵌套和循环引用json
type TypeName { fieldA: String fieldB: Boolean fieldC: Int fieldD: CustomType }
查询类型用于获取数据,相似REST GET。Query是Schema的起点,是根级类型之一,Query描述了咱们能够获取的数据。下面的例子中定义了两种查询,getBooks,getAuthors。后端
type Query { getBooks: [Book] getAuthors: [Author] }
传统的REST API若是要获取两个列表须要发起两次http请求, 可是在gql中容许在一次请求中同时查询。数组
query { getBooks { title } getAuthors { name } }
突变类型相似与REST API中POST,PUT,DELETE。与查询类型相似,Mutation是全部指定数据操做的起点。下面的例子中定义了addBook mutation。它接受两个参数title,author均为String类型,mutation将会返回Book类型的结果。若是突变或者查询须要对象做为参数,咱们则须要定义输入类型。
type Mutation { addBook(title: String, author: String): Book }
下面的突变操做中会在添加操做后,返回书的标题和做者的姓名
mutation { addBook(title: "Fox in Socks", author: "Dr. Seuss") { title author { name } } }
输入类型容许将对象做为参数传递给Query和Mutation。输入类型为普通的对象类型,使用input关键字进行定义。当不一样参数须要彻底相同的参数的时候,也可使用输入类型。
input PostAndMediaInput { title: String body: String mediaUrls: [String] } type Mutation { createPost(post: PostAndMediaInput): Post }
Scheam中支持多行文本和单行文本的注释风格
type MyObjectType { """ Description Description """ myField: String! otherField( "Description" arg: Int ) }
如何自定义标量类型?咱们将下面的字符串添加到Scheam的字符串中。MyCustomScalar是咱们自定义标量的名称。而后须要在 resolver中传递GraphQLScalarType的实例,自定义标量的行为。
scalar MyCustomScalar
咱们来看下把Date类型做为标量的例子。首先在Scheam中添加Date标量
const typeDefs = gql` scalar Date type MyType { created: Date } `
接下来须要在resolvers解释器中定义标量的行为。坑爹的是文档中只是简单的给出了示例,并无解释一些参数的具体做用。我在stackoverlfow上看到了一个不错的解释。
serialize是将值发送给客户端的时候,将会调用该方法。parseValue和parseLiteral则是在接受客户端值,调用的方法。parseLiteral则会对Graphql的参数进行处理,参数会被解析转换为AST抽象语法树。parseLitera会接受ast,返回类型的解析值。parseValue则会对变量进行处理。
const { GraphQLScalarType } = require('graphql') const { Kind } = require('graphql/language') const resolvers = { Date: new GraphQLScalarType({ name: 'Date', description: 'Date custom scalar type', // 对来自客户端的值进行处理, 对变量的处理 parseValue(value) { return new Date(value) }, // 对返回给客户端的值进行处理 serialize(value) { return value.getTime() }, // 对来自客户端的值进行处理,对参数的处理 parseLiteral(ast) { if (ast.kind === Kind.INT) { return parseInt(ast.value, 10) } return null }, }), }
接口是一个抽象类型,包含了一些字段,若是对象类型须要实现这个接口,须要包含这些字段
interface Avengers { name: String } type Ironman implements Avengers { id: ID! name: String }
解析器提供了将gql的操做(查询,突变或订阅)转换为数据的行为,它们会返回咱们在Scheam的指定的数据,或者该数据的Promise。解析器拥有四个参数,parent, args, context, info。
咱们没有为Scheam中全部的字段编写解析器,可是查询依然会成功。gql拥有默认的解析器。若是父对象拥有同名的属性,则不须要为字段编写解释器。它会从上层对象中读取同名的属性。
咱们能够为Schema中任何字段编写解析器,不只仅是查询和突变。这也是GraphQL如此灵活的缘由。
下面例子中,咱们为性别gender字段单独编写解析器,返回emoji表情。gender解析器的第一个参数是父类型的解析结果。
const typeDefs = gql` type Query { users: [User]! } type User { id: ID! gender: Gender name: String role: Role } enum Gender { MAN WOMAN } type Role { id: ID! name: String } ` const resolves = { User: { gender(user) { const { gender } = user return gender === 'MAN' ? '👨' : '👩' } } }
ApolloServer是一个开源的GraphQL框架,在ApolloServer 2中。ApolloServer能够单独的做为服务器,同时ApolloServer也能够做为Express,Koa等Node框架的插件
就像咱们以前所说的同样。在ApolloServer2中,ApolloServer能够单独的构建一个GraphQL服务器(具体能够参考Apollo的文档)。可是我在我的的demo项目中,考虑到了社区活跃度以及中间件的丰富度,最终选择了Koa2做为开发框架,ApolloServer做为插件使用。下面是Koa2与Apollo构建服务的简单示例。
const Koa = require('koa') const { ApolloServer } = require('apollo-server-koa') const typeDefs = require('./schemas') const resolvers = require('./resolvers') const app = new Koa() const mode = process.env.mode // KOA的中间件 app.use(bodyparser()) app.use(response()) // 初始化REST的路由 initRouters() // 建立apollo的实例 const server = new ApolloServer({ // Schema typeDefs, // 解析器 resolvers, // 上下文对象 context: ({ ctx }) => ({ auth: ctx.req.headers['x-access-token'] }), // 数据源 dataSources: () => initDatasource(), // 内省 introspection: mode === 'develop' ? true : false, // 对错误信息的处理 formatError: (err) => { return err } }) server.applyMiddleware({ app, path: config.URL.graphql }) module.exports = app.listen(config.URL.port)
从ApolloServer中导出gql函数。并经过gql函数,建立typeDefs。typeDefs就是咱们所说的SDL。typeDefs中包含了gql中全部的数据类型,以及查询和突变。能够视为全部数据类型及其关系的蓝图。
const { gql } = require('apollo-server-koa') const typeDefs = gql` type Query { # 会返回User的数组 # 参数是pagestart,pagesize users(pagestart: Int = 1, pagesize: Int = 10): [User]! } type Mutation { # 返回新添加的用户 addUser(user: User): User! } type User { id: ID! name: String password: String createDate: Date } ` module.exports = typeDefs
因为咱们须要把全部数据类型,都写在一个Schema的字符串中。若是把这些数据类型都在放在一个文件内,对将来的维护工做是一个障碍。咱们能够借助merge-graphql-schemas,将schema进行拆分。
const { mergeTypes } = require('merge-graphql-schemas') // 多个不一样的Schema const NoteSchema = require('./note.schema') const UserSchema = require('./user.schema') const CommonSchema = require('./common.schema') const schemas = [ NoteSchema, UserSchema, CommonSchema ] // 对Schema进行合并 module.exports = mergeTypes(schemas, { all: true })
咱们在构建Scheam后,须要将数据源链接到Scheam API上。在个人demo示例中,我将GraphQL API分层到REST API的上面(至关于对REST API作了聚合)。Apollo的数据源,封装了全部数据的存取逻辑。在数据源中,能够直接对数据库进行操做,也能够经过REST API进行请求。咱们接下来看看如何构建一个REST API的数据源。
// 安装apollo-datasource-rest // npm install apollo-datasource-rest const { RESTDataSource } = require('apollo-datasource-rest') // 数据源继承RESTDataSource class UserAPI extends RESTDataSource { constructor() { super() // baseURL是基础的API路径 this.baseURL = `http://127.0.0.1:${config.URL.port}/user/` } /** * 获取用户列表的方法 */ async getUsers (params, auth) { // 在服务内部发起一个http请求,请求地址 baseURL + users // 咱们会在KoaRouter中处理这个请求 let { data } = await this.get('users', params, { headers: { 'x-access-token': auth } }) data = Array.isArray(data) ? data.map(user => this.userReducer(user)) : [] // 返回格式化的数据 return data } /** * 对用户数据进行格式化的方法 */ userReducer (user) { const { id, name, password, createDate } = user return { id, name, password, createDate } } } module.exports = UserAPI
如今一个数据源就构建完成了,很简单吧😊。咱们接下来将数据源添加到ApolloServer上。之后咱们能够在解析器Resolve中获取使用数据源。
const server = new ApolloServer({ typeDefs, resolvers, context: ({ ctx }) => ({ auth: ctx.req.headers['x-access-token'] }), // 添加数据源 dataSources: () => { UserAPI: new UserAPI() }, introspection: mode === 'develop' ? true : false, formatError: (err) => { return err } })
目前咱们还不能运行查询或者变动。咱们如今须要编写解析器。在以前的介绍中,咱们知道了解析器提供了将gql的操做(查询,突变或订阅)转换为数据的行为。解析器主要分为三种,查询解析器,突变解析器,类型解析器。下面是一个查询解析器和突变解析器的示例,它分别位于解析器对象的Query字段,Mutation字段中。由于是根解析器,因此第一个parent为空。第二个参数,是查询或变动传递给咱们的参数。第三个参数则是咱们apollo的上下文context对象,咱们能够从上下文对象上拿到以前咱们添加的数据源。解析器须要返回符合Scheam模式的数据,或者该数据的Promise。突变解析器,查询解析器中的字段应当和Scheam中的查询类型,突变类型的字段是对应的。
module.exports = { // 查询解析器 Query: { users (_, { pagestart, pagesize }, { dataSources, auth }) { // 调用UserAPI数据源的getUsers方法, 返回User的数组 return dataSources.UserAPI.getUsers({ pagestart, pagesize }, auth) } }, // 突变解析器 Mutation: { // 调用UserAPI数据源的addUser方法 addUser (_, { user }, { dataSources, auth }) { return dataSources.UserAPI.addUser(user, auth) } } }
咱们接着将解析器链接到AppleServer中。
const server = new ApolloServer({ // Schema typeDefs, // 解析器 resolvers, // 添加数据源 dataSources: () => { UserAPI: new UserAPI() } })
好了到了目前为止,graphql这一层咱们基本完善了,咱们的graphql层最终会在数据源中调用REST API接口。接下来的操做就是传统的MVC的那一套。相信熟悉Koa或者Express的小伙伴必定都很熟悉。若是有不熟悉的小伙伴,能够参阅源码中routes文件夹以及controller文件夹。下面一个请求的流程图。
关于鉴权Apollo提供了多种解决方案。
Schema鉴权适用于不对外公共的服务, 这是一种全有或者全无的鉴权方式。若是须要实现这种鉴权只须要修改context
const server = new ApolloServer({ context: ({ req }) => { const token = req.headers.authorization || '' const user = getUser(token) // 全部的请求都会通过鉴权 if (!user) throw new AuthorizationError('you must be logged in'); return { user } } })
更多的状况下,咱们须要公开一些无需鉴权的API(例如登陆接口)。这时咱们须要更精细的权限控制,咱们能够将权限控制放到解析器中。
首先将权限信息添加到上下文对象上
const server = new ApolloServer({ context: ({ ctx }) => ({ auth: ctx.req.headers.authorization }) })
针对特定的查询或者突变的解析器进行权限控制
const resolves = { Query: { users: (parent, args, context) => { if (!context.auth) return [] return ['bob', 'jake'] } } }
我采用的方案,是在GraphQL以外受权。我会在REST API中使用中间件的形式进行鉴权操做。可是咱们须要将request.header中包含的权限信息传递给REST API
// 数据源 async getUserById (params, auth) { // 将权限信息传递给REST API const { data } = await this.get('/', params, { headers: { 'x-access-token': auth } }) data = this.userReducer(data) return data }
// *.router.js const Router = require('koa-router') const router = new Router({ prefix: '/user' }) const UserController = require('../controller/user.controller') const authentication = require('../middleware/authentication') // 适用鉴权中间件 router.get('/users', authentication(), UserController.getUsers) module.exports = router
// middleware authentication.js const jwt = require('jsonwebtoken') const config = require('../config') const { promisify } = require('util') const redisClient = require('../config/redis') const getAsync = promisify(redisClient.get).bind(redisClient) module.exports = function () { return async function (ctx, next) { const token = ctx.headers['x-access-token'] let decoded = null if (token) { try { // 验证jwt decoded = await jwt.verify(token, config.jwt.secret) } catch (error) { ctx.throw(403, 'token失效') } const { id } = decoded try { // 验证redis存储的jwt await getAsync(id) } catch (error) { ctx.throw(403, 'token失效') } ctx.decoded = decoded // 经过验证 await next() } else { ctx.throw(403, '缺乏token') } } }