GraphQL 搭配 Koa 最佳入门实践

17,473 阅读15分钟

【文章已过时了,可以直接看代码,有最新更新哦】

GraphQL一种用为你 API 而生的查询语言,2018已经到来,PWA还没有大量投入生产应用之中就已经火起来了,GraphQL的应用或许也不会太远了。前端的发展的最大一个特点就是变化快,有时候应对各种需求场景的变化,不得不去对接口开发很多版本或者修改。各种业务依赖强大的基础数据平台快速生长,如何高效地为各种业务提供数据支持,是所有人关心的问题。而且现在前端的解决方案是将视图组件化,各个业务线既可以是组件的使用者,也可以是组件的生产者,如果能够将其中通用的内容抽取出来提供给各个业务方反复使用,必然能够节省宝贵的开发时间和开发人力。那么问题来了,前端通过组件实现了跨业务的复用,后端接口如何相应地提高开发效率呢?GraphQL,就是应对复杂场景的一种新思路。

官方解释:

GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。

下面介绍一下GraphQL的有哪些好处:

  • 请求你所要的数据不多不少

  • 获取多个资源只用一个请求

  • 自定义接口数据的字段

  • 强大的开发者工具

  • API 演进无需划分版本

本篇文章中将搭配koa实现一个GraphQL查询的例子,逐步从简单kao服务到mongodb的数据插入查询再到GraphQL的使用, 让大家快速看到:

  • 搭建koa搭建一个后台项目
  • 后台路由简单处理方式
  • 利用mongoose简单操作mongodb
  • 掌握GraphQL的入门姿势

项目如下图所示

1、搭建GraphQL工具查询界面。

2、前端用jq发送ajax的使用方式

入门项目我们都已经是预览过了,下面我们动手开发吧!!!

lets do it

首先建立一个项目文件夹,然后在这个项目文件夹新建一个server.js(node服务)、config文件夹mongodb文件夹router文件夹controllers文件夹以及public文件夹(这个主要放前端静态数据展示页面),好啦,项目的结构我们都已经建立好,下面在server.js文件夹里写上

server.js

// 引入模块
import Koa from 'koa'
import KoaStatic from 'koa-static'
import Router from 'koa-router'
import bodyParser from 'koa-bodyparser'


const app = new Koa()
const router = new Router();

// 使用 bodyParser 和 KoaStatic 中间件
app.use(bodyParser());
app.use(KoaStatic(__dirname + '/public'));

// 路由设置test
router.get('/test', (ctx, next) => {
  ctx.body="test page"
});

app
  .use(router.routes())
  .use(router.allowedMethods());

app.listen(4000);

console.log('graphQL server listen port: ' + 4000)

在命令行npm install koa koa-static koa-router koa-bodyparser --save

安装好上面几个模块,

然后运行node server.js,不出什么意外的话,你会发现报如下图的一个error

原因是现在的node版本并没有支持es6的模块引入方式。

放心 我们用神器babel-polyfill转译一下就阔以了。详细的请看阮一峰老师的这篇文章

下面在项目文件夹新建一个start.js,然后在里面写上以下代码:

start.js

require('babel-core/register')({
  'presets': [
    'stage-3',
    ["latest-node", { "target": "current" }]
  ]
})

require('babel-polyfill')
require('./server')

然后 在命令行,运行npm install babel-core babel-polyfill babel-preset-latest-node babel-preset-stage-3 --save-dev安装几个开发模块。

安装完毕之后,在命令行运行 node start.js,之后你的node服务安静的运行起来了。用koa-router中间件做我们项目路由模块的管理,后面会写到router文件夹中统一管理。

打开浏览器,输入localhost:4000/test,你就会发现访问这个路由node服务会返回test page文字。如下图

yeah~~kao服务器基本搭建好之后,下面就是,链接mongodb然后把数据存储到mongodb数据库里面啦。

实现mongodb的基本数据模型

tip:这里我们需要mongodb存储数据以及利用mongoose模块操作mongodb数据库

  • mongodb文件夹新建一个index.jsschema文件夹, 在 schema文件夹文件夹下面新建info.jsstudent.js

  • config文件夹下面建立一个index.js,这个文件主要是放一下配置代码。

又一波文件建立好之后,先在config/index.js下写上链接数据库配置的代码。

config/index.js

export default {
  dbPath: 'mongodb://localhost/graphql'
}

然后在mongodb/index.js下写上链接数据库的代码。

mongodb/index.js

// 引入mongoose模块
import mongoose from 'mongoose'
import config from '../config'

// 同步引入 info model和 studen model
require('./schema/info')
require('./schema/student')

// 链接mongodb
export const database = () => {
  mongoose.set('debug', true)

  mongoose.connect(config.dbPath)

  mongoose.connection.on('disconnected', () => {
    mongoose.connect(config.dbPath)
  })
  mongoose.connection.on('error', err => {
    console.error(err)
  })

  mongoose.connection.on('open', async () => {
    console.log('Connected to MongoDB ', config.dbPath)
  })
}

上面我们我们代码还加载了info.jsstuden.js这两个分别是学生的附加信息和基本信息的数据模型,为什么会分成两个信息表?原因是顺便给大家介绍一下联表查询的基本方法(嘿嘿~~~)

下面我们分别完成这两个数据模型

mongodb/schema/info.js

// 引入mongoose
import mongoose from 'mongoose'

// 
const Schema = mongoose.Schema

// 实例InfoSchema
const InfoSchema = new Schema({
  hobby: [String],
  height: String,
  weight: Number,
  meta: {
    createdAt: {
      type: Date,
      default: Date.now()
    },
    updatedAt: {
      type: Date,
      default: Date.now()
    }
  }
})
// 在保存数据之前跟新日期
InfoSchema.pre('save', function (next) {
  if (this.isNew) {
    this.meta.createdAt = this.meta.updatedAt = Date.now()
  } else {
    this.meta.updatedAt = Date.now()
  }

  next()
})
// 建立Info数据模型
mongoose.model('Info', InfoSchema)

上面的代码就是利用mongoose实现了学生的附加信息的数据模型,用同样的方法我们实现了student数据模型

mongodb/schema/student.js

import mongoose from 'mongoose'

const Schema = mongoose.Schema
const ObjectId = Schema.Types.ObjectId


const StudentSchema = new Schema({
  name: String,
  sex: String,
  age: Number,
  info: {
    type: ObjectId,
    ref: 'Info'
  },
  meta: {
    createdAt: {
      type: Date,
      default: Date.now()
    },
    updatedAt: {
      type: Date,
      default: Date.now()
    }
  }
})

StudentSchema.pre('save', function (next) {
  if (this.isNew) {
    this.meta.createdAt = this.meta.updatedAt = Date.now()
  } else {
    this.meta.updatedAt = Date.now()
  }

  next()
})

mongoose.model('Student', StudentSchema)

实现保存数据的控制器

数据模型都链接好之后,我们就添加一些存储数据的方法,这些方法都写在控制器里面。然后在controler里面新建info.jsstudent.js,这两个文件分别对象,操作info和student数据的控制器,分开写为了方便模块化管理。

  • 实现info数据信息的保存,顺便把查询也先写上去,代码很简单

controlers/info.js

import mongoose from 'mongoose'
const Info = mongoose.model('Info')

// 保存info信息
export const saveInfo = async (ctx, next) => {
  // 获取请求的数据
  const opts = ctx.request.body
  
  const info = new Info(opts)
  const saveInfo = await info.save() // 保存数据
  console.log(saveInfo)
  // 简单判断一下 是否保存成功,然后返回给前端
  if (saveInfo) {
    ctx.body = {
      success: true,
      info: saveInfo
    }
  } else {
    ctx.body = {
      success: false
    }
  }
}

// 获取所有的info数据
export const fetchInfo = async (ctx, next) => {
  const infos = await Info.find({}) // 数据查询

  if (infos.length) {
    ctx.body = {
      success: true,
      info: infos
    }
  } else {
    ctx.body = {
      success: false
    }
  }
}

上面的代码,就是前端用post(路由下面一会在写)请求过来的数据,然后保存到mongodb数据库,在返回给前端保存成功与否的状态。也简单实现了一下,获取全部附加信息的的一个方法。下面我们用同样的道理实现studen数据的保存以及获取。

  • 实现studen数据的保存以及获取

controllers/sdudent.js

import mongoose from 'mongoose'
const Student = mongoose.model('Student')

// 保存学生数据的方法
export const saveStudent = async (ctx, next) => {
  // 获取前端请求的数据
  const opts = ctx.request.body
  
  const student = new Student(opts)
  const saveStudent = await student.save() // 保存数据

  if (saveStudent) {
    ctx.body = {
      success: true,
      student: saveStudent
    }
  } else {
    ctx.body = {
      success: false
    }
  }
}

// 查询所有学生的数据
export const fetchStudent = async (ctx, next) => {
  const students = await Student.find({})

  if (students.length) {
    ctx.body = {
      success: true,
      student: students
    }
  } else {
    ctx.body = {
      success: false
    }
  }
}

// 查询学生的数据以及附加数据
export const fetchStudentDetail = async (ctx, next) => {

  // 利用populate来查询关联info的数据
  const students = await Student.find({}).populate({
    path: 'info',
    select: 'hobby height weight'
  }).exec()

  if (students.length) {
    ctx.body = {
      success: true,
      student: students
    }
  } else {
    ctx.body = {
      success: false
    }
  }
}

实现路由,给前端提供API接口

数据模型和控制器在上面我们都已经是完成了,下面就利用koa-router路由中间件,来实现请求的接口。我们回到server.js,在上面添加一些代码。如下

server.js

import Koa from 'koa'
import KoaStatic from 'koa-static'
import Router from 'koa-router'
import bodyParser from 'koa-bodyparser'

import {database} from './mongodb' // 引入mongodb
import {saveInfo, fetchInfo} from './controllers/info' // 引入info controller
import {saveStudent, fetchStudent, fetchStudentDetail} from './controllers/student' // 引入 student controller

database() // 链接数据库并且初始化数据模型

const app = new Koa()
const router = new Router();

app.use(bodyParser());
app.use(KoaStatic(__dirname + '/public'));

router.get('/test', (ctx, next) => {
  ctx.body="test page"
});

// 设置每一个路由对应的相对的控制器
router.post('/saveinfo', saveInfo)
router.get('/info', fetchInfo)

router.post('/savestudent', saveStudent)
router.get('/student', fetchStudent)
router.get('/studentDetail', fetchStudentDetail)

app
  .use(router.routes())
  .use(router.allowedMethods());

app.listen(4000);

console.log('graphQL server listen port: ' + 4000)

上面的代码,就是做了,引入mongodb设置,info以及student控制器,然后链接数据库,并且设置每一个设置每一个路由对应的我们定义的的控制器。

安装一下mongoose模块 npm install mongoose --save

然后在命令行运行node start,我们服务器运行之后,然后在给info和student添加一些数据。这里是通过postman的谷歌浏览器插件来请求的,如下图所示

yeah~~~保存成功,继续按照步骤多保存几条,然后按照接口查询一下。如下图

嗯,如图都已经查询到我们保存的全部数据,并且全部返回前端了。不错不错。下面继续保存学生数据。

tip: 学生数据保存的时候关联了信息里面的数据哦。所以把id写上去了。

同样的一波操作,我们多保存学生几条信息,然后查询学生信息,如下图所示。

好了 ,数据我们都已经保存好了,铺垫也做了一大把了,下面让我们真正的进入,GrapgQL查询的骚操作吧~~~~

重构路由,配置GraphQL查询界面

别忘了,下面我们建立了一个router文件夹,这个文件夹就是统一管理我们路由的模块,分离了路由个应用服务的模块。在router文件夹新建一个index.js。并且改造一下server.js里面的路由全部复制到router/index.js

顺便在这个路由文件中加入,graphql-server-koa模块,这是koa集成的graphql服务器模块。graphql server是一个社区维护的开源graphql服务器,可以与所有的node.js http服务器框架一起工作:express,connect,hapi,koa和restify。可以点击链接查看详细知识点。

加入graphql-server-koa的路由文件代码如下:

router/index.js


import { graphqlKoa, graphiqlKoa } from 'graphql-server-koa'
import {saveInfo, fetchInfo} from '../controllers/info'
import {saveStudent, fetchStudent, fetchStudentDetail} from '../controllers/student'


const router = require('koa-router')()

router.post('/saveinfo', saveInfo)
      .get('/info', fetchInfo)
      .post('/savestudent', saveStudent)
      .get('/student', fetchStudent)
      .get('/studentDetail', fetchStudentDetail)
      .get('/graphiql', async (ctx, next) => {
        await graphiqlKoa({endpointURL: '/graphql'})(ctx, next)
      })
module.exports = router

之后把server.js的路由代码去掉之后的的代码如下:

server.js


import Koa from 'koa'
import KoaStatic from 'koa-static'
import Router from 'koa-router'
import bodyParser from 'koa-bodyparser'

import {database} from './mongodb'

database()

const GraphqlRouter = require('./router')

const app = new Koa()
const router = new Router();

const port = 4000

app.use(bodyParser());
app.use(KoaStatic(__dirname + '/public'));

router.use('', GraphqlRouter.routes())

app.use(router.routes())
   .use(router.allowedMethods());

app.listen(port);

console.log('GraphQL-demo server listen port: ' + port)

恩,分离之后简洁,明了了很多。然后我们在重新启动node服务。在浏览器地址栏输入http://localhost:4000/graphiql,就会得到下面这个界面。如图:

没错,什么都没有 就是GraphQL查询服务的界面。下面我们把这个GraphQL查询服务完善起来。

编写GraphQL Schema

看一下我们第一张图,我们需要什么数据,在GraphQL查询界面就编写什么字段,就可以查询到了,而后端需要定义好这些数据格式。这就需要我们定义好GraphQL Schema。

首先我们在根目录新建一个graphql文件夹,这个文件夹用于存放管理graphql相关的js文件。然后在graphql文件夹新建一个schema.js

这里我们用到graphql模块,这个模块就是用javascript参考实现graphql查询。向需要详细学习,请使劲戳链接。

我们先写好info的查询方法。然后其他都差不多滴。

graphql/schema.js


// 引入GraphQL各种方法类型

import {
  graphql,
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLString,
  GraphQLID,
  GraphQLList,
  GraphQLNonNull,
  isOutputType
} from 'graphql';

import mongoose from 'mongoose'
const Info = mongoose.model('Info') // 引入Info模块

// 定义日期时间 类型
const objType = new GraphQLObjectType({
  name: 'mete',
  fields: {
    createdAt: {
      type: GraphQLString
    },
    updatedAt: {
      type: GraphQLString
    }
  }
})

// 定义Info的数据类型
let InfoType = new GraphQLObjectType({
  name: 'Info',
  fields: {
    _id: {
      type: GraphQLID
    },
    height: {
      type: GraphQLString
    },
    weight: {
      type: GraphQLString
    },
    hobby: {
      type: new GraphQLList(GraphQLString)
    },
    meta: {
      type: objType
    }
  }
})

// 批量查询
const infos = {
  type: new GraphQLList(InfoType),
  args: {},
  resolve (root, params, options) {
    return Info.find({}).exec() // 数据库查询
  }
}

// 根据id查询单条info数据

const info = {
  type: InfoType,
  // 传进来的参数
  args: {
    id: {
      name: 'id',
      type: new GraphQLNonNull(GraphQLID) // 参数不为空
    }
  },
  resolve (root, params, options) {
    return Info.findOne({_id: params.id}).exec() // 查询单条数据
  }
}

// 导出GraphQLSchema模块

export default new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Queries',
    fields: {
      infos,
      info
    }
  })
})


看代码的时候建议从下往上看~~~~,上面代码所说的就是,建立info和infos的GraphQLSchema,然后定义好数据格式,查询到数据,或者根据参数查询到单条数据,然后返回出去。

写好了info schema之后 我们在配置一下路由,进入router/index.js里面,加入下面几行代码。

router/index.js


import { graphqlKoa, graphiqlKoa } from 'graphql-server-koa'
import {saveInfo, fetchInfo} from '../controllers/info'
import {saveStudent, fetchStudent, fetchStudentDetail} from '../controllers/student'

// 引入schema
import schema from '../graphql/schema'

const router = require('koa-router')()

router.post('/saveinfo', saveInfo)
      .get('/info', fetchInfo)
      .post('/savestudent', saveStudent)
      .get('/student', fetchStudent)
      .get('/studentDetail', fetchStudentDetail)




router.post('/graphql', async (ctx, next) => {
        await graphqlKoa({schema: schema})(ctx, next) // 使用schema
      })
      .get('/graphql', async (ctx, next) => {
        await graphqlKoa({schema: schema})(ctx, next) // 使用schema
      })
      .get('/graphiql', async (ctx, next) => {
        await graphiqlKoa({endpointURL: '/graphql'})(ctx, next) // 重定向到graphiql路由
      })
module.exports = router

详细请看注释,然后被忘记安装好npm install graphql-server-koa graphql --save这两个模块。安装完毕之后,重新运行服务器的node start(你可以使用nodemon来启动本地node服务,免得来回启动。)

然后刷新http://localhost:4000/graphiql,你会发现右边会有查询文档,在左边写上查询方式,如下图

重整Graphql代码结构,完成所有数据查询

现在是我们把schema和type都写到一个文件上面了去了,如果数据多了,字段多了变得特别不好维护以及review,所以我们就把定义type的和schema分离开来,说做就做。

graphql文件夹新建info.jsstuden.js,文件,先把info type 写到info.js代码如下

graphql/info.js

import {
  graphql,
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLString,
  GraphQLID,
  GraphQLList,
  GraphQLNonNull,
  isOutputType
} from 'graphql';

import mongoose from 'mongoose'
const Info = mongoose.model('Info')


const objType = new GraphQLObjectType({
  name: 'mete',
  fields: {
    createdAt: {
      type: GraphQLString
    },
    updatedAt: {
      type: GraphQLString
    }
  }
})

export let InfoType = new GraphQLObjectType({
  name: 'Info',
  fields: {
    _id: {
      type: GraphQLID
    },
    height: {
      type: GraphQLString
    },
    weight: {
      type: GraphQLString
    },
    hobby: {
      type: new GraphQLList(GraphQLString)
    },
    meta: {
      type: objType
    }
  }
})


export const infos = {
  type: new GraphQLList(InfoType),
  args: {},
  resolve (root, params, options) {
    return Info.find({}).exec()
  }
}


export const info = {
  type: InfoType,
  args: {
    id: {
      name: 'id',
      type: new GraphQLNonNull(GraphQLID)
    }
  },
  resolve (root, params, options) {
    return Info.findOne({
      _id: params.id
    }).exec()
  }
}

分离好info type 之后,一鼓作气,我们顺便把studen type 也完成一下,代码如下,原理跟info type 都是相通的,

graphql/student.js


import {
  graphql,
  GraphQLSchema,
  GraphQLObjectType,
  GraphQLString,
  GraphQLID,
  GraphQLList,
  GraphQLNonNull,
  isOutputType,
  GraphQLInt
} from 'graphql';

import mongoose from 'mongoose'

import {InfoType} from './info'
const Student = mongoose.model('Student')


let StudentType = new GraphQLObjectType({
  name: 'Student',
  fields: {
    _id: {
      type: GraphQLID
    },
    name: {
      type: GraphQLString
    },
    sex: {
      type: GraphQLString
    },
    age: {
      type: GraphQLInt
    },
    info: {
      type: InfoType
    }
  }
})


export const student = {
  type: new GraphQLList(StudentType),
  args: {},
  resolve (root, params, options) {
    return Student.find({}).populate({
      path: 'info',
      select: 'hobby height weight'
    }).exec()
  }
}


tips: 上面因为有了联表查询,所以引用了info.js

然后调整一下schema.js的代码,如下:


import {
  GraphQLSchema,
  GraphQLObjectType
} from 'graphql';
// 引入 type 
import {info, infos} from './info'
import {student} from './student'

// 建立 schema
export default new GraphQLSchema({
  query: new GraphQLObjectType({
    name: 'Queries',
    fields: {
      infos,
      info,
      student
    }
  })
})

看到代码是如此的清新脱俗,是不是深感欣慰。好了,graophql数据查询都已经是大概比较完善了。 课程的数据大家可以自己写一下,或者直接到我的github项目里面copy过来我就不一一重复的说了。

下面写一下前端接口是怎么查询的,然后让数据返回浏览器展示到页面的。

前端接口调用

public文件夹下面新建一个index.htmljs文件夹css文件夹,然后在js文件夹建立一个index.js, 在css文件夹建立一个index.css,代码如下

public/index.html


<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <title>GraphQL-demo</title>
  <link rel="stylesheet" href="./css/index.css">
</head>
<body>
  <h1 class="app-title">GraphQL-前端demo</h1>
  <div id="app">
    <div class="course list">
      <h3>课程列表</h3>
      <ul id="courseList">
        <li>暂无数据....</li>
      </ul>
    </div>
    <div class="student list">
      <h3>班级学生列表</h3>
      <ul id="studentList">
        <li>暂无数据....</li>
      </ul>
    </div>
  </div>
  <div class="btnbox">
    <div class="btn" id="btn1">点击常规获取课程列表</div>
    <div class="btn" id="btn2">点击常规获取班级学生列表</div>
    <div class="btn" id="btn3">点击graphQL一次获取所有数据,问你怕不怕?</div>
  </div>
  <div class="toast"></div>
  <script src="https://cdn.bootcss.com/jquery/1.10.2/jquery.js"></script>
  <script src="./js/index.js"></script>
</body>
</html>

我们主要看js请求方式 代码如下


window.onload = function () {

  $('#btn2').click(function() {
    $.ajax({
      url: '/student',
      data: {},
      success:function (res){
        if (res.success) {
          renderStudent (res.data)
        }
      }
    })
  })

  $('#btn1').click(function() {
    $.ajax({
      url: '/course',
      data: {},
      success:function (res){
        if (res.success) {
          renderCourse(res.data)
        }
      }
    })
  })

  function renderStudent (data) {
    var str = ''
    data.forEach(function(item) {
      str += '<li>姓名:'+item.name+',性别:'+item.sex+',年龄:'+item.age+'</li>'
    })
    $('#studentList').html(str)
  }

  function renderCourse (data) {
    var str = ''
    data.forEach(function(item) {
      str += '<li>课程:'+item.title+',简介:'+item.desc+'</li>'
    })
    $('#courseList').html(str)
  }
  
  // 请求看query参数就可以了,跟查询界面的参数差不多

  $('#btn3').click(function() {
    $.ajax({
      url: '/graphql',
      data: {
        query: `query{
          student{
            _id
            name
            sex
            age
          }
          course{
            title
            desc
          }
        }`
      },
      success:function (res){
        renderStudent (res.data.student)
        renderCourse (res.data.course)
      }
    })
  })
}

css的代码 我就不贴出来啦。大家可以去项目直接拿嘛。

所有东西都已经完成之后,重新启动node服务,然后访问,http://localhost:4000/就会看到如下界面。界面丑,没什么设计美化细胞,求轻喷~~~~

操作点击之后就会想第二张图一样了。

所有效果都出来了,本篇文章也就到此结束了。

附上项目地址: github.com/naihe138/Gr…

ps:喜欢的话丢一个小星星(star)给我嘛