webapp开发之Apollo GraphQL 在 webapp 中应用的思考
小标 2018-05-25 来源 : 阅读 508 评论 0

摘要:本次的webapp开发中,主要介绍了Apollo GraphQL 在 webapp 中应用的思考,希望对大家学习webapp开发有所帮助。

本次的webapp开发中,主要介绍了Apollo GraphQL 在 webapp 中应用的思考,希望对大家学习webapp开发有所帮助。

GraphQL 作为 FaceBook 2015年推出的 API 定义/查询 语言,在历经了两年的发展之后,社区已相对发达和完善。对于 GraphQL 的一些基础概念,本文不再一一赘述,目前社区相关的文章已经很多,有兴趣的同学可以去 google,或者直接看GraphQL 官方教程 Apollo GraphQL Server 官方文档。

而 Apollo GraphQL 作为目前社区最流行的 GraphQL 解决方案提供商,提供了从 client 到 server 的一整套完整的工具链。在这里我也准备以 Apollo 为例,通过一步步搭建 Apollo GraphQL Server 的方式,来给大家展示 GraphQL 的特点,以及我的一些思考(主要是我的思考

setup

创建基于 express 的 GraphQL server

//server.jsimportexpressfrom'express'; import{ graphiqlExpress, graphqlExpress} from'apollo-server-express'; importschemafrom'./models'; constPORT=8080; constapp=express(); ...app. use( '/graphql', graphqlExpress({ schema })); app. use( '/graphiql', graphiqlExpress({ endpointURL :'/graphql'})); if( process. env. NODE_ENV==='development') { glob( path. resolve( __dirname, './mock/**/*.js'), {}, ( er, modules) =>modules. forEach( module=>require( module). default(app)));} app. listen( PORT, () =>console. log( `> Listening at port ${PORT}`));

执行 node server.js,这样我们就能启动一个 GraphQL server 了。

注意我们这里使用了 apollo-server-express 提供的 graphiqlExpress 插件,graphiql 是一个用于浏览器端调试 graphql 接口的 GUI 工具。服务启动后,我们在浏览器打开 //localhost:8080/graphiql就可以看到这样一个页面

 webapp开发之Apollo GraphQL 在 webapp 中应用的思考

定义 API schema

我们在 server.js 中定义了这样一个 endpoint : app.use('/graphql', graphqlExpress({ schema }));

这里传入的 schema 是什么呢?它大概长这样:

import{ makeExecutableSchema} from'graphql-tools'; //The GraphQL schema in string formconsttypeDefs=`type User { id: ID!name: Stringage: Int}type Query { user(id: ID!): User }schema { query: Query }`; //The resolversconstresolvers={ Query :{ user({id}) { returnhttp. get( `/users/${id}`)}}}; //Put together a schemaconstschema=makeExecutableSchema({ typeDefs, resolvers}); app. use( '/graphql', graphqlExpress({ schema }));

这里的关键是用了 graphql-tools 这个库提供的 makeExecutableSchema 组合了 schema 定义和对应的 resolver。resolver 是 Apollo GraphQL 工具链中提出的一个概念,什么用呢?就是在我们客户端请求过来的 schema 中的 field 如果在 GraphQL Server 中有对应的 resolver,那么在返回数据时候,这些 field 就由对应的 resolver 的执行结果填充(支持返回 promise)。

客户端请求

这里借助 graphiql 面板的功能来发送请求:

 webapp开发之Apollo GraphQL 在 webapp 中应用的思考

看一下 http request payload 信息:

 webapp开发之Apollo GraphQL 在 webapp 中应用的思考

响应体:

 webapp开发之Apollo GraphQL 在 webapp 中应用的思考

也就是说,无论你是用你熟悉的 http lib 还是社区的 apollo client,只要按照 GraphQL Server 要求的既定格式发请求就 ok 了。

这里我们使用了 GraphQL 中的 variable 语法,事实上在这种需要传参的动态查询场景下,我们应该总是使用这种方式发送请求:即一个 static query + variable 的方式,而不是在运行时动态的生成 query string。这也是官方建议的最佳实践。

更复杂的嵌套查询场景

假设我们有这样一个场景,即我们需要取到 User Entity 下的 nick 字段,而 nick 数据并不来自于 user 接口,而是需要根据 userId 调用另一个接口取得。这时候我们服务端的代码需要这样写。

// schematype User { id: ID! name: String age: Int nick: String} //resolverUser :{ nick({ id }) { returngetUserNick(id); }}

resolver 的参数列表中包含了当前所在 Entity 已有的数据,所以这里可以直接在函数的入参里取到已查询出来的 userId。

看下效果:

 webapp开发之Apollo GraphQL 在 webapp 中应用的思考

服务端的请求:

 webapp开发之Apollo GraphQL 在 webapp 中应用的思考

可以看到,这里多出了查询 nick 的请求。也就是说,GraphQL Server 只有在客户端提交了包含相应字段的 query 时,才会真正去发送相应的请求。更多 resolver 说明可以看这里。

其他

在真实的生产环境中,我们通常会有更多更复杂的场景,比如接口的权限认证、分页、缓存、批量提交、schema 模块化等需求,好在社区都有相对应的一些解决方案,这不是本文的重点所以不在这里一一介绍了,有兴趣的可以去看下我之前写的 graphql-server-startkit,或者官方的 demo。

实践

如果你真实的使用过 Apollo GraphQL,你会经历如下过程:

00001. 

定义一个 schema 用于描述查询入口

00002. 

// schema.graphqltype User { id: ID! name: String nick: String age: Int gender: String}type Query { user(id: ID!): User}schema { query: Query}

00003. 

00004. 

编写 resolver 解析对应类型

00005. 

constresolvers={ Query:{ user(root, { id }) { returngetUser(id); } }, User:{ nick({ id }) { returngetUserNick(id); } }};

00006. 

00007. 

编写客户端请求代码调用 GraphQL 接口,通常我们会封装一个 get 方法

00008. 

functiongetUser(id) { //以 axios 为例returnaxios.post('/graphql', { query:'query userQuery($id: ID!) {↵ user(id: $id) {↵ id↵ name↵ nick↵ }↵}', operationName:"userQuery", variables:{id}});}

00009. 

如果你的项目中加入了静态类型系统,那么你的代码可能就会变成这样:

00010. 

//以 ts 为例interfaceUser{ id:numbername:stringnick:stringage:numbergender:string}functiongetUser(id:number):User{ returnaxios.post('/graphql', { query: 'query userQuery($id: ID!) {↵ user(id: $id) {↵ id↵ name↵ nick↵ }↵}', operationName: "userQuery", variables: {id}});}

00011. 

写到这里你可能已经发现,不仅是 entity 类型定义,就连接口的封装,我们在服务端和客户端都重复了一遍(虽然一个用的 GraphQL Type Language 一个用的 TS)… 这还是最简单的场景,如果业务模型复杂起来,你在两端需要重复的代码会更多(比如类型的嵌套定义和 resolve)。这时候你可能会想起 DRY 原则,然后开始思考有没**有什么方式可以使得类型及接口定义能两端复用,或者根据一端的定义自动生成另一端的代码?**甚至你开始怀疑,到底有没有引入 GraphQL 的必要?

思考

GraphQL 作为一个标准化并自带类型系统的 API Layer,其工程价值我也不再过多广告了。只是在实践过程中,既然我们无法完全避免服务端与客户端的实体与接口定义重复(使用 apollo-codegen 可以避免一部分),而且对于大部分小团队而言,运维一个 productive nodejs system 实际上都是力有未逮。**那么我们是不是可以考虑在纯客户端构建一个类 GraphQL 的 API Layer 呢?**这样既可以有效的避免编码重复,也能大大的降低对团队的要求,可操作的空间也比增加一个 nodejs 中间层大得多。

我们可以回忆一下,通常对于一个前端而言,促使我们需要一个 API Layer 的原因是什么:

00001. 

后端接口设计不够 restful,命名垃圾,用的时候看见那个*一样的 url 就难受。

00002. 

00003. 

后端同学只愿意写 microservice,提供聚合服务的 web api 被认为没有技术含量,不愿意写。你需要一个数据,他告诉你需要调 a、b、c 三个接口,然后根据 id 组装合并。

00004. 

00005. 

接口返回的数据格式各种嵌套及不合理,不是前端想要的结构。

00006. 

00007. 

接口返回的数据字段命名随意或者风格不统一,我有强迫症用这种接口会发疯。

00008. 

00009. 

后端返回的 数据格式/字段名 一旦变了,前端视图绑定部分的代码需要修改。

00010. 

通常情况下,碰到这些问题,你可能去跟后端同学据理力争,要求他们提供调用体验更良好设计更优雅的接口。没错这很好,毕竟为了追求完美去跟各种人撕(跟后端撕、跟产品撕、跟UI撕)是一个前端工程师基本的职业素养。但是如果你每天都被撕逼弄得心力交瘁,甚至是你根本找不到撕的对象(比如数据来源接口来着几个不同部门,甚至是一些祖传的没人敢动的接口),这些时候大概就是你迫切希望有一个 API Layer 的时候了。

如何在客户端实现一个 API Layer

其实很简单,你只需要在客户端把 Apollo Server 中要写的 resolvers 写一遍,然后配上一些性能提升手段(如缓存等),你的 API Layer 就完成了。

比如我们在src下新建一个 loaders/apis 目录,所有的数据拉取接口都放在这里。比如这样:

//UserLoader.tsexportinterfaceUser{ id :numbername :stringnick :string} exportdefaultclassUserLoader{ asyncgetUser( id:number) :User{ constbase =awaitPromise. all([ http. get( '//xxx.com/users/${id}'), this. getUserNick( id)]); constuser =base. reduce(( acc, info) =>({ ...acc, ...info}), {}); returnuser; } getUserNick( id:number) :string{ returnhttp. get( `//xxx.com/nicks/${id}`); }}

然后在你业务需要的地方注入相应 loader 调用接口即可,如:

import{ inject} from'mmlpx'; importUserLoaderfrom'./UserLoader'; //Controller.tsexportdefaultclassController{ @ inject( UserLoader) userLoader =null; asyncdoSomething() { //...constuser =awaitthis. userLoader. getUser( this. id); //...}}

如果你不喜欢依赖注入的方式,loaders/apis 层直接 export function getUser 也可以。

如果你碰到了上面描述的第 3、4 、5 三种问题,你可能还需要在这一层做一下数据格式化。比如这样:

asyncgetUser( id: number): User{ constbase =awaitPromise. all([ http. get( '//xxx.com/users/${id}'), this. getUserNick( id)]); constuser =base. reduce(( acc, info) =>({ ...acc, ...info}), {}); return{ id: user. id, name: user. user_name, //重命名字段nick: user. nick. userNick//剔除原始数据中无意义的层次结构};}

经过这一层的数据处理,我们就能确保我们的应用运行在前端自己定义的数据模型之下。这样之后后端接口不论是数据结构还是字段名的变更,我们只需要在这一层做简单调整即可,而不会影响到我们上层的业务及视图。相应的,我们的业务层逻辑不再会直接对接接口 url,而是将其隐藏在 API Layer 下,这样不仅能提升业务代码的可读性,也能做到眼不见为净。。。

总结

熟悉 GraphQL 的同学可能会很快意识到,我这不过是在客户端做了一个简单的 API 封装嘛,并不能解决在 GraphQL 出现之前的 lots of roundtrips 及 overfetching 问题。但事实上是 roundtrip 的问题我们可以通过客户端缓存来缓解(如果你用的是 axios 你可能需要 axios-extensions ),而且 roundtrip 的问题其实本质上我们不过是将客户端的 http 开销转移到服务端了而已。在客户端与服务端均不考虑缓存的情况,客户端反而会少一个请求。。。overfetching 问题则取决于 backend service 的粒度,如果 endpoint 不够 micro,即便是 GraphQL,也会出现接口数据冗余问题,毕竟 GraphQL 不生产数据,它只是数据的搬运工。。。而如果 endpoint 粒度足够小,那么我在客户端 API 层多开几个接口(换成 Apollo 也要多写几个 resolver),一样可以按需取数据。服务端 API Layer 只有一个不可替代的优势就是,如果我们的数据源接口是不支持跨域或者仅内网可见的,那么就只能在服务端开个口子做代理了。另外一个优势就是,GraphQL Server 的 http 开销是可控的,毕竟机器是我们自己控制,而客户端的环境则不可控(受限于终端设备及网络环境,比如低版本浏览器或者低速网络,均会导致 http 开销的性能权重增大)。

可能有同学会说,服务端 API Layer 部署一次任何系统都可以共享其服务,而客户端 API Layer 的作用域只在某一项目。其实,如果我们把某一项目需要共享的 API Layer 打成一个 npm 包发布出去,不也能达到同样的效果吗,很多平台的 js sdk 不都是这个思路么(这里只讨论 web 开发范畴)。

在我看来,不论你是否会搭建一个服务端的 API Layer,**我们其实都需要有一个客户端 API Layer 从数据源头来保证客户端数据的模型统一及一致性,从而有足够的能力应对接口的变迁。**如果你考虑的再远一点,在 API Layer 服务的业务模型层,我们同样需要有一套独立的 Service/Model Layer 来应对视图框架的变迁。这个暂且按下不表,后面会再写篇文字来详细说一下我的思路。

事实上,对于大部分团队而言,客户端 API Layer 已经够用了,增加一层 GraphQL 并不是那么必要。而且如果没有很好的支持将客户端接口转换成 GraphQL Schema 和 resolver 的工具时,我们并不能很愉快的 coding,毕竟两端重复的工作还是有点多。

本文由职坐标整理并发布,希望对同学们有所帮助。了解更多详情请关注职坐标移动开发Webapp频道!

本文由 @小标 发布于职坐标。未经许可,禁止转载。
喜欢 | 0 不喜欢 | 0
看完这篇文章有何感觉?已经有0人表态,0%的人喜欢 快给朋友分享吧~
评论(0)
后参与评论

您输入的评论内容中包含违禁敏感词

我知道了

助您圆梦职场 匹配合适岗位
验证码手机号,获得海同独家IT培训资料
选择就业方向:
人工智能物联网
大数据开发/分析
人工智能Python
Java全栈开发
WEB前端+H5

请输入正确的手机号码

请输入正确的验证码

获取验证码

您今天的短信下发次数太多了,明天再试试吧!

提交

我们会在第一时间安排职业规划师联系您!

您也可以联系我们的职业规划师咨询:

小职老师的微信号:z_zhizuobiao
小职老师的微信号:z_zhizuobiao

版权所有 职坐标-一站式IT培训就业服务领导者 沪ICP备13042190号-4
上海海同信息科技有限公司 Copyright ©2015 www.zhizuobiao.com,All Rights Reserved.
 沪公网安备 31011502005948号    

©2015 www.zhizuobiao.com All Rights Reserved

208小时内训课程