GraphQL —— 標量型別

VanMess發表於2018-12-19

標量(ScalarTypeDefinition)是 GraphQL 中不可分割的原子資料型別,在服務中充當葉子節點。對於客戶端而言,合法的查詢集(Select Set)必須到達葉子節點,也就是到達標量型別節點。

GraphQL 規範提供了五種標量:

  1. Int: 32 位有符號整型,超出精度範圍後,引擎會丟擲異常
  2. Float: 有符號雙精度浮點數,超出精度範圍後,引擎會丟擲異常
  3. String: 字串,用於表示 UTF-8 字元序列
  4. Boolean: bool 值
  5. ID: 資源唯一標誌符

1. ID 特性

上述五種型別與其他語言對應的型別定義相似,相信讀者老爺們都已經非常熟悉,無需贅述,唯一值得探討的是 ID 型別。

  1. 表現上 ID 型別只是一個字串格式的值,引擎支援字串解析值,也支援將 Int 解析值轉換為字串型別;
  2. 語義上"ID" 型別應該用於唯一標誌一個資源物件,也就是說,使用相同 ID 值,無論查詢多少次,結果都應該是同一物件,這一點有助於實現快取,是 GraphQL 推薦的快取方案;
  3. 引擎並不限制解析值的唯一性,查詢結果包含多個 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("引數型別錯誤");
  }
});
複製程式碼

下面我們一個一個看這些配置:

  1. name: 欄位名,請保持與 schema 中定的標量型別名稱保持一致
  2. description: 型別描述,在一些診斷工具上還是很有用的
  3. serialize: 序列化函式,用於將結果轉換為適合 http 傳輸的數值型別
  4. parseValue: 解析函式,用於將客戶端通過 variables 引數傳遞的數值為 Date 型別
  5. parseLiteral: 同樣是解析函式,將客戶端傳遞的 字面量引數 解析為 Date 型別

配置中的 parseValueparseLiteral 兩個函式功能上相似,都用於解析客戶端引數,分別處理兩種引數傳遞方式:

# variables 引數
# 引擎將呼叫 parseValue 函式
query (before: Datetime){
  users(before: $before) {
    id
    name
  }
}

variables {
  before: "1991-02-19"
}

# 字面量引數
# 引擎將呼叫 parseLiteral 函式
query {
  users(before: "1991-02-19") {
    id
    name
  }
}
複製程式碼

最後說一些注意的點:

  1. 如果型別確定不會作為 InputType,可以省略 parseValueparseLiteral
  2. 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 最為特殊,用於唯一標誌一個資源例項。 在標準標量之外,也可以按需定義新的標量,規則如上。

相關文章