GraphQL-前端開發的利劍與橋梁
基本概念
GraphQL
GraphQL 是一種用於 API 的查詢語言,由Facebook開發和開源,是使用基於類型系統來執行查詢的服務端運行時(類型系統由你的數據定義)。GraphQL並沒有和任何特定數據庫或者存儲引擎綁定,而是依靠你現有的代碼和數據支撐。
背景介紹
相信看了上面的基本概念,大家都是和我一樣一臉萌萌噠。所以這里就需要介紹一下其產生的背景和原因。
在我們目前的前后端開發過程中,大部分都是以http請求服務端接口的方式完成交互過程的。在這種場景下,每當需求變化,就需要修改或創建一個新的接口去滿足特定的需求。
舉個栗子:
在一個商品詳情頁,當我們需要獲取商品詳情時,服務端會給前端一個接口,例如:
https://www.example.com/getInfoById?infoId=000000
當前端請求接口時,會返回給一個固定格式的數據,例如:
{
data:{
title:'商品的標題',
content:'商品的描述內容',
special:'商品特點',
price:'商品價格',
image:'商品的圖片'
}
}
前端接收到數據后,進行各種相應的處理展示,最終將包含有商品標題,商品描述,商品特點,商品價格,商品圖片信息的頁面展示給用戶。
一切看起來都很美好,直到有一天……
產品大搖大擺的走過來,輕描淡寫的說道:“能不能把商品的特點去掉,加一個商品的庫存,另外還需要再加一個賣家的模塊進去。包含賣家的名稱和頭像,可以點進賣家的詳情頁,也不用太着急,午飯前上線就行。”
於是前后端坐在一起開始商量,前端弱弱的說:“能不能改一下你的接口,把產品不要的都去掉,產品需要的都加上”。
后端心里說,你當我傻啊,於是一邊砸吧嘴一邊趕忙說道:“這樣改風險太大,好多數據都不在一個表,我不好查。這樣,詳情頁那個接口我就不改了,你不顯示不就完了嘛,萬一哪天產品那小子的小子再想起來加上,咱倆還得忙活。庫存再給你一個接口,賣家信息再給你一個接口,完美,就這么定了。”
前端還想再說什么,可后端的背影已經隨着產品越走越遠。
就在前端絕望之時,霹靂一聲震天響,graphql閃亮登場。
在graphql模式下,假設我們的服務端部分已經部署完成,前端使用vue框架,那么前端部分的請求就可以簡化為:
apollo: {
goods: {
query() {
return gql`{
goods(infoId:"${this.infoId}"){
title
content
price
image
}
}`
}
},
store: {
query() {
return gql`{
store(infoId:"${this.infoId}"){
store
}
}`
}
},
seller: {
query() {
return gql`{
seller(infoId:"${this.infoId}"){
name
age
}
}`
}
}
}
可以看到graphql為我們定義了一種類似sql的查詢語言,而這種查詢語言是用於api的。和之前的數據請求處理不同,在這里,我們只要定義好需要的數據,其他的不再關心,我們就可以按需索取需要的數據。這對於我們的開發提供了更大的自由與便利,只要數據支持,我們就可以擺脫對於服務端接口的依賴,提高生產效率,贏得自由,完成前端的逆襲。
前后端實踐
講完了故事,我們開始講一些實際的干貨。
對於graphql,網上已經有很多實踐經驗,以下部分是在參考完成實踐經驗並自我實踐后給出的總結歸納。
服務端
服務端的技術選型,我們使用了eggjs框架,配合egg-graphqlegg-graphql插件完成。
1.安裝依賴包
$ npm install --save egg-graphql
2.開啟插件
// config/plugin.js
exports.graphql = {
enable: true,
package: 'egg-graphql',
};
//開啟 cros 跨域訪問
exports.cors = {
enable: true,
package: 'egg-cors'
}
3.配置graphql路由和跨域
//config/config.default.js
// graphql路由
config.graphql = {
router: '/graphql',
// 是否加載到 app 上,默認開啟
app: true,
// 是否加載到 agent 上,默認關閉
agent: false,
// 是否加載開發者工具 graphiql, 默認開啟。路由同 router 字段。使用瀏覽器打開該可見。
graphiql: true,
// graphQL 路由前的攔截器
onPreGraphQL: function*(ctx) {},
// 開發工具 graphiQL 路由前的攔截器,建議用於做權限操作(如只提供開發者使用)
onPreGraphiQL: function*(ctx) {},
}
// cors跨域
config.cors = {
origin: '*',
allowMethods: 'GET,HEAD,PUT,POST,DELETE,PATCH,OPTIONS'
}
4.開啟graphql中間件
//config/config.default.js
exports.middleware = [ 'graphql' ];
項目配置告於段落。
5.編寫業務員代碼
下面開始寫代碼。
目錄結構如下:
├── app
│ ├── graphql //graphql 代碼,所有和graphql相關的代碼都在這里,已經在前面做好了配置
│ │ └── common //通用類型定義,graphql有一套自己的系統類型,除此之外還可以自定義
│ │ | |── scalars //自定義類型定義
│ │ | | └── date.js // 日期類型實現
│ │ | └── resolver.js //合並所有全局類型定義
│ │ | └── schema.graphql // schema 定義
│ │ └──goods // 商品詳情的graphql模型
│ │ └── connector.js //連接數據服務
│ │ └── resolver.js //類型實現,和goods中schema.graphql定義的模型相對應,是其具體的實現
│ │ └── schema.graphql //schema 定義,在這里定義商品詳情數據對象
│ │ └──store // 庫存的graphql模型
│ │ └── connector.js //連接數據服務
│ │ └── resolver.js //類型實現
│ │ └── schema.graphql //schema 定義,在這里定義商品詳情數據對象
│ │ └──seller // 賣家信息的graphql模型
│ │ └── connector.js //連接數據服務
│ │ └── resolver.js //類型實現
│ │ └── schema.graphql //schema 定義,在這里定義商品詳情數據對象
│ │ └──query // 所有的查詢都會經過這里,這里是一個總的入口
│ │ └── schema.graphql //schema 定義
│ ├── service
│ │ └── goods.js //商品詳情的具體實現
│ │ └── store.js //庫存的具體業務邏輯
│ │ └── seller.js //賣家信息的具體業務邏輯
│ └── router.js
app/graphql/query/schema.graphql是整個graphql查詢的總入口,所有需要查詢的對象都要在這里定義。它的定義形式如下:
#定義查詢對象,在graphql里的注釋使用#號
type Query {
goods(
#查詢條件,相當於接口的入參商品id
infoId: ID!
):Goods #Goods是在app/graphql/goods/schema.graphql中定義的商品詳情
}
在總入口中涉及到的查詢對象,都需要在graphql文件夾下建立相應的文件夾,如上文中提到的goods,就在app/graphql文件夾中存在相應的goods文件夾。goods文件夾包含三個部分:schema.graphql,resolve.js和connector.js。
schema.graphql中需要定義總入口中提及的Goods對象:
# 商品
type Goods {
# 流水號
infoId: ID!
# 商品標題
title:String!,
# 商品內容
content:'String!,
#商品特點
special:'String!,
#商品價格
price:'nt!,
#商品圖片
image:'String!,
}
graphql自帶一組默認標量類型,包括Int,Float,String,Boolean,ID。在定義字段時需要注明類型,這也是graphql的特點之一,是支持強類型的。如果非空,就在類型后面跟上一個!號。graphql還包括枚舉類型,列表和自定義類型,具體可以查看相關文檔。
resolve.js是數據類型的具體實現,依賴connector.js完成:
'use strict'
module.exports = {
Query: {
goods(root, {infoId}, ctx) {
return ctx.connector.goods.fetchById(infoId)
}
}
connector.js是連接數據的具體實現,可以使用dataloader來降低數據訪問頻次,提高性能:
'use strict'
//引入dataloader,是由facebook推出,能大幅降低數據庫的訪問頻次,經常在Graphql場景中使用
const DataLoader = require('dataloader')
class GoodsConnector {
constructor(ctx) {
this.ctx = ctx
this.loader = new DataLoader(id=>this.fetch(id))
}
fetch(id) {
const goods = this.ctx.service.goods
return new Promise(function(resolve, reject) {
const goodsInfo = goods.getInfoById(id)
resolve([goodsInfo]) //注意這里需要返回數組形式
})
}
fetchById(id) {
return this.loader.load(id)
}
}
module.exports = GoodsConnector
上面代碼中涉及的this.ctx.service.goods就是app/service文件夾下的goods.js文件導出的方法對象,也就是獲取數據的具體業務邏輯:
const Service = require('egg').Service
const {createAPI} = require('../util/request')//實現的http請求
class GoodsService extends Service {
// 獲取商品詳情
async getInfoById(infoId) {
const result = await createAPI(this, 'example/getInfoById', 'get', {infoId})
return result
}
}
module.exports = GoodsService
獲取數據可以用你能實現的任何方式,可以直接從數據庫獲取,也可以用http從現有的接口獲取。
這樣一個使用egg框架實現的graphql服務就完成了。
下面說一下前端。
前端
我們會使用vue配合Apollo完成前端搭建。
1 安裝依賴包
npm install --save vue-apollo apollo-client
2.引用apollo
import { ApolloClient } from 'apollo-client'
import { HttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import VueApollo from 'vue-apollo'
3.配置鏈接
const httpLink = new HttpLink({
// 需要配置一個絕對路徑
uri: 'http://exzample.com/graphql',
})
4.創建ApolloClient實例和PROVIDER
// Create the apollo client
const apolloClient = new ApolloClient({
link: httpLink,
cache: new InMemoryCache(),
connectToDevTools: true,
})
const apolloProvider = new VueApollo({
defaultClient: apolloClient,
})
4.在vue中引入使用
Vue.use(VueApollo);
5.根實例引用
var vm = new Vue({
el: '#app',
provide: apolloProvider.provide(),
router,
components: {
app: App
},
render: createEle => createEle('app')
})
6.使用
<script>
import gql from "graphql-tag";
export default {
data() {
return {
goods:{},
infoId:123123
};
},
apollo: {
goods: {
query() {
return gql`{
goods(infoId:"${this.infoId}"){
title
content
price
image
}
}`
}
},
}
};
</script>
展望
graphql對於目前接口數量多,難維護,擴展成本高,數據格式不可預知,文檔難維護等問題給出了一個相對完善的方案,相信在未來,它將是我們工作中不可或缺的一部分。