GraphQL初體驗,Node.js構建GraphQL API指南

image

原文: https://blog.heroku.com
做者:CHRIS CASTLE

微信搜索【前端全棧開發者】關注這個脫髮、擺攤、賣貨、持續學習的程序員,第一時間閱讀最新文章,會優先兩天發表新文章。關注便可大禮包,準能爲你節省很多錢!javascript

在過去的幾年中,GraphQL已經成爲一種很是流行的API規範,該規範專一於使客戶端(不管客戶端是前端仍是第三方)的數據獲取更加容易。前端

在傳統的基於REST的API方法中,客戶端發出請求,而服務器決定響應:java

curl https://api.heroku.space/users/1

{
  "id": 1,
  "name": "Luke",
  "email": "luke@heroku.space",
  "addresses": [
    {
    "street": "1234 Rodeo Drive",
    "city": "Los Angeles",
    "country": "USA"
    }
  ]
}

可是,在GraphQL中,客戶端能夠精確地肯定其從服務器獲取的數據。例如,客戶端可能只須要用戶名和電子郵件,而不須要任何地址信息:git

curl -X POST https://api.heroku.space/graphql -d '
query {
  user(id: 1) {
    name
    email
  }
}


{
  "data":
    {
    "name": "Luke",
    "email": "luke@heroku.space"
    }
}

經過這種新的模式,客戶能夠經過縮減響應來知足他們的需求,從而向服務器進行更高效的查詢。對於單頁應用(SPA)或其餘前端重度客戶端應用,能夠經過減小有效載荷大小來加快渲染時間。可是,與任何框架或語言同樣,GraphQL也須要權衡取捨。在本文中,咱們將探討使用GraphQL做爲API的查詢語言的利弊,以及如何開始構建實現。程序員

爲何選擇GraphQL?

與任何技術決策同樣,瞭解GraphQL爲你的項目提供了哪些優點是很重要的,而不是簡單地由於它是一個流行詞而選擇它。github

考慮一個使用API鏈接到遠程數據庫的SaaS應用程序。你想要呈現用戶的我的資料頁面,你可能須要進行一次API GET 調用,以獲取有關用戶的信息,例如用戶名或電子郵件。而後,你可能須要進行另外一個API調用以獲取有關地址的信息,該信息存儲在另外一個表中。隨着應用程序的發展,因爲其構建方式的緣由,你可能須要繼續對不一樣位置進行更多的API調用。雖然每個API調用均可以異步完成,但你也必須處理它們的響應,不管是錯誤、網絡超時,甚至暫停頁面渲染,直到收到全部數據。如上所述,這些響應的有效載荷可能超過了渲染你當前頁面的須要,並且每一個API調用都有網絡延遲,總的延遲加起來可能很可觀。web

使用GraphQL,你無需進行多個API調用(例如 GET /user/:idGET /user/:id/addresses ),而是進行一次API調用並將查詢提交到單個端點:typescript

query {
  user(id: 1) {
    name
    email
    addresses {
    street
    city
    country
    }
  }
}

而後,GraphQL僅提供一個端點來查詢所需的全部域邏輯。若是你的應用程序不斷增加,你會發現本身在你的架構中添加了更多的數據存儲——PostgreSQL多是存儲用戶信息的好地方,而Redis多是存儲其餘種類信息的好地方——對GraphQL端點的一次調用將解決全部這些不一樣的位置,並以他們所請求的數據響應客戶端。數據庫

若是你不肯定應用程序的需求以及未來如何存儲數據,則GraphQL在這裏也頗有用。要修改查詢,你只需添加所需字段的名稱:express

addresses {
      street
+     apartmentNumber   # new information
      city
      country
    }

這極大地簡化了隨着時間的推移而發展你的應用程序的過程。

定義一個GraphQL schema

image

有各類編程語言的GraphQL服務器實現,但在你開始以前,你須要識別你的業務域中的對象,就像任何API同樣。就像REST API可能會使用JSON模式同樣,GraphQL使用SDL或Schema定義語言來定義它的模式,這是一種描述GraphQL API可用的全部對象和字段的冪等方式。SDL條目的通常格式以下:

type $OBJECT_TYPE {
  $FIELD_NAME($ARGUMENTS): $FIELD_TYPE
}

讓咱們之前面的例子爲基礎,定義一下user和address的條目是什麼樣子的。

type User {
  name:     String
  email:    String
  addresses:   [Address]
}

type Address {
  street:   String
  city:     String
  country:  String
}

user 定義了兩個 String 字段,分別是 nameemail ,它還包括一個稱爲 addresses 的字段,它是 Addresses 對象的數組。 Addresses 還定義了它本身的幾個字段。 (順便說一下,GraphQL模式不只有對象,字段和標量類型,還有更多,你也能夠合併接口,聯合和參數,以構建更復雜的模型,但本文中不會介紹。)

咱們還須要定義一個類型,這是咱們GraphQL API的入口點。你還記得,前面咱們說過,GraphQL查詢是這樣的:

query {
  user(id: 1) {
    name
    email
  }
}

query 字段屬於一種特殊的保留類型,稱爲 Query ,這指定了獲取對象的主要入口點。(還有用於修改對象的 Mutation 類型。)在這裏,咱們定義了一個 user 字段,該字段返回一個 User 對象,所以咱們的架構也須要定義此字段:

type Query {
  user(id: Int!): User
}

type User { ... }
type Address { ... }

字段中的參數是逗號分隔的列表,格式爲 $NAME: $TYPE! 是GraphQL表示該參數是必需的方式,省略表示它是可選的。

根據你選擇的語言,將此模式合併到服務器中的過程會有所不一樣,但一般,將信息用做字符串就足夠了。Node.js有 graphql 包來準備GraphQL模式,但咱們將使用 graphql-tools 包來代替,由於它提供了一些更多的好處。讓咱們導入該軟件包並閱讀咱們的類型定義,覺得未來的開發作準備:

const fs = require('fs')
const { makeExecutableSchema } = require("graphql-tools");

let typeDefs = fs.readFileSync("schema.graphql", {
  encoding: "utf8",
  flag: "r",
});

設置解析器

schema設置了構建查詢的方式,但創建schema來定義數據模型只是GraphQL規範的一部分。另外一部分涉及實際獲取數據,這是經過使用解析器完成的,解析器是一個返回字段基礎值的函數。

讓咱們看一下如何在Node.js中實現解析器。咱們的目的是圍繞着解析器如何與模式一塊兒操做來鞏固概念,因此咱們不會圍繞着如何設置數據存儲來作太詳細的介紹。在「現實世界」中,咱們可能會使用諸如knex之類的東西創建數據庫鏈接。如今,讓咱們設置一些虛擬數據:

const users = {
  1: {
    name: "Luke",
    email: "luke@heroku.space",
    addresses: [
    {
      street: "1234 Rodeo Drive",
      city: "Los Angeles",
      country: "USA",
    },
    ],
  },
  2: {
    name: "Jane",
    email: "jane@heroku.space",
    addresses: [
    {
      street: "1234 Lincoln Place",
      city: "Brooklyn",
      country: "USA",
    },
    ],
  },
};

Node.js中的GraphQL解析器至關於一個Object,key是要檢索的字段名,value是返回數據的函數。讓咱們從初始 user 按id查找的一個簡單示例開始:

const resolvers = {
  Query: {
    user: function (parent, { id }) {
      // 用戶查找邏輯
    },
  },
}

這個解析器須要兩個參數:一個表明父的對象(在最初的根查詢中,這個對象一般是未使用的),一個包含傳遞給你的字段的參數的JSON對象。並不是每一個字段都具備參數,可是在這種狀況下,咱們將擁有參數,由於咱們須要經過用戶ID來檢索其用戶。該函數的其他部分很簡單:

const resolvers = {
  Query: {
    user: function (_, { id }) {
      return users[id];
    },
  }
}

你會注意到,咱們沒有明肯定義 UserAddresses 的解析器,graphql-tools 包足夠智能,能夠自動爲咱們映射這些。若是咱們選擇的話,咱們能夠覆蓋這些,可是如今咱們已經定義了咱們的類型定義和解析器,咱們能夠創建咱們完整的模式:

const schema = makeExecutableSchema({ typeDefs, resolvers });

運行服務器

最後,讓咱們來運行這個demo吧!由於咱們使用的是Express,因此咱們可使用 express-graphql 包來暴露咱們的模式做爲端點。該程序包須要兩個參數:schema和根value,它有一個可選參數 graphiql,咱們將稍後討論。

使用GraphQL中間件在你喜歡的端口上設置Express服務器,以下所示:

const express = require("express");
const express_graphql = require("express-graphql");

const app = express();
app.use(
  "/graphql",
  express_graphql({
    schema: schema,
    graphiql: true,
  })
);
app.listen(5000, () => console.log("Express is now live at localhost:5000"));

將瀏覽器導航到 http://localhost:5000/graphql,你應該會看到一種IDE界面。在左側窗格中,你能夠輸入所需的任何有效GraphQL查詢,而在右側你將得到結果。

這就是 graphiql: true 所提供的:一種方便的方式來測試你的查詢,你可能不想在生產環境中公開它,可是它使測試變得容易得多。

嘗試輸入上面展現的查詢:

query {
  user(id: 1) {
    name
    email
  }
}

要探索GraphQL的類型化功能,請嘗試爲ID參數傳遞一個字符串而不是一個整數。

# 這不起做用
query {
  user(id: "1") {
    name
    email
  }
}

你甚至能夠嘗試請求不存在的字段:

# 這不起做用
query {
  user(id: 1) {
    name
    zodiac
  }
}

只需用schema表達幾行清晰的代碼,就能夠在客戶機和服務器之間創建強類型的契約。這樣能夠防止你的服務接收虛假數據,並向請求者清楚地代表錯誤。
image

性能考量

儘管GraphQL爲你解決了不少問題,但它並不能解決構建API的全部固有問題。特別是緩存和受權這兩個方面,只是須要一些預案來防止性能問題。GraphQL規範並無爲實現這兩種方法提供任何指導,這意味着構建它們的責任落在了你身上。

緩存

基於REST的API在緩存時不須要過分關注,由於它們能夠構建在web的其餘部分使用的現有HTTP頭策略之上。GraphQL不具備這些緩存機制,這會對重複請求形成沒必要要的處理負擔。考慮如下兩個查詢:

query {
  user(id: 1) {
    name
  }
}

query {
  user(id: 1) {
    email
  }
}

在沒有某種緩存的狀況下,只是爲了檢索兩個不一樣的列,會致使兩個數據庫查詢來獲取ID爲 1User。實際上,因爲GraphQL還容許使用別名,所以如下查詢有效,而且還執行兩次查找:

query {
  one: user(id: 1) {
    name
  }
  two: user(id: 2) {
    name
  }
}

第二個示例暴露了如何批處理查詢的問題。爲了快速高效,咱們但願GraphQL以儘量少的往返次數訪問相同的數據庫行。

dataloader程序包旨在解決這兩個問題。給定一個ID數組,咱們將一次性從數據庫中獲取全部這些ID;一樣,後續對同一ID的調用也將從緩存中獲取該項目。要使用 dataloader 來構建這個,咱們須要兩樣東西。首先,咱們須要一個函數來加載全部請求的對象。在咱們的示例中,看起來像這樣:

const DataLoader = require('dataloader');
const batchGetUserById = async (ids) => {
   // 在現實生活中,這將是數據庫調用
  return ids.map(id => users[id]);
};
// userLoader如今是咱們的「批量加載功能」
const userLoader = new DataLoader(batchGetUserById);

這樣能夠解決批處理的問題。要加載數據並使用緩存,咱們將使用對 load 方法的調用來替換以前的數據查找,並傳入咱們的用戶ID:

const resolvers = {
  Query: {
    user: function (_, { id }) {
      return userLoader.load(id);
    },
  },
}

受權

對於GraphQL來講,受權是一個徹底不一樣的問題。簡而言之,它是識別給定用戶是否有權查看某些數據的過程。咱們能夠想象一下這樣的場景:通過認證的用戶能夠執行查詢來獲取本身的地址信息,但應該沒法獲取其餘用戶的地址。

爲了解決這個問題,咱們須要修改解析器函數。 除了字段的參數外,解析器還能夠訪問它的父節點,以及傳入的特殊上下文值,這些值能夠提供有關當前已認證用戶的信息。由於咱們知道地址是一個敏感字段,因此咱們須要修改咱們的代碼,使對用戶的調用不僅是返回一個地址列表,而是實際調用一些業務邏輯來驗證請求:

const getAddresses = function(currUser, user) {
  if (currUser.id == user.id) {
    return user.addresses
  }

  return [];
}

const resolvers = {
  Query: {
    user: function (_, { id }) {
      return users[id];
    },
  },
  User: {
    addresses: function (parentObj, {}, context) {
      return getAddresses(context.currUser, parentObj);
    },
  },
};

一樣,咱們不須要爲每一個 User 字段顯式定義一個解析程序,只需定義一個咱們要修改的解析程序便可。

默認狀況下,express-graphql 會將當前的HTTP請求做爲上下文的值來傳遞,但在設置服務器時能夠更改:

app.use(
  "/graphql",
  express_graphql({
    schema: schema,
    graphiql: true,
    context: {
      currUser: user // 當前通過身份驗證的用戶
    }
  })
);

Schema最佳實踐

GraphQL規範中缺乏的一個方面是缺少對版本控制模式的指導。隨着應用程序的成長和變化,它們的API也會隨之變化,極可能須要刪除或修改GraphQL字段和對象。但這個缺點也是積極的:經過仔細設計你的GraphQL schema,你能夠避免在更容易實現(也更容易破壞)的REST端點中明顯的陷阱,如命名的不一致和混亂的關係。

此外,你應該儘可能將業務邏輯與解析器邏輯分開。你的業務邏輯應該是整個應用程序的單一事實來源。在解析器中執行驗證檢查是頗有誘惑力的,但隨着模式的增加,這將成爲一種難以維持的策略。

GraphQL何時不合適?

GraphQL不能像REST同樣精確地知足HTTP通訊的需求。例如,不管查詢成功與否,GraphQL僅指定一個狀態碼——200 OK。在這個響應中會返回一個特殊的錯誤鍵,供客戶端解析和識別出錯的地方,所以,錯誤處理可能會有些棘手。

一樣,GraphQL只是一個規範,它不會自動解決你的應用程序面臨的每一個問題。性能問題不會消失,數據庫查詢不會變得更快,總的來講,你須要從新思考關於你的API的一切:受權、日誌、監控、緩存。版本化你的GraphQL API也多是一個挑戰,由於官方規範目前不支持處理中斷的變化,這是構建任何軟件不可避免的一部分。若是你有興趣探索GraphQL,你須要投入一些時間來學習如何將其與你的需求進行最佳整合。

瞭解更多

社區圍繞這個新範例彙集,併爲前端和後端工程師提供了很棒的GraphQL資源列表。前端和後端工程師均可以使用。你也能夠經過在官方的遊樂場上提出真實的請求來查看查詢和類型是什麼樣子的。

咱們還有一個[Code[ish]播客集](https://www.heroku.com/podcas...,專門介紹GraphQL的好處和成本。

相關文章
相關標籤/搜索