原文发表在: https://github.com/kuitos/kui...html
熟悉 Apollo GraphQL 的同窗可直接跳过这一章,从 实践 一章看起。前端
GraphQL 做为 FaceBook 2015年推出的 API 定义/查询 语言,在历经了两年的发展以后,社区已相对发达和完善。对于 GraphQL 的一些基础概念,本文再也不一一赘述,目前社区相关的文章已经不少,有兴趣的同窗能够去 google,或者直接看GraphQL 官方教程 Apollo GraphQL Server 官方文档。node
而 Apollo GraphQL 做为目前社区最流行的 GraphQL 解决方案提供商,提供了从 client 到 server 的一整套完整的工具链。在这里我也准备以 Apollo 为例,经过一步步搭建 Apollo GraphQL Server 的方式,来给你们展现 GraphQL 的特色,以及个人一些思考(主要是个人思考?)。ios
建立基于 express 的 GraphQL servergit
// server.js import express from 'express'; import { graphiqlExpress, graphqlExpress } from 'apollo-server-express'; import schema from './models'; const PORT = 8080; const app = 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 了。github
注意咱们这里使用了 apollo-server-express
提供的 graphiqlExpress
插件,graphiql 是一个用于浏览器端调试 graphql 接口的 GUI 工具。服务启动后,咱们在浏览器打开 http://localhost:8080/graphiql
就能够看到这样一个页面web
咱们在 server.js 中定义了这样一个 endpoint : app.use('/graphql', graphqlExpress({ schema }));
typescript
这里传入的 schema 是什么呢?它大概长这样:express
import { makeExecutableSchema } from 'graphql-tools'; // The GraphQL schema in string form const typeDefs = ` type User { id: ID! name: String age: Int } type Query { user(id: ID!): User } schema { query: Query } `; // The resolvers const resolvers = { Query: { user({id}) { return http.get(`/users/${id}`)}} }; // Put together a schema const schema = makeExecutableSchema({ typeDefs, resolvers }); app.use('/graphql', graphqlExpress({ schema }));
这里的关键是用了 graphql-tools
这个库提供的 makeExecutableSchema
组合了 schema 定义和对应的 resolver。resolver 是 Apollo GraphQL 工具链中提出的一个概念,什么用呢?就是在咱们客户端请求过来的 schema 中的 field 若是在 GraphQL Server 中有对应的 resolver,那么在返回数据时候,这些 field 就由对应的 resolver 的执行结果填充(支持返回 promise)。npm
这里借助 graphiql 面板的功能来发送请求:
看一下 http request payload 信息:
响应体:
也就是说,不管你是用你熟悉的 http lib 仍是社区的 apollo client,只要按照 GraphQL Server 要求的既定格式发请求就 ok 了。
这里咱们使用了 GraphQL 中的 variable 语法,事实上在这种须要传参的动态查询场景下,咱们应该老是使用这种方式发送请求:即一个 static query + variable 的方式,而不是在运行时动态的生成 query string。这也是官方建议的最佳实践。
假设咱们有这样一个场景,即咱们须要取到 User Entity 下的 nick 字段,而 nick 数据并不来自于 user 接口,而是须要根据 userId 调用另外一个接口取得。这时候咱们服务端的代码须要这样写。
// schema type User { id: ID! name: String age: Int nick: String }
// resolver User: { nick({ id }) { return getUserNick(id); } }
resolver 的参数列表中包含了当前所在 Entity 已有的数据,因此这里能够直接在函数的入参里取到已查询出来的 userId。
看下效果:
服务端的请求:
能够看到,这里多出了查询 nick 的请求。也就是说,GraphQL Server 只有在客户端提交了包含相应字段的 query 时,才会真正去发送相应的请求。更多 resolver 说明能够看这里。
在真实的生产环境中,咱们一般会有更多更复杂的场景,好比接口的权限认证、分页、缓存、批量提交、schema 模块化等需求,好在社区都有相对应的一些解决方案,这不是本文的重点因此不在这里一一介绍了,有兴趣的能够去看下我以前写的 graphql-server-startkit,或者官方的 demo。
若是你真实的使用过 Apollo GraphQL,你会经历以下过程:
定义一个 schema 用于描述查询入口
// schema.graphql type User { id: ID! name: String nick: String age: Int gender: String } type Query { user(id: ID!): User } schema { query: Query }
编写 resolver 解析对应类型
const resolvers = { Query: { user(root, { id }) { return getUser(id); } }, User: { nick({ id }) { return getUserNick(id); } } };
编写客户端请求代码调用 GraphQL 接口,一般咱们会封装一个 get 方法
function getUser(id) { // 以 axios 为例 return axios.post('/graphql', { query: 'query userQuery($id: ID!) {↵ user(id: $id) {↵ id↵ name↵ nick↵ }↵}', operationName: "userQuery", variables: {id}}); }
若是你的项目中加入了静态类型系统,那么你的代码可能就会变成这样:
// 以 ts 为例 interface User { id: number name: string nick: string age: number gender: string } function getUser(id: number): User { return axios.post('/graphql', { query: 'query userQuery($id: ID!) {↵ user(id: $id) {↵ id↵ name↵ nick↵ }↵}', operationName: "userQuery", variables: {id}}); }
写到这里你可能已经发现,不只是 entity 类型定义,就链接口的封装,咱们在服务端和客户端都重复了一遍(虽然一个用的 GraphQL Type Language 一个用的 TS)… 这仍是最简单的场景,若是业务模型复杂起来,你在两端须要重复的代码会更多(好比类型的嵌套定义和 resolve)。这时候你可能会想起 DRY 原则,而后开始思考有没有什么方式可使得类型及接口定义能两端复用,或者根据一端的定义自动生成另外一端的代码?甚至你开始怀疑,到底有没有引入 GraphQL 的必要?
GraphQL 做为一个标准化并自带类型系统的 API Layer,其工程价值我也再也不过多广告了。只是在实践过程当中,既然咱们没法彻底避免服务端与客户端的实体与接口定义重复(使用 apollo-codegen 能够避免一部分),并且对于大部分小团队而言,运维一个 productive nodejs system 实际上都是力有未逮。那么咱们是否是能够考虑在纯客户端构建一个类 GraphQL 的 API Layer 呢?这样既能够有效的避免编码重复,也能大大的下降对团队的要求,可操做的空间也比增长一个 nodejs 中间层大得多。
咱们能够回忆一下,一般对于一个前端而言,促使咱们须要一个 API Layer 的缘由是什么:
一般状况下,碰到这些问题,你可能去跟后端同窗力排众议,要求他们提供调用体验更良好设计更优雅的接口。没错这很好,毕竟为了追求完美去跟各类人撕(跟后端撕、跟产品撕、跟UI撕)是一个前端工程师基本的职业素养。可是若是你天天都被撕逼弄得心力交瘁,甚至是你根本找不到撕的对象(好比数据来源接口来着几个不一样部门,甚至是一些祖传的没人敢动的接口),这些时候大概就是你迫切但愿有一个 API Layer 的时候了。
其实很简单,你只须要在客户端把 Apollo Server 中要写的 resolvers 写一遍,而后配上一些性能提高手段(如缓存等),你的 API Layer 就完成了。
好比咱们在src
下新建一个 loaders/apis
目录,全部的数据拉取接口都放在这里。好比这样:
// UserLoader.ts export interface User { id: number name: string nick: string } export default class UserLoader { async getUser(id: number): User { const base = await Promise.all([http.get('//xxx.com/users/${id}'), this.getUserNick(id)]); const user = base.reduce((acc, info) => ({...acc, ...info}), {}); return user; } getUserNick(id: number): string { return http.get(`//xxx.com/nicks/${id}`); } }
而后在你业务须要的地方注入相应 loader 调用接口便可,如:
import { inject } from 'mmlpx'; import UserLoader from './UserLoader'; // Controller.ts export default class Controller { @inject(UserLoader) userLoader = null; async doSomething() { // ... const user = await this.userLoader.getUser(this.id); // ... } }
若是你不喜欢依赖注入的方式,loaders/apis 层直接 export function getUser
也能够。
若是你碰到了上面描述的第 三、4 、5 三种问题,你可能还须要在这一层作一下数据格式化。好比这样:
async getUser(id: number): User { const base = await Promise.all([http.get('//xxx.com/users/${id}'), this.getUserNick(id)]); const user = 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 开销受终端设备及网络环境影响,好比低版本浏览器或者低速网络,均会致使 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,毕竟两端重复的工做仍是有点多。