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/
就會看到如下介面。介面醜,沒什麼設計美化細胞,求輕噴~~~~
操作點選之後就會想第二張圖一樣了。
所有效果都出來了,本篇文章也就到此結束了。
附上專案地址: github.com/naihe138/Gr…
ps:喜歡的話丟一個小星星(star)給我嘛