一、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
喜歡這篇文章?歡迎打賞~~