GraphQL真香入門教程

  • 2019 年 10 月 8 日
  • 筆記

看完復聯四,我整理了這份 GraphQL 入門教程,哈哈真香。。。

歡迎關注我: [個人主頁] (https://github.com/pingan8787) [個人部落格] (http://www.pingan8787.com/) [個人知識庫] (http://js.pingan8787.com/) 微信公眾號「前端自習課」

下面開始本文內容:

一、GraphQL介紹

GraphQL 是 Facebook 開發的一種 API 的查詢語言,與 2015 年公開發布,是 REST API 的替代品。

GraphQL 既是一種用於 API 的查詢語言也是一個滿足你數據查詢的運行時GraphQL 對你的 API 中的數據提供了一套易於理解的完整描述,使得客戶端能夠準確地獲得它需要的數據,而且沒有任何冗餘,也讓 API 更容易地隨著時間推移而演進,還能用於構建強大的開發者工具。

官網: http://graphql.org/ 中文網: http://graphql.cn/

1. 特點

  • 請求你所要的數據,不多不少;

如: hero 中有 name, age, sex 等,可以只取得需要的欄位。

  • 獲取多個資源,只用一個請求;

典型的 REST API 請求多個資源時得載入多個 URL,而 GraphQL 可以通過一次請求就獲取你應用所需的所有數據。這樣也能保證在較慢的移動網路連接下,使用 GraphQL 的應用也能表現得足夠迅速。

  • 描述所有可能類型的系統。便於維護,根據需求平滑演進,添加或隱藏欄位;

GraphQL 使用類型來保證應用只請求可能的數據,還提供了清晰的輔助性錯誤資訊。應用可以使用類型,而避免編寫手動解析程式碼。

2. 簡單案例

這裡先看下簡單案例,體驗下 GraphQL 的神奇之處(後面詳細介紹)。 我們這樣定義查詢語句:

query {      hero  }

然後得到的就是我們所要查詢的 hero 欄位:

{      "data": {          "hero": "I'm iron man"      }  }

這樣用起來,是不是更舒服呢?

二、GraphQL與restful對比

1. restful介紹

全稱: RepresentationalStateTransfer 表屬性狀態轉移。 本質上就是定義 uri ,通過 API 介面來取得資源。通用系統架構,不受語言限制。

例子: 餓了嗎介面。 如:介面 restapi/shopping/v3/restaurants?latitude=13 就是個典型的 restful 介面,定義資源 + 查詢條件。

2. 與 GraphQL 比較

  • restful 一個介面只能返回一個資源, GraphQL一次可以獲取多個資源。
  • restful 用不同 url 來區分資源, GraphQL 用類型區分資源。

三、使用express構建基本helloworld

1. 簡單案例

首先創建一個文件夾 demo ,並初始化一個 package.json,安裝 express / graphql / express-graphql 依賴包:

npm init -y  npm install express graphql express-graphql -S

新建一個 hello.js,引入文件:

const express = require('express')  const { buildSchema } = require('graphql')  const graphqlHTTP = require('express-graphql')

創建一個 schema 來定義查詢語句和類型, buildSchema() 方法需要傳入的參數是字元串類型,如下面的 hero 查詢欄位,後面的 String 類型表示欄位返回的數據類型:

const schema = buildSchema(`      type Query {          hero: String      }  `)

創建一個 root 處理器,處理對應的查詢,這裡的 hello 處理器對應的是 schema 中的 hero 欄位查詢的處理,這裡直接返回 I'm iron man 的結果:

const root = {      hero: () => {          return "I'm iron man"      }  }

當然,處理器中也可以是其他複雜操作,後面會介紹。

然後實例化 express ,並且將路由轉發給 graphqlHTTP 處理:

const app = express()  app.use('/graphql', graphqlHTTP({      schema,      rootValue: root,      graphiql: true  }))  app.listen(3000)

graphqlHTTP 中的三個參數介紹:

  • schema:定義的查詢語句和類型
  • rootValue:處理對應查詢的處理器
  • graphiql:是否開啟調試窗口,開發階段開啟,生產階段關閉

接下來運行項目,在命令行中執行 node hello.js,這裡可以在 graphiql 上做調試,打開地址 localhost:3000/graphiql 就可以愉快的查詢了。

另外我們可以在 graphiql 介面右側打開 Docs 查看我們定義的所有欄位和描述資訊。

最終程式碼:

const express = require('express')  const { buildSchema } = require('graphql')  const graphqlHTTP = require('express-graphql')    // 構建schema,這裡定義查詢的語句和類型  const schema = buildSchema(`      type Query {          hero: String      }  `)    // 定義查詢所對應的 resolver,也就是查詢對應的處理器  const root = {      hero: () => {          return "I'm iron man"      }  }    const app = express()    // 將路由轉發給 graphqlHTTP 處理  app.use('/graphql', graphqlHTTP({      schema: schema,      rootValue: root,      graphiql: true  }))    app.listen(3000)

2. 自定義類型查詢

我們前面的查詢中,已經將 hero 欄位定義為 String 類型,但是常常開發中,我們又會碰到欄位是多個類型,即欄位也能指代對象類型(Object),比如一個 user 欄位會有 nameage 等欄位,而 name 返回字元串類型, age 返回數值類型。

這時候,我們可以對這個對象的欄位進行次級選擇(sub-selection)。GraphQL 查詢能夠遍歷相關對象及其欄位,使得客戶端可以一次請求查詢大量相關數據,而不像傳統 REST 架構中那樣需要多次往返查詢。

我們可以新建一個查詢類型來定義 user 欄位返回的類型:

const schema = buildSchema(`      type User {          # 查詢可以有備註!          name: String          age: Int      }      type Query {          hero: String          user: User      }  `)

在處理器中我們也要加上:

const root = {      hero: () => {          return "I'm iron man"      },      user: () => {          return {              name: 'leo',              age: 18          }      }  }

這邊 Int/String 參數類型的問題,下一章介紹

四、參數類型和參數傳遞

1. 基本參數類型

String, Int, Float, BooleanID,這些基本參數類型可以在 schema 聲明中直接使用。

  • Int:有符號 32 位整數。
  • Float:有符號雙精度浮點值。
  • StringUTF‐8 字元序列。
  • Booleantrue 或者 false
  • IDID 標量類型表示一個唯一標識符,通常用以重新獲取對象或者作為快取中的鍵。 ID 類型使用和 String 一樣的方式序列化;然而將其定義為 ID 意味著並不需要人類可讀型

另外,我們可以使用 [類型] 來表示一類數組,如:

  • [Int] 表示整型數組;
  • [String] 表示字元串型數組;

2. 參數傳遞

使用方式和 JS 參數傳遞一樣,小括弧內定義形參,但是參數需要定義類型

使用 ! 代表參數不能為空。

下面案例:參數 teamNameString 類型,必須傳遞,而 number 參數也是 Int 類型,但是是非必須傳遞,最後輸出的結果也是 String 類型。

type Query {      getHero(teamName: String!, number: Int): [String]  }

下面一個案例:

//...省略其他  const schema = buildSchema(`      type Query {          getHero(teamName: String!): [String]      }  `)    const root = {      getHero: ({teamName}) => {          // 這裡的操作 實際開發中常常用在請求資料庫          const hero = {              '三國': ['張飛', '劉備', '關羽'],              '復仇者聯盟': ['鋼鐵俠', '美國隊長', '綠巨人']          }          return hero[teamName]      }  }  //...省略其他

這時候我們在 GraphiQL 上輸入查詢,就會得到 復仇者聯盟 的英雄數據了。

// 查詢  query {      getHero(teamName:"復仇者聯盟")  }    // 結果  {      "data": {          "getHero": [              "鋼鐵俠",              "美國隊長",              "綠巨人"          ]      }  }

3. 自定義返回類型

在實際開發中,我們返回的數據類型可能是一個對象,對象中可能既有 Int 類型的屬性,也有 String 類型的值,等等,這裡我們可以使用 自定義返回類型 來處理:

//...省略其他  const schema = buildSchema(`      type Hero {          name: String          age: Int          doSomething(thing: String): String      }      type Query {          getSuperHero(heroName: String!): Hero      }  `)  const root = {      getSuperHero: ({heroName}) => {          // 這裡的操作 實際開發中常常用在請求資料庫          const name = heroName          const age = 18          const doSomething = ({thing}) => {              return `I'm ${name}, I'm ${thing} now`          }          return { name, age, doSomething }      }  }  //...省略其他

這裡指定了 getSuperHero 欄位的返回類型是 Hero 類型,隨後在上面定義了 Hero

其中 Hero 類型中的 doSomething也是可以傳遞指定類型參數,並且指定返回類型。

下面看下輸出情況:

// 查詢  query {      getSuperHero(heroName:"IronMan") {          name          age          doSomething      }  }    // 結果  {      "data": {          "getSuperHero": {              "name": "IronMan",              "age": 46,              "doSomething": "I'm IronMan, I'm undefined now"          }      }  }

這裡也可以給 doSomething 傳遞參數,就會獲取到不同結果:

// 查詢  query {      getSuperHero(heroName:"IronMan") {          name          age          doSomething(thing:"watching TV")      }  }    // 結果  {      "data": {          "getSuperHero": {              "name": "IronMan",              "age": 46,              "doSomething": "I'm IronMan, I'm watching TV now"          }      }  }

五、GraphQL客戶端

這一節我們學習如何在客戶端中訪問 graphql 的介面。

1. 後端定義介面

我們先在後端將介面開發完成,這裡跟前面差不多,但需要多一步,使用 express 向外暴露一個文件夾,供用戶訪問靜態資源文件:

這裡直接使用前一節的程式碼啦~

// index.js  開發 graphql 介面  //...省略其他  const schema = buildSchema(`      type Hero {          name: String          age: Int          doSomething(thing: String): String      }      type Query {          getSuperHero(heroName: String!): Hero      }  `)  const root = {      getSuperHero: ({heroName}) => {          // 這裡的操作 實際開發中常常用在請求資料庫          const name = heroName          const age = 46          const doSomething = ({thing}) => {              return `I'm ${name}, I'm ${thing} now`          }          return { name, age, doSomething }      }  }  const app = express()  app.use('/graphql', graphqlHTTP({      schema, rootValue: root, graphiql: true  }))  // 公開文件夾 使用戶訪問靜態資源  app.use(express.static('public'))  app.listen(3000)

這樣我們就給前端頁面提供一個可以訪問靜態資源的功能。

這裡還需要在根目錄創建一個 public 文件夾,並在文件夾中添加 index.html 文件,此時的目錄結構:

|-node_modules  |-public  |---index.html  |-index.js  |-package.json

2. 前端頁面請求

然後給 index.html 添加按鈕和事件綁定:

這裡的變數 query 是個字元串類型,定義查詢條件,在條件 GetSuperHero 中的參數,需要用 $ 符號來標識,並在實際查詢 getSuperHero 中,作為參數的參數類型設置進來。

然後定義變數 variables ,指定屬性的值,之後通過 fetch 發起請求:

<button onclick="getData()">獲取數據</button>  <script>  function getData(){      const query = `          query GetSuperHero($heroName: String, $thing: String){              getSuperHero(heroName: $heroName){                  name                      age                      doSomething(thing: $thing)              }          }      `      // 如果不需要其他參數 至少要傳一個參數 否則會報錯      // const query = `      //     query GetSuperHero($heroName: String){      //         getSuperHero(heroName: $heroName){      //              name      //         }      //     }      // `      const variables = {heroName: '鋼鐵俠', thing: 'watching TV'}        fetch('./graphql', {          method: 'POST',          headers: {              'Content-Type': 'application/json',              'Accept': 'application/json'          },          body: JSON.stringify({              query, variables          })      })      .then(res => res.json())      .then(json => {          console.log(json)      })  }  </script>

當我們寫完以後,點擊 獲取數據 就會在控制台列印下面的數據:

{      "data":{          "getSuperHero":{              "name":"鋼鐵俠",              "age":46,              "doSomething": "I'm 鋼鐵俠, I'm watching TV now"          }      }  }

3. 注意點

  • 請求中的 query 參數需要對照好有 $ 符號的變數。

查詢語句 queryGetSuperHero($heroName:String) 里參數 $heroName 中的 heroName ; 查詢語句 getSuperHero(heroName:$heroName) 里類型 $heroName 中的 heroName ; 變數 variables 中的 heroName 屬性;

這三個名稱需要一樣

  • 請求中需要將數據序列化操作
body: JSON.stringify({ query, variables })

六、使用Mutations修改數據

1. Mutation 使用

根據前面的學習,我們知道,要做查詢操作,需要使用 Query 來聲明:

type Query {      queryHero(heroName: String): String  }

當我們要做修改操作,需要用到的是 Mutation :

type Mutation {      createHero(heroName: String): String  }

如果 Mutation 中欄位的形參是自定義類型,則類型需要用 input 標識:

const schema = buildSchema(`      # 輸入類型 用 input 標識      input HeroInput {          name: String          age: Int      }      # 查詢類型      type Hero {          name: String          age: Int      }      type Mutation {          createHero(heroName: String): Hero          updateHero(heroName: String, hero: HeroInput): Hero      }  `)

注意下:這裡需要至少定義一個 Query 不然 GraphiQL 會不顯示查詢:

type Query {      hero: [Hero]  }

2. Mutation 使用案例

先創建一個 schema ,內容為上一步【1. Mutation 使用】中定義的內容,這裡不重複寫。 然後模擬創建一個本地資料庫 localDb, 用於模擬存放添加的超級英雄數據:

const localDb = {}

接下來聲明 root 實現 schema 中的欄位方法:

const root = {      hero() {          // 這裡需要轉成數組 因為前面定義了返回值是  [Hero]  類型          let arr = []          for(const key in localDb){              arr.push(localDb[key])          }          return arr      },      createHero({ input }) {          // 相當於資料庫的添加操作          localDb[input.name] = input          return localDb[input.name]      },      updateHero({ id, input }) {          // 相當於資料庫的更新操作          const update = Object.assign({}, localDb[id], input)          localDb[id] = update          return update      }  }

最後配置 graphqlHTTP 方法和啟動伺服器,這裡就不多重複咯。

最終程式碼:

//...省略其他  const schema = buildSchema(`      # 輸入類型 用 input 標識      input HeroInput {          name: String          age: Int      }      # 查詢類型      type Hero {          name: String          age: Int      }      type Mutation {          createHero(input: HeroInput): Hero          updateHero(id: ID!, input: HeroInput): Hero      }      # 需要至少定義一個 Query 不要GraphiQL會不顯示查詢      type Query {          hero: [Hero]      }  `)    const localDb = {}    const root = {      hero() {          // 這裡需要轉成數組 因為前面定義了返回值是  [Hero]  類型          let arr = []          for(const key in localDb){              arr.push(localDb[key])          }          return arr      },      createHero({ input }) {          // 相當於資料庫的添加操作          localDb[input.name] = input          return localDb[input.name]      },      updateHero({ id, input }) {          // 相當於資料庫的更新操作          const update = Object.assign({}, localDb[id], input)          localDb[id] = update          return update      }  }  //...省略其他

現在我們可以啟動伺服器,在 GraphiQL 上測試下效果了。

我們是使用 mutationcreateHero 欄位添加兩條數據:

mutation {      createHero(input: {          name: "鋼鐵俠"          age: 40      }){          name          age      }  }
mutation {      createHero(input: {          name: "美國隊長"          age: 41      }){          name          age      }  }

然後使用 queryhero 欄位查詢添加的結果:

query {      hero {          name          age      }  }

這樣我們就獲取到剛才的添加結果:

{      "data": {          "hero": [              {                  "name": "鋼鐵俠",                  "age": 40              },              {                  "name": "美國隊長",                  "age": 41              }          ]    }  }

然後我們開始更新數據,使用 mutationupdateHero 欄位將 美國隊長age 值修改為 18:

mutation {      updateHero(id: "美國隊長", input: {          age: 18      }){          age      }  }

再使用 queryhero 欄位查詢下新的數據,會發現 美國隊長age 值已經更新為 18:

{      "data": {          "hero": [              {                  "name": "鋼鐵俠",                  "age": 40              },              {                  "name": "美國隊長",                  "age": 18              }          ]      }  }

七、認證和中間件

我們知道,修改數據的介面不能讓所有人隨意訪問,所以需要添加許可權認證,讓有許可權的人才可以訪問。 在 express 中,可以很簡單的使用中間件來將請求進行攔截,將沒有許可權的請求過濾並返回錯誤提示。

中間件實際上是一個函數,在介面執行之前,先攔截請求,再決定我們是否接著往下走,還是返回錯誤提示。

這在【六、使用Mutations修改數據】的最終程式碼上,在添加這個中間件:

//... 省略其他  const app = express()  const middleWare = (req, res, next) => {      // 這裡是簡單模擬許可權      // 實際開發中 更多的是和後端進行 token 交換來判斷許可權      if(req.url.indexOf('/graphql') !== -1 && req.headers.cookie.indexOf('auth') === -1){          // 向客戶端返回一個錯誤資訊          res.send(JSON.stringify({              err: '暫無許可權'          }))          return      }      next() // 正常下一步  }  // 註冊中間件  app.use(middleWare)    //... 省略其他

這裡的許可權判斷,只是簡單模擬,實際開發中,更多的是和後端進行 token 交換來判斷許可權(或者其他形式)。 我們重啟伺服器,打開 http://localhost:3000/graphql ,發現頁面提示錯誤了,因為 cookies 中沒有含有 auth 字元串。

如果這裡提示 TypeError:Cannotreadproperty'indexOf'ofundefined ,可以先不用管,因為瀏覽器中沒有 cookies 的原因,其實前面的許可權判斷邏輯需要根據具體業務場景判斷。

為了方便測試,我們在 chrome 瀏覽器控制台的 application 下,手動設置一個含有 auth 字元串的一個 cookies ,只是測試使用哦。

設置完成後,我們就能正常進入頁面。

八、ConstructingTypes

在前面的介紹中,我們要創建一個 schema 都是使用 buildSchema 方法來定義,但我們也可以使用另外一種定義方式。 就是這裡要學習使用的構造函數 graphql.GraphQLObjectType 定義,它有這麼幾個優點和缺點:

  • 優點:報錯提醒更直觀,結構更清晰,更便於維護。
  • 缺點:程式碼量上升。

1. 定義type(類型)

這裡先將前面定義的 Hero 類型進行改造:

const graphql = require('graphql') // 需要引入  const HeroType = new graphql.GraphQLObjectType({      name: 'Hero',      fields: {          name:{ type: graphql.GraphQLString },          age:{ type: graphql.GraphQLInt },      }  })

兩者區別在於:

區別

buildSchema

graphql.GraphQLObjectType

參數類型

字元串

對象

類名

跟在 type 字元後面,這裡是 typeHero

在參數對象的 name 屬性上

屬性定義

定義在類型後,鍵值對形式

定義在參數對象 fields 屬性中,值為對象,每個屬性名為鍵名,值也是對象,其中 type屬性的值為 graphql 中的屬性,下面會補充

補充: fields 屬性中的子屬性的類型通常有:

  • graphql.GraphQLString
  • graphql.GraphQLInt
  • graphql.GraphQLBoolean ….

即在 GraphQL後面跟上基本類型名稱。

2. 定義query(查詢)

定義查詢的時候,跟之前類似,可以參照下面對比圖理解,這裡比較不同的是,多了個 resolve 的方法,這個方法是用來執行處理查詢的邏輯,其實就是之前的 root 中的方法。

const QueryType = new graphql.GraphQLObjectType({      name: 'Query',      fields: {          // 一個個查詢方法          getSuperHero: {              type: HeroType,              args: {                  heroName: { type: graphql.GraphQLString }              },              // 方法實現 查詢的處理函數              resolve: function(_, { heroName }){                  const name = heroName                  const age = 18                  return { name, age }              }          }      }  })

3. 創建 schema

創建的時候只需實例化並且將參數傳入即可:

// step3 構造 schema  const schema = new graphql.GraphQLSchema({ query: QueryType})

最後使用也是和前面一樣:

const app = express()    app.use('/graphql', graphqlHTTP({      schema,      graphiql: true  }))  app.listen(3000)

九、與資料庫結合實戰

我們試著使用前面所學的內容,開發一個簡單的實踐項目: 通過 GraphiQL 頁面,往 Mongodb 中插入和更新數據,主要用到【六、使用Mutations修改數據】章節的操作。

1. 搭建並啟動本地 Mongodb 資料庫

首先我們可以到 Mongodb 官網 選擇對應平台和版本下載安裝。

下載安裝步驟,可以參考 mongoDB下載、安裝和配置,這裡就不多介紹喲~~

安裝完成後,我們打開兩個終端,分別執行下面兩行命令:

// 終端1  啟動資料庫  mongod --dbpath c:leoappmongodbdatadb    // 終端2  進入資料庫命令行操作模式  mongo

2. 連接資料庫,創建 Schema 和 Model

首先我們新建一個文件 db.js ,並 npm install mongoose 安裝 mongoose ,然後寫入下面程式碼,實現連接資料庫

const express = require('express')  const { buildSchema } = require('graphql')  const graphqlHTTP = require('express-graphql')  const mongoose = require('mongoose')    const DB_PATH = 'mongodb://127.0.0.1:27017/hero_table'  const connect = () => {      // 連接資料庫      mongoose.connect(DB_PATH)      // 連接斷開      mongoose.connection.on('disconnected', () => {          mongoose.connect(DB_PATH)      })      // 連接失敗      mongoose.connection.on('error', err => {          console.error(err)      })      // 連接成功      mongoose.connection.on('connected', async () => {          console.log('Connected to MongoDB connected', DB_PATH)      })  }  connect()

然後創建 SchemaModel

let HeroSchema = new mongoose.Schema({      name: String,      age: Number  })  let HeroModel = mongoose.model('hero',HeroSchema, 'hero_table')

3. 聲明查詢語句

這一步,還是先使用【六、使用Mutations修改數據】章節的操作邏輯,也就是先用字元串創建查詢,而不使用 GraphQLObjectType 創建:

const schema = buildSchema(`      # 輸入類型 用 input 標識      input HeroInput {          name: String          age: Int      }      # 查詢類型      type Hero {          name: String          age: Int      }      type Mutation {          createHero(input: HeroInput): Hero          updateHero(hero: String!, input: HeroInput): Hero      }      # 需要至少定義一個 Query 不要GraphiQL會不顯示查詢      type Query {          hero: [Hero]      }  `)

這邊案例有稍作修改

4. 實現添加數據和更新數據的邏輯

這邊處理添加數據和更新數據的邏輯,就要修改之前聲明的 root 的操作內容了:

const root = {      hero() {          return new Promise( (resolve, reject) => {              HeroModel.find((err, res) => {                  if(err) {                      reject(err)                      return                  }                  resolve(res)              })          })      },      createHero({ input }) {          // 實例化一個Model          const { name, age } = input          const params = new HeroModel({ name, age })          return new Promise( (resolve, reject) => {              params.save((err, res) => {                  if(err) {                      reject(err)                      return                  }                  resolve(res)              })          })      },      updateHero({ hero, input }) {          const { age } = input          return new Promise ((resolve, reject) => {              HeroModel.update({name: hero}, {age}, (err, res) => {                  if(err) {                      reject(err)                      return                  }                  resolve(res)              })          })      }  }

5. 模擬測試

最後我們在 GraphiQL 頁面上模擬測試一下,首先添加兩個英雄,鋼鐵俠和美國隊長,並設置他們的 age/name 屬性:

mutation {      createHero(input: {          name: "鋼鐵俠"          age: 40      }){          name          age      }  }
mutation {      createHero(input: {          name: "美國隊長"          age: 20      }){          name          age      }  }

頁面和介面沒有報錯,說明我們添加成功,資料庫中也有這兩條數據了:

在測試下查詢:

query {      hero {          name          age      }  }

查詢也正常,接下來測試下更新,將美國隊長的 age 修改為 60:

mutation {      updateHero(hero: "美國隊長", input: {          age: 60      }){          age      }  }

到這一步,我們也算是將這個練習做完了。

總結

  • GraphQL 是一種 API 的查詢語言,是 REST API 的替代品。
  • GraphQL 可以使用一個請求,獲取所有想要的數據。
  • 創建查詢的方式有兩種:使用 buildSchema 或者 GraphQLObjectType
  • 查詢操作用 Query,修改操作用 Mutations
  • 查詢類型用 type ,輸入類型用 input

其實 GraphQL 還是很簡單好用的呢~~~


本文首發在 [pingan8787個人部落格] (http://www.pingan8787.com),如需轉載請保留個人介紹