Node.js 服务端实践之 GraphQL 初探

0.问题来了

DT 时代,各种业务依赖强大的基础数据平台快速生长,如何高效地为各种业务提供数据支持,是所有人关心的问题。

现有的业务场景一般是这样的,业务方提出需求,然后寻找开发资源,由后端提供数据,让前端实现各种不同的业务视图。这样的做法存在很多的重复劳动,如果能够将其中通用的内容抽取出来提供给各个业务方反复使用,必然能够节省宝贵的开发时间和开发人力。

前端的解决方案是将视图组件化,各个业务线既可以是组件的使用者,也可以是组件的生产者。那么问题来了,前端通过组件实现了跨业务的复用,后端接口如何相应地提高开发效率呢?

我们假设某个业务需要以下数据内容 a:

{  user(id: 3500401) {    id,    name,    isViewerFriend  }}

对,这不是 JSON,但是我们仍然可以看懂它表示的是查询 id 为 3500401 用户的 id,name 和 isViewerFriend 信息。用户信息对于各个业务都是通用的,假设另外一个业务需要这样的用户信息 b:

{  user(id: 3500401) {    name,    profilePicture(size: 50)  {      uri,      width,      height    }  }}

对比一下,我们发现只是少了两个字段,多了一个字段而已。如果要实现我们的目标,即复用同一个接口来支持这两种业务的话,会有以下几种做法:

  1. 用同一个接口,这个接口提供了所有数据。这样做的好处是实现起来简单,但缺点是对业务做判断的逻辑会增多,而且对于业务来说,响应内容中有些数据根本用不到;
  2. 使用参数来区分不同的业务方并返回相应的数据。好处仍然是实现简单,虽然不会有用不到的数据返回,但是仍然需要增加业务逻辑判断,会造成以后维护的困难。

此外,这样还会造成不同业务之间的强依赖,每次发布都需要各个业务线一起测试和回归。不重用接口则没法提高开发效率,重用接口则会有这些问题,那么到底有没有“好一点”的解决方案呢?

这是我们在处理复杂的前后端分离中经常要面临的一个思考。

1.GraphQL,一种新的思路

我们知道,用户信息对应的数据模型是固定的,每次请求其实是对这些数据做了过滤和筛选。对应到数据库操作,就是数据的查询操作。如果客户端也能够像“查询”一样发送请求,那不就可以从后端接口这个大的“大数据库”去过滤筛选业务需要的数据了吗?

GraphQL 就是基于这样的思想来设计的。上面提到的(a)和(b)类型的数据结构就是 GraphQL 的查询内容。使用上面的查询,GraphQL 服务器会分别返回如下响应内容。

a 查询对应的响应:

{  "user" : {    "id": 3500401,    "name": "Jing Chen",    "isViewerFriend": true  }}

b 查询对应的响应:

{  "user" : {    "name": "Jing Chen",    "profilePicture": {      "uri": "http: //someurl.cdn/pic.jpg",      "width": 50,      "height": 50    }  }}

只需要改变查询内容,前端就能定制服务器返回的响应内容,这就是 GraphQL 的客户端指定查询(Client Specified Queries)。假如我们能够将基础数据平台做成一个 GraphQL 服务器,不就能为这个平台上的所有业务提供统一可复用的数据接口了吗?

了解了 GraphQL 的这些信息,我们一起来动手实践吧。

2.使用 Node.js 实现 GraphQL 服务器

我们先按照官方文档搭建一个 GraphQL 服务器:

$ mkdir graphql-intro && cd ./graphql-intro$ npm install express --save$ npm install babel --save$ touch ./server.js$ touch ./index.js

index.js 的内容如下:

//index.js//require `babel/register` to handle JavaScript coderequire('babel/register');require('./server.js');

server.js 的内容如下:

//server.jsimport express from 'express';

let app = express();let PORT = 3000;

app.post('/graphql', (req, res) => {  res.send('Hello!');});

let server = app.listen(PORT, function() {  let host = server.address().address;  let port = server.address().port;

console.log('GraphQL listening at http://%s:%s', host, port);});

然后执行代码: nodemon index.js:

如果没有安装 nodemon,需要先 npm install -g nodemon,也推荐使用 node-dev 模块

测试是否有效:

curl -XPOST http://localhost:3000/graphql

接着编写 GraphQL Schema

接下来是添加 GraphQL Schema(Schema 是 GraphQL 请求的入口,用户的 GraphQL 请求会对应到具体的 Schema),首先回忆一下 GraphQL 请求是这样的:

query getHightScore { score }

上面的请求是获取 getHightScore 的 score 值。也可以加上查询条件,例如:

query getHightScore(limit: 10) { score }

这样的请求格式就是 GraphQL 中的 schema。通过 schema 可以定义服务器的响应内容。

接下来我们在项目中使用 graphql:

npm install graphql --save

使用 body-parser 来处理请求内容:npm install body-parser --save。 而 graphql 这个 npm 包会负责组装服务器 schema 并处理 GraphQL 请求。

创建 schema:touch ./schema.js

//schema.jsimport {  GraphQLObjectType,  GraphQLSchema,  GraphQLInt} from 'graphql';

let count = 0;

let schema = new GraphQLSchema({  query: new GraphQLObjectType({    name: 'RootQueryType',    fields: {      count: {        type: GraphQLInt,        resolve: function() {          return count;        }      }    }  })});

export default schema;

这段代码创建了一个 GraphQLSchema 实例。这个 schema 的顶级查询对象会返回一个 RootQueryType 对象,这个 RootQueryType 对象有一个整数类型的 count 域。GraphQL 除了支持整数( Interger ),还支持字符串( String )、列表( List )等多种类型的数据。

连接 schema

下面是将 GraphQL schema 和服务器连接起来,我们需要修改 server.js 为如下所示:

//server.jsimport express from 'express';import schema from './schema';

import { graphql } from 'graphql';import bodyParser from 'body-parser';

let app = express();let PORT = 3000;

//Parse post content as textapp.use(bodyParser.text({ type: 'application/graphql' }));

app.post('/graphql', (req, res) => {  //GraphQL executor  graphql(schema, req.body)  .then((result) => {    res.send(JSON.stringify(result, null, 2));  })});

let server = app.listen(PORT, function() {  let host = server.address().address;  let port = server.address().port;

console.log('GraphQL listening at http://%s:%s', host, port);});

验证下效果:

curl -v -XPOST -H "Content-Type:application/graphql"  -d 'query RootQueryType { count }' http://localhost:3000/graphql

结果如下图所示:

GraphQL 查询还可以省略掉 query RootQueryType 前缀,即:

检查服务器

GraphQL 最让人感兴趣的是可以编写 GraphQL 查询来让 GraphQL 服务器告诉我们它支持那些查询,即官方文档提到的自检性(introspection)。

例如:

curl -XPOST -H 'Content-Type:application/graphql'  -d '{__schema { queryType { name, fields { name, description} }}}' http://localhost:3000/graphql

而我们实际的 GraphQL 查询请求内容为:

{  __schema {    queryType {      name,      fields {        name,        description      }    }  }}

基本上每个 GraphQL 根域都会自动加上一个 __schema 域,这个域有一个子域叫 queryTyp。我们可以通过查询这些域来了解 GraphQL 服务器支持那些查询。我们可以修改 schema.js 来为 count 域加上 description:

let schema = new GraphQLSchema({  query: new GraphQLObjectType({    name: 'RootQueryType',    fields: {      count: {        type: GraphQLInt,        //Add description        description: 'The count!',        resolve: function() {          return count;        }      }    }  })});

验证一下:

curl -XPOST -H 'Content-Type:application/graphql'  -d '{__schema { queryType { name, fields { name, description} }}}' http://localhost:3000/graphql

变异(mutation,即修改数据)

GraphQL中将对数据的修改操作称为 mutation。在 GraphQL Schema 中按照如下形式来定义一个 mutation:

let schema = new GraphQLSchema({  query: ...  mutation: //TODO});

mutation 查询和普通查询请求(query)的重要区别在于 mutation 操作是序列化执行的。例如 GraphQL 规范中给出的示例,服务器一定会序列化处理下面的 mutation 请求:

{  first: changeTheNumber(newNumber: 1) {    theNumber  },  second: changeTheNumber(newNumber: 3) {    theNumber  },  third: changeTheNumber(newNumber: 2) {    theNumber  }}

请求结束时 theNumber 的值会是 2。下面为我们的服务器添加一个 mutation 查询,修改 schema.js 为如下所示:

//schema.jsimport {  GraphQLObjectType,  GraphQLSchema,  GraphQLInt} from 'graphql';

let count = 0;

let schema = new GraphQLSchema({  query: new GraphQLObjectType({    name: 'RootQueryType',    fields: {      count: {        type: GraphQLInt,        //Add description        description: 'The count!',        resolve: function() {          return count;        }      }    }  }),  //Note:this is the newly added mutation query  mutation: new GraphQLObjectType({    name: 'RootMutationType',    fields: {      updateCount: {        type: GraphQLInt,        description: 'Update the count',        resolve: function() {          count += 1;          return count;        }      }    }  })});

export default schema;

验证:

curl -XPOST -H 'Content-Type:application/graphql' -d 'mutation RootMutationType { updateCount }' http://localhost:3000/graphql

搭建好 GraphQL 服务器后,我们来模拟下业务场景的实际需求,对于电商平台来说,最常用的就是商品信息,假设目前的商品数据模型可以用下面的 GraphQLObject 来表示:

var ItemType =  new GraphQLObjectType({  name: "item",  description: "item",  fields: {    id: {      type: GraphQLString,      description: "item id"    },    title: {      type: GraphQLString,      description: "item title"    },    price: {      type: GraphQLString,      description: "item price",      resolve: function(root, param, context) {        return (root.price/100).toFixed(2);      }    },    pic: {      type: GraphQLString,      description: "item pic url"    }  }});

查询商品的 schema 如下所示:

var ItemSchema = new GraphQLSchema({  query: {    name: "ItemQuery",    description: "query item",    fields: {      item: {        type: ItemType,        description: "item",        args: {          id: {            type: GraphQLInt,            required: true    //itemId required for query          }        },        resolve: function(root, obj, ctx) {          return yield ItemService(obj['id']);        }      }    }  }});

通过如下 query 可以查询 id 为 12345 的商品信息:

query ItemQuery(id: 12345){  id  title  price  pic}

商品详情页展示时需要加上优惠价格信息,我们可以修改 ItemType,为它加上一个 promotion 字段:

var ItemType =  new GraphQLObjectType({  name: "item",  description: "item",  fields: {    id: {      type: GraphQLString,      description: "item id"    },    title: {      type: GraphQLString,      description: "item title"    },    price: {      type: GraphQLString,      description: "item price",      resolve: function(root, param, context) {        return (root.price/100).toFixed(2);      }    },    pic: {      type: GraphQLString,      description: "item pic url"    },    promotion: {      type: GraphQLInt,      description: "promotion price"    }  }});

商品详情页的查询为:

query ItemQuery(id: 12345){  id  title  price  pic  promotion}

ItemSchema 无需修改,只要在 ItemService 的返回结果中加上 promotion 就可以了。这样接口的修改对于原有业务是透明的,而新的业务也能基于已有的代码快速开发和迭代。

再假设有一个新的页面,只需要用到宝贝的图片信息,业务方可以使用下面的查询:

query ItemQuery(id: 12345){  id  pic}

服务器代码不用做任何修改。

4.总结

至此我们已经实现了一个 GraphQL 基础服务器。在实际业务中数据模型肯定会更加复杂,而 GraphQL 也提供了强大的类型系统(Type System)让我们能够轻松地描述各种数据模型,它提供的抽象层能够为依赖同一套数据模型的不同业务方提供灵活的数据支持。关于 GraphQL 在淘宝更多的生产实践,请持续关注我们博客未来的系列文章。

参考资料

该文章来自:http://taobaofed.org/blog/2015/11/26/graphql-basics-server-implementation/作者:云翮

时间: 2024-09-12 00:00:11

Node.js 服务端实践之 GraphQL 初探的相关文章

android客户端和js服务端RSA加密解密

问题描述 android客户端和js服务端RSA加密解密 android客户端利用js服务端的公钥加密数据再发给js服务端解密,可是js客户端总返回给我解密失败,找不到问题在哪,求大神指教!!!

Node.js:Windows7下搭建的Node.js服务(来玩玩服务器端的javascript吧,这可不是前端js插件)_javascript技巧

这里只是纯粹的搭建,连环境都没有,还玩什么服务器端js,一切都成了浮云,让我们先搭建一个环境,输入一个"hello world"的页面. 对的,windows7下的搭建,你只需一步一步跟着我做,就ok了,不用去了解过多的细节,那不是我们现在要关心的,我们现在首要目的是把环境搭建好,要不然就没有下一步了. Step 1. 下载node.js在windows下是要安装在Cygwin下的,去Cygwin网站下载Cygwin安装程序. Cygwin网站:http://cygwin.com/ 直

Node.js应用之静态文件分发器

我不久之前翻译过一篇文章: asp.net使用httphandler打包多CSS或JS文件以加快页面加载速度 采用打包并压缩和在浏览器与客户端同时构建缓存的技术,来对页面的加载进行优化.最近在学习Node.js,下面我们来看看Node.js在这方面能做些什么. Node.js的优势是网络通信.I/O不阻塞,可见它是高并发需求的有效解决方案.在Web开发中有许多文件是静态文件,例如CSS文件.JS文件.对它们的请求,通常是页面加载到客户端后,浏览器重新发出的异步请求.通常Web服务器能处理的并发请

使用Node.js完成的第一个项目的实践总结

项目简介 这是一个资产管理项目,主要的目的就是实现对资产的无纸化管理.通过为每个资产生成二维码,来联合移动终端完成对资产的审核等.这个项目既提供了Web端的管理界面也提供移动端(Andorid)的资产审核.派发等相关功能. 我们用Node.js构建该项目的Web端以及移动端的Serveice API. 项目主框架:Express 简介 Express 是一个非常流行的node.js的web框架.基于connect(node中间件框架).提供了很多便于处理http请求等web开发相关的扩展. Ex

[译] Node.js 之战: 如何在生产环境中调试错误

本文讲的是[译] Node.js 之战: 如何在生产环境中调试错误, 原文地址:Node.js War Stories: Debugging Issues in Production 原文作者:Gergely Nemeth 译文出自:掘金翻译计划 译者:mnikn 校对者:lsvih.Aladdin-ADD Node.js 之战: 在生产环境中调试错误 在这篇文章,这篇文章讲述了 Netflix.RisingStack 和 nearForm 在生产环境中遇到 Node.js 错误的故事 - 因此

shell脚本转发80端口数据包给Node.js服务器_linux shell

注意:千万不要图省事直接使用ROOT用户运行Node.js服务!这将带来无法预计的安全问题!但是使用80端口作为HTTP默认端口这一习惯是从MS时代就延续至今的,怎么办呢?网上有人滔滔不绝地说用NginX做反向代理之类的,其实我觉得没必要这么夸张,只需要使用ROOT用户做一个普通端口与80端口的数据转发就好了,使用iptables语句如下: 复制代码 代码如下: iptables -t nat -A PREROUTING -p tcp --dport 80 -j REDIRECT --to-po

使用Node.js实现一个简单的FastCGI服务器实例_node.js

本文是我最近对Node.js学习过程中产生的一个想法,提出来和大家一起探讨. Node.js的HTTP服务器 使用Node.js可以非常容易的实现一个http服务,最简的例子如官方网站的示例: 复制代码 代码如下: var http = require('http');http.createServer(function (req, res) {    res.writeHead(200, {'Content-Type': 'text/plain'});    res.end('Hello Wo

Apache 代理 Node.js 服务器

我在安装 Ghost 博客时,需要转发 Apache 请求给 node.js 服务器,当时为了快速搞定,找了些资料,拷了些配置,看它可以运行,也就没再搭理. 说来,我根本不知道为什么要加个 ProxyPassReverse. 前些天写的 blog React.js 服务端渲染里,同样需要让示例在服务器上运行.与安装 Ghost 时唯一的区别是,我的 blog 现在已经换成了 https. 所以配置过程是这样的: 打开 /etc/httpd/conf.d/vhost.conf 文件,这是 Cent

「八面玲珑的 Node.js」 - Node 地下铁第三期广州站线下沙龙总结

前言 转眼2016年就要结束了,距上次地下铁沙龙已经过去了大半年,我们在这冬天来到温暖的广州,跟朋友们相聚一堂,一起学习探讨开发Nodejs过程中的心得,以及Nodejs领域内的新动向. 本次活动受广州地主UC前端团队的大力支持,由UC提供了会场,博文视点.图灵.稀土掘金提供了本次活动的赞助.  广州的同学非常热情,会场里面座无虚席. 回顾 Thrift下的Node.js跨语言异构 Node.js 越来越成为主流选型,在实际工作和复杂的历史遗留问题中,往往需要使用 Node.js 和其它服务,组