creeperyang / blog

前端博客,关注基础知识和性能优化。
MIT License
2.63k stars 211 forks source link

GraphQL 快速入门 #35

Open creeperyang opened 7 years ago

creeperyang commented 7 years ago

GraphQL 简介

GraphQL is a query language for your API, and a server-side runtime for executing queries by using a type system you define for your data. GraphQL isn't tied to any specific database or storage engine and is instead backed by your existing code and data.

GraphQL 是一种 API 查询语言,同时也是基于 自定义类型系统 执行查询的一个服务端运行时(runtime) 。GraphQL 本身不绑定任何数据库或存储引擎,而是可以在现有代码和数据上提供支持。

介绍概念后,一般应该紧跟介绍 GraphQL 的特性,跟前辈 Restful API 的比较等等。不过我觉得对初学者来说(包括我),这样的先后顺序并不友好,特别是对 GraphQL 这样全新的技术,以 demo 来快速了解怎么用,有个大概印象会更好。

所以下面紧接 GraphQL 的应用。

GraphQL 使用

所有示例基于 GraphQL 的 node.js 实现(graphql-js),请确保已安装以下依赖:

1. 最简示例

GraphQL 查询本质是客户端发送字符串到服务端,服务端解析后返回 JSON 给客户端。下面我们尝试构建最基本的 查询。

const { graphql, buildSchema } = require('graphql')

// 使用 GraphQL 的 schema 语言构造一个 schema
const schema = buildSchema(`
  type Query {
    hello: String
  }
`)

// root 为每个 API endpoint 提供 resolver 函数
const root = {
  hello() {
    return `Hello world!`
  }
}
// query
const query = `{ hello }`

graphql(schema, query).then(response => {
  console.log('No root:', response)
  // No root: { data: { hello: null } }
})
graphql(schema, query, root).then(response => {
  console.log('With root:', response)
  // With root: { data: { hello: 'Hello world!' } }
})

这个例子中没有引入客服端和服务端的区分,只是展示了怎么创建 Schema ,query 怎么被处理。下面一个例子结合 express 构建一个正常的 GraphQL 服务。

2. 集成 GraphQL 服务端

服务端使用 expressexpress-graphql 搭建 GraphQL 服务。

const express = require('express')
const graphqlHTTP = require('express-graphql')
const { buildSchema } = require('graphql')

// 创建 schema,需要注意到:
// 1. 感叹号 ! 代表 not-null
// 2. rollDice 接受参数
const schema = buildSchema(`
  type Query {
    quoteOfTheDay: String
    random: Float!
    rollThreeDice: [Int]
    rollDice(numDice: Int!, numSides: Int): [Int]
  }
`)

// The root provides a resolver function for each API endpoint
const root = {
  quoteOfTheDay: () => {
    return Math.random() < 0.5 ? 'Take it easy' : 'Salvation lies within'
  },
  random: () => {
    return Math.random()
  },
  rollThreeDice: () => {
    return [1, 2, 3].map(_ => 1 + Math.floor(Math.random() * 6))
  },
  rollDice: ({ numDice, numSides }) => {
    const output = []
    for (let i = 0; i < numDice; i++) {
      output.push(1 + Math.floor(Math.random() * (numSides || 6)))
    }
    return output
  }
}

const app = express()
app.use('/graphql', graphqlHTTP({
  schema: schema,
  rootValue: root,
  graphiql: true
}))
app.listen(4000)
console.log('Running a GraphQL API server at localhost:4000/graphql')

客户端代码:

const fetch = require('node-fetch')

fetch('http://localhost:4000/graphql', {
  method: 'POST',
  headers: {
    'Content-Type': 'application/json',
    'Accept': 'application/json'
  },
  body: JSON.stringify({
    query: `{
      random,
      quoteOfTheDay,
      rollThreeDice,
      rollDice(numDice: 9, numSides: 6)
    }`
  })
}).then(res => {
  return res.json()
}).then(data => {
  console.log(data)
  /**
   *
   { data:
     { random: 0.3099461279459266,
       quoteOfTheDay: 'Salvation lies within',
       rollThreeDice: [ 3, 1, 2 ],
       rollDice: [ 2, 6, 4, 1, 1, 5, 2, 1, 1 ] } }
   */
})

上面代码中我们展示了怎集成 express-graphql,前端怎么向后台发起查询等等。需要注意到

  1. 基本类型有:String, Int, Float, Boolean, ID 5种,我们试验了 String|Float|Int 等 3 种。其中 ID 对应 JavaScript 中的 Symbol

  2. 默认情况下,指定基本类型时返回 null 是合法的。但如果在后面加上感叹号则表示不允许为空,比如这里的 Float!

  3. 给基本类型加上方括号则表示为数组,如 [Int]

  4. 此外,查询时可以使用参数,参数用圆括号包裹,如 rollDice(numDice: 3, numSides: 6)

    当然,有时候我们可能不希望把参数直接写死,那么可以这样传参:

    body: JSON.stringify({
      query: `query RollDice($dice: Int!, $sides: Int) {
        rollDice(numDice: $dice, numSides: $sides)
      }`,
      variables: { dice: 9, sides: 6 }
    })
creeperyang commented 7 years ago

GraphQL 快速入门 在上面一段差不多可以结束了,一个对 GraphQL 没有概念的同学读后可以有基本的印象,也可以稍微写一些相关代码——即快速上手的目的达成。

但平心而论,希望扎实掌握还是需要补一补基础,在公司内部某个应用实践 GraphQL 之后,加上读了一些资料,下面还是补冲更多的基础知识。

用心学 GraphQL 之 Query

在 GraphQL 官网我们可以看到大大的标题:A query language for your API。GraphQL 首先是一个用于 API 的查询语言,同时也是服务端用于解析查询,返回数据的运行时。我们首先从查询语言这个角度入手。

Basic Query Syntax

一个最基本的查询:

// 请求
{
  user {
    name
  }
}

把这个查询从浏览器发给后端,那么后端 GraphQL 服务会返回:

// 返回
{
  data: {
    user: {
      name: "Lee Byron"
    }
  }
}

是不是非常简单直观(我们暂时不考虑后端实现)?

稍微加一点难度,假设后端有这样的数据:

[
  {id: 1, name: "Lee Byron"},
  {id: 2, name: "Sam"}
]

我们希望查询 id: 1 的用户的姓名,怎么来(怎么运用参数查询)?

query FetchUser {
  user(id: 1) {
    name
  }
}

是不是还是很简单?可能不是,这里引入了一些新概念,下面一一解释:

  1. query 是什么?是 operation,在 GraphQL 中有 query|mutation 两种操作。顾名思义,query 是查询,mutation 是插入/更新/删除操作。

  2. FetchUser 是 query 的名字,可任意取。好的名字可以提示其它开发这这个 query 是做什么的。

  3. user 是 field (字段),而 name 是 sub-field (子字段)。

  4. id: 1 是参数(argument on the user field)。参数是无所谓顺序的,(id: 1, name: "Mike")(name: "Mike", id: 1) 对 GraphQL 服务器来说是一样的。

  5. 以上所有称为一个 document (文档)。

当一个 query 没有 参数 或 directives 或 名字,那么关键字 query 可以省略,如我们开头所示。

Querying with Field Aliases & Fragments

现在我们更深入一点 Query。

字段别名(Field Aliases)

接着上面的例子:

// Request
query FetchUser {
  user(id: 1) {
    name
  }
}

// Response:
{
  data: {
    user: {
      name: "Lee Byron"
    }
  }
}

假如我们想一次查询获取多个用户呢?现在 data 已经有个 user 属性了,多个用户怎么办?

所以我们引入 别名:你可以给任意 field 一个有效的 name,然后数据就会匹配到这个 name 上。

// Request
query FetchLeeAndSam {
  lee: user(id: 1) {
    name
  }
  sam: user(id: 2) {
    name
  }
}

// Response:
{
  data: {
    lee: {
      name: "Lee Byron"
    },
    sam: {
      name: "Sam"
    }
  }
}

可以看到,原来的 user 被换成了各自的别名。

同时,这里提一下,query 中每个字段我们都可以用逗号来分隔,不过这是可选的,看个人爱好:

query {
  user {
    name,
    age
  }
}

片段(Fragments)

假设产品经理突然希望每个用户的 email 也要获取,我们可能要这样做:

// Request
query FetchLeeAndSam {
  lee: user(id: 1) {
    name
    email
  }
  sam: user(id: 2) {
    name
    email
  }
}

可以看到,我们在重复自己。假设我们又要添加属性呢?我们需要 fragment 来解决这个问题:

// Request
query FetchWithFragment {
  lee: user(id: 1) {
    ...UserFragment
  }
  sam: user(id: 2) {
    ...UserFragment
  }
}

fragment UserFragment on User {
  name
  email
}

// Response:
{
  data: {
    lee: {
      name: "Lee Byron",
      email: lee@example.com
    },
    sam: {
      name: "Sam",
      email: sam@example.com
    }
  }
}
  1. ... 是 spread 操作符,跟 ES6 语法类似。
  2. on User表明 UserFragment 只能运用在 User 类型上。

Inline Fragment

Inline Fragment 用于根据运行时的类型有条件地得到类型。

query inlineFragmentTyping {
  shows(titles: ["The Matrix", "Mr. Robot"]) {
    title
    ... on Movie {
      sequel {
        name
        date
      }
    }
    ... on Tvshow {
      episode {
        name
        date
      }
    }
  }
}
  1. "The Matrix" 是 Movie 类型,所以会得到 sequel 数据;
  2. "Mr. Robot" 是 Tvshow 类型,所以会得到 episode 数据。

Querying with Directives

如果我们需要一种方法去动态更改 query 的结构和形状,比如 include/skip 某个字段,那么指令(Directives)就是我们所需要的。

现在 GraphQL 提供 @include@skip 两种指令(之后可能会更多)。

如果两个指令在某个字段上都出现,那么只有在 (skip: false) && (include: true) 时才包括这个字段。

query myQuery($someTest: Boolean) {
  experimentalField @include(if: $someTest)
}
dengnan123 commented 6 years ago

学习了