GraphQL 从入门到实践

《GraphQL 从入门到实践》

本文起首引见了 GraphQL,再经由历程 MongoDB + graphql + graph-pack 的组合实战运用 GraphQL,细致论述怎样运用 GraphQL 来举行增编削查和数据定阅推送,并附有运用示例,边用边学印象深入~

假如愿望将 GraphQL 运用到前后端星散的临盆环境,请期待后续文章。

本文实例代码:Github

0. 什么是 GraphQL

GraphQL 是一种面向数据的 API 查询作风。

传统的 API 拿到的是前后端商定好的数据花样,GraphQL 对 API 中的数据供应了一套易于明白的完全形貌,客户端能够准确地取得它须要的数据,没有任何冗余,也让 API 更轻易地跟着时候推移而演进,还能用于构建壮大的开辟者东西。

1. 概述

前端的开辟跟着 SPA 框架周全提高,组件化开辟也随之成为大势所趋,各个组件离别治理着各自的状况,组件化给前端仔带来轻易的同时也带来了一些懊恼。比方,组件须要担任把异步要求的状况分发给子组件或关照给父组件,这个历程当中,由组件间通讯带来的构造庞杂度、来源不明的数据源、不知从何定阅的数据相应会使得数据流变得乱七八糟,也使得代码可读性变差,以及可保护性的下降,为今后项目的迭代带来极大难题。

试想一下你都开辟完了,产物通知你要大改一番,从接口到组件构造都得改,后端也骂骂咧咧不肯合营让你从好几个 API 里取数据自身组合,这酸爽 😅

在一些产物链庞杂的场景,后端须要供应对应 WebApp、WebPC、APP、小顺序、快运用等各端 API,此时 API 的粒度大小就显得分外重要,粗粒度会致使挪动端不必要的流量消耗,细粒度则会形成函数爆炸 (Function Explosion);在此情形下 Facebook 的工程师于 2015 年开源了 GraphQL 范例,让前端自身形貌自身愿望的数据情势,效劳端则返回前端所形貌的数据构造。

简朴运用能够参照下面这个图:

《GraphQL 从入门到实践》

比方前端愿望返回一个 ID 为 233 的用户的称号和性别,并查找这个用户的前十个雇员的名字和 Email,再找到这个人父亲的电话,和这个父亲的狗的名字(别问我为什么有这么新鲜的查找 🤪),那末我们能够经由历程 GraphQL 的一次 query 拿到悉数信息,无需从好几个异步 API 内里往返找:

query {
  user (id : "233") {
    name
    gender
    employee (first: 10) {
      name
      email
    }
    father {
      telephone
      dog {
          name
      }
    }
  }
}

返回的数据花样则刚好是前端供应的数据花样,不多不少,是否是心动了 😏

2. 几个重要观点

这里先引见几个对明白 GraphQL 比较重要的观点,其他类似于指令、团结范例、内联片断等更庞杂的用法,参考 GraphQL 官网文档 ~

2.1 操纵范例 Operation Type

GraphQL 的操纵范例能够是 querymutationsubscription,形貌客户端愿望举行什么样的操纵

  1. query 查询:猎取数据,比方查找,CRUD 中的 R
  2. mutation 变动:对数据举行变动,比方增添、删除、修正,CRUD 中的 CUD
  3. substription 定阅:当数据发作变动,举行音讯推送

这些操纵范例都将在后文现实用到,比方这里举行一个查询操纵

query {
  user { id }
}

2.2 对象范例和标量范例 Object Type & Scalar Type

假如一个 GraphQL 效劳接收到了一个 query,那末这个 query 将从 Root Query 最先查找,找到对象范例(Object Type)时则运用它的剖析函数 Resolver 来猎取内容,假如返回的是对象范例则继承运用剖析函数猎取内容,假如返回的是标量范例(Scalar Type)则完毕猎取,直到找到末了一个标量范例。

  1. 对象范例:用户在 schema 中定义的 type
  2. 标量范例:GraphQL 中内置有一些标量范例 StringIntFloatBooleanID,用户也能够定义自身的标量范例

比方在 Schema 中声明

type User {
  name: String!
  age: Int
}

这个 User 对象范例有两个字段,name 字段是一个为 String 的非空标量,age 字段为一个 Int 的可空标量。

2.3 形式 Schema

假如你用过 MongoOSE,那你应当对 Schema 这个观点很熟悉,翻译过来是『形式』。

它定义了字段的范例、数据的构造,形貌了接口数据要求的划定规矩,当我们举行一些毛病的查询的时刻 GraphQL 引擎会担任通知我们那里有题目,和细致的毛病信息,对开辟调试非常友爱。

Schema 运用一个简朴的强范例形式语法,称为形式形貌言语(Schema Definition Language, SDL),我们能够用一个实在的例子来展现一下一个实在的 Schema 文件是怎样用 SDL 编写的:

# src/schema.graphql

# Query 进口
type Query {
    hello: String
    users: [User]!
    user(id: String): [User]!
}

# Mutation 进口
type Mutation {
    createUser(id: ID!, name: String!, email: String!, age: Int,gender: Gender): User!
    updateUser(id: ID!, name: String, email: String, age: Int, gender: Gender): User!
    deleteUser(id: ID!): User
}

# Subscription 进口
type Subscription {
    subsUser(id: ID!): User
}

type User implements UserInterface {
    id: ID!
    name: String!
    age: Int
    gender: Gender
    email: String!
}

# 罗列范例
enum Gender {
    MAN
    WOMAN
}

# 接口范例
interface UserInterface {
    id: ID!
    name: String!
    age: Int
    gender: Gender
}

这个简朴的 Schema 文件从 Query、Mutation、Subscription 进口最先定义了各个对象范例或标量范例,这些字段的范例也多是其他的对象范例或标量范例,构成一个树形的构造,而用户在向效劳端发送要求的时刻,沿着这个树挑选一个或多个分支就能够猎取多组信息。

注重:在 Query 查询字段时,是并行实行的,而在 Mutation 变动的时刻,是线性实行,一个接着一个,防备同时变动带来的竞态题目,比方说我们在一个要求中发送了两个 Mutation,那末前一个将一直在后一个之前实行。

2.4 剖析函数 Resolver

前端要求信息抵达后端以后,须要由剖析函数 Resolver 来供应数据,比方如许一个 Query:

query {
  hello
}

那末同名的剖析函数应当是如许的

Query: {
  hello (parent, args, context, info) {
    return ...
  }
}

剖析函数接收四个参数,离别为

  1. parent:当前上一个剖析函数的返回值
  2. args:查询中传入的参数
  3. context:供应给一切剖析器的上下文信息
  4. info:一个保留与当前查询相干的字段特定信息以及 schema 细致信息的值

剖析函数的返回值能够是一个详细的值,也能够是 Promise 或 Promise 数组。

一些经常使用的解决方案如 Apollo 能够帮省略一些简朴的剖析函数,比方一个字段没有供应对应的剖析函数时,会从上层返回对象中读取和返回与这个字段同名的属性。

2.5 要求花样

GraphQL 最常见的是经由历程 HTTP 来发送要求,那末怎样经由历程 HTTP 来举行 GraphQL 通讯呢

举个栗子,怎样经由历程 Get/Post 体式格局来实行下面的 GraphQL 查询呢

query {
  me {
    name
  }
}

Get 是将要求内容放在 URL 中,Post 是在 content-type: application/json 情况下,将 JSON 花样的内容放在要求体里

# Get 体式格局
http://myapi/graphql?query={me{name}}

# Post 体式格局的要求体
{
  "query": "...",
  "operationName": "...",
  "variables": { "myVariable": "someValue", ... }
}

返回的花样平常也是 JSON 体

# 准确返回
{
  "data": { ... }
}

# 实行时发作毛病
{
  "errors": [ ... ]
}

假如实行时发作毛病,则 errors 数组里有细致的毛病信息,比方毛病信息、毛病位置、抛错现场的挪用客栈等信息,轻易举行定位。

3. 实战

这里运用 MongoDB + graph-pack 举行一下简朴的实战,并在实战中一同进修一下,细致代码拜见 Github ~

MongoDB 是一个运用的比较多的 NoSQL,能够轻易的在社区找到许多现成的解决方案,报错了也轻易找到解决要领。

graph-pack 是集成了 Webpack + Express + Prisma + Babel + Apollo-server + Websocket 的支撑热更新的零设置 GraphQL 效劳环境,这里将其用来演示 GraphQL 的运用。

3.1 环境布置

起首我们把 MongoDB 启起来,这个历程就不赘述了,网上许多教程;

搭一下 graph-pack 的环境

npm i -S graphpack

package.jsonscripts 字段加上:

"scripts": {
    "dev": "graphpack",
    "build": "graphpack build"
}

建立文件构造:

.
├── src
│   ├── db                    // 数据库操纵相干
│   │   ├── connect.js        // 数据库操纵封装
│   │   ├── index.js        // DAO 层
│   │   └── setting.js        // 设置
│   ├── resolvers            // resolvers
│   │   └── index.js
│   └── schema.graphql        // schema
└── package.json

这里的 schema.graphql 是 2.3 节的示例代码,其他完成拜见 Github,重要关注 src/dbsrc/resolverssrc/schema.graphql 这三个处所

  1. src/db:数据库操纵层,包含 DAO 层和 Service 层(假如对分层不太相识能够看一下末了一章)
  2. src/resolvers:Resolver 剖析函数层,给 GraphQL 的 Query、Mutation、Subscription 要求供应 resolver 剖析函数
  3. src/schema.graphql:Schema 层

然后 npm run dev ,浏览器翻开 http://localhost:4000/ 就能够运用 GraphQL Playground 最先调试了,左侧是要求信息栏,左下是要求参数栏和要求头设置栏,右侧是返回参数栏,细致用法能够参考 Prisma 文档

《GraphQL 从入门到实践》

3.2 Query

起首我们来尝尝 hello world,我们在 schema.graphql 中写上 Query 的一个进口 hello,它接收 String 范例的返回值

# src/schema.graphql

# Query 进口
type Query {
    hello: String
}

src/resolvers/index.js 中补充对应的 Resolver,这个 Resolver 比较简朴,直接返回的 String

// src/resolvers/index.js

export default {
    Query: {
        hello: () => 'Hello world!'
    }
}

我们在 Playground 中举行 Query

# 要求
query {
  hello
}

# 返回值
{
  "data": {
    "hello": "Hello world!"
  }
}

Hello world 老是云云兴奋,下面我们来举行轻微庞杂一点的查询

查询进口 users 查找一切用户列表,返回一个不可空但长度能够为 0 的数组,数组中假如有元素,则必需为 User 范例;另一个查询进口 user 接收一个字符串,查找 ID 为这个字符串的用户,并返回一个 User 范例的可空字段

# src/schema.graphql

# Query 进口
type Query {
    user(id: String): User
    users: [User]!
}

type User {
    id: ID!
    name: String!
    age: Int
    email: String!
}

增添对应的 Resolver

// src/resolvers/index.js

import Db from '../db'

export default {
    Query: {
        user: (parent, { id }) => Db.user({ id }),
        users: (parent, args) => Db.users({})
    }
}

这里的两个要领 Db.userDb.users 离别是查找对应数据的函数,返回的是 Promise,假如这个 Promise 被 resolve,那末传给 resolve 的数据将被作为效果返回。

然后举行一次查询就能够查找我们所愿望的一切信息

# 要求
query {
  user(id: "2") {
    id
    name
    email
    age
  }
  users {
    id
    name
  }
}

# 返回值
{
  "data": {
    "user": {
      "id": "2",
      "name": "李四",
      "email": "mmmmm@qq.com",
      "age": 18
    },
    "users": [{
        "id": "1",
        "name": "张三"
      },{
        "id": "2",
        "name": "李四"
      }]
  }
}

注重这里,返回的数组只愿望拿到 idname 这两个字段,因而 GraphQL 并没有返回过剩的数据,怎样,是否是很知心呢

3.3 Mutation

晓得怎样查询数据,还得相识增添、删除、修正,毕竟这是 CRUD 工程师必备的几板斧,不过这里只引见比较庞杂的修正,别的两个要领能够看一下 Github 上。

# src/schema.graphql

# Mutation 进口
type Mutation {
    updateUser(id: ID!, name: String, email: String, age: Int): User!
}

type User {
    id: ID!
    name: String!
    age: Int
    email: String!
}

同理,Mutation 也须要 Resolver 来处置惩罚要求

// src/resolvers/index.js

import Db from '../db'

export default {
    Mutation: {
        updateUser: (parent, { id, name, email, age }) => Db.user({ id })
            .then(existUser => {
                if (!existUser)
                    throw new Error('没有这个id的人')
                return existUser
            })
            .then(() => Db.updateUser({ id, name, email, age }))
    }
}

Mutation 进口 updateUser 拿到参数以后起首举行一次用户查询,假如没找到则抛错,这个错将作为 error 信息返回给用户,Db.updateUser 这个函数返回的也是 Promise,不过是将转变以后的信息返回

# 要求
mutation UpdataUser ($id: ID!, $name: String!, $email: String!, $age: Int) {
  updateUser(id: $id, name: $name, email: $email, age: $age) {
    id
    name
    age
  }
}

# 参数
{"id": "2", "name": "王五", "email": "xxxx@qq.com", "age": 19}

# 返回值
{
  "data": {
    "updateUser": {
      "id": "2",
      "name": "王五",
      "age": 19
    }
  }
}

如许完成了对数据的变动,且拿到了变动后的数据,并给定愿望的字段。

### 3.4 Subscription

GraphQL 另有一个有意思的处所就是它能够举行数据定阅,当前端提议定阅要求以后,假如后端发明数据转变,能够给前端推送及时信息,我们用一下看看。

按例,在 Schema 中定义 Subscription 的进口

# src/schema.graphql

# Subscription 进口
type Subscription {
    subsUser(id: ID!): User
}

type User {
    id: ID!
    name: String!
    age: Int
    email: String!
}

补充上它的 Resolver

// src/resolvers/index.js

import Db from '../db'

const { PubSub, withFilter } = require('apollo-server')
const pubsub = new PubSub()
const USER_UPDATE_CHANNEL = 'USER_UPDATE'

export default {
    Mutation: {
        updateUser: (parent, { id, name, email, age }) => Db.user({ id })
            .then(existUser => {
                if (!existUser)
                    throw new Error('没有这个id的人')
                return existUser
            })
            .then(() => Db.updateUser({ id, name, email, age }))
            .then(user => {
                pubsub.publish(USER_UPDATE_CHANNEL, { subsUser: user })
                return user
            })
    },
    Subscription: {
        subsUser: {
            subscribe: withFilter(
                (parent, { id }) => pubsub.asyncIterator(USER_UPDATE_CHANNEL),
                (payload, variables) => payload.subsUser.id === variables.id
            ),
            resolve: (payload, variables) => {
                console.log('🚢 接收到数据: ', payload)
            }
        }
    }
}

这里的 pubsub 是 apollo-server 里担任定阅和宣布的类,它在接收定阅时供应一个异步迭代器,在后端以为须要宣布定阅的时刻向前端宣布 payload。withFilter 的作用是过滤掉不须要的定阅音讯,细致用法参照定阅过滤器

起首我们宣布一个定阅要求

# 要求
subscription subsUser($id: ID!) {
  subsUser(id: $id) {
    id
    name
    age
    email
  }
}

# 参数
{ "id": "2" }

我们用方才的数据更新操纵来举行一次数据的变动,然后我们将猎取到并打印出 pubsub.publish 宣布的 payload,如许就完成了数据定阅。

在 graph-pack 中数据推送是基于 websocket 来完成的,能够在通讯的时刻翻开 Chrome DevTools 看一下。

4. 总结

如今前后端的构造也许如下图。后端经由历程 DAO 层与数据库衔接完成数据耐久化,效劳于处置惩罚营业逻辑的 Service 层,Controller 层接收 API 要求挪用 Service 层处置惩罚并返回;前端经由历程浏览器 URL 举行路由掷中猎取目的视图状况,而页面视图是由组件嵌套构成,每一个组件保护着各自的组件级状况,一些轻微庞杂的运用还会运用集中式状况治理的东西,比方 Vuex、Redux、Mobx 等。前后端只经由历程 API 来交换,这也是如今前后端星散开辟的基本。

《GraphQL 从入门到实践》

假如运用 GraphQL,那末后端将不再产出 API,而是将 Controller 层保护为 Resolver,和前端商定一套 Schema,这个 Schema 将用来天生接口文档,前端直接经由历程 Schema 或天生的接口文档来举行自身希冀的要求。

经由几年一线开辟者的填坑,已经有一些不错的东西链能够运用于开辟与临盆,许多言语也供应了对 GraphQL 的支撑,比方 JavaScript/Nodejs、Java、PHP、Ruby、Python、Go、C# 等。

一些比较著名的公司比方 Twitter、IBM、Coursera、Airbnb、Facebook、Github、携程等,内部或外部 API 从 RESTful 转为了 GraphQL 作风,特别是 Github,它的 v4 版外部 API 只运用 GraphQL。据一位在 Twitter 事情的大佬说硅谷不少一线二线的公司都在想办法转到 GraphQL 上,然则同时也说了 GraphQL 还须要时候生长,由于将它运用到临盆环境须要前后端大批的重构,这无疑须要高层的推进和决计。

正如尤雨溪所说,为什么 GraphQL 两三年前没有普遍运用起来呢,能够有下面两个缘由:

  1. GraphQL 的 field resolve 假如根据 naive 的体式格局来写,每一个 field 都对数据库直接跑一个 query,会发生大批冗余 query,虽然收集层面的要求数被优化了,但数据库查询能够会成为机能瓶颈,这内里有很大的优化空间,但并非那末轻易做。FB 自身没有这个题目,由于他们内部数据库这一层也是笼统掉的,写 GraphQL 接口的人不须要挂念 query 优化的题目。
  2. GraphQL 的利好重如果在于前端的开辟效力,但落地却须要效劳端的尽力合营。假如是小公司或许全部公司都是全栈,那能够能够做,但在许多前后端分工比较明白的团队里,要推进 GraphQL 照样会碰到种种合作上的阻力。

约莫能够归纳综合为机能瓶颈和团队分工的缘由,愿望跟着社区的生长,基本设施的完美,会逐渐有完美的解决方案提出,让宽大前后端开辟者们能够早日用上此利器。

网上的帖子大多深浅不一,以至有些前后矛盾,鄙人的文章都是进修历程当中的总结,假如发明毛病,迎接留言指出~

参考:

  1. GraphQL | 一种为你的 API 而生的查询言语
  2. JSON-RPC 2.0 范例 – wiki . leozvc
  3. GraphQL 为什么没有火起来? – 尤雨溪的回复 – 知乎
  4. GraphQL什么鬼 | kazaff’s blog

PS:迎接人人关注我的民众号【前端下午茶】,一同加油吧~

《GraphQL 从入门到实践》

别的能够到场「前端下午茶交换群」微信群,长按辨认下面二维码即可加我挚友,备注加群,我拉你入群~

《GraphQL 从入门到实践》

    原文作者:SHERlocked93
    原文地址: https://segmentfault.com/a/1190000018479539
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞