GraphQL入門有這一篇就足夠了


一、GraphQL是什么?
關於GraphQL是什么,網上一搜一大堆。根據官網的解釋就是一種用於 API 的查詢語言。

一看到用於API的查詢語言,我也是一臉懵逼的。博主你在開玩笑吧?你的翻譯水平不過關?API還能查嗎?API不是后端寫好,前端調用的嗎?

的確可以,這就是GraphQL強大的地方。
引用官方文檔的一句話:

ask exactly what you want.

二、為什么要使用GraphQL?
在實際工作中往往會有這種情景出現:比如說我需要展示一個游戲名的列表,可接口卻會把游戲的詳細玩法,更新時間,創建者等各種各樣的 (無用的) 信息都一同返回。

問了后端,原因大概如下:

原來是為了兼容PC端和移動端用同一套接口
或者在整個頁面,這里需要顯示游戲的標題,可是別的地方需要顯示游戲玩法啊,避免多次請求我就全部返回咯
或者是因為有時候項目經理想要顯示“標題+更新時間”,有時候想要點擊標題展開游戲玩法等等需求,所以把游戲相關的信息都一同返回
簡單說就是:

兼容多平台導致字段冗余
一個頁面需要多次調用 API 聚合數據
需求經常改動導致接口很難為單一接口精簡邏輯
有同學可能會說那也不一定要用GraphQL啊,比方說第一個問題,不同平台不同接口不就好了嘛

http://api.xxx.com/web/getGameInfo/:gameID
http://api.xxx.com/app/getGameInfo/:gameID
http://api.xxx.com/mobile/getGameInfo/:gameID

 

或者加個參數也行

http://api.xxx.com/getGameInfo/:gameID?platfrom=web


這樣處理的確可以解決問題,但是無疑加大了后端的處理邏輯。你真的不怕后端程序員打你?

這個時候我們會想,接口能不能不寫死,把靜態變成動態?

回答是可以的,這就是GraphQL所做的!

三、GraphQL嘗嘗鮮——(GraphQL簡單例子)
下面是用GraphQL.js和express-graphql搭建一個的普通GraphQL查詢(query)的例子,包括講解GraphQL的部分類型和參數,已經掌握了的同學可以跳過。

1. 先跑個hello world
新建一個graphql文件夾,然后在該目錄下打開終端,執行npm init --y初始化一個packjson文件。
安裝依賴包:npm install --save -D express express-graphql graphql
新建sehema.js文件,填上下面的代碼

//schema.js
const {
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
} = require('graphql');
const queryObj = new GraphQLObjectType({
name: 'myFirstQuery',
description: 'a hello world demo',
fields: {
hello: {
name: 'a hello world query',
description: 'a hello world demo',
type: GraphQLString,
resolve(parentValue, args, request) {
return 'hello world !';
}
}
}
});
module.exports = new GraphQLSchema({
query: queryObj
});


這里的意思是新建一個簡單的查詢,查詢名字叫hello,會返回字段hello world !,其他的是定義名字和查詢結果類型的意思。

同級目錄下新建server.js文件,填上下面的代碼

// server.js
const express = require('express');
const expressGraphql = require('express-graphql');
const app = express();

const schema = require('./schema');
app.use('/graphql', expressGraphql({
schema,
graphiql: true
}));

app.get('/', (req, res) => res.end('index'));

app.listen(8000, (err) => {
if(err) {throw new Error(err);}
console.log('*** server started ***');
});


這部分代碼是用express跑起來一個服務器,並通過express-graphql把graphql掛載到服務器上。

運行一下node server,並打開http://localhost:8000/

 

 

 


如圖,說明服務器已經跑起來了
打開http://localhost:8000/graphql,是類似下面這種界面說明已經graphql服務已經跑起來了!

在左側輸入 (graphql的查詢語法這里不做說明)

{
hello
}

 

 

 

點擊頭部的三角形的運行按鈕,右側就會顯示你查詢的結果了


2. 不僅僅是hello world
先簡單講解一下代碼:

const queryObj = new GraphQLObjectType({
name: 'myFirstQuery',
description: 'a hello world demo',
fields: {}
});

 

GraphQLObjectType是GraphQL.js定義的對象類型,包括name、description 和fields三個屬性,其中name和description 是非必填的。fields是解析函數,在這里可以理解為查詢方法

hello: {
name: 'a hello world query',
description: 'a hello world demo',
type: GraphQLString,
resolve(parentValue, args, request) {
return 'hello world !';
}
}

 

對於每個fields,又有name,description,type,resolve參數,這里的type可以理解為hello方法返回的數據類型,resolve就是具體的處理方法。

說到這里有些同學可能還不滿足,如果我想每次查詢都想帶上一個參數該怎么辦,如果我想查詢結果有多條數據又怎么處理?

下面修改schema.js文件,來一個加強版的查詢(當然,你可以整理一下代碼,我這樣寫是為了方便閱讀)

const {
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
GraphQLInt,
GraphQLBoolean
} = require('graphql');

const queryObj = new GraphQLObjectType({
name: 'myFirstQuery',
description: 'a hello world demo',
fields: {
hello: {
name: 'a hello world query',
description: 'a hello world demo',
type: GraphQLString,
args: {
name: { // 這里定義參數,包括參數類型和默認值
type: GraphQLString,
defaultValue: 'Brian'
}
},
resolve(parentValue, args, request) { // 這里演示如何獲取參數,以及處理
return 'hello world ' + args.name + '!';
}
},
person: {
name: 'personQuery',
description: 'query a person',
type: new GraphQLObjectType({ // 這里定義查詢結果包含name,age,sex三個字段,並且都是不同的類型。
name: 'person',
fields: {
name: {
type: GraphQLString
},
age: {
type: GraphQLInt
},
sex: {
type: GraphQLBoolean
}
}
}),
args: {
name: {
type: GraphQLString,
defaultValue: 'Charming'
}
},
resolve(parentValue, args, request) {
return {
name: args.name,
age: args.name.length,
sex: Math.random() > 0.5
};
}
}
}
});

module.exports = new GraphQLSchema({
query: queryObj 
});

 

重啟服務后,繼續打開http://localhost:8000/graphql,在左側輸入

{
hello(name:"charming"),
person(name:"charming"){
name,
sex,
age
}
}

 

右側就會顯示出:


你可以在左側僅輸入person方法的sex和age兩個字段,這樣就會只返回sex和age的信息。動手試一試吧!

{
person(name:"charming"){
sex,
age
}
}

 

當然,結果的順序也是按照你輸入的順序排序的。

定制化的數據,完全根據你查什么返回什么結果。這就是GraphQL被稱作API查詢語言的原因。

四、GraphQL實戰
下面我將搭配koa實現一個GraphQL查詢的例子,逐步從簡單koa服務到mongodb的數據插入查詢,再到GraphQL的使用,最終實現用GraphQL對數據庫進行增刪查改。

項目效果大概如下:

 

 

 

有點意思吧?那就開始吧~
先把文件目錄建構建好

1. 初始化項目

初始化項目,在根目錄下運行npm init --y,
然后安裝一些包:npm install koa koa-static koa-router koa-bodyparser --save -D
新建config、controllers、graphql、mongodb、public、router這幾個文件夾。裝逼的操作是在終端輸入mkdir config controllers graphql mongodb public router回車,ok~
2. 跑一個koa服務器
新建一個server.js文件,寫入以下代碼

// server.js
import Koa from 'koa'
import Router from 'koa-router'
import bodyParser from 'koa-bodyparser'

const app = new Koa()
const router = new Router();
const port = 4000

app.use(bodyParser());

router.get('/hello', (ctx, next) => {
ctx.body="hello world"
});


app.use(router.routes())
.use(router.allowedMethods());

app.listen(port);

console.log('server listen port: ' + port)

 

執行node server跑起來服務器,發現報錯了:


這是正常的,這是因為現在的node版本並沒有支持es6的模塊引入方式。

百度一下就會有解決方案了,比較通用的做法是用babel-polyfill進行轉譯。

詳細的可以看這一個參考操作:How To Enable ES6 Imports in Node.JS

具體操作是:新建一個start.js文件,寫入:

// start.js
require('babel-register')({
presets: [ 'env' ]
})
require('babel-polyfill')
require('./server.js')


安裝相關包:npm install --save -D babel-preset-env babel-polyfill babel-register

修改package.json文件,把"start": "start http://localhost:4000 && node start.js"這句代碼加到下面這個位置:


運行一下npm run start,打開http://localhost:4000/hello,結果如圖:

說明koa服務器已經跑起來了。

那么前端頁面呢?

(由於本文內容不是講解前端,所以前端代碼自行去github復制)

在public下新建index.html文件和js文件夾,代碼直接查看我的項目public目錄下的 index.html 和 index-s1.js 文件
修改server.js,引入koa-static模塊。koa-static會把路由的根目錄指向自己定義的路徑(也就是本項目的public路徑)

//server.js
import Koa from 'koa'
import Router from 'koa-router'
import KoaStatic from 'koa-static'
import bodyParser from 'koa-bodyparser'

const app = new Koa()
const router = new Router();
const port = 4000

app.use(bodyParser());

router.get('/hello', (ctx, next) => {
ctx.body="hello world"
});


app.use(KoaStatic(__dirname + '/public'));
app.use(router.routes())
.use(router.allowedMethods());

app.listen(port);

console.log('server listen port: ' + port)


打開http://localhost:4000/,發現是類似下面的頁面:

 

 

 


這時候頁面已經可以進行簡單的交互,但是還沒有和后端進行數據交互,所以是個靜態頁面。

3. 搭一個mongodb數據庫,實現數據增刪改查

注意: 請先自行下載好mongodb並啟動mongodb。

a. 寫好鏈接數據庫的基本配置和表設定

在config文件夾下面建立一個index.js,這個文件主要是放一下鏈接數據庫的配置代碼。

// config/index.js
export default {
dbPath: 'mongodb://localhost/todolist'
}

 

在mongodb文件夾新建一個index.js和 schema文件夾, 在 schema文件夾文件夾下面新建list.js

在mongodb/index.js下寫上鏈接數據庫的代碼,這里的代碼作用是鏈接上數據庫

// mongodb/index.js
import mongoose from 'mongoose'
import config from '../config'

require('./schema/list')

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)
})
}

 

在mongodb/schema/list.js定義表和字段:

//mongodb/schema/list.js
import mongoose from 'mongoose'

const Schema = mongoose.Schema
const ObjectId = Schema.Types.ObjectId

const ListSchema = new Schema({
title: String,
desc: String,
date: String,
id: String,
checked: Boolean,
meta: {
createdAt: {
type: Date,
default: Date.now()
},
updatedAt: {
type: Date,
default: Date.now()
}
}
})

ListSchema.pre('save', function (next) {// 每次保存之前都插入更新時間,創建時插入創建時間
if (this.isNew) {
this.meta.createdAt = this.meta.updatedAt = Date.now()
} else {
this.meta.updatedAt = Date.now()
}
next()
})
mongoose.model('List', ListSchema)

 

b. 實現數據庫增刪查改的控制器

建好表,也鏈接好數據庫之后,我們就要寫一些方法來操作數據庫,這些方法都寫在控制器(controllers)里面。

在controllers里面新建list.js,這個文件對應操作list數據的控制器,單獨拿出來寫是為了方便后續項目復雜化的模塊化管理。

// controllers/list.js
import mongoose from 'mongoose'
const List = mongoose.model('List')
// 獲取所有數據
export const getAllList = async (ctx, next) => {
const Lists = await List.find({}).sort({date:-1}) // 數據查詢
if (Lists.length) {
ctx.body = {
success: true,
list: Lists
}
} else {
ctx.body = {
success: false
}
}
}
// 新增
export const addOne = async (ctx, next) => {
// 獲取請求的數據
const opts = ctx.request.body

const list = new List(opts)
const saveList = await list.save() // 保存數據
console.log(saveList)
if (saveList) {
ctx.body = {
success: true,
id: opts.id
}
} else {
ctx.body = {
success: false,
id: opts.id
}
}
}
// 編輯
export const editOne = async (ctx, next) => {
const obj = ctx.request.body
let hasError = false
let error = null
List.findOne({id: obj.id}, (err, doc) => {
if(err) {
hasError = true
error = err
} else {
doc.title = obj.title;
doc.desc = obj.desc;
doc.date = obj.date;
doc.save();
}
})
if (hasError) {
ctx.body = {
success: false,
id: obj.id
}
} else {
ctx.body = {
success: true,
id: obj.id
}
}
}

// 更新完成狀態
export const tickOne = async (ctx, next) => {
const obj = ctx.request.body
let hasError = false
let error = null
List.findOne({id: obj.id}, (err, doc) => {
if(err) {
hasError = true
error = err
} else {
doc.checked = obj.checked;
doc.save();
}
})
if (hasError) {
ctx.body = {
success: false,
id: obj.id
}
} else {
ctx.body = {
success: true,
id: obj.id
}
}
}

// 刪除
export const delOne = async (ctx, next) => {
const obj = ctx.request.body
let hasError = false
let msg = null
List.remove({id: obj.id}, (err, doc) => {
if(err) {
hasError = true
msg = err
} else {
msg = doc
}
})
if (hasError) {
ctx.body = {
success: false,
id: obj.id
}
} else {
ctx.body = {
success: true,
id: obj.id
}
}
}

 

c. 實現路由,給前端提供API接口

數據模型和控制器都已經設計好了,下面就利用koa-router路由中間件,來實現請求的接口。

我們回到server.js,在上面添加一些代碼。如下:

// server.js
import Koa from 'koa'
import Router from 'koa-router'
import KoaStatic from 'koa-static'
import bodyParser from 'koa-bodyparser'
import {database} from './mongodb' 
import {addOne, getAllList, editOne, tickOne, delOne} from './controllers/list'

database() // 鏈接數據庫並且初始化數據模型

const app = new Koa()
const router = new Router();
const port = 4000

app.use(bodyParser());

router.get('/hello', (ctx, next) => {
ctx.body = "hello world"
});

// 把對請求的處理交給處理器。
router.post('/addOne', addOne)
.post('/editOne', editOne)
.post('/tickOne', tickOne)
.post('/delOne', delOne)
.get('/getAllList', getAllList)

app.use(KoaStatic(__dirname + '/public'));
app.use(router.routes())
.use(router.allowedMethods());

app.listen(port);

console.log('server listen port: ' + port)

 

上面的代碼,就是做了:

1. 引入mongodb設置、list控制器,
2. 鏈接數據庫
3. 設置每一個設置每一個路由對應的我們定義的的控制器。

安裝一下mongoose:npm install --save -D mongoose

運行一下npm run start,待我們的服務器啟動之后,就可以對數據庫進行操作了。我們可以通過postman來模擬請求,先插幾條數據:

 

 

查詢全部數據:

 

 

d. 前端對接接口

前端直接用ajax發起請求就好了,平時工作中都是用axios的,但是我懶得弄,所以直接用最簡單的方法就好了。

引入了JQuery之后,改寫public/js/index.js文件:略(項目里的public/index-s2.js的代碼)

項目跑起來,發現已經基本上實現了前端發起請求對數據庫進行操作了。
至此你已經成功打通了前端后台數據庫,可以不要臉地稱自己是一個小全棧了!

不過我們的目的還沒有達到——用grapql實現對數據的操作!

4. 用grapql實現對數據的操作

GraphQL 的大部分討論集中在數據獲取(query),但是任何完整的數據平台也都需要一個改變服務端數據的方法。
REST 中,任何請求都可能最后導致一些服務端副作用,但是約定上建議不要使用 GET 請求來修改數據。GraphQL 也是類似 —— 技術上而言,任何查詢都可以被實現為導致數據寫入。然而,建一個約定來規范任何導致寫入的操作都應該顯式通過變更(mutation)來發送。

簡單說就是,GraphQL用mutation來實現數據的修改,雖然mutation能做的query也能做,但還是要區分開這連個方法,就如同REST中約定用GET來請求數據,用其他方法來更新數據一樣。

a. 實現查詢
查詢的話比較簡單,只需要在接口響應時,獲取數據庫的數據,然后返回;

const objType = new GraphQLObjectType({
name: 'meta',
fields: {
createdAt: {
type: GraphQLString
},
updatedAt: {
type: GraphQLString
}
}
})
let ListType = new GraphQLObjectType({
name: 'List',
fields: {
_id: {
type: GraphQLID
},
id: {
type: GraphQLString
},
title: {
type: GraphQLString
},
desc: {
type: GraphQLString
},
date: {
type: GraphQLString
},
checked: {
type: GraphQLBoolean
},
meta: {
type: objType
}
}
})
const listFields = {
type: new GraphQLList(ListType),
args: {},
resolve (root, params, options) {
return List.find({}).exec() // 數據庫查詢
}
}
let queryType = new GraphQLObjectType({
name: 'getAllList',
fields: {
lists: listFields,
}
})

export default new GraphQLSchema({
query: queryType
})

 

把增刪查改都講完再更改代碼~
b. 實現增刪查改

一開始說了,其實mutation和query用法上沒什么區別,這只是一種約定。
具體的mutation實現方式如下:

const outputType = new GraphQLObjectType({
name: 'output',
fields: () => ({
id: { type: GraphQLString},
success: { type: GraphQLBoolean },
})
});

const inputType = new GraphQLInputObjectType({
name: 'input',
fields: () => ({
id: { type: GraphQLString },
desc: { type: GraphQLString },
title: { type: GraphQLString },
date: { type: GraphQLString },
checked: { type: GraphQLBoolean }
})
});
let MutationType = new GraphQLObjectType({
name: 'Mutations',
fields: () => ({
delOne: {
type: outputType,
description: 'del',
args: {
id: { type: GraphQLString }
},
resolve: (value, args) => {
console.log(args)
let result = delOne(args)
return result
}
},
editOne: {
type: outputType,
description: 'edit',
args: {
listObj: { type: inputType }
},
resolve: (value, args) => {
console.log(args)
let result = editOne(args.listObj)
return result
}
},
addOne: {
type: outputType,
description: 'add',
args: {
listObj: { type: inputType }
},
resolve: (value, args) => {
console.log(args.listObj)
let result = addOne(args.listObj)
return result
}
},
tickOne: {
type: outputType,
description: 'tick',
args: {
id: { type: GraphQLString },
checked: { type: GraphQLBoolean },
},
resolve: (value, args) => {
console.log(args)
let result = tickOne(args)
return result
}
},

}),
});

export default new GraphQLSchema({
query: queryType,
mutation: MutationType
})

 

c. 完善其余代碼

在實現前端請求Graphql服務器時,最困擾我的就是參數以什么樣的格式進行傳遞。后來在Graphql界面玩Graphql的query請求時發現了其中的訣竅…

關於前端請求格式進行一下說明:

 

 

如上圖,在玩Graphql的請求時,我們就可以直接在控制台network查看請求的格式了。這里我們只需要模仿這種格式,當做參數發送給Graphql服務器即可。

記得用反引號: `` ,來拼接參數格式。然后用data: {query: params}的格式傳遞參數,代碼如下:

let data = {
query: `mutation{
addOne(listObj:{
id: "${that.getUid()}",
desc: "${that.params.desc}",
title: "${that.params.title}",
date: "${that.getTime(that.params.date)}",
checked: false
}){
id,
success
}
}`
}
$.post('/graphql', data).done((res) => {
console.log(res)
// do something
})

 


最后更改server.js,router/index.js,controllers/list.js,public/index.js改成github項目對應目錄的文件代碼即可。

完整項目的目錄如下:


五、后記
對於Vue開發者,可以使用vue-apollo使得前端傳參更加優雅~

對上文有疑問或者有建議意見的,可以加我QQ:820327571,並備注:Graphql

六、參考文獻


————————————————
版權聲明:本文為CSDN博主「__Charming__」的原創文章,遵循 CC 4.0 BY-SA 版權協議,轉載請附上原文出處鏈接及本聲明。
原文鏈接:https://blog.csdn.net/qq_41882147/article/details/82966783

喜歡這篇文章?歡迎打賞~~

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM