GraphQL的初学者指南——使用Node.js和Apollo构建您的第一个GraphQL API

来源:https://medium.com/techtalkers/a-beginners-guide-to-graphql-12d60d3fba03

由Facebook于2015年发布的GraphQL提供了一种新的、有前途的替代传统REST API的方法。从那时起,包括GitHub、Shopify和Intuit在内的许多公司都在它们的生态系统中采用了GraphQL。虽然REST api可能不会很快过时,但GraphQL正在迅速受到许多开发人员的欢迎和喜爱。因此,对GraphQL的需求急剧增长,并预计在未来十年将呈指数级增长。在本文结束时,您将很好地理解GraphQL的工作方式以及如何使用Node.js构建api。在本教程中,我们将构建一个功能完整的GraphQL API,它将向数组添加和查询“用户”。

注意:在生产环境中,您应该查询和修改数据库(如MongoDB或Firebase),但为了简单起见,在本教程中我们将使用硬编码数据。

前提条件:本教程是为那些对JavaScript和Node.js有很好理解的人准备的。建议您具备事先的GraphQL知识,但不要求您遵循。

一、GraphQL是什么?

来自GraphQL官方网站的GraphQL演示。



GraphQL或“图形查询语言”,顾名思义,是一种用于api的查询语言。

SQL是一种用于管理关系数据库的查询语言,而GraphQL是一种允许客户端(前端)从API请求数据的查询语言。

二、GraphQL的优点

为什么在传统的REST API上使用GraphQL ?让我们来看看GraphQL相对于RESTful对等物所提供的一些优势:

一个端点:对于传统的REST api,您必须基于希望请求的数据创建特定的端点。这使得扩展API变得很困难,因为不久之后,您可能会发现自己不得不管理数十甚至数百条必须记住的路由。

更少的服务器请求:GraphQL允许您使用一个服务器请求进行多个查询和更改。当您的服务器每天只允许有限数量的请求时,这很有用。

声明性数据获取:与REST API不同,GraphQL只获取您实际需要的数据。您需要做的就是指定要返回的字段。

类型系统:GraphQL使用类型系统( type system)来描述数据,这使得开发更加容易。如果你是TypeScript的粉丝,这是双赢的。

自文档化:GraphQL是自文档化的,这意味着GraphQL将自动记录您的所有查询和更改。

GraphQL还有许多值得了解的优点(和缺点)。我建议阅读GraphQL: webab Technology提供的所有你需要知道的东西,以了解更多关于GraphQL的信息(点击https://medium.com/@weblab_tech/graphql-everything-you-need-to-know-58756ff253d8)。

三、什么是 Apollo 服务器?

注意:Apollo服务器也有一个express集成,但在本教程中我们不需要express .js。

因为GraphQL只是一种查询语言,所以我们需要一个库来为我们处理样板代码。幸运的是,这样的库已经存在了。

进入Apollo Server,这是一个Node.js库,它提供了一个简单易用的GraphQL服务器。其他的GraphQL服务器库也存在,比如express-graphql,但是Apollo server允许更好的可伸缩性,并支持更大的社区。Apollo Server还提供了一个整洁的GraphQL接口,用于在开发过程中执行查询和更改。

现在我们已经解决了这个问题,我们终于可以开始构建我们的GraphQL API了。以下代码的链接:https://github.com/advaithmalka/graphql-server-tut

步骤1:安装依赖项

注意:确保你安装了一个代码编辑器,就像Visual Studio code一样。

首先,您需要创建一个包含所有服务器文件的目录。我们可以在终端中这样做:

mkdir graphql-server-tut

cd graphql-server-tut

然后,在我们的项目中初始化NPM:

npm init -y

这会在当前目录中创建一个package.json文件。

现在,我们可以开始为我们的服务器安装所需的依赖:

npm install apollo-server graphql

这里,我们安装了两个必需的依赖项。

apollo-server:允许我们轻松地创建GraphQL服务器。

graphql:apollo-server所需的依赖项。

一旦安装了这些包,我们就可以开始为服务器编程了。

步骤2:创建类型定义

我们首先需要在当前目录中创建index.js文件。当我们完成API时,该文件将作为进入服务器的入口点。

接下来,我们需要从apollo-server的NPM模块中导入ApolloServer和gql。

const { ApolloServer, gql } = require(“apollo-server”);

现在,由于GraphQL是一种类型化语言,我们需要用schema定义数据。

将以下代码放在index.js文件中。

const { ApolloServer, gql } = require("apollo-server");

// typeDefs tell the GraphQL server what data to expect

// Notice the gql tag, this converts your string into GraphQL strings that can be read by Apollo

const typeDefs = gql`

  type Query {

    hello: String!

    randomNubmer: Int!

  }

`

// the Query type outlines all the queries that can be called by the client

// hello and randomNumber are the names of the queries

// The exclamation mark (!) tells Apollo Server that a result is required

// Here, we define two queries, one returns a String and another returns a Int

在这里,在导入ApolloServer和gql之后,我们创建了一个包含我们的模式的多行GraphQL字符串。大多数开发人员将他们的模式命名为typedefs,因为当我们稍后初始化ApolloServer时,我们需要将我们的模式传递给具有相同名称的对象键。

在这段代码中有一些关键的事情需要注意:

(1)我们的typedef被传递到gql标签中。这个标记将对我们的类型定义进行消毒,并使Apollo服务器能够读取它们。这也允许在开发过程中自动完成

(2)查询类型(Query type )列出了服务器可以执行的所有可能查询。

(3)hello和randomNumber是两个不同的查询。

(4)在冒号(:)之后定义返回值的类型。在本例中,hello返回一个字符串类型,而randomNumber返回一个整数类型。

(5)类型后面的感叹号(!)表示返回值是必需的。

步骤3:创建解析器函数

现在,我们需要告诉服务器在调用特定查询时要做什么或返回什么。我们可以通过创建解析器函数来解决这个问题。

将下面的代码复制到你的index.js文件中:

// When a query is called a resolver with the same name is run

// The API returns whatever is returned by the resolver

// We are using arrow functions so the "return" keyword is not required

const resolvers = {

  // The name of the resolver must match the name of the query in the typeDefs

  Query: {

    // When the hello query is invoked "Hello world" should be returned

    hello: () => "Hello world!",

    // When we call the randomNumber query, it should return a number between 0 and 10

    randomNumber: () => Math.round(Math.random() * 10),

  },

};

让我们逐行看看这段代码做了什么:

(1)解析器应该匹配我们的类型定义。就像我们在typedef中有一个查询类型一样,我们在解析器中有一个查询对象。

(2)查询对象包含与typedef对应的解析器。(每个查询都有一个名称相同的对应解析器函数)

(3)无论在解析器中返回什么,查询都会返回给客户机。

注意:在现实世界中,解析器通常从数据库中获取数据并返回给客户端。不幸的是,获取和修改数据库超出了本教程的范围。

第四步:把它们放在一起

最后在这里!我们一直在等待的步骤是:该运行服务器了。

首先,我们需要创建一个ApolloServer实例,并传入typeDefs和解析器。

我们可以这样做:

// Create an instance of ApolloServer and pass in our typeDefs and resolvers

const server = new ApolloServer({

  // If the object key and value have the same name, you can omit the key

  typeDefs,

  resolvers,

});

// Start the server at port 8080

server.listen({ port: 8080 }).then(({ url }) => console.log(`GraphQL server running at ${url}`));

让我们来看看这里发生了什么:

首先,我们创建了一个ApolloServer实例(在步骤2中导入),并传入typeDefs和解析器,这是在步骤2和步骤3中创建的。

然后,我们在端口8080(默认为4000)上启动服务器。

这就是你的index.js文件现在的样子:

const { ApolloServer, gql } = require("apollo-server");

// typeDefs tell the GraphQL server what data to expect

// Notice the gql tag, this converts your string into GraphQL strings that can be read by Apollo

const typeDefs = gql`

  type Query {

    hello: String!

    randomNumber: Int!

  }

`;

// the Query type outlines all the queries that can be called by the client

// hello and randomNumber are the names of the queries

// The exclamation mark (!) tells Apollo Server that a result is required

// Here, we define two queries, one returns a String and another returns a Int

// When a query is called a resolver with the same name is run

// The API returns whatever is returned by the resolver

// We are using arrow functions so the "return" keyword is not required

const resolvers = {

  // The name of the resolver must match the name of the query in the typeDefs

  Query: {

    // When the hello query is invoked "Hello world" should be returned

    hello: () => "Hello world!",

    // When we call the randomNumber query, it should return a number between 0 and 10

    randomNumber: () => Math.round(Math.random() * 10),

  },

};

// Create an instance of ApolloServer and pass in our typeDefs and resolvers

const server = new ApolloServer({

  // If the object key and value have the same name, you can omit the key

  typeDefs,

  resolvers,

});

// Start the server at port 8080

server.listen({ port: 8080 }).then(({ url }) => console.log(`GraphQL server running at ${url}`));

我们的服务器已经准备好了!在终端中,通过键入node index来运行index.js文件。

如果您按照正确的步骤操作,您应该会看到登录到控制台的“GraphQL server running at http://localhost:8080/”。

当你导航到localhost:8080时,你应该看到GraphQL playground:

The GraphQL playground

这个整洁的接口来自于 apollo-server模块,允许您直接对服务器执行查询,而不需要连接前端。

注意:如果您的节点环境被设置为生产环境,那么GraphQL playground将不可用。

使用GraphQL playground时有几个很酷的特性需要注意:

(1)在右侧,您将看到两个选项卡:Schema和Docs。

(2)当API的大小增加时,您可以参考Schema选项卡来查看服务器可以执行的所有可用查询和更改。

(3)还记得我提到过GraphQL是自文档化的吗?您可以在Docs选项卡中看到为您的API生成的文档GraphQL。

(4)GraphQL playground还允许您向查询或突变(query or mutation)添加HTTP头。如果您只想让授权用户使用您的API,这是非常有用的。

四、查询我们的API

现在我们的服务器已经设置好了,我们可以通过GraphQL playground向它发送请求。

要执行查询,请将以下代码粘贴到GraphQL playground中:

query {

hello

randomNumber

}

这里,我们调用前面步骤中设置的两个查询。一旦你点击播放按钮,你应该看到发送回的数据对应于我们的解析器返回的数据:

由GraphQL查询返回的数据。

GraphQL的美妙之处在于,您只返回您指定的内容;如果你删除hello查询,它将不再显示在数据中:


GraphQL使用声明性数据获取。

五、创建更高级的API

现在,由于您希望了解一点ApolloServer的工作原理,我们将创建一个能够添加和查询用户的API。这一次,我们不仅可以查询数据,还可以添加数据

步骤1:创建我们的“数据库”

在本教程中,我们将使用硬编码数据,并将所有数据存储在一个数组中,而不是使用MongoDB或Firebase这样的实际数据库。

首先,我们将创建一个名为users的数组。每个用户都有一个姓、名和电子邮件字段。如果我们愿意,我们可以像这样在数组中包含一些硬编码的数据:

const users = [

  {

    firstName: "GraphQL",

    lastName: "isCool",

    email: "GraphQL@isCool.com"

  },

];

您可以随意向数组中添加硬编码的数据。

步骤2:设置typeDefs

现在,我们需要一种方法来查询“数据库”中的所有用户。让我们更新我们的typeDefs来允许这个函数:

const typeDefs = gql`

  # GraphQL enables us to create our own types

  # Notice the "User" type matches the shape of our "database"

  type User {

    firstName: String!

    lastName: String!

    email: String!

  }

  type Query {

    hello: String!

    randomNumber: Int!

    # This query is going to return all the users in our array

    # Since our "database" is an array containing objects, we need to create a "User" type

    # Brackets around the type indicates the query is returning an array

    queryUsers: [User]!

  }

`;

这里有几件事需要注意:

(1)queryUsers查询返回一个对象数组(因此有方括号)。

(2)我们可以使用type关键字后跟类型名创建自己的GraphQL类型。

(3)在大括号({})中,我们指定type将返回的字段(我们的用户类型将返回三个字段:firstName、lastName和email。这三个都是字符串,并且是必需的)。

步骤3:配置解析器

我们只需要再添加一行代码来完成查询:

const resolvers = {

  Query: {

    hello: () => "Hello world!",

    randomNumber: () => Math.round(Math.random() * 10),

    // queryUsers simply returns our users array

    queryUsers: () => users,

  },

};

这行新代码创建了一个解析器函数,当调用该函数时,将返回users数组。

步骤4:测试查询

这次我们的问题看起来有点不同:

query {

queryUsers {

firstName

lastName

email

}

}


当调用queryUsers时,数据应该与此类似。

当我们调用queryUsers查询时,我们需要指定我们希望API以大括号({})返回哪些字段。上面的代码返回所有三个字段,但是如果客户端只需要每个用户的姓和名,你可以省略电子邮件字段来节省带宽:


只查询您需要的,以提高速度和减少带宽占用

向数组中添加用户

如果我们的API只能显示硬编码的用户,那么它就没有多大用处。在本节中,我们还将允许我们的API向数组添加用户。

步骤1:向我们的typeedefs添加一个Mutation

当您执行除从数据库读取(创建、更新、删除)之外的任何其他操作时,都应该使用GraphQL Mutation。

所有Mutation必须是GraphQL Mutation类型:

const typeDefs = gql`

  type User {

    firstName: String!

    lastName: String!

    email: String!

  }

  type Query {

    hello: String!

    randomNumber: Int!

    queryUsers: [User]!

  }

  # Mutations must be in their own type

  type Mutation {

    # We are creating a mutation called "addUser" that takes in 3 arguments

    # These arguments will be available to our resolver, which will push the new user to the "users" array

    # Notice that this mutation will return a single User, which will be the one that was created

    addUser(firstName:String!, lastName:String!, email:String!): User!

  }

`;

我想从这段代码中记下一些事情:

(1)我们正在创建一个名为addUser的新Mutation

(2)addUser接受三个参数:firstName、lastName和email。所有三个参数都是string类型的,并且是必需的(在括号中指定)

(3)addUser返回一个用户类型:一个包含新用户的姓、名和电子邮件的对象。

步骤2:添加addUser解析器

在我们开始编写解析器之前,让我们计划一下它应该完成什么。

首先,当我们运行该解析器时,它将需要从Mutation中获取firstName、lastName和email参数。然后,它需要将该数据作为一个新对象推送到users数组。最后,我们只返回传递到Mutation中的数据。

注意:在使用真实的数据库时,应该实现try, catch块来处理可能发生的错误。

更新您的解析器以匹配以下内容:

const resolvers = {

  Query: {

    hello: () => "Hello world!",

    randomNumber: () => Math.round(Math.random() * 10),

    queryUsers: () => users,

  },

  // All mutation resolvers must be in the Mutation object; just like our typeDefs

  Mutation: {

    // Once again notice the name of the resolver matches what we defined in our typeDefs

    // The first argument to any resolver is the parent, which is not important to us here

    // The second argument, args, is an object containing all the arguments passed to the resolver

    addUser: (parent, args) => {

      users.push(args); // Push the new user to the users array

      return args; // Returns the arguments provided, this is the new user we just added

    },

  },

};

让我们看看这个解析器会做什么:

(1)就像查询一样,突变必须匹配我们的typeDefs并进入Mutation对象。

(2)每个解析器(不仅仅是Mutation)都可以访问4个参数,您可以在文档中了解更多信息。对于这个解析器,我们只需要第二个参数。

(3)第二个参数args将包含新用户的姓、名和电子邮件。如果您愿意,可以在解析器函数中查看console.log参数args,以查看它包含哪些数据。

(4)因为我们的“数据库”只是一个对象数组,我们可以简单地将args对象推入用户数组。

(5)我们的Mutation需要返回创建的新用户。我们可以通过返回args对象来做到这一点。

就是这样!只需要不到10行代码,我们的服务器现在就可以添加和查询用户了!现在,让我们看看如何调用GraphQL突变。

第四步:呼唤我们的Mutation

调用Mutation非常类似于在GraphQL中调用查询:

mutation {

  addUser(

    firstName: "John",

    lastName: "Doe",

    email: "john.doe@somemail.com"

  ) {

    firstName

    lastName

    email

  }

}

提示:我将把上面的GraphQL查询放在一个新的GraphQL Playground选项卡中。

注意关于上面的查询的一些事情:

(1)为了表示一个Mutation,我们使用Mutation关键字代替查询。

(2)我们可以通过在圆括号中指定参数来将参数传递给GraphQL查询,就像JavaScript函数一样。

(3)在这里,我们创建一个名为John Doe的新用户,电子邮件为john.doe@somemail.com。如果你愿意随时改变参数。

(4)与queryUsers查询一样,我们可以选择要返回的字段。请记住,此Mutation只返回创建的新用户。

从Mutation返回的数据应该是这样的:


从addUser突变返回的数据。

如果您再次运行queryUsers查询,而没有重启服务器,您应该看到一个新用户添加到数组:


“John Doe”已经成功添加到我们的用户数组中!

注意:重新启动服务器时,新数据将丢失,因此请确保在实际应用程序中使用数据库。

太棒了!我们的API现在可以查询并向数组中添加用户了!为了挑战自己,我建议给每个用户增加几个字段。比如年龄,生日,喜欢的食物。如果您熟悉MongoDB或Firebase,请尝试将数据库与API集成,而不是将数据存储在数组中。

祝贺您使用Apollo服务器构建了您的第一个GraphQL API !本教程只介绍了GraphQL的功能。还有很多东西需要学习,比如订阅、片段、指令等等!无论您是在寻找REST API的替代方案,还是在寻找试图扩展知识的新开发人员,GraphQL都是一个很好的选择,而且我肯定您会喜欢使用它。

编码快乐!

https://www.howtographql.com/

©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 206,126评论 6 481
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 88,254评论 2 382
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 152,445评论 0 341
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 55,185评论 1 278
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 64,178评论 5 371
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,970评论 1 284
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,276评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,927评论 0 259
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 43,400评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,883评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,997评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,646评论 4 322
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,213评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 30,204评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,423评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,423评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,722评论 2 345

推荐阅读更多精彩内容