71

GraphQL 搭配 Koa 最佳入门实践

 6 years ago
source link: https://segmentfault.com/a/1190000012720317?amp%3Butm_medium=referral
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

GraphQL 搭配 Koa 最佳入门实践

代码有更新,点击直接查看

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

官方解释:

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

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

  • 请求你所要的数据不多不少
  • 获取多个资源只用一个请求
  • 自定义接口数据的字段
  • 强大的开发者工具
  • API 演进无需划分版本

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

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

项目如下图所示

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

1460000021276341

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

1460000021276342

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

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

1460000021276343

原因是现在的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文字。如下图

1460000021276344

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/就会看到如下界面。界面丑,没什么设计美化细胞,求轻喷~~~~

1460000021276342

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

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

附上项目地址: https://github.com/naihe138/GraphQL-demo

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


About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK