GraphQL从入门到实战

《GraphQL从入门到实战》

媒介

原本这篇文章预备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 {
      id
      name
      createDate
    }
  }
}

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: String
  fieldB: Boolean
  fieldC: Int
  fieldD: 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") {
    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
}

剖析器 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: 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

什么是ApolloServer?

《GraphQL从入门到实战》

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)

构建Schema

从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 })

衔接数据源

《GraphQL从入门到实战》

我们在构建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
  }
})

编写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({
  // Schema
  typeDefs,
  // 剖析器
  resolvers,
  // 增加数据源
  dataSources: () => {
    UserAPI: new UserAPI()
  }
})

好了到了现在为止,graphql这一层我们基础完美了,我们的graphql层终究会在数据源中挪用REST API接口。接下来的操纵就是传统的MVC的那一套。置信熟习Koa或许Express的小伙伴肯定都很熟习。假如有不熟习的小伙伴,能够参阅源码中routes文件夹以及controller文件夹。下面一个要求的流程图。

《GraphQL从入门到实战》

其他

关于鉴权

关于鉴权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 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')
    }
  }
}
    原文作者:张越
    原文地址: https://segmentfault.com/a/1190000019425030
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞