標量(ScalarTypeDefinition)是 GraphQL 中不可分割的原子資料型別,在服務中充當葉子節點。對於客戶端而言,合法的查詢集(Select Set)必須到達葉子節點,也就是到達標量型別節點。
GraphQL 規範提供了五種標量:
- Int: 32 位有符號整型,超出精度範圍後,引擎會丟擲異常
- Float: 有符號雙精度浮點數,超出精度範圍後,引擎會丟擲異常
- String: 字串,用於表示
UTF-8
字元序列 - Boolean: bool 值
- ID: 資源唯一標誌符
1. ID 特性
上述五種型別與其他語言對應的型別定義相似,相信讀者老爺們都已經非常熟悉,無需贅述,唯一值得探討的是 ID
型別。
- 表現上
ID
型別只是一個字串格式的值,引擎支援字串解析值,也支援將Int
解析值轉換為字串型別; - 語義上"
ID
" 型別應該用於唯一標誌一個資源物件,也就是說,使用相同 ID 值,無論查詢多少次,結果都應該是同一物件,這一點有助於實現快取,是 GraphQL 推薦的快取方案; - 引擎並不限制解析值的唯一性,查詢結果包含多個 ID 值相同的節點是合法的。
我們來看一下例子加深印象:
[
// 字串型別
{id: '1'},
// int 型別,引擎會將其轉換為字串
{id: 1},
// float 型別
// 非法值,引擎不支援float轉換
// 將丟擲 `TypeError` 錯誤
{id: 1.2},
// 與上面第一條重複
// 合法值,引擎並不強制 `ID` 值的唯一性
{id: '1'}
]
複製程式碼
2. 自定義標量型別
除規範定義的標量外,還可以按需定義業務範疇內的標量。語法非常簡單:
scalar Datetime
複製程式碼
注意,這只是語義範疇定義,還需要定義序列化、反序列化函式:
new GraphQLScalarType({
name: "Datetime",
description: "日期時間標量型別",
// 序列化函式
serialize(value) {
return value.toString();
},
// 解析函式
parseValue(value) {
if (typeof value === "string") {
return new Date(value);
}
throw new Error("引數型別錯誤");
},
// 解析函式
parseLiteral(ast) {
if (ast.kind === Kind.STRING) {
return new Date(ast.value);
}
throw new Error("引數型別錯誤");
}
});
複製程式碼
下面我們一個一個看這些配置:
name
: 欄位名,請保持與 schema 中定的標量型別名稱保持一致description
: 型別描述,在一些診斷工具上還是很有用的serialize
: 序列化函式,用於將結果轉換為適合 http 傳輸的數值型別parseValue
: 解析函式,用於將客戶端通過 variables 引數傳遞的數值為 Date 型別parseLiteral
: 同樣是解析函式,將客戶端傳遞的 字面量引數 解析為 Date 型別
配置中的 parseValue
、parseLiteral
兩個函式功能上相似,都用於解析客戶端引數,分別處理兩種引數傳遞方式:
# variables 引數
# 引擎將呼叫 parseValue 函式
query (before: Datetime){
users(before: $before) {
id
name
}
}
variables {
before: "1991-02-19"
}
# 字面量引數
# 引擎將呼叫 parseLiteral 函式
query {
users(before: "1991-02-19") {
id
name
}
}
複製程式碼
最後說一些注意的點:
- 如果型別確定不會作為
InputType
,可以省略parseValue
、parseLiteral
。 parseValue
接收到的是variables
物件中對應的值;而parseLiteral
接收的則是引擎從query
語句中解析出的 AST 節點。AST 節點內容形如:
{
// 字面量型別
"kind": "StringValue",
// 字面量值
"value": "1991-02-19",
// 指明字面量是否為 [BlockStringValue](https://facebook.github.io/graphql/June2018/#BlockStringValue()) 型別
"block": false,
// token 位置
"loc":
{
"start": 18,
"end": 30
}
}
複製程式碼
3. 返回物件的標量
標量型別也支援返回結構化的物件,只要能為引擎提供符合規則的 serialize
函式,一切皆有可能。我們可以寫出這樣一個標量:
// Address 物件型別,不過這是一個標量
new GraphQLScalarType({
name: "Address",
description: "物件型別的標量",
serialize(value) {
// value 為物件型別
// value = { city: '深圳', province: '廣東省', country: '中國' }
return value;
}
});
複製程式碼
但是要注意,標量型別是 不可分割 的,不能再傳入查詢子集:
# 合法請求
query {
users {
id
name
# Address 型別值
bornOrigin
}
}
複製程式碼
返回結果:
{
"data": {
"users": [
{
"id": "1",
"name": "foo",
"bornOrigin": {
"city": "深圳",
"province": "廣東省",
"country": "中國"
}
}
]
}
}
複製程式碼
完整程式碼在 此處。 雖然合乎規則,但用 標量型別 來返回一個無法被拆解的物件,違反了 按需載入 這一重要原則,並不值得推崇,除非實在找不到更好的解決方案。 比如,有時候我們需要處理高度動態的資訊結構,我們期望以結構化、可預期的形式傳輸資訊,此時我們就不得不採用這種方案了。 以日誌為例,一個稍上規模的系統,日誌格式多種多樣,如果要一一列舉,一一轉化成 GraphQL 的 SDL,開發、維護成本都非常高,那用一個標量型別表示這多種多樣的格式,價效比就很高了。
總結
標量是 GraphQL 中的原子型別,一般充當查詢的葉子節點。
GraphQL 規範提供了五種標量型別,其中 ID
最為特殊,用於唯一標誌一個資源例項。
在標準標量之外,也可以按需定義新的標量,規則如上。