基于nodejs的Express框架以及GraphQL的JavaScript实现构建一个简单的GraphQL服务器

GraphQL是什么?

GraphQL 是Facebook开发的一个应用层查询语言. 后端定义基于图的模式. 客户端可以按需查询需要的数据。GraphQL也并不是一个具体的后端编程框架,如果将REST看做适合于简单逻辑的查询标准,那么GraphQL可以做一个独立的抽象层,通过对于多个REST风格的简单的接口的排列组合提供更多复杂多变的查询方式。与REST相比,GraphQL定义了更严格、可扩展、可维护的数据查询方式。

GraphQL

这个图展示的是查询流程,查询流程分为几个步骤, 涉及多个组件, 包括客户端应用程序(Web, 手机, 桌面等App), 一个GraphQL服务器用于解析查询, 以及多个不同的数据来源。

GraphQL与之前Netflix出品的Falcor,都是致力于解决相同的问题:如何有效处理日益增长不断变化的Web/Mobile端复杂的数据需求。REST最大的功劳在于前后端分离与无状态请求,而REST的资源化的请求方式只适合面向简单的请求,对于具有复杂资源间关联的请求就有点无能为力。

如果你是学习后端的应该知道REST API需要为每个请求匹配相应的数据需求。而GraphQL的优点在与客户端数据要求发生变化时, 不需要修改后端. 因此, 你不必因为客户端数据需求的变更而改变你的后端。这解决了管理REST API中的最大的问题。

为什么解决了REST API的大问题, 看如下阐述:

注解:
只要你的业务模型没有发生变化, 从数据模型不会发生变化, 那么我们就不需要修改后端API。 前端按照需要的字段进行查询即可。如果业务发生了变化, 我们只需要修改GraphQL的模式定义, 并且实现对应的服务器端数据查询逻辑即可。传统的REST查询那些字段是固定的, 客户端不能指定, GraphQL可以让客户端指定要获取那些字段的数据, 这给客户端带来了极大的灵活性, 让前后端进一步分离。查询是可以嵌套的, 返回的JSON对象结构和GraphQL查询的结构是一样的, 这样更方便客户端自己定义数据的结构.

例如我们常见的需求,获取了一篇博客文章和对应评论与作者信息的数据。

GraphQL同样能够让客户端程序高效地批量获取数据。 例如, 看一看下面这个GraphQL请求:

{
  latestPost {
    _id,
    title,
    content,
    author {
      name
    },
    comments {
      content,
      author {
        name
      }
    }
  }
}

这个 GraphQL 请求获取了一篇博客文章和对应评论与作者信息的数据。下面是请求的返回结果:

{
  "data": {
    "latestPost": {
      "_id": "03390abb5570ce03ae524397d215713b",
      "title": "New Feature: Tracking Error Status with Kadira",
      "content": "Here is a common feedback we received from our users ...",
      "author": {
        "name": "Pahan Sarathchandra"
      },
      "comments": [
        {
          "content": "This is a very good blog post",
          "author": {
            "name": "Arunoda Susiripala"
          }
        },
        {
          "content": "Keep up the good work",
          "author": {
            "name": "Kasun Indi"
          }
        }
      ]
    }
  }
}

如果你使用的是REST的话,你要么需要前端查询多次,要么需要去添加一个新的接口调用多个REST API的请求才能获取这些信息,专门针对前端这种较为特殊的请求进行响应,而这样又不可避免地导致后端代码的冗余,毕竟很有可能这个特殊的请求与返回哪天就被废了。

GraphQL是一个规范

GraphQL是一个规范,因此, 它可以用于任何平台或语言。它有一个参考的实现 JavaScript, 由Facebook维护。还有许多社区维护的实现有许多种语言。之前我们用简短的描述说明了GraphQL是什么, 对其有了一个基本的映像, 现在我们通过实际的操作来感受GraphQL具体是一个什么东西。

首先在浏览器中打开: https://sandbox.learngraphql.com ,我们会看到下图的GraphiQL查询界面, 其界面窗口如下所示:

GraphiQL查询界面

然后在左侧的查询窗口中输入下面的查询语句:

{
  posts (category: PRODUCT) {
    _id,
    title,
    summary
  }
}

然后右侧出现如下内容:

{
  "data": {
    "posts": [
      {
        "_id": "03390abb5570ce03ae524397d215713b",
        "title": "New Feature: Tracking Error Status with Kadira",
        "summary": "Lot of users asked us to add a feature to set status for errors in the Kadira Error Manager. Now, we've that functionality."
      },
      {
        "_id": "0be4bea0330ccb5ecf781a9f69a64bc8",
        "title": "What Should Kadira Build Next?",
        "summary": "We are working on the next few major feature releases for Kadira. We would like to know your preference. Pre-order the feature you would most like to see in the next major release (scheduled for August 1)."
      },
      {
        "_id": "19085291c89f0d04943093c4ff16b664",
        "title": "Awesome Error Tracking Solution for Meteor Apps with Kadira",
        "summary": "Error tracking is so much important and goes side by side with performance issues. This is the public beta announcement of Kadira's error tracking solution."
      },
      {
        "_id": "1afff9dfb0b97b5882c72cb60844e034",
        "title": "Tracking Meteor CPU Usage with Kadira",
        "summary": "We've replaced EventLoop Utilization chart with actual CPU Usage. See why?"
      },
      {
        "_id": "3d7a3853bf435c0f00e46e15257a94d9",
        "title": "Introducing Kadira Debug, Version 2",
        "summary": "Today, we are introducing a new version of Kadira Debug. It comes with many UI improvements and support for CPU profiling."
      }
    ]
  }
}

这个体验了一下GraphQL是怎么工作的。下面就该构建案例

官方的简单的Quick Start教程

这里是以nodejs为基础的,所以需要nodejs环境请自行准备。

首先创建项目文件夹,然后进入该文件在该目录里打开控制台,然后使用npm安装必要的依赖以及nodejs的express框架:

npm init // npm初始

再一路回车到entry point: (index.js)可以键入 app.js 或者你所希望的名称,这里我们输入app.js,这是当前应用的入口文件。再一路回车。然后为了配合graphql我们需要进行安装相关依赖:

npm install express --save // 装 Express 并将其保存到依赖列表中
npm install graphql express express-graphql --save // express-graphql
npm install babel --save
npm install body-parser --save

先创建一个名为 app.js 的文件,然后输入以下代码:

// Import the required libraries
var graphql = require('graphql');
var graphqlHTTP = require('express-graphql');
var express = require('express');

// Import the data you created above
var data = require('./data.json');

// 用“id”和“name”两个字符串字段定义User类型。User的类型是GraphQLObjectType,他的子字段具有自己的类型(在这种情况下,GraphQLString)。
var userType = new graphql.GraphQLObjectType({
  name: 'User',
  fields: {
    id: { type: graphql.GraphQLString },
    name: { type: graphql.GraphQLString },
  }
});

// 定义一个顶级字段架构“User”,它接收一个参数“id”,并根据ID,来返回用户。请注意,`query`是GraphQLObjectType,就像“User”。然而我们在上面定义的“user”这个字段,是一个userType。
var schema = new graphql.GraphQLSchema({
  query: new graphql.GraphQLObjectType({
    name: 'Query',
    fields: {
      user: {
        type: userType,
        // `args` describes the arguments that the `user` query accepts:` args `描述参数,接受` user`查询。
        args: {
          id: { type: graphql.GraphQLString }
        },
        // 怎么去"resolve" 或者实现解决函数的描述?传入查询,在这种情况下,我们使用从上面的“ID”参数作为一个key,获取来自'data'的'User'
        resolve: function (_, args) {
          return data[args.id];
        }
      }
    }
  })
});

express()
  .use('/graphql', graphqlHTTP({ schema: schema, pretty: true }))
  .listen(3000);

console.log('GraphQL server running on http://localhost:3000/graphql');

再创建一个名为data.json的文件,内容如下:

{
  "1": {
    "id": "1",
    "name": "Dan"
  },
  "2": {
    "id": "2",
    "name": "Marie"
  },
  "3": {
    "id": "3",
    "name": "Jessie"
  }
}

在你创建的目录下打开控制台,然后使用node命令启动服务器:

node app.js

运行app.js,默认会打开:3000端口,如果你直接访问http://localhost:3000/graphql页面会得到如下反馈:

{
  "errors": [
    {
      "message": "Must provide query string."
    }
  ]
}

然后我们在该目录下开启另一个控制台,键入以下内容:

curl -XPOST -H "Content-Type:application/graphql"  -d '{ user(id:"1"){name} }' http://localhost:3000/graphql

然后会得到如下反馈:

{
  "data": {
    "user": {
      "name": "Dan"
    }
  }
}

再来一个例子:

我们把刚才的app.js里面的内容进行注释,然后写入以下内容:

var express = require('express');
var app = express();

var schema = require('./schema');
var graphql = require('graphql');
var bodyParser = require('body-parser');

app.use(bodyParser.text({ type: 'application/graphql' }));

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

app.get('/', function (req, res) {
  res.send('Hello World!');
});

var server = app.listen(3000, function () {
  var host = server.address().address;
  var port = server.address().port;

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

同目录下新建一个文件:schema.js

var graphql = require('graphql');

let count = 0;

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

//把当前模块的路由暴露出去
module.exports = schema;

然后使用node命令启动服务器:

node app.js

运行app.js,然后我们在该目录下开启另一个控制台,键入以下内容:

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

最终会得到如下反馈:

{
  "data": {
    "count": 0
  }
}

Express 应用生成器创建简单的Quick Start应用

上面的案例是入门的,大部分情况下我们开发并不会这样使用,我们大部分情况下需要通过 Express 应用生成器创建应用,首先我们需要全局安装express:npm install express-generator -g,然后自己想一个文件名进行express初始化:express -e 你的文件名(英文),这里我起的名字为nodejs_express,然后初始化完成,它会提醒你如图:

express -e 你的文件名
cd 你的文件名 && npm install
DEBUG=你的文件名:* npm start // 说明:启动这个应用

然后在浏览器中打开 http://localhost:3000/ 网址就可以看到这个应用了。

通过 Express 应用生成器创建的应用一般都有如下目录结构:

初始目录

为了配合graphql我们需要进行安装相关依赖

npm install express --save
npm install graphql express express-graphql --save
npm install babel --save

安装完依赖我们在项目目录下新建两个文件一个是dao,一个是graphql,在dao文件下新建一个data.json文件,在graphql文件下新建index.js,效果如图:

目录

这里我们使用我们新建的文件和app.js,其他的文件暂时用不到。

/dao/data.json 还是上面的内容:

{
  "1": {
    "id": "1",
    "name": "Dan"
  },
  "2": {
    "id": "2",
    "name": "Marie"
  },
  "3": {
    "id": "3",
    "name": "Jessie"
  }
}

然后我们打开app.js,这里面是框架默认配置,我们需要在这里面引入我们刚才添加配置的依赖和我们需要的文件,在var app = express();这句代码上面添加这些代码:

//添加项
var ejs = require('ejs');

// 自需引入模块------
var graphql = require('graphql');
var graphqlHTTP = require('express-graphql');
var graphqlIndex = require('./graphql/index.js');
// 自需引入模块------

在它下面添加这些代码:

// 自需引入模块使用------
app.use('/graphql', graphqlHTTP({ schema: graphqlIndex, pretty: true }))
// 自需引入模块使用------

app.js的整体:

var express = require('express');
var path = require('path');
var favicon = require('serve-favicon');
var logger = require('morgan');
var cookieParser = require('cookie-parser');
var bodyParser = require('body-parser');

var index = require('./routes/index');
var users = require('./routes/users');

//添加项
var ejs = require('ejs');

// 自需引入模块------
var graphql = require('graphql');
var graphqlHTTP = require('express-graphql');
var graphqlIndex = require('./graphql/index.js');
// 自需引入模块------

var app = express();

// 自需引入模块使用------
app.use('/graphql', graphqlHTTP({ schema: graphqlIndex, pretty: true }))
// 自需引入模块使用------

// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');

// uncomment after placing your favicon in /public
//app.use(favicon(path.join(__dirname, 'public', 'favicon.ico')));
app.use(logger('dev'));
app.use(bodyParser.json());
app.use(bodyParser.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));

app.use('/', index);
app.use('/users', users);

// catch 404 and forward to error handler
app.use(function(req, res, next) {
  var err = new Error('Not Found');
  err.status = 404;
  next(err);
});

// error handler
app.use(function(err, req, res, next) {
  // set locals, only providing error in development
  res.locals.message = err.message;
  res.locals.error = req.app.get('env') === 'development' ? err : {};

  // render the error page
  res.status(err.status || 500);
  res.render('error');
});

module.exports = app;

/graphql/index.js 的内容如下:

// Import the required libraries
var graphql = require('graphql');

var express = require('express');

// Import the data you created above
var data = require('../dao/data.json');

// 用“id”和“name”两个字符串字段定义User类型。User的类型是GraphQLObjectType,他的子字段具有自己的类型(在这种情况下,GraphQLString)。
var userType = new graphql.GraphQLObjectType({
  name: 'User',
  fields: {
    id: { type: graphql.GraphQLString },
    name: { type: graphql.GraphQLString },
    id_card: { type: graphql.GraphQLString },
  }
});

// 定义一个顶级字段架构“User”,它接收一个参数“id”,并根据ID,来返回用户。请注意,`query`是GraphQLObjectType,就像“User”。然而我们在上面定义的“user”这个字段,是一个userType。
var schema = new graphql.GraphQLSchema({
  query: new graphql.GraphQLObjectType({
    name: 'Query',
    fields: {
      user: { // user-查询名
        type: userType,
        // `args` describes the arguments that the `user` query accepts:` args `描述参数,接受` user`查询。
        args: {
          id: { type: graphql.GraphQLString }
        },
        // 怎么去"resolve" 或者实现解决函数的描述?传入查询,在这种情况下,我们使用从上面的“ID”参数作为一个key,获取来自'data'的'User'
        resolve: function (_, args) {
          return data[args.id]; // 返回id
        }
      }
    }
  })
});
// curl -XPOST -H "Content-Type:application/graphql"  -d '{ user(id:"1"){name} }' http://localhost:3000/graphql

//把当前模块的路由暴露出去
module.exports = schema;

我们在该项目中使用DEBUG=你的文件名:* npm start的方式运行项目,打开:3000端口,如果你直接访问http://localhost:3000/graphql页面会得到如下反馈:

{
  "errors": [
    {
      "message": "Must provide query string."
    }
  ]
}

然后我们在该目录下开启另一个控制台,键入以下内容:

curl -XPOST -H "Content-Type:application/graphql"  -d '{ user(id:"1"){name} }' http://localhost:3000/graphql

然后会得到如下反馈:

{
  "data": {
    "user": {
      "name": "Dan"
    }
  }
}

小结:

到这里基本上构建一个简单的基于nodejs的Express框架及GraphQL的javascript实现的简单服务器。

同时在末尾放上参考网站,和相关学习网站:

GraphiQL入门
GraphiQL实验
GraphiQL入门指南
GraphiQL框架join-monster
搭建你的第一个 GraphQL 服务器
GraphQL什么鬼
GraphiQL官网
从REST到GraphQL的思维变迁
[GraphQL系列教程-英文](https://learngraphql.com/basics/introduction
一系列的关于GraphQL相关的资源的搜集
GraphQL and Authentication
GraphQL.js-GraphQL的JavaScript实现
expressjs

提示:后面还有精彩敬请期待,请大家关注我的专题:web前端。如有意见可以进行评论,每一条评论我都会认真对待。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 204,732评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,496评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,264评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,807评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,806评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,675评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,029评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,683评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 41,704评论 1 299
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,666评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,773评论 1 332
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,413评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,016评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,978评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,204评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,083评论 2 350
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,503评论 2 343

推荐阅读更多精彩内容