此篇文章介紹 graphql 基礎知識,會過於無聊。如果你想快速上手,可以使用我的腳手架 shfshanyue/apollo-server-starter。如果你想用它寫一個前端,可以參考我的 shfshanyue/shici。
本文地址: graphql schema and query
我們先寫一個關於 graphql.js
的 hello, world
示例,並且圍繞它展開對 graphql
的學習
const {
graphql,
GraphQLSchema,
GraphQLObjectType,
GraphQLString,
} = require('graphql')
// schema,由 web 框架實現時,這部分放在後端裡
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'RootQueryType',
fields: {
hello: {
type: GraphQLString,
resolve() {
return 'hello, world'
}
}
}
})
})
// query,由 web 框架實現時,這部分放在前端裡
const query = '{ hello }'
// 查詢,這部分放在服務端裡
graphql(schema, query).then(result => {
// {
// data: { hello: "hello, world" }
// }
console.log(result)
})
複製程式碼
由上,也可以看出 graphql
很關鍵的兩個要素:schema
和 query
。而當我們開發 web 應用時,schema
將會是服務端的主體,而 query
存在於前端中,類似 REST 中的 API。
schema and query
我們先抽出 schema
的程式碼部分,這裡有 GraphQLSchema
,GraphQLObjectType
和 GraphQLString
等諸多 API。
const schema = new GraphQLSchema({
query: new GraphQLObjectType({
name: 'RootQueryType',
fields: {
hello: {
type: GraphQLString,
resolve() {
return 'hello, world'
}
}
}
})
})
複製程式碼
正如在 React
中使用jsx
簡化了 React.createElement
的寫法。graphql
對於 schema 也有一套它自己的 DSL (Domain Specified Language)
,也更為簡單,易懂。在程式碼中以 graphql
或 gql
作為檔名字尾,用法如下
# schama,在後端進行維護
type Query {
hello: String
}
# query,在前端進行管理
{
hello
}
複製程式碼
以及查詢結果
{
hello: 'hello, world'
}
複製程式碼
在前端中所有查詢的 gql 往往會通過 graphql/gql 字尾的檔案來統一維護,這裡有一份程式碼示例: shfshanyue/shici:query.gql
你看到這裡,想必有兩個疑問:
- 以上的
graphql
代表什麼,以及我們如何書寫graphql
- 相比 js 程式碼,
DSL
少了一個resolve
函式,而它又是什麼
Object Type and Field
這裡引入 graphql 中的兩個基本術語,object type
與 field
。它們是組成 graphql 最基本的元件,如同細胞是生物體的基本單位。
這裡來一個更復雜的 schema
,如下所示
# schema
schema {
query: Query
}
type Query {
hello: String
todos: [Todo!]!
}
type Todo {
id: ID!
title: String!
}
# query
{
# 只能查詢 Query 下的欄位
todos {
id
title
}
hello
}
複製程式碼
如果說 graphql 是資料庫的進一步抽象,則 object type
類似於 sql 中的 table
,field
類似於 sql 中的 column
。
那我們仔細審視以上示例,能從其中得到一些資訊:
type
標註為graphql.js
中的型別:GraphQLObjectType
{}
代表一個query
(查詢),其中由若干欄位組成,用以查你所需要的資料Query
是一個特殊的object type
,表示為RootQueryType
,它會放到schema
中。如同C語言中的main
函式,可以理解為graphql
的入口查詢。正因如此,它所包含的field
沒有緊密的內關聯關係。hello
與todos
是 Query 下的兩個field
,一切前端的查詢均要從Query
的field
查起。如在以上的 query 示例中,只能查詢todos
與hello
。Todo
是一個自定義名稱的object type
,可以理解為對應資料庫中的一個 todo 的表。id
與title
是 Todo 下的field
,可以理解他們為 Todo 的屬性,它們往往由一些基本屬性以及聚合屬性 (count, sum) 組成。[Todo!]!
代表返回結果將是一個Todo
的陣列。[]
代表返回為陣列,!
代表返回不能為空,[!]
代表陣列中的每一項都不能為空。id: ID!
代表 Todo 的 id 全域性唯一title: String!
代表 Todo 的 title 是不為空的字串
到了這裡,你會發現,對於 graphql schema 的認識還有一些資訊尚未涉及:ID
與 String
,它們被稱作 scalar type
,你可以理解為資料型別。 正是因為 scalar,graphql 才成為強型別查詢語言。
Query: query everyting
由上所述,Query
為 graphql
的入口查詢處,我們可以並且只可以查詢 Query
下的任意欄位 (field)。因此,他組成了 graphql
最核心的功能: 查詢你所需要的任何資料。
# schema
type Query {
hello: String
todos: [Todo!]!
}
// 以下三個 query 一般會在前端進行統一管理
# query 1
{
hello
}
# query 2
{
todos {
id
}
}
# query 3
{
hello
todos {
id
title
}
}
複製程式碼
查詢結果如下
{ hello: 'hello, world' }
{ todos: [{ id: 1 }] }
{ hello: 'hello, world', todos: [{ id: 1, title: 'learn react' }] }
複製程式碼
在前端我們根據 Query
組合成各種查詢,而我們為了在查詢過程中方便辨認,可以為查詢新增 operationName
:
query HELLO {
hello
}
query TODOS {
todos {
id
}
}
query HELLO_AND_TODOS {
hello
todos {
id
title
}
}
複製程式碼
Scalar Type
在 graphql
中有一些內建的 scalar
型別,用以表示 graphql 中 field
的資料型別,這也是 graphql 為強型別語言的基礎。內建型別如下所示
Int
,代表32位有符號型整數Float
String
Boolean
ID
,唯一識別符號,一般可視為資料庫中的主鍵。在object type
中,一般會把 id 設定為ID
型別,依賴它做一些快取的操作。
正因為 scalar
與 !
,來保證了 graphql 的 query 是強型別的。所以當我們看到如下的 query 時,可以在前端大膽放心的使用: data.todos.map(todo => todo.title.replace(' ', ''))
。既不用擔心 data.todos
突然報錯 Cannot read property 'map' of null
,也不用擔心 Cannot read property 'title' of null
# schema
type Query {
hello: String
todos: [Todo!]!
}
type Todo {
id: ID!
title: String!
}
# query
{
todos {
id
title
}
}
複製程式碼
如果不使用 graphql,你無法保證響應中資料的型別。你可能需要使用 lodash 來做一些邊界的處理
const data = {
todos: [{
id: 1,
title: 'learn react'
}]
}
// 使用 graphql 後
data.todos.map(todo => todo.title.replace(' ', ''))
// 如果不使用 graphql,你無法保證返回資料型別。你可能需要使用 lodash 來做一些邊界的處理
_.map(data.todos, todo => _.replace(todo.title, ' ', ''))
複製程式碼
當在資料庫中,一個欄位除了整型,浮點型等基本型別外,還會有更多而且比較重要和常用的資料型別:json
和 datetime
。既然 scalar
用以表示 field
的資料型別,那麼它如何表示 json
與 datetime
或者更多的資料型別呢?
這時,可以使用 graphql.js
的API graphql.GraphqlQLScalarType
來自定義 scalar
resolve function
再回到剛開始的 hello, world
的示例,用 graphql
表示如下
# schema
type Query {
hello: String
}
# query
query HELLO {
hello
}
複製程式碼
對以上章節的內容再梳理一遍:
- 可以對
hello
進行查詢,因為該欄位在Query
下 HELLO
查詢所得到的data.hello
是一個字串
恩?我們好像把最重要的內容給漏了,hello
中的內容到底是什麼?!而 resolve
函式就是做這個事的
// 使用 graphql.js 的寫法,把 schema 與 resolve 寫在一起
new GraphQLObjectType({
name: 'RootQueryType',
fields: {
hello: {
type: GraphQLString,
resolve() {
return 'hello, world'
}
}
}
})
// 單獨把 resolve 函式給寫出來
function Query_hello_resolve () {
return 'hello, world'
}
複製程式碼
於是我們再補齊以上內容
# schema
type Query {
hello: String
}
# query
{
hello
}
複製程式碼
由此得到的資料示例
{
hello: 'hello, world'
}
複製程式碼
context and args
檢視一個很典型的 REST 服務端的一段邏輯:抽取使用者ID以及讀取引數(querystring/body)
app.use('/', (ctx, next) {
ctx.user.id = getUserIdByToken(ctx.headers.authorization)
next()
})
app.get('/todos', (ctx) => {
const userId = ctx.user.id
const status = args.status
return db.Todo.findAll({})
})
複製程式碼
而在 graphql 中,使用 resolve
函式為 field
提供資料,而 context,args 都會作為 resolve
函式的引數
# schema
type Query {
# 如同 REST 一般,可以攜帶引數,並顯式宣告
todos (status: String): [Todo!]!
}
type Todo {
id: ID!
title: String!
}
# query
{
# 查詢時,在這裡指定引數 (args)
todos (status: "TODO") {
id
title
}
# 同時也可以指定別名,特別是當有 args 時
done: todos (status: "DONE") {
id
title
}
}
# query with variables
query TODOS ($status: String) {
done: todos (status: $status) {
id
title
}
}
複製程式碼
返回資料示例
{
todos: [{ id: 1, title: '松風吹解帶' }],
done: [{ id: 2, title: '山月照彈琴' }],
}
複製程式碼
當然,我們也是通過 Query
以及 Todo 的 resolve 函式來確定內容,對於如何獲取以上資料如下所示
// Query 的 resolve 函式
const Query = {
todos (obj, args, ctx, info) {
// 從 ctx 中取一些上下文資訊,如最常見的 user
const userId = ctx.user.id
const status = args.status
return db.Todo.findAll({})
}
}
// Todo 的 resolve 函式
const Todo = {
title (obj) {
return obj.title
}
}
複製程式碼
- obj,代表該欄位所屬的
object type
,如Todo.title
中obj
表示todo
- args,代表所傳過來的引數
- ctx,上下文
- info,
GraphQLResolveInfo
,關於本次查詢的元資訊,比如 AST,你可以對它進行解析
從這裡可以看出來:graphql 的引數都是顯式宣告,並且強型別。這一點比 REST 要好一些
# query with variables
query TODOS ($status: String) {
done: todos (status: $status) {
id
title
}
}
複製程式碼
Mutation
graphql
能夠簡化一切的查詢,或者說它是簡化了服務端開發人員 CRUD
中的 Read
。那麼,如何對資源進行修改呢?這裡就提到了 Mutation
。
# 在後端的 schema
schema {
query: Query
mutation: Mutation
}
type Mutation {
addTodo (title: String!): Todo
}
# 在前端的 query
mutation ADD_TODO {
addTodo (title: "學習 React") {
id
title
}
}
複製程式碼
// 以上示例返回結果
{
addTodo: { id: 128, title: '學習React' }
}
複製程式碼
以上是一個新增 Todo 的例子,從這裡可以注意到幾點
Mutation
與Query
同樣屬於特殊的object type
,同樣,所有關於資料的更改操作都要從Mutation
中找起,也需要放到schema
中Mutation
與Query
分別為 graphql 的兩大類操作,在前端進行Mutation
查詢時,需要新增mutation
欄位 (Query
查詢時,在前端新增query
欄位,但這不是必選的)
input type and variables
type Mutation {
addTodo (title: String!): Todo
}
mutation ADD_TODO {
addTodo (title: "學習 React") {
id
title
}
}
複製程式碼
以上是一個關於新增 Todo 的 mutation,在我們新增一個 Todo 時,它僅有一個屬性: title。如果它擁有更多個屬性呢?這時,可以使用 input type
,把某一資源的所有屬性聚合起來。並且配合 variables
一起使用傳遞資料
input InputTodo {
title: String!
}
type Mutation {
addTodo (todo: InputTodo!): Todo
}
mutation ADD_TODO ($todo: InputTodo!) {
addTodo (todo: $todo) {
id
title
}
}
複製程式碼
// $todo 的值,在前端獲取資料時,使用 variables 傳入
{
title: '學習 React'
}
複製程式碼
更多文章
- 前端部署發展史
- docker 簡易入門
- 個人伺服器運維指南
- 當我有一臺伺服器時我做了什麼
- 如果你想搭建一個部落格
- github actions 入門指南及部落格自動部署實踐
- 使用 k8s 部署你的第一個應用: Pod,Deployment 與 Service
- 使用 k8s 為你的應用配置域名: Ingress
- 使用 k8s 為你的域名加上 https
我是山月,我會定期分享全棧文章在個人公眾號中。如果你對全棧面試,前端工程化,graphql,devops,個人伺服器運維以及微服務感興趣的話,可以關注我。如果想進群交流,可以新增我微信 shanyue94,備註加群。