本文將從GraphQL是什么,為什么要使用GraphQL,使用GraphQL創建簡單例子,以及GraphQL實戰,四個方面對GraphQL進行闡述。說得不對的地方,希望大家指出斧正。
github項目地址:https://github.com/Charming2015/graphql-todolist
一、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
- 新建schema.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 = 4000app.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 = 4000app.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.ObjectIdconst 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.bodyconst 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.bodylet hasError = falselet error = nullList.findOne({id: obj.id}, (err, doc) => {if(err) {hasError = trueerror = 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.bodylet hasError = falselet error = nullList.findOne({id: obj.id}, (err, doc) => {if(err) {hasError = trueerror = 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.bodylet hasError = falselet msg = nullList.remove({id: obj.id}, (err, doc) => {if(err) {hasError = truemsg = 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 = 4000app.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使得前端傳參更加優雅~
六、參考文獻
- graphql官網教程
- GraphQL.js
- 30分鐘理解GraphQL核心概念
- 我的前端故事----我為什么用GraphQL
- GraphQL 搭配 Koa 最佳入門實踐
---------------------
作者:__Charming__
來源:CSDN
原文:https://blog.csdn.net/qq_41882147/article/details/82966783
版權聲明:本文為作者原創文章,轉載請附上博文鏈接!
內容解析By:CSDN,CNBLOG博客文章一鍵轉載插件