GraphQL 搭配 Koa 最佳入門實踐

GraphQL一種用爲你 API 而生的查詢語言,2018已經到來,PWA尚未大量投入生產應用之中就已經火起來了,GraphQL的應用或許也不會太遠了。前端的發展的最大一個特色就是變化快,有時候應對各類需求場景的變化,不得不去對接口開發不少版本或者修改。各類業務依賴強大的基礎數據平臺快速生長,如何高效地爲各類業務提供數據支持,是全部人關心的問題。並且如今前端的解決方案是將視圖組件化,各個業務線既能夠是組件的使用者,也能夠是組件的生產者,若是可以將其中通用的內容抽取出來提供給各個業務方反覆使用,必然可以節省寶貴的開發時間和開發人力。那麼問題來了,前端經過組件實現了跨業務的複用,後端接口如何相應地提升開發效率呢?GraphQL,就是應對複雜場景的一種新思路。javascript

官方解釋:css

GraphQL 既是一種用於 API 的查詢語言也是一個知足你數據查詢的運行時。 GraphQL 對你的 API 中的數據提供了一套易於理解的完整描述,使得客戶端可以準確地得到它須要的數據,並且沒有任何冗餘,也讓 API 更容易地隨着時間推移而演進,還能用於構建強大的開發者工具。html

下面介紹一下GraphQL的有哪些好處:前端

  • 請求你所要的數據很少很多java

  • 獲取多個資源只用一個請求node

  • 自定義接口數據的字段jquery

  • 強大的開發者工具git

  • API 演進無需劃分版本es6

本篇文章中將搭配koa實現一個GraphQL查詢的例子,逐步從簡單kao服務到mongodb的數據插入查詢再到GraphQL的使用, 讓你們快速看到:github

  • 搭建koa搭建一個後臺項目
  • 後臺路由簡單處理方式
  • 利用mongoose簡單操做mongodb
  • 掌握GraphQL的入門姿式

項目以下圖所示

一、搭建GraphQL工具查詢界面。

二、前端用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)給我嘛

相關文章
相關標籤/搜索