7月份咱们前端团队推进落地了一个 toB 类型的系统,因为服务端也由咱们前端工程师来承接,因此服务端技术选型上咱们有了话语权,API 这一起咱们选择了 GraphQL 。本文将阐述我学习 GraphQL 这门技术的一些思考。javascript
学习一门新技术,首先要把问题域弄清楚。社区有大量 GraphQL 与传统 API 解决方案(含 REST API)对比文章,总结下来,传统 API 存在如下问题:html
针对以上问题,GraphQL 给出了较为完善的解决方案。前端
接下来我经过一个实例讲解 GraphQL 解决问题的思路,客户端的述求:根据性别查询团队成员列表,返回 id
、 gender
、 name
、 nickName
,GrahpQL 的处理过程以下图:java
请求参数在发送到服务端以前会先通过 GraphQL Client 转换成客户端 Schema,这段 Schema 实际上是一段 query
开头的字符串,描述了客户端的对数据的述求:调用哪一个方法,传递什么样的参数,返回哪些字段。服务端拿到这段 Schema 以后,经过事先定义好的服务端 Schema 接收请求参数并执行对应的 resolve
函数提供数据服务。整个过程能够想象成咱们吃自助餐的过程,服务端 Schema 就比如自助餐线,摆上咱们能提供的全部食物;客户端 Schema 就描述了咱们想要吃的食物,按需获取就行了。 node
讲到这里,好奇心强的同窗可能已经开始思考这个问题了:客户端 Schema 本质上就是一段字符串,服务端如何识别并响应这段字符串?react
识别与响应客户端 Schema 依赖于官方类库 graphql-js ,服务端拿到客户端 Schema 字符串后会作以下处理:webpack
为了识别客户端 Schema, graphql-js
定义了一系列的特征标识符:ios
export const TokenKind = Object.freeze({ BANG: '!', DOLLAR: '$', PAREN_L: '(', PAREN_R: ')', SPREAD: '...', COLON: ':', EQUALS: '=', BRACKET_L: '[', BRACKET_R: ']', ... });
并定义了 AST 语法树规范,规定语法树支持如下节点:git
/** * The set of allowed kind values for AST nodes. */ export const Kind = Object.freeze({ // Name NAME: 'Name', // Document DOCUMENT: 'Document', OPERATION_DEFINITION: 'OperationDefinition', VARIABLE_DEFINITION: 'VariableDefinition', VARIABLE: 'Variable', // Values INT: 'IntValue', FLOAT: 'FloatValue', STRING: 'StringValue', BOOLEAN: 'BooleanValue', ... });
有了特征字符串与 AST 语法树规范,GraphQL Server 对客户端 Schema 进行逐字符扫描(charCodeAt),最终解析阶段的产出物为 document
,上文示例中的客户端 Schema 解析完成以后的部分 document
:github
{ "kind":"Document", "definitions":[ { "kind":"OperationDefinition", "operation":"query", "name":{ "kind":"Name", "value":"DisplayMember", "loc":{ "start":13, "end":26 } }, "selectionSet":{ "kind":"SelectionSet", "selections":[ { "kind":"Field", "alias":null, "name":{ "kind":"Name", "value":"fetchByGender", "loc":{ "start":37, "end":50 } }, "arguments":[ { "kind":"Argument", "name":{ "kind":"Name", "value":"gender", "loc":{ "start":51, "end":57 } }, "value":{ "kind":"StringValue", "value":"M", "loc":{ "start":59, "end":62 } }, "loc":{ "start":51, "end":62 } } ], ...
若是客户端 Schema 不符合服务端定义的 AST 规范,解析过程会直接抛出语法异常 Syntax Error
,拿上文的示例举例,我将客户端 Schema 中的 fetchByGender(gender: "M")
改成 fetchByGender(gender)
,只传递参数名,不传递参数值,则服务端会响应:
{ "errors":[ { "message":"Syntax Error GraphQL request (3:29) Expected :, found ) 2: query DisplayMember { 3: fetchByGender(gender) { ^ 4: list { ", "locations":[ { "line":3, "column":29 } ] } ] }
结构化的报错信息也是 GraphQL 的一大特色,定位问题很是方便。只要语法没问题解析阶段就能顺利完成,而后进入校验阶段。
校验阶段用于验证客户端 Schema 是否按照服务端 Schema 定义好的方式获取数据,好比:获取数据的方法名是否有误,必填项是否有值等等,校验范围一共有几十种,我没有办法一一举例。拿上文的示例举例,我将客户端 Schema 中的 fetchByGender
改成 fetchByGen
, fetchByGen
在服务端根本没有定义,则服务端会响应:
{ "errors":[ { "message":"Cannot query field "fetchByGen" on type "Query". Did you mean "fetchByGender"?", "locations":[ { "line":3, "column":9 } ] } ] }
不只返回结构化的报错信息,还很是人性化的告诉你正确的调用方式是什么。校验阶段经过以后会进入执行阶段
执行阶段依赖的输入为:解析阶段的产出物 document
,服务端 Schema;其中 document
准确描述了客户端对数据的述求:请求哪一个方法,参数是什么,须要哪些字段;服务端 Schema 描述了提供数据的方式;拿上文的示例举例,服务端 Schema 须要这样定义:
const graphqlApi = require('graphql'); const { GraphQLObjectType, GraphQLList, GraphQLNonNull, GraphQLSchema, GraphQLString, } = graphqlApi; const dataSource = require('./dataSource'); const memType = new GraphQLObjectType({ name: 'Male', description: 'A member gender is Male.', fields: () => ({ id: { type: new GraphQLNonNull(GraphQLString), description: 'The id of member', }, name: { type: GraphQLString, description: 'The name of the character.', }, nickName: { type: GraphQLString, description: 'The nickName of the character.', }, gender: { type: GraphQLString, description: 'The gender of the character.', }, list: { type: new GraphQLList(memType), description: 'The mems list by gender.', }, }) }); const queryType = new GraphQLObjectType({ name: 'Query', fields: () => ({ fetchByGender: { type: memType, args: { gender: { description: 'gender of the human', type: new GraphQLNonNull(GraphQLString), }, }, resolve: (root, { gender }) => { // 访问数据库或三方 API 查询成员列表 return { list: dataSource.getMembers(gender), }; }, }, }), }); module.exports = new GraphQLSchema({ query: queryType, types: [memType], });
执行服务端 Schema 中的 resolve
函数,获得执行阶段的输出:
{ "data":{ "fetchByGender":{ "list":[ { "id":"1", "gender":"M", "name":"童开宏", "nickName":"慕冥" } ] } } }
固然要完成服务端 Schema 的定义,你须要学习 GraphQL 的 类型系统 ,你们翻阅 API 文档便可。
原理弄清楚以后咱们须要对 GraphQL 这门技术的边界有一个清醒的认识:
apollo
提供的 Webpack 插件 ,固然也有一些 GraphQL 客户端连发送 Ajax 请求的活儿也干了,无非是在底层调用其余类库好比 axios
发请求。因为 GraphQL 经过客户端 Schema 而不是经过 URL 描述数据述求,因此理论上服务端只须要对客户端暴露一个地址便可,解决了接口数量众多维护成本高的问题;同时,服务端提供的是全量字段,客户端可按需获取,面对接口扩展的需求,服务端没有开发成本;最后,经过 GraphiQL 可视化调试界面展示服务端能提供的全部数据,开发过程再也不依赖接口文档:
GraphQL 官方提供核心能力:
咱们还缺什么?
官方只提供了 JavaScript 语言支持,社区爱好者很快在不一样编程语言中实现了 GraphQL 的理念:JAVA 、 .NET 等等,更多语言支持,请查看 官网
官方提供的 Relay 解决了 GraphQL 与 React 相结合的问题,Apollo Client 提供了与其余前端框架融合的解决方案,好比 Vue、Angular 等等。
开发体验
graphql-tools
:在上文示例代码的服务端 Schema 中,咱们将类型的定义(typeDefs)与处理函数的定义(resolvers)放在同一个文件中,职责上不够单一,借助 graphql-tools 咱们能够将两者分不一样的文件定义;egg-graphql
:与 Node 框架 egg 相结合,制定 目录规范 并提供语法糖提升开发效率;GraphQL 的优势上文已经讲过了,真的是从业务痛点出发,解决了传统 API 存在的问题,可是 GraphQL 在解决问题的同时也带了一些新的问题,这些问题在某种程度上阻碍了这门技术的普及:
query IAmEvil { author(id: "abc") { posts { author { posts { author { posts { author { # that could go on as deep as the client wants! } } } } } } } }
这样的查询语句会给数据库带来很大的性能开销,服务端不得不作 限流 来规避这样的问题,这也带来了额外的开发成本。
任何技术都有利弊,你们要结合本身的场景权衡收益作出适合本身的技术选型。
文章可随意转载,但请保留此 原文连接。
很是欢迎有激情的你加入 ES2049 Studio,简历请发送至 caijun.hcj(at)alibaba-inc.com 。