Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

GraphQL 实践(一): GraphQL 入门 #51

Open
nodejh opened this issue Nov 22, 2019 · 0 comments
Open

GraphQL 实践(一): GraphQL 入门 #51

nodejh opened this issue Nov 22, 2019 · 0 comments
Labels

Comments

@nodejh
Copy link
Owner

nodejh commented Nov 22, 2019

GraphQL 实践系列文章包含以下几篇文章,将在近期更新:

前言

作为前端,你一定很痛恨接口文档写的很差,你一定很抱怨接口字段冗余,你一定很讨厌要请求多个接口才能得到想要的数据。作为后端,你一定很痛恨写接口文档,你一定很会抱怨为什么前端不自己去取想要的值,你一定讨厌前端对你的接口指指点点。

如何解决前后端的分歧?如何提高开发效率?那就是 GraphQL。

GraphQL 是一个 API 查询语言,跟 RESTful API 是同一类的技术。换句话说,GraphQL 是一个可以替代 RESTful API 的产品。

接下来本文就详细介绍一下基于 Node.js 的 GraphQL 实践。

相关技术栈:

GraphQL 简介

相信大家都已经了解,Egg.js 应用一般都是是经典的 MVC 模式:

  • router (路由)
  • controller
  • service
  • public (view)

我们一般会在 router 中定义接口,也就是 RESTful API 中的路由,如 router.get('/api/user', controller.user.index),然后在 controller 实现控制器的逻辑,一般是参数校验、权限控制、调用 service 以及返回结果。service 中则是业务的具体实现,比如操作数据库等。

而当我们使用 GraphQL 的时候,替代的则是 routercontroller。因为 GraphQL 已经帮我们实现了路由和参数校验,并且我们可以在 GraphQL 中进行服务(service)的调用。

那么 GraphQL 是怎么实现的呢?这就需要引出 GraphQL 的两个概念:schemaresolver

Schema

Schema 是 GraphQL 中对数据的描述,与 TypeScript 中的类型定义有点类似。举个例子:

enum Gender {
  MALE
  FEMALE
  NONE
}

type User {
  name: String!
  gender: Gender!
  tags: [String!]!
}

如上所示,Gender 是一个枚举类型,它的值是 MALE、FEMALE 和 NONE 之一。

User 是一个 GraphQL 对象类型,拥有 nametags 两个字段。其中 name 的类型是 String!! 表示这个字段是非空的。[String!]! 表示一个 String 数组,这个数组内元素非空,tags 也非空。

schema 中有两个特殊的类型:查询(query)和变更类型(mutation)。每一个 GraphQL 服务都有一个 query 类型,可能有一个 mutation 类型。query 用于查询数据,mutation 用于创建或更新数据。这两个类型和常规对象类型一样,但是它们之所以特殊,是因为它们定义了每一个 GraphQL 查询的入口,相当于 RESTful 中的路由。

假设我现在需要两个接口,分别是查询所有用户信息和根据 name 查询对应用户信息,则 schema 中需要一个 Query 类型,且其上有 users 和 user 字段:

type Query {
  "查询所有用户列表"
  users: [User!]!
  "根据 name 查询对应的用户信息"
  user(name: String!): User
}

从 schema 中可以看出, GraphQL 是强类型的,其类型主要有对象类型、枚举类型、标量类型、接口、联合和输入类型。

标量类型是 GraphQL 查询的叶子节点,对应的字段没有任何次级字段。GraphQL 的默认标量类型有:

  • Int:有符号 32 位整数。
  • Float:有符号双精度浮点值。
  • String:UTF‐8 字符序列。
    Boolean:true 或者 false。
  • ID:ID 标量类型表示一个唯一标识符,通常用以重新获取对象或者作为缓存中的键。ID 类型使用和 String 一样的方式序列化;然而将其定义为 ID 意味着并不需要人类可读型。如 MongoDB 的 id 字段。

我们也可以通过 scalar 字段自定义其他标量,如 scalar Date

关于 GraphQL 类型详细的说明可以参考文档:Schema 和类型

Resolver

定义好了 schema 之后,就需要实现 schema 中的 query 或 mutation。在 GraphQL 中,每个类型的每个字段都由一个 resolver 函数支持,当一个字段被执行时,相应的 resolver 被调用以产生下一个值。

简单来说,就是 schema 中的每个字段,都需要对应一个 resolver 函数,也就相当于 RESTful 中的 controller,resolver 函数是对 GraphQL 入口的实现。

以前面的 Schema 为例,我们需要定义 Query 中 users 和 user 两个字段的 resolver 函数:

const resolvers = {
  Query: {
    users: () => [{ name: "Jack", gender: "MALE", tags: ["Alibaba"] }],
    user: (parent, args, context, info) => {
      const { name } = args;
      // find user by name...
      return { name, gender: "MALE", tags: ["Alibaba"] }
    },
  },
};

resolver 函数也支持 promise,如:

const resolvers = {
  Query: {
    users: async (parent, info, context) => {
      return await context.service.user.findAll();
    }
  },
};

实例

为了更直观看到效果,我们可以通过 apollo-server 将前面写的 schema 和 resolvers 都启动起来:

$ mkdir server
$ npm init -y
$ npm install apollo-server --save
// server/index.js
const { ApolloServer, gql } = require('apollo-server');

// The GraphQL schema
const typeDefs = gql`
  enum Gender {
    MALE
    FEMALE
    NONE
  }

  type User {
    name: String!
    gender: Gender!
    tags: [String!]!
  }

  type Query {
    "查询所有用户列表"
    users: [User!]!
    "根据 name 查询对应的用户信息"
    user(name: String!): User
  }
`;

// A map of functions which return data for the schema.
const resolvers = {
  Query: {
    users: () => [{ name: "Jack", gender: "MALE", tags: ["Alibaba"] }, { name: 'Joe', gender: 'MALE', tags: [] }],
    user: (parent, args, context, info) => {
      const { name } = args;
      // find user by name...
      return { name, gender: "MALE", tags: [name] }
    },
  },
};

const server = new ApolloServer({
  typeDefs,
  resolvers,
});

server.listen().then(({ url }) => {
  console.log(`🚀 Server ready at ${url}`);
});

通过 node index.js 启动之后在浏览器中打开对应的 URL(默认是 http://localhost:4000),就可以看到一个强大的 GraphQL 开发者工具(playground),我们可以在左侧查询语句查询,执行后右侧就会显示对应的数据。

egg-graphql-plugin.gif

查询 query

请求你所要的数据,不多不少

在 RESTful API 中,我们一般是通过接口查询数据,如查询所有用户列表可能是通过 HTTP GET 方法请求 /api/users 接口;而在 GraphQL 中,没有了路由的概念,取而代之的是入口,类似的,我们可以通过 GraphQL 的查询语句对 GraphQL Schema 中的入口进行查询。

查询条件:

query {
  users {
    name
  }
}

该查询的含义是,查询 users,并且返回 name 字段。查询结果如下:

{
  "data": {
    "users": [
      {
        "name": "Jack"
      },
      {
        "name": "Joe"
      }
    ]
  }
}

如果我们还需要得到 gendertags,则可以这样写:

query {
  users {
    name
    gender
    tags
  }
}

查询结果:

{
  "data": {
    "users": [
      {
        "name": "Jack",
        "gender": "MALE",
        "tags": [
          "Alibaba"
        ]
      },
      {
        "name": "Joe",
        "gender": "MALE",
        "tags": []
      }
    ]
  }
}

对比 RESTful API,GraphQL 的查询语句多了对返回字段描述。我们需要什么字段,则查询什么字段,这就解决了字段冗余的问题。同时 GraphQL 查询也总能得到可预测的结果,查询结果的字段一定是与查询条件一一对应的。

获取多个资源,只用一个请求

当然,GraphQL 的能力不止于此。

设想另一个场景:查询所有用户名列表,并且返回用户名为 "Jack" 的详细信息。

如果使用 RESTful API,我们可能需要发起两个 HTTP 请求分别查询 /api/users/api/user?name=Jack 接口。但使用 GraphQL,我们只需要定义一个查询条件即可:

query GetUser {
  users {
    name
  }
  user(name: "Jack") {
    name
    gender
    tags
  }
}

在这个查询中,我们查询了两个入口:

  • 查询 users,返回 name 字段
  • 以 { "name": "Jack" } 为参数查询user,返回 namegendertags 字段

通过浏览器开发者工具查看执行查询条件后的请求,可以发现只发送了一个请求,参数分别为 operationName queryvariables

  • operationName 是我们定义的操作名称,也就是 GetUser,可以省略
  • query 是查询条件
  • variables 是变量,上面的查询中暂时没有使用到变量,所以现在是空对象

nodejs-graphql-query.png

GraphQL 服务器接收到这个 HTTP 请求后,就会根据查询条件对 Schema 进行解析,也就是根据查询的字段,执行 Schema 中对应字段的 resolver 函数。

这里需要注意的是,查询(query)字段时,是并行执行,而变更(mutation)字段时,是线性执行,一个接着一个。

这也是 GraphQL 的强大之处,我们只需要写好 schema 以及 resolver,GraphQL 会自动根据查询语句帮我们实现服务的编排。这也解决了前后端协作中的另一个问题:前端需要聚合多个接口才能获取想要的数据。

变更 mutation

前面基本都在说数据的获取,但是任何完整的数据平台也都需要一个改变服务端数据的方法。

在 RESTful API 中,任何请求都可能最后导致一些服务端副作用,但是约定上建议不要使用 GET 请求来修改数据。GraphQL 也是类似,技术上而言,任何查询都可以被实现为导致数据写入,但 GraphQL 建立了一个规范,任何修改数据的操作都应该使用 mutation 来发送。

比如创建一个用户,首先 schema 应该使用 Mutation 来定义:

input UserInput {
  name: String!
  gender: Gender!
}

type Mutation {
  createUser(user: UserInput!): User!
  createUserTag(tag: String!): User!
}

input 表示输入对象,看上去和普通对象一摸一样,除了关键字是 input 而不是 type。它的特别之处在于,输入对象可以用在复杂的参数中,经常是 mutation 的参数,比如上面 createUser 的参数。

定义了 Mutation 之后,同样需要定义对应的 resolver 函数:

const resolvers = {
  Query: {
    // ...
  },
  Mutation: {
    createUser: (parent, args, context, info) => {
      const { user: { name, gender } } = args;
      // insert user to db...
      return { name, gender, tags: [] };
    },
    createUserTag: (parent, args, context, info) => {
      const { tag } = args;
      return { name: "Jack", gender: "MALE", tags: [ tag ] }
    },
  },
};

于是我们就可以像下面这样来请求创建用户的 GraphQL 接口了:

mutation CreateUser {
  createUser(user: { "name": "Jack", "gender": "MALE" }) {
    name
    gender
  }
}

或者使用变量:

mutation CreateUser($user: UserInput!) {
  createUser(user: $user) {
    name
    gender
  }
}

在开发者工具中,可以在左下角的 QUERY VARIABLES 面板中添加变量:

nodejs-graphql-query.png

关于查询和变更的更多内容,可以参考 GraphQL 的文档:查询和变更

前端开发

到此为止,我们已经使用 GraphQL 构建了一个简单的服务端及接口,并且在 playground 中进行了查询和变更。那么如何在前端中使用 GraphQL 接口呢?

前面我们已经知道 GraphQL 每一次执行,其实是向服务端发起的一个 HTTP 请求。但如果每次我们都自己手动去构建请求参数还是挺麻烦的,好在有很多开源的 GraphQL Client 简化了我们的工作,比较推荐的依旧是 Apollo Client,它支持 React/Vue/Angular 等多种框架。

初始化项目

以 React.js 为例,首先初始化一个项目:

$ npx create-react-app client

然后安装 GraphQL Client 相关依赖包:

$ cd client
$ yarn add apollo-boost @apollo/react-hooks graphql

由于 create-react-app 默认使用的 yarn,所以我们在前端使用 yarn 安装依赖包。当然也可以使用 npm:npm install apollo-boost @apollo/react-hooks graphql --save。

创建 Client

在安装完依赖包之后,就可以创建一个 GraphQL 的 Client,用来向 GraphQL 服务端请求数据。

参照下述代码修改 src/index.js, 其中 uri 就是我们 GraphQL 服务端的地址,ApolloProvider 中存储了所有 GraphQL 数据,所以建议将其作为应用的根组件。

// ...
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from '@apollo/react-hooks';
// ...

const client = new ApolloClient({
  uri: 'http://localhost:4000',
});

const Root = () => (
  <ApolloProvider client={client}>
    <App />
  </ApolloProvider>
);

ReactDOM.render(<Root />, document.getElementById('root'));

获取数据

通常对于一个 React 项目,我们需要用 Redux 来获取数据,需要写各种 actions、dispatch,非常繁琐。但现在使用 GraphQL,一切都变得非常简单!

@apollo/react-hooks 提供了 useQueryuseMutation 等 hooks 可以让我们非常方便地获取数据。

import React from 'react';
import gql from 'graphql-tag';
import { useQuery } from '@apollo/react-hooks';

// 定义查询语句
const GET_USERS = gql`
query {
  users {
    name
  }
}
`;

function Users() {
  // 使用 useQuery hook 获取数据
  const { loading, error, data } = useQuery(GET_USERS);

  if (loading) return 'Loading...';
  if (error) return `Error! ${error.message}`;

  return (
    <div>
      {data.users.map(user => <p>{user.name}</p>)}
    </div>
  );
}

export default Users;

首先我们定义了一个查询语句,就跟在 playground 中定义的一样,只不过这里需要将其放在 gql 中。然后使用 useQuery 获取数据,useQuery 返回 loading error data 等属性,我们就可以很方便地处理页面的加载状态和接口失败情况。

在前面也提到了,GraphQL 的查询是可预测的,根据查询条件,我们也可以清楚知道,data 属性一定会有一个 users 字段,且每个 user 有且只有 name 字段。

需要注意的是,如果在服务端的 GraphQL Schema 中,name 允许为空,则 user.name 则可能为 null;如果 name 不允许为空,则当服务端 user.name 为 null 的时候,服务端就会报错。

通过 yarn start 启动前端,然后在浏览器中就可以看到如下的页面:

获取数据

带参数的查询

带参数的查询也非常简单,我们只需要在查询语句中定义参数变量名称,并在 useQueryvariables 中把变量传递进去。

在下面的代码中,GET_USER 这个查询的变量名称是 String 类型的 $userName,且不能为空。通过 variables 传递变量值的时候,我们只需要使用 userName 即可。

import React from 'react';
import gql from 'graphql-tag';
import { useQuery } from '@apollo/react-hooks';

// 定义查询语句
const GET_USER = gql`
query GET_USER($userName: String!) {
  user(name: $userName) {
    name
    gender
    tags
  }
}
`;

function User() {
  // 使用 useQuery hook 获取数据
  const { loading, error, data } = useQuery(GET_USER, {
    variables: {
      userName: 'Jack',
    }
  });

  if (loading) return 'Loading...';
  if (error) return `Error! ${error.message}`;

  return (
    <div>
      <p>Name: {data.user.name}</p>
      <p>Gender: {data.user.gender}</p>
      <p>Tags: {data.user.tags.join(',')}</p>
    </div>
  );
}

export default User;

总结

通过前面简单的了解就可以感受到使用 GraphQL,不管是服务端还是前端,写起来都非常简单,因为 GraphQL 以及其生态中的工具帮我们做了很多工作,节省了很多开发成本。

在开发服务端的时候,我们的 service 可以更原子化,不用关心前端到底需要什么字段,一切都可以面向后端的最佳实践,根据 GraphQL Schema 来编写 service;而前端则可以根据 Schema 来自由组合数据和服务,不必再频繁要求后端增减字段或接口。

在开发前端的时候,则更简单了,不用再繁琐地重复编写各种 action,直接用 hooks 就能实现数据的获取和更新,也很简单就能知道 loading 和异常状态,简单直接且高效。

GraphQL Playground 则为我们提供了调试和文档服务。我们可以在 playground 中方便地调试 GraphQL 接口,并且有各种语法提示,不必再使用 postman 等 HTTP 工具去请求接口。同时 playground 还自带了文档功能,其文档就是根据 GraphQL Schema 自动生成的,这样文档可以随着代码实时更新,开发者再也不用花时间去写文档,使用者也能时刻看到最新最准确的文档。

总的来说,使用 GraphQL 能极大提高开发效率。

@nodejh nodejh added the GraphQL label Nov 22, 2019
@nodejh nodejh changed the title GraphQL 实践(一): GraphQL 简介 GraphQL 实践(一): GraphQL 入门 Nov 26, 2019
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant