前言

本来这篇文章准备51假期期间就发出来的,但是因为自己的笔记本电脑出了一点问题,所以拖到了现在?。为了大家更好的学习GraphQL,我写一个前后端的GraphQL的Demo,包含了登陆,增加数据,获取数据一些常见的操作。前端使用了Vue和TypeScript,后端使用的是Koa和GraphQL。

这个是预览的地址: GraphQLDeom 默认用户root,密码root

这个是源码的地址: learn-graphql

GraphQL入门以及相关概念

什么是GraphQL?

按照官方文档中给出的定义, "GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具"。但是我在使用之后发现,gql需要后端做的太多了,类型系统对于前端很美好,但是对于后端来说可能意味着多次的数据库查询。虽然gql实现了http请求上的优化,但是后端io的性能也应当是我们所考虑的。

查询和变更

GraphQL中操作类型主要分为查询和变更(还有subscription订阅),分别对应query,mutation关键字。query,mutation的操作名称operation name是可以省略的。但是添加操作名称可以避免歧义。操作可以传递不同的参数,例如getHomeInfo中分页参数,AddNote中笔记的属性参数。下文中,我们主要对query和mutation进行展开。


query getHomeInfo {users(pagestart: ${pagestart}, pagesize: ${pagesize}) {data {idnamecreateDate}}
}mutation AddNote {addNote(note: {title: "${title}",detail: "${detail}",uId: "${uId}"}) {code}
}
复制代码

Schema

全称Schema Definition Language。GraphQL实现了一种可读的模式语法,SDL和JavaScript类似,这种语法必须存储为String格式。我们需要区分GraphQL Schema和Mongoose Schema的区别。GraphQL Schema声明了返回的数据和结构。Mongoose Schema则声明了数据存储结构。

类型系统

标量类型

GraphQL提供了一些默认的标量类型, Int, Float, String, Boolean, ID。GraphQL支持自定义标量类型,我们会在后面介绍到。

对象类型

对象类型是Schema中最常见的类型,允许嵌套和循环引用


type TypeName {fieldA: StringfieldB: BooleanfieldC: IntfieldD: CustomType
}
复制代码

查询类型

查询类型用于获取数据,类似REST GET。Query是Schema的起点,是根级类型之一,Query描述了我们可以获取的数据。下面的例子中定义了两种查询,getBooks,getAuthors。


type Query {getBooks: [Book]getAuthors: [Author]
}
复制代码
  • getBooks,获取book列表
  • getAuthors,获取作者的列表

传统的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") {titleauthor {name}}
}
复制代码

输入类型

输入类型允许将对象作为参数传递给Query和Mutation。输入类型为普通的对象类型,使用input关键字进行定义。当不同参数需要完全相同的参数的时候,也可以使用输入类型。


input PostAndMediaInput {title: Stringbody: StringmediaUrls: [String]
}type Mutation {createPost(post: PostAndMediaInput): Post
}
复制代码

如何描述类型?(注释)

Scheam中支持多行文本和单行文本的注释风格


type MyObjectType {"""DescriptionDescription"""myField: String!otherField("Description"arg: Int)
}
复制代码

?自定义标量类型

如何自定义标量类型?我们将下面的字符串添加到Scheam的字符串中。MyCustomScalar是我们自定义标量的名称。然后需要在 resolver中传递GraphQLScalarType的实例,自定义标量的行为。


scalar MyCustomScalar
复制代码

我们来看下把Date类型作为标量的例子。首先在Scheam中添加Date标量


const typeDefs = gql`scalar Datetype 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
}
复制代码

解析器 resolvers

解析器提供了将gql的操作(查询,突变或订阅)转换为数据的行为,它们会返回我们在Scheam的指定的数据,或者该数据的Promise。解析器拥有四个参数,parent, args, context, info。

  • parent,父类型的解析结果
  • args,操作的参数
  • context,解析器的上下文,包含了请求状态和鉴权信息等
  • info,Information about the execution state of the operation which should only be used in advanced cases

默认解析器

我们没有为Scheam中所有的字段编写解析器,但是查询依然会成功。gql拥有默认的解析器。如果父对象拥有同名的属性,则不需要为字段编写解释器。它会从上层对象中读取同名的属性。

类型解析器

我们可以为Schema中任何字段编写解析器,不仅仅是查询和突变。这也是GraphQL如此灵活的原因。

下面例子中,我们为性别gender字段单独编写解析器,返回emoji表情。gender解析器的第一个参数是父类型的解析结果。


const typeDefs = gql`type Query {users: [User]!}type User {id: ID!gender: Gendername: Stringrole: Role}enum Gender {MANWOMAN}type Role {id: ID!name: String}
`const resolves = {User: {gender(user) {const { gender } = userreturn gender === 'MAN' ? '?' : '?'}}
}
复制代码

ApolloServer

什么是ApolloServer?

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({// SchematypeDefs,// 解析器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)
复制代码

构建Schema

从ApolloServer中导出gql函数。并通过gql函数,创建typeDefs。typeDefs就是我们所说的SDL。typeDefs中包含了gql中所有的数据类型,以及查询和突变。可以视为所有数据类型及其关系的蓝图。

const { gql } = require('apollo-server-koa')const typeDefs = gql`type Query {# 会返回User的数组# 参数是pagestart,pagesizeusers(pagestart: Int = 1, pagesize: Int = 10): [User]!}type Mutation {# 返回新添加的用户addUser(user: User): User!}type User {id: ID!name: Stringpassword: StringcreateDate: 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 } = userreturn {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}
})复制代码

编写resolvers

目前我们还不能运行查询或者变更。我们现在需要编写解析器。在之前的介绍中,我们知道了解析器提供了将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({// SchematypeDefs,// 解析器resolvers,// 添加数据源dataSources: () => {UserAPI: new UserAPI()}
})
复制代码

好了到了目前为止,graphql这一层我们基本完善了,我们的graphql层最终会在数据源中调用REST API接口。接下来的操作就是传统的MVC的那一套。相信熟悉Koa或者Express的小伙伴一定都很熟悉。如果有不熟悉的小伙伴,可以参阅源码中routes文件夹以及controller文件夹。下面一个请求的流程图。

其他

关于鉴权

关于鉴权Apollo提供了多种解决方案。

Schema鉴权

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之外的授权

我采用的方案,是在GraphQL之外授权。我会在REST API中使用中间件的形式进行鉴权操作。但是我们需要将request.header中包含的权限信息传递给REST API

// 数据源async getUserById (params, auth) {// 将权限信息传递给REST APIconst { 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 = nullif (token) {try {// 验证jwtdecoded = await jwt.verify(token, config.jwt.secret)} catch (error) {ctx.throw(403, 'token失效')}const { id } = decodedtry {// 验证redis存储的jwtawait getAsync(id)} catch (error) {ctx.throw(403, 'token失效')}ctx.decoded = decoded// 通过验证await next()} else {ctx.throw(403, '缺少token')}}
}
复制代码

转载于:https://juejin.im/post/5cd68a9b51882568047fa6eb

GraphQL从入门到实战相关推荐

  1. NestJS + TypeGraphQL + MySQL 从入门到实战视频教程(35 个视频)

    我这里有视频教程,全部是我自己辛苦录的,有兴趣的可以看下. NestJS + TypeGraphQL + MySQL 从入门到实战视频教程(35 个视频) NestJS 是 Nodejs 后端使用最多 ...

  2. 《Go语言从入门到实战》学习笔记(1)——Go语言学习路线图、简介

    非常有幸在<极客时间>上看到<Go语言从入门到实战>这门课程,本课程的作者给出了较为详细的学习路线图,具体如下: 学习路线图  学习目的 个人学习的目的主要是了解Go语言的基本 ...

  3. PyTorch深度学习入门与实战(案例视频精讲)

    作者:孙玉林,余本国 著 出版社:中国水利水电出版社 品牌:智博尚书 出版时间:2020-07-01 PyTorch深度学习入门与实战(案例视频精讲)

  4. 7-Python3从入门到实战—基础之数据类型(字典-Dictionary)

    Python从入门到实战系列--目录 字典的定义 字典是另一种可变容器模型,且可存储任意类型对象:使用键-值(key-value)存储,具有极快的查找速度: 字典的每个键值(key=>value ...

  5. 《Android 开发入门与实战(第二版)》——6.6节配置改变

    本节书摘来自异步社区<Android 开发入门与实战(第二版)>一书中的第6章,第6.6节配置改变,作者eoe移动开发者社区 组编 , 姚尚朗 , 靳岩,更多章节内容可以访问云栖社区&qu ...

  6. 【前端开发】HTML入门与实战

    [什么是HTML]: HTML: 超文本标记语言,标准通用标记语言下的一个应用. "超文本"就是指页面内可以包含图片.链接,甚至音乐.程序等非文字元素. HTML 是用来描述网页的 ...

  7. 5-Python3从入门到实战—基础之数据类型(列表-List)

    Python从入门到实战系列--目录 列表定义 list:列表(list)是Python内置的一种数据类型,list是一种有序的集合,索引从0开始,可以进行截取.组合等: //创建列表list1 = ...

  8. 《树莓派Python编程入门与实战》——3.5 关于Python交互式shell

    本节书摘来异步社区<树莓派Python编程入门与实战>一书中的第3章,第3.5节,作者:[美]Richard Blum,更多章节内容可以访问云栖社区"异步社区"公众号查 ...

  9. js模板字符串自定义类名_【Vue.js 入门到实战教程】07Vue 组件注册 | 基本使用和组件嵌套...

    来源 | https://xueyuanjun.com/post/21929除了前面介绍的基本语法之外,Vue.js 还支持通过组件构建复杂的功能模块,组件可以称得上是 Vue.js 的灵魂,是 Vu ...

  10. 《树莓派Python编程入门与实战(第2版)》——3.9 小结

    本节书摘来自异步社区<树莓派Python编程入门与实战(第2版)>一书中的第3章,第3.9节,作者[美] Richard Blum Christine Bresnahan,陈晓明 马立新 ...

最新文章

  1. 2018-4-5 丘成桐---现代几何学与计算机科学---自我总结
  2. gensim models.word2vec – Word2vec embeddings API
  3. 基于互联网云脑架构分析百度的现状与未来
  4. php函数substr_replace中文乱码的替代解决方法
  5. Centos上安装EPEL
  6. winform(MDI窗体容器、权限设置)
  7. [Windows]查看运行进程的参数【wmic】
  8. AI队列长度检测:计算区域中的人数
  9. 随想录(内存屏障示例代码)
  10. 计算机网络 职中,职中计算机网络基础期中考试试卷.pdf
  11. 数据分析——常见数据指标汇总
  12. 法拉科机器人接头_图解FANUC机器人I/O信号板接口定义与拆装
  13. Error: watch task has to be a function(optionally generated by using gulp.parallel or gulp. series)
  14. 图片数据增强的方法——收藏
  15. 近50年前500部精品电影合集
  16. mysql老司机之路
  17. Math.atan和Math.atan2函数
  18. 首期“赛迪区块链技术与应用培训班”成功举办
  19. css画表格多级表头,element UI实现动态生成多级表头
  20. python 表情包制作工具_python中tkinter模块制作表情包爬取工具遇到的问题

热门文章

  1. 在 Centos 安装 MySQL
  2. mbstring未安装
  3. Selenium 新手入门(C#)1. 用vs运行调用Selenium打开页面
  4. Android----Intent详解
  5. 专业的现场调音机架软件 - Deskew Technologies Gig Performer 4 Mac
  6. iReal Pro 2020 for Mac(模拟乐队音乐陪练工具)
  7. 天体物理科研作图格式-要求-plot_example.py
  8. 报错:The following signatures couldn‘t be verified because the public key is not available: NO_PUBKEY
  9. iOS底层探索之dyld(上):动态链接器流程分析
  10. Camtasia 2020软件的媒体库介绍