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工具查询界面。
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.js
和schema文件夹
, 在schema文件夹
文件夹下面新建info.js
和student.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.js
和 studen.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.js
和student.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.js
,studen.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.html
,js文件夹
,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/
就会看到如下界面。界面丑,没什么设计美化细胞,求轻喷~~~~
操作点击之后就会想第二张图一样了。
所有效果都出来了,本篇文章也就到此结束了。
附上项目地址: https://github.com/naihe138/GraphQL-demo
ps:喜欢的话丢一个小星星(star)给我嘛