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)给我嘛
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。