記錄了組內技術分享會, 有同樣需求的同學可以參考一下
分享全程下來時間大約 55分鐘
前言
痛點:網上找的資料,文章, GraphQL的官網,一看就是很‘自我’的大神寫的(太爛了)完全管讀者能不能看懂,舉例子只講用法!不告訴程式碼怎麼實現的(但是當你學完這一篇你就可以看懂了), 並且從來不曬出整體程式碼,導致根本不知道他們怎麼玩的,有被冒犯到!!可以說那些資料都不適合入門。
定位:GraphQL並不是必須用的技術, 但它是必須會的技術,之所以說它必會是因為可以靠它為‘前端’這一行業佔領更大的‘領地’, 同時它的思想是值得琢磨與體會的。
是啥:他不是json更不是js, 他是GraphQL自身的語法, 相容性非常好.
選擇:GraphQL為了使開發更高效、簡潔、規範而生,如果對工程對團隊造成了負擔可以果斷捨棄(別猶豫,這小子不是必需品),畢竟服務端多一層處理是會消耗效能的,那麼就要理智計算他的“損失收益比”了。
前提:學習這門技術需要有一定的“前後互動”的知識儲備, 比如會node, 這裡會介紹如何在node端使用, 如何整合入koa2專案裡面使用, 並且會捨棄一些大家都用的技術, 不做跟風黨。
正文
一. GraphQL到底幹啥的?用它有啥好處哦?
這裡是關鍵, 一定要引起大家的興趣, 不然很難進行。
①: 嚴格要求返回值
比如下面是後端返回的程式碼
{
name:1,
name:2,
name:3,
}
前端想要的程式碼
{
name:1
}
從上面可以看出, name2 與 name3 其實我不想要, 那你傳給我那麼多資料幹什麼,單純為了浪費頻寬嗎?但是吧。。也可理解為某些場景下確實很雞肋,但是請求多了效果就厲害了。
②: 設想一個場景, 我想要通過文章的id獲取作者資訊, 再通過作者資訊裡面的作者id請求他其他的作品列表, 那麼我就需要兩步才能完成, 但是如果這兩個請求在服務端完成那麼我們只用寫一個請求, 而且還不用找後端同學寫新介面。
③: 控制預設值: 比如一個作品列表的陣列, 沒有作品的時候後端很肯能給你返的是null
, 但是我們更想保證一致性希望返回[]
,這個時候可以用GraphQL進行規避.
二. 原生GraphQL嚐鮮。
隨便建個空白專案npm install graphql
.
入口路徑如下src->index.js
var { graphql, buildSchema } = require('graphql');
// 1: 定義模板/對映, 有用mongoose運算元據庫經驗的同學應該很好理解這裡
var schema = buildSchema(`
type Query {
# 我是備註, 這裡的備註都是單個井號;
hello: String
name: String
}
`);
// 2: 資料來源,可以是熱熱乎乎從mongodb裡面取出來的資料
var root = {
hello: () => 'Hello!',
name:'金毛cc',
age:5
};
// 3: 描述語句, 我要取什麼樣的資料, 我想要hello與name 兩個欄位的資料, 其他的不要給我
const query = '{ hello, name }'
// 4: 把準備好的食材放入鍋內, 模型->描述->總體的返回值
graphql(schema, query, root).then((response) => {
console.log(JSON.stringify(response));
});
上面的程式碼直接 node就可以,結果如下: {"data":{"hello":"Hello!","name":"金毛cc"}}
;
逐一攻克
1: buildSchema
建立資料模型
var schema = buildSchema(
// 1. type: 指定型別的關鍵字
// 2. Query: 你可以理解為返回值的固定型別
// 3. 他並不是json,他是graphql的語法, 一定要注意它沒有用','
// 4. 返回兩個值, 並且值為字串型別, 注意: string小寫會報錯
` type Query {
hello: String
name: String
}
`);
GraphQL 中內建有一些標量型別 String、 Int、 Float、 Boolean、 ID,這幾種都叫scalar型別, 意思是單個型別
2: const query = '{ hello, name }'
做外層{}基本不變, hello的意思就是我要這一層的hello欄位, 注意這裡用','分割, 之後會把這個','優化掉.
到這裡一個最基本的例子寫出來了, 感覺也沒啥是吧, 我當時學到這裡也感覺會很順利, 但是... 接下來文章斷層, 官網表達不清, 社群不完善等等問題克服起來好心酸.
三. 利用庫更好的原生開發
畢竟這樣每次node
命令執行不方便, 並且結果出現在控制檯裡也不好看, 所以我們要用一個專業工具'yoga'.
yarn add graphql-yoga
const { GraphQLServer } = require('graphql-yoga');
// 型別定義 增刪改查
const typeDefs = `
type Query{
hello: String! #一定返回字串
name: String
id:ID!
}
`
const resolvers = {
Query:{
hello(){
return '我是cc的主人'
},
name(){
return '魯魯'
},
id(){
return 9
},
}
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(()=>{
console.log('啟動成功, 預設是4000')
})
當然最好用nodemon來啟動檔案 npm install nodemon -g
-
hello: String!
像這種加了個'!'就是一定有值的意思, 沒值會報錯. - Query 這裡定義的返回值, 對應函式的返回值會被執行.
- new GraphQLServer 的傳參 定義的資料模型, 返回值, 因為具體的請求語句需要我們在web上面輸入.
- id的型別使用ID這個會把id轉換為字串,這樣設計也許是為了相容所有形式的id.
- server.start 很貼心的起一個服務配置好後效果如下:左邊是輸入, 右邊是返回的結果
四. 多層物件定義
我們返回data給前端,基本都會有多層, 那麼定義多層就要有講究了
const {GraphQLServer} = require('graphql-yoga');
const typeDefs = `
type Query{
me: User! # 這裡把me這個key對應的value定義為User型別, 並且必須有值
}
type User { # 首字母必須大寫
name:String
}
`
const resolvers = {
Query:{
me(){
return {
id:9,
name:'lulu'
}
}
}
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(()=>{
console.log('啟動成功, 預設是4000')
})
- User型別不是原生自帶 , 所以我們要自己用type關鍵字定義一個User資料型別.(首字母必須大寫)
- Query裡面return的值, 必須滿足User型別的定義規則
當我們取name
的值時:
我剛才故意在返回值裡面寫了id, 那麼可以取到值麼?
結論: 就算資料裡面有, 但是型別上沒有定義, 那麼這個值就是取不到的.
五. 陣列
定義起來會有些特殊
const { GraphQLServer } = require('graphql-yoga');
const typeDefs = `
type Query {
# 返回是陣列
arr:[Int!]!
}
`
const resolvers = {
Query: {
arr() {
return [1, 2, 3, 4]
}
}
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(() => {
console.log('啟動成功, 預設是4000')
})
- arr:[Int!] 如果寫成 arr:[] 會報錯, 也就是說必須把陣列裡面的型別定義完全.
- Query裡面的返回值必須嚴格按照type裡面定義的返回, 不然會報錯.
結果如下:
六. 傳參(前端可以傳引數,供給服務端函式的執行
)這個思路很神奇吧.
const { GraphQLServer } = require('graphql-yoga');
const typeDefs = `
type Query{
greeting(name: String):String # 需要傳參的地方, 必須在這裡定義好
me: User!
}
type User { # 必須大寫
name:String
}
`
const resolvers = {
Query: {
// 四個引數大有文章
greeting(parent, args, ctx, info) {
return '預設值' + args.name
},
me() {
return {
id: 9,
name: 'lulu'
}
}
}
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(() => {
console.log('啟動成功, 預設是4000')
})
-
greeting(name: String):String
greeting是key沒的說, 他接收一個name引數為字串型別, 這裡必須指明引數名字, 返回值也必須是字串型別, 也就是greeting是一個字串. -
greeting(parent, args, ctx, info) {
這裡我們用到 args也就是引數的集合是個物件, 我們args.name就可以取到name的值, 剩下的值後面用到會講. - 既然說了要傳參, 那就必須傳參不然會報錯
因為左側的引數是要放在url請求上的, 所以要用雙引號;
七. 關聯關係
就像資料庫建表一樣, 我們不可能把所有資料放在一張表裡, 我們可能會用一個id來指定另一張表裡面的某些值的集合.
const { GraphQLServer } = require('graphql-yoga');
const typeDefs = `
type Query{
lulu: User!
}
type User{
name:String
age: Int
chongwu: Chongwu!
}
type Chongwu{
name:String!
age:Int
}
`
// 自定義的資料
const chongwuArr = {
1: {
name: 'cc',
age:8
},
2: {
name: '芒果',
age:6
},
9: {
name: '芒果主人',
age:24
}
}
const resolvers = {
Query: {
lulu() {
return {
name: '魯路修',
age: 24,
chongwu: 9
}
},
},
// 注意, 它是與Query並列的
User:{
// 1: parent指的就是 user, 通過他來得到具體的引數
chongwu(parent,args,ctx,info){
console.log('=======', parent.chongwu ) // 9
return chongwuArr[parent.chongwu]
}
}
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(() => {
console.log('啟動成功, 預設是4000')
})
這裡資料量有點多, 我慢慢解析
- lulu屬於User類, User類裡面的chongwu(寵物)屬於Chongwu類, 我們需要根據chongwu輸入的id 查詢出 展示哪個寵物.
- 由於這個寵物的列表可能來自外部, 所以他的定義方式需要與Query同級.
- parent 指的就是父級資料, 也就是通過他可以獲取到輸入的id.
效果如下:
這裡就可以解釋剛開始的一個問題, 就是那個通過文章id找到作者, 通過作者找到其他文章的問題, 這裡的知識點就可以讓我們把兩個介面合二為一, 或者合n為一.
八. 不是獲取, 是操作.
有沒有發現上面我演示的都是獲取資料, 接下來我們來說說運算元據, 也就是'增刪改'沒有'查'
graphql規定此類操作需要放在Mutation
這個類裡面, 類似vuex會要求我們按照他的規範進行書寫
const { GraphQLServer } = require('graphql-yoga');
const typeDefs = `
type Query{
hello: String!
}
# 是操作而不是獲取, 增刪改:系列
type Mutation{
createUser(name:String!, age:Int!):CreateUser
# 這裡面可以繼續書寫create函式...
}
type CreateUser{
id:Int
msg:String
}
`
const resolvers = {
Query: {
hello() {
return '我是cc的主人'
},
},
// query並列
Mutation: {
createUser(parent, args, ctx, info) {
const {name,age} = args;
// 這裡我們拿到了引數, 那麼就可以去awit 建立使用者
return {
msg:'建立成功',
id:999
}
}
}
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(() => {
console.log('啟動成功, 預設是4000')
})
-
Mutation
是特殊類, 也是與Query
並列. - 一個
Mutation
裡面可以寫多個函式, 因為他是個集合. - 為函式的返回值也可以定義型別
效果如下: 接收id與提示資訊
九. input特殊型別
const { GraphQLServer } = require('graphql-yoga');
const typeDefs = `
type Query{
hello: String!
}
# 是操作而不是獲取, 增刪改:系列
type Mutation{
# 這個data隨便叫的, 叫啥都行, 就是單獨搞了個obj包裹起來而已, 不咋地
createUser(data: CreateUserInput):CreateUser
}
type CreateUser{
id:Int
msg:String
}
# input 定義引數
input CreateUserInput{
# 裡面的型別只能是基本型別
name: String!
age:Int!
}
`
const resolvers = {
Query: {
hello() {
return '我是cc的主人'
},
},
// query並列
Mutation: {
createUser(parent, args, ctx, info) {
// **這裡注意了, 這裡就是data了, 而不是分撒開的了**
const { data } = args;
return {
msg: '建立成功',
id: 999
}
}
}
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(() => {
console.log('啟動成功, 預設是4000')
})
- 把引數放在
data
裡面, 然後定義data的類 - 注意三個關鍵點的程式碼都要改
效果與上面的沒區別, 只是多包了層data如圖:
這個只能說有利有弊吧, 多包了一層, 還多搞出一個類, 看似可以封裝了實則'雞肋啊雞肋'
十. '更雞肋的'MutationType
特殊型別
const {GraphQLServer} = require('graphql-yoga');
// 非常雞肋, 這種事做不做, 該不該你做, 心裡沒點數
const typeDefs = `
type Query{
hello: MutationType
}
enum MutationType{
aaaa
bbbb
cccc
}
`
const resolvers = {
Query:{
hello(){
// 只能返回選單裡面的內容, 這樣可以保證不出格... p用
return 'bbbb'
},
}
}
const server = new GraphQLServer({
typeDefs,
resolvers
})
server.start(()=>{
console.log('啟動成功, 預設是4000')
})
- 我定義了一個
MutationType
的類, 限制只能用'aaa','bbb','ccc'中的一個字串. - 這不貓捉耗子麼? graphql本身定位不是幹這個事的, 這種事情交給統一的資料校驗模組完成, 他做了校驗的話那麼其他情況他管不管? 關了又如何你又不改資料, 就會個報錯公雞想下蛋.
- 完全不建議用這個, 當做瞭解, 具體的校驗模組自己在中介軟體或者utils裡面寫.
十一. 整合進koa2專案
1. 終於到實戰了, 講了那麼多的原生就是為了從最基本的技術點來理解這裡
2. 並不一定完全使用graphql的規範, 完全可以只有3個介面用它
3. 我們剛才寫的那些type都是在模板字串裡面, 所以肯定有人要他模板拆解開, 以物件的形式去書寫才符合人類的習慣.
先建立一個koa的工程
// 若果你沒有koa的話, 建議你先去學koa, koa知識點比較少所以我暫時沒寫相應的文章.koa2 graphqlx
// main:工程名 不要與庫重名npm install graphql koa-graphql koa-mount --save
大朗快把庫安裝好.
app.js檔案裡面
const Koa = require('koa')
const app = new Koa()
const views = require('koa-views')
const json = require('koa-json')
const onerror = require('koa-onerror')
const bodyparser = require('koa-bodyparser')
const logger = require('koa-logger')
////// 看這裡
const mount = require('koa-mount');
const graphqlHTTP = require('koa-graphql');
const GraphQLSchema=require('./schema/default.js');
//////
const index = require('./routes/index')
const users = require('./routes/users')
// error handler
onerror(app)
// middlewares
app.use(bodyparser({
enableTypes:['json', 'form', 'text']
}))
app.use(json())
app.use(logger())
app.use(require('koa-static')(__dirname + '/public'))
app.use(views(__dirname + '/views', {
extension: 'pug'
}))
// 每一個路徑, 對應一個操作
app.use(mount('/graphql', graphqlHTTP({
schema: GraphQLSchema,
graphiql: true // 這裡可以關閉除錯模式, 預設是false
})));
// logger
app.use(async (ctx, next) => {
const start = new Date()
await next()
const ms = new Date() - start
console.log(`${ctx.method} ${ctx.url} - ${ms}ms`)
})
// routes
app.use(index.routes(), index.allowedMethods())
app.use(users.routes(), users.allowedMethods())
// error-handling
app.on('error', (err, ctx) => {
console.error('server error', err, ctx)
});
module.exports = app
- 我直接吧預設配置也粘進來了, 這樣可以保證你拿走就用
- graphiql: true 這個時候開啟了除錯模式 會出現下圖的除錯介面, 預設是false
- mount 來包裹整體的路由
- graphqlHTTP 定義請求相關資料
- GraphQLSchema 使我們接下來要寫的一個操作模組.
這個畫面是不是似曾相識!
schema->default.js
const {
GraphQLID,
GraphQLInt,
GraphQLList,
GraphQLString,
GraphQLSchema,
GraphQLNonNull,
GraphQLObjectType,
GraphQLInputObjectType,
} = require('graphql');
// id對應的詳情
let idArr = {
1:{
name:'我是id1',
age:'19'
},
2:{
name:'我是id2',
age:'24'
}
}
// 定義id的類
let GID= new GraphQLObjectType({
name: 'gid',
fields: {
name: { type: GraphQLString },
age: { type: GraphQLString },
}
})
// 引數型別 不太對
let cs = new GraphQLInputObjectType({
name:'iddecanshu',
fields: {
id: { type: GraphQLString },
}
})
//定義導航Schema型別
var GraphQLNav = new GraphQLObjectType({
name: 'nav',
fields: {
cc:{ // 傳參
type:GraphQLString,
// args:new GraphQLNonNull(cs), // 1; 這種是錯的
args:{
data: {
type:new GraphQLNonNull(cs), // 這種可以用data為載體了
}
},
// args:{ // 3:這種最好用了。。。
// id:{
// type:GraphQLString
// }
// },
resolve(parent,args){
return '我傳的是' + args.data.id
}
},
// greeting(name: String):String
title: { type: GraphQLString },
url: { type: GraphQLString },
id: {
// type:GraphQLList(GID), // 這裡很容易忽略
type:GraphQLNonNull(GID), // 反覆查詢也沒有專門obj的 這裡用非空代替
async resolve(parent,args){
// console.log('wwwwwwwww', idArr[parent.id])
// 這個bug我tm。。。。。
// 需要是陣列形式。。。。不然報錯
// "Expected Iterable, but did not find one for field \"nav.id\".",
// return [idArr[parent.id]];
// 2: 更改型別後就對了
return idArr[parent.id] || {}
}
},
}
})
//定義根
var QueryRoot = new GraphQLObjectType({
name: "RootQueryType",
fields: {
navList: {
type: GraphQLList(GraphQLNav),
async resolve(parent, args) {
var navList = [
{ title: 'title1', url: 'url1', id:'1' },
{ title: 'title2', url: 'url2', id:'2' }
]
return navList;
}
}
}
})
//增加資料
const MutationRoot = new GraphQLObjectType({
name: "Mutation",
fields: {
addNav: {
type: GraphQLNav,
args: {
title: { type: new GraphQLNonNull(GraphQLString) },
},
async resolve(parent, args) {
return {
msg: '插入成功'
}
}
}
}
})
module.exports = new GraphQLSchema({
query: QueryRoot,
mutation: MutationRoot
});
十二. koa2中的使用原理"逐句"解析
①引入
- 這個是原生自帶的, 比如我們會把
GraphQLID
這種象徵著單一型別的類單獨拿到. - 定義type的方法也變成了 GraphQLObjectType這樣的例項化類來定義.
const {
GraphQLID,
GraphQLInt,
GraphQLList,
GraphQLString,
GraphQLSchema,
GraphQLNonNull,
GraphQLObjectType,
GraphQLInputObjectType,
} = require('graphql');
②單一的類
- 我們例項化
GraphQLObjectType
匯出一個'type' - 使用 type:GraphQLString的形式規定一個變數的型別
- name: 這裡的name可以理解為一個說明, 有時候可以通過獲取這個值做一些事.
let GID= new GraphQLObjectType({
name: 'gid',
fields: {
name: { type: GraphQLString },
age: { type: GraphQLString },
}
})
③ 定義根類
- fields必須要寫, 在它裡面才可以定義引數
-
GraphQLList
意思就是必須為陣列 - type不能少, 裡面要規定好這組返回資料的具體型別
- resolve也是必須有的沒有會報錯, 並且必須返回值與type一致
var QueryRoot = new GraphQLObjectType({
name: "RootQueryType",
fields: {
navList: {
type: GraphQLList(GraphQLNav),
async resolve(parent, args) {
var navList = [
{ title: 'title1', url: 'url1', id:'1' },
{ title: 'title2', url: 'url2', id:'2' }
]
return navList;
}
}
}
})
十三. koa2裡面的關聯關係與傳參
這裡的關聯關係是指, 之前我們說過的 id 指向另一個表
let GID= new GraphQLObjectType({
name: 'gid',
fields: {
name: { type: GraphQLString },
age: { type: GraphQLString },
}
})
var GraphQLNav = new GraphQLObjectType({
name: 'nav',
fields: {
cc:{
type:GraphQLString,
args:{
data:
type:new GraphQLNonNull(cs), // 這種可以用data為載體了
}
},
resolve(parent,args){
return '我傳的是' + args.data.id
}
},
id: {
type:GraphQLNonNull(GID),
async resolve(parent,args){
return idArr[parent.id] || {}
}
},
}
})
- 上面cc這個變數比較特殊, 他需要args這個key來規範引數, 這裡可以直接寫參也可以像這裡一樣抽象一個類.
- id他規範了id對應的是一個物件, 裡面有name有age
- cc想要拿到傳參就需要args.data 因為這裡我們用的input類來做的
實際效果如圖所示:
十四. 對整合在koa2內的工程化思考(資料模型分塊)
1. 從上面那些例子裡面可看出, 我們可以用/api/blog這種路由path為單位, 去封裝一個個的資料模型
2. 每個模型裡面其實都需要運算元據庫
3. 說實話增加的程式碼有點多, 這裡只演示了2個介面就已經這麼大了
4. 學習成本是不可忽略的, 而且這裡面各種古怪的語法報錯
十五. 前端的呼叫
這裡我們以vue為例import axios from "axios";
這個是前提query=
這個是關鍵點, 我們以後的引數都要走這裡
方式1(暴力調取)
created(){
// 1: 查詢列表
// ①: 一定要轉碼, 因為url上不要有{} 空格
axios
.get(
"/graphql?query=%7B%0A%20%20navList%20%7B%0A%20%20%20%20title%0A%20%20%20%20url%0A%20%20%7D%0A%7D%0A"
)
.then(res => {
console.log("返回值: 1", res.data);
});
}
方式2(封裝函式)
methods: {
getQuery() {
const res = `
{
navList {
title
url
id {
name
age
}
}
}`;
return encodeURI(res);
},
},
created() {
axios.get(`/graphql?query=${this.getQuery()}`).then(res => {
console.log("返回值: 2", res.data);
});
}
方式3(函式傳參)
methods: {
getQuery2(id) {
const res = `
{
navList {
cc(data:{id:"${id}"})
title
url
id {
name
age
}
}
}`;
return encodeURI(res);
}
},
created() {
axios.get(`/graphql?query=${this.getQuery2(1)}`).then(res => {
console.log("返回值: 3", res.data);
});
}
十六. 前端外掛的調研
- 一看前面的傳參方式就會發覺, 這肯定不合理啊, 一定要把字串解構出來.
-
vue-apollo
技術棧是當前比較主流的, 但是這個庫寫的太碎了, 並且配置起來還要更改我本來的程式碼習慣. - 又在github上面找了一些模板的庫, 但是並沒有讓我從書寫字串的尷尬中解脫出來
所以暫時沒有找到我認可的庫, 當然了自己暫時也並不想把時間放在開發這個外掛上.
十七. 我想要的外掛什麼樣的
- 可以使我,採用物件或者json的方式來定義query
- 不需要定義那麼多概念, 開箱即用, 我只需要那個核心的模板解決方案
- 不要改變我本來的程式碼書寫方式, 比如說
vue-apollo
提供了新的請求方式, 可是我的專案都是axios, 我就是不想換 憑什麼要換
十八. 學習graphql有多少阻礙
- 網上的資料真的對初學者很不友好, 根本不舉例子, 導致每個知識點都會我自己試了幾十次試出來的.
- 光學這一個技術不夠, 還要思考服務端的重構與新規範, 前端也要新規範.
- 這個技術也出來好幾年了, 但是社群真的不敢恭維, 所以啊還是需要自己更多的精力投入進來.
十九. 此類技術趨勢的思考
- 前端工程師越來越不滿足自己的程式碼只在瀏覽器上執行了, 我們也要參與到服務端互動的環節中.
- 身為當今2020年的前端如果眼光還侷限在web端有可能會被後浪排在沙灘上了.
- 個人不太喜歡這種丟擲很多新的知識點, 和架構體系的庫, 應該是越少的學習成本與程式碼成本做到最多的事.
- 前端技術還在摸索階段, 也可以說是擴充套件階段, 預計這幾年還是會出現各種新的概念, 但是吧正所謂"合久必分,分久必合", 在不就的將來前端的範圍很可能重新劃定.
二十. end
說了這麼多也是因為學習期間遇到了好多坑, 不管怎麼樣最後還是可以使用起來了, 在之後的使用過程中還是會不斷的總結並記錄, 遇到問題我們可以一起討論.
希望和你一起進步.