node基于express的GraphQL API服务器

相信有很多仁兄在2018年底都看到过 2018 JavaScript 现状调查报告
这篇文章。其中有一张图甚是有趣:


可以看得出来 GraphQL 的趋势大好,而且也越来越受欢迎了,似乎不学习要跟不上时代了。既然这样我们就来学习一下怎么用node搭建GraphQL服务端API架构 (本文将会为您讲述最全面的node GraphQL知识, 文章末尾会附带完整的demo供给各位参考)


GraphQL是什么

简单的说明一下GraphQL是Facebook开源的一种规范应用层查询语言,它具有很多优点:客户端可以自定义查询语句,通过自定义不仅提高了灵活性,而且服务端只返回客户端所需要的数据,减少网络的开销,提高了性能;服务端收到客户端的请求,首先做类型检查,及时反馈,而且更加安全;自动生成文档,降低维护成本;服务端通过新增字段,deprecated字段,避免版本的繁杂和紊乱。
附上官方的文档 GraphQL
附上express实现graphql的文档 express-graphql


GraphQL vs RESTful

RESTful:服务端决定有哪些数据获取方式,客户端只能挑选使用,如果数据过于冗余也只能默默接收再对数据进行处理;而数据不能满足需求则需要请求更多的接口。
GraphQL:给客户端自主选择数据内容的能力,客户端完全自主决定获取信息的内容,服务端负责精确的返回目标数据。


实现GraphQL服务器

首先我们先来实现一个最简单的node GraphQL服务器

项目基础环境
mkdir graphql-api
# 进入项目文件夹
cd graphql-api
# 初始化package文件
npm init # 该命令中的所有步骤全部回车

为了方便下面的步骤,我们在graphql-api文件夹中手动创建下面的目录结构,并安装指定的依赖


项目文件结构
# 安装项目依赖
npm install express express-graphql graphql
index.js代码
const express = require('express')
const expressGraphql = require('express-graphql')
const app = express()
// 配置路由
app.use('/graphql', expressGraphql(req => {
  return {
    schema: require('./graphql/schema'), // graphql相关代码主目录
    graphiql: true // 是否开启可视化工具
    // ... 此处还有很多参数,为了简化文章,此处就一一举出, 具体可以看 刚才开篇提到的 express文档,
    // 也可以在文章末尾拉取项目demo进行查阅
  }
}))
// 服务使用3000端口
app.listen(3000, () => {
  console.log("graphql server is ok");
});
graphql/schema.js代码
const {
  GraphQLSchema,
  GraphQLObjectType
} = require('graphql')
// 规范写法,声明query(查询类型接口) 和 mutation(修改类型接口)
module.exports = new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query',
    description: '查询数据',
    fields: () => ({
      // 查询类型接口方法名称
      fetchObjectData: require('./queries/fetchObjectData')
    })
  }),
  mutation: new GraphQLObjectType({
    name: 'Mutation',
    description: '修改数据',
    fields: () => ({
      // 修改类型接口方法名称
      updateData: require('./mutations/updateData')
    })
  })
})
graphql/queries/fetchObjectData.js代码

先在graphql/queries文件夹下创建fetchObjectData.js文件, 并填入以下代码

const {
  GraphQLID,
  GraphQLInt,
  GraphQLFloat,
  GraphQLString,
  GraphQLBoolean,
  GraphQLNonNull,
  GraphQLObjectType
} = require('graphql')
// 定义接口返回的数据结构
const userType = new GraphQLObjectType({
  name: 'userItem',
  description: '用户信息',
  fields: () => ({
    id: {
      type: GraphQLID,
      description: '数据唯一标识'
    },
    username: {
      type: GraphQLString,
      description: '用户名'
    },
    age: {
      type: GraphQLInt,
      description: '年龄'
    },
    height: {
      type: GraphQLFloat,
      description: '身高'
    },
    isMarried: {
      type: GraphQLBoolean,
      description: '是否已婚',
      deprecationReason: "这个字段现在不需要了"
    }
  })
})
// 定义接口
module.exports = {
  type: userType,
  description: 'object类型数据例子',
  // 定义接口传参的数据结构
  args: {
    isReturn: {
      type: new GraphQLNonNull(GraphQLBoolean),
      description: '是否返回数据'
    }
  },
  resolve: (root, params, context) => {
    const { isReturn } = params
    if (isReturn) {
      // 返回的数据与前面定义的返回数据结构一致
      return {
        "id": "5bce2b8c7fde05hytsdsc12c",
        "username": "Davis",
        "age": 23,
        "height": 190.5,
        "isMarried": true
      }
    } else {
      return null
    }
  }
}
graphql/mutations/updateData.js代码

先在graphql/mutations文件夹下创建updateData.js文件, 并填入以下代码

const {
  GraphQLInt
} = require('graphql')

let count = 0

module.exports = {
  type: GraphQLInt,  // 定义返回的数据类型
  description: '修改例子',
  args: {  // 定义传参的数据结构
    num: {
      type: GraphQLInt,
      description: '数量'
    }
  },
  resolve: (root, params) => {
    let { num } = params
    count += num
    return count
  }
}

好了,到此为止,简单的GraphQL服务器就搭建好了,让我们来启动看看

node index.js  // 启动项目

然后我们在浏览器打开 http://localhost:3000/graphql 如下图所示

我们可以看到页面分为3栏,左边的是调用api用的,中间是调用api返回的结果 右边实际上就是我们刚才定义接口相关的东西,也就是api文档。
我们在左边粘贴以下代码:

query fetchObjectData {
  fetchObjectData(
    isReturn: true
  ) {
    id
    username
    age
    height
    isMarried
  }
}

mutation updateData {
  updateData(
    num: 2
  )
}

我们点一下左上角的按钮,会发现我们刚才左边填的2个方法名都在这列出来了,我们分别选择 fetchObjectData 和 updateData



api返回结果如下:


fetchObjectData 结果

updateData 结果

自此,我们从创建的GraphQL服务器就算完成了,代码也能正常运行,是否有些小激动!!
别着急,正所谓授人以鱼不如授人以渔,接下来我们细细分析一下我们刚才填写的GraphQL代码,了解其规范语法,这样我们才能真正的掌握GraphQL。

语法规范解析

以下的代码仅为诠释一下GraphQL的规范用法
不要直接拷贝到前面所述的代码中!!
不要直接拷贝到前面所述的代码中!!
不要直接拷贝到前面所述的代码中!!

1、导入GraphQL.js及类型
graphql 无论在定义接口参数和接口返回结果时, 都需要先定义好其中所包含数据结构的类型, 这不难理解,可以理解为我们定义的就是数据模型,其中常用的类型如下。

const {
    GraphQLList,  // 数组列表
    GraphQLObjectType, // 对象
    GraphQLString, // 字符串
    GraphQLInt,  // int类型
    GraphQLFloat,  // float类型
    GraphQLEnumType,  // 枚举类型
    GraphQLNonNull,  // 非空类型
    GraphQLSchema // schema(定义接口时使用)
} = require('graphql')

2、定义schema
schema实例中,一般规范为
query: 定义查询类的接口
mutation: 定义修改类的接口

new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Query', // 查询实例名称
    description: '查询数据', // 接口描述
    fields: () => ({
      // 查询类型接口方法名称
      fetchDataApi1: require('./queries/fetchDataApi1'),
      fetchDataApi2: require('./queries/fetchDataApi2'),
      fetchDataApi3: require('./queries/fetchDataApi3'),
      ...
    })
  }),
  mutation: new GraphQLObjectType({
    name: 'Mutation',
    description: '修改数据',
    fields: () => ({
      // 修改类型接口方法名称
      updateDataApi1: require('./mutations/updateDataApi1'),
      updateDataApi2: require('./mutations/updateDataApi2'),
      ...
    })
  })
})

3、接口方法定义

// 引用需要用到的数据类型
const {
  GraphQLID,
  GraphQLString,
  GraphQLNonNull,
  GraphQLObjectType
} = require('graphql')
// 第一部分 定义接口返回的数据结构
// 不难看出来,下面定义的是
/*
   {
      id
      username
   }
*/
const userType = new GraphQLObjectType({
  name: 'userItem',
  description: '用户信息',
  fields: () => ({
    id: {
      type: GraphQLID,
      description: '数据唯一标识'
    },
    username: {
      type: GraphQLString,
      description: '用户名'
    }
  })
})
// 第二部分 定义接口
module.exports = {
  type: userType,
  description: 'object类型数据例子',
  // 定义接口传参的数据结构
  args: {
    isReturn: {
      type: new GraphQLNonNull(GraphQLBoolean),
      description: '是否返回数据'
    }
  },
  resolve: (root, params, context) => {
    const { isReturn } = params
    // 返回的数据与前面定义的返回数据结构一致
    return {
       "id": "5bce2b8c7fde05hytsdsc12c",
       "username": "Davis"
    }
  }
}

我们来分析一下上图中第一部分的内容,GraphQLObjectType是GraphQL.js定义的对象类型,包括name、description 和fields三个属性:
name: 可作为对象的全局唯一名称
description: 是对象的描述
fields: 是解析函数也就是定义具体数据结构, 该对象包含什么键值

我们再来看一下第二部分的内容
type: 代表这个接口需要返回的数据结构是什么
description: 接口的描述
args: 接口的传参数据结构定义
resolve: 接口内部具体的实现逻辑(需求代码都写在这里面)
我们在处理完业务逻辑之后只需要返回与 type 中定义的数据结构一样的数据即可。

相信这时候你会发现resolve中又有三个参数是什么鬼,稳住,不要慌,这时候我们看回一开始我们的index.js文件


index.js部分代码截图

省略号这个地方是可以再定义很多参数的,而 rootValue和context这两个参数正好就对应了接口resolve方法中的root和resolve,我们可以看看官方文档对这两个参数的解释:



一般我们可以通过rootValue来做api认证,context可以传递用户信息、数据库链接等,也可以不用,这完全取决于你的业务场景

总结

1、简单点说,其实我们在编写GraphQL 代码的时候,你可以理解为他跟typescript有些相似,都需要事先定义好传参的数据结构,返回结果的数据结构,虽然这个比方不合理,但是这样讲更通俗易懂些
2、description 虽然不是必填的,但是建议都写上,因为它的作用其实就是api文档的解释。
3、GraphQL 真的很强大,虽然一开始接触的时候会有点蒙,啥玩意???这定义那定义的,虽然比RESTful风格的api上手难理解很多,但是我相信,当你学会使用GraphQL的时候,你会发现这玩意还是挺有意思的。
4、GraphQL 的用法还有很多,为了文章尽量简单易懂, 本文只是列出了其中一种用法,在我的demo中还有更多的用法,比如GraphQLEnumType(枚举类型)、GraphQLUnionType(联合类型)、GraphQLInterfaceType(接口类型)...有兴趣的看一下。

附上本人完整的demo: graphql-api

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