如果希望将 GraphQL 运用到前后端分离的生产环境,请期待后续文章。
本文实例代码:Github
0. 什么是 GraphQL
GraphQL 是一种面向数据的 API 查询风格。
传统的 API 拿到的是前后端约定好的数据格式,GraphQL 对 API 中的数据供应了一套易于理解的完全描述,客户端能够准确地得到它须要的数据,没有任何冗余,也让 API 更随意马虎地随着韶光推移而演进,还能用于构建强大的开拓者工具。
1. 概述
前真个开拓随着 SPA 框架全面遍及,组件化开拓也随之成为大势所趋,各个组件分别管理着各自的状态,组件化给前端仔带来便利的同时也带来了一些烦恼。比如,组件须要卖力把异步要求的状态分发给子组件或关照给父组件,这个过程中,由组件间通信带来的构造繁芜度、来源不明的数据源、不知从何订阅的数据相应会使得数据流变得凌乱无章,也使得代码可读性变差,以及可掩护性的降落,为往后项目的迭代带来极大困难。
试想一下你都开拓完了,产品见告你要大改一番,从接口到组件构造都得改,后端也骂骂咧咧不愿合营让你从好几个 API 里取数据自己组合,这酸爽
在一些产品链繁芜的场景,后端须要供应对应 WebApp、WebPC、APP、小程序、快运用等各端 API,此时 API 的粒度大小就显得格外主要,粗粒度会导致移动端不必要的流量损耗,细粒度则会造成函数爆炸 (Function Explosion);在此情景下 Facebook 的工程师于 2015 年开源了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 的操作类型可以是 query、 mutation 或 subscription,描述客户端希望进行什么样的操作
query 查询:获取数据,比如查找,CRUD 中的 Rmutation 变更:对数据进行变更,比如增加、删除、修正,CRUD 中的 CUDsubstription 订阅:当数据发生变动,进行推送这些操作类型都将在后文实际用到,比如这里进行一个查询操作
query { user { id }}
2.2 工具类型和标量类型 Object Type & Scalar Type
如果一个 GraphQL 做事接管到了一个 query,那么这个 query 将从 RootQuery 开始查找,找到工具类型(Object Type)时则利用它的解析函数 Resolver 来获取内容,如果返回的是工具类型则连续利用解析函数获取内容,如果返回的是标量类型(Scalar Type)则结束获取,直到找到末了一个标量类型。
工具类型:用户在 schema 中定义的 type标量类型:GraphQL 中内置有一些标量类型 String、 Int、 Float、 Boolean、 ID,用户也可以定义自己的标量类型比如在 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 ... }}
解析函数接管四个参数,分别为
parent:当前上一个解析函数的返回值args:查询中传入的参数context:供应给所有解析器的高下文信息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.json 的 scripts 字段加上:
\"大众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/db、 src/resolvers、 src/schema.graphql 这三个地方
src/db:数据库操作层,包括 DAO 层和 Service 层(如果对分层不太理解可以看一下末了一章)src/resolvers:Resolver 解析函数层,给 GraphQL 的 Query、Mutation、Subscription 要求供应 resolver 解析函数src/schema.graphql:Schema 层然后 npm run dev ,浏览器打开 http://localhost:4000/ 就可以利用 GraphQL Playground 开始调试了,左边是要求信息栏,左下是要求参数栏和要求头设置栏,右边是返回参数栏,详细用法可以参考 Prisma 文档
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.jsexport 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.jsimport Db from '../db'export default { Query: { user: (parent, { id }) => Db.user({ id }), users: (parent, args) => Db.users({}) }}
这里的两个方法 Db.user、 Db.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\公众: \公众李四\"大众 }] }}
把稳这里,返回的数组只希望拿到 id、 name 这两个字段,因此 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.jsimport 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.jsimport 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;前端通过浏览器 URL 进行路由命中获取目标视图状态,而页面视图是由组件嵌套组成,每个组件掩护着各自的组件级状态,一些轻微繁芜的运用还会利用集中式状态管理的工具,比如 Vuex、Redux、Mobx 等。前后端只通过 API 来互换,这也是现在前后端分离开拓的根本。
如果利用 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 两三年前没有广泛利用起来呢,可能有下面两个缘故原由:
GraphQL 的 field resolve 如果按照 naive 的办法来写,每一个 field 都对数据库直接跑一个 query,会产生大量冗余 query,虽然网络层面的要求数被优化了,但数据库查询可能会成为性能瓶颈,这里面有很大的优化空间,但并不是那么随意马虎做。FB 本身没有这个问题,由于他们内部数据库这一层也是抽象掉的,写 GraphQL 接口的人不须要顾虑 query 优化的问题。GraphQL 的利好紧张是在于前真个开拓效率,但落地却须要做事真个全力合营。如果是小公司或者全体公司都是全栈,那可能可以做,但在很多前后端分工比较明确的团队里,要推动 GraphQL 还是会碰着各种协作上的阻力。大约可以概括为性能瓶颈和团队分工的缘故原由,希望随着社区的发展,根本举动步伐的完善,会逐渐有完善的办理方案提出,让广大前后端开拓者们可以早日用上此利器。
关注微信"大众年夜众号:安徽思恒信息科技有限公司,理解更多技能内容……