GraphQL 一种用为你 API 而生的查询语言,2018 已经到来,PWA 还没有大量投入生产应用之中就已经火起来了,GraphQL 的应用或许也不会太远了。前端的发展的最大一个特点就是变化快,有时候应对各种需求场景的变化,不得不去对接口开发很多版本或者修改。各种业务依赖强大的基础数据平台快速生长,如何高效地为各种业务提供数据支持,是所有人关心的问题。而且现在前端的解决方案是将视图组件化,各个业务线既可以是组件的使用者,也可以是组件的生产者,如果能够将其中通用的内容抽取出来提供给各个业务方反复使用,必然能够节省宝贵的开发时间和开发人力。那么问题来了,前端通过组件实现了跨业务的复用,后端接口如何相应地提高开发效率呢?GraphQL,就是应对复杂场景的一种新思路。
官方解释:
GraphQL 既是一种用于 API 的查询语言也是一个满足你数据查询的运行时。 GraphQL 对你的 API 中的数据提供了一套易于理解的完整描述,使得客户端能够准确地获得它需要的数据,而且没有任何冗余,也让 API 更容易地随着时间推移而演进,还能用于构建强大的开发者工具。
下面介绍一下 GraphQL 的有哪些好处:
本篇文章中将搭配 koa 实现一个 GraphQL 查询的例子,逐步从简单 kao 服务到 mongodb 的数据插入查询再到 GraphQL 的使用,
让大家快速看到:
项目如下图所示
1、搭建 GraphQL 工具查询界面。
2、前端用 jq 发送 ajax 的使用方式
入门项目我们都已经是预览过了,下面我们动手开发吧!!!
首先建立一个项目文件夹,然后在这个项目文件夹新建一个 server.js(node 服务)、config 文件夹、mongodb 文件夹、router 文件夹、controlers 文件夹以及 publick 文件夹 (这个主要放前端静态数据展示页面),好啦,项目的结构我们都已经建立好,下面在 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 数据库里面啦。
top:这里我们需要 mongodb 存储数据以及利用 mongoose 模块操作 mongodb 数据库
又一波文件建立好之后,先在 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 数据的控制器,分开写为了方便模块化管理。
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 数据的保存以及获取。
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
- }
- }
- }
数据模型和控制器在上面我们都已经是完成了,下面就利用 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 查询的骚操作吧~~~~
别忘了,下面我们建立了一个 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 查询界面就编写什么字段,就可以查询到了,而后端需要定义好这些数据格式。这就需要我们定义好 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
详细请看注释,然后被忘记安装好
这两个模块。安装完毕之后,重新运行服务器的 node start(你可以使用 nodemon 来启动本地 node 服务,免得来回启动。)
- npm install graphql - server - koa graphql--save
然后刷新
,你会发现右边会有查询文档,在左边写上查询方式,如下图
- http://localhost:4000/graphiql
现在是我们把 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) 给我嘛
来源: https://segmentfault.com/a/1190000012720317