0%

GraphQL 用戶端 (1)

完成 GraphQL 伺服器後,接下來要在用戶端設定 GraphQL。基本上,用戶端只是一個與伺服器溝通的 app。因為 GraphQL 的靈活性,你或許會幫網路瀏覽器建構 app、也可能會幫手機建構原生 app,你可以在用戶端用任何一種語言編寫 GraphQL 服務。

使用 GraphQL API

最簡單的起步方式就是直接發一個 HTTP 請求給 GraphQL 端點。

抓取請求

我們可以用 cURL 來傳送請求給 GraphQL 服務,很多的硬體都有內建 cURL 程式,這意味著,我們可能可以幫冰箱的螢幕建構 GraphQL 服務。你只要設定一些不同的值即可:

cURL request
1
2
3
4
curl -X POST \
-H "Content-Type: application/json" \
--data '{ "query": "{totalUsers, totalPhotos}" }' \
http://10.11.xx.xxx:4000/graphql

傳送這個請求後,你應該會在終端機裡面看到正確的 JSON 資料結果:

cURL request result
1
{"data":{"totalUsers":3,"totalPhotos":45}}

因為我們使用 cURL,所以可以使用 “可傳送 HTTP 請求” 的任何東西。我們改用 fetch 來建立一個在瀏覽器內運作的用戶端:

Browser fetch
1
2
3
4
5
6
7
8
9
10
11
12
13
const query = `{totalUsers, totalPhotos}`;
const url = 'http://localhost:4000/graphql';

const opts = {
method: 'POST',
headers: { 'Content-Type' : 'application/json' },
body: JSON.stringify({ query })
};

fetch(url, opts)
.then(res => res.json())
.then(console.log)
.catch(console.error);

抓取資料後,我們可以從流覽器的主控台看到預期的結果:

Browser fetch result
1
2
3
4
5
6
{
"data": {
"totalPhotos": 45,
"totalUsers": 3
}
}

我們可以使用結果資料來建立 app,直接更改 DOM 列出 totalUsers 與 totalPhotos:

GraphQL request
1
2
3
4
5
6
7
8
fetch(url, opts)
.then(res => res.json())
.then(({data}) => `
<h1>photos: ${data.totalPhotos}</h1>
<h1>users: ${data.totalUsers}</h1>
`)
.then(text => document.body.innerHTML = text)
.catch(console.error);

請小心,當請求完成後,這會覆寫原本在內文中的任何東西。

當你知道如何用你最喜歡的用戶端來傳送 HTTP 請求時,你就擁有一項工具可建立 “可與任何 GraphQL API 溝通的用戶端 app” 了。 但是我們有更強大的 GraphQL 用戶端可用。

Apollo Client

使用 REST 有一個很大的好處在於它可以方便我們處裡快取。使用 REST 時,你可以將請求回應的資料存放在之前用來使用該請求的 URL 底下的快取,這沒甚麼問題。但是在 GraphQL 使用快取有點麻煩。一個 GraphQL API 沒有多個路由,每一個請求都是用單一的端點來傳送與接收的,所以我們無法將路由回傳的資料放在用來請求它的 URL 底下。

為了建立穩健、高性能的 app,我們必須設法將 query 與它們產生的物件存入快取。當我們想要建立快速、高效的 app 時,使用本地化的快取解決方案是必要的做法。我們可以自行建立這類的機制,也可以信賴一些已經經過嚴格審查的用戶端套件。

目前最引人注目的 GraphQL 用戶端解決方案是 Relay 與 Apollo Client。Relay 只與 Facebook 的 React 和 React Native 相容,所以我們選擇使用更廣泛的 Apollo Client。

Apollo Client 是 Meteor Development Group 開發的,它是一種社群驅動的專案,目的是建立靈活的 GraphQL 用戶端解決方案來處理諸如快取、積極 UI 更新 (optimistic UI update) 等工作。這個團隊建立了支援綁定 (binding) 的套件供 React、Angular、Ember、Vue、iOS 與 Android 使用。

我們已經在伺服器用了一些 Apollo 團隊創造的工具了,但 Apollo Client 特別關注請求碼在用戶端與伺服器之間的傳送與接收。它使用 Apollo Link 來處理網路請求,用 Apollo Cache 來處理所有快取。接下來 Apollo Client 會將連結 (Apollo Link) 與快取 (Apollo Cache) 包起來,並高效的管理與 GraphQL 服務的所有互動。

Apollo Client 和 Vue

Apollo Client 是一個 NPM 套件,要在 Web 瀏覽器或移動應用程序中使用此 Apollo Client 客戶端,您需要一個能夠在客戶端上加載 NPM 套件的構建系統。 一些常見的選擇包括 Browserify,Webpack 和 Meteor 1.3+。這理我們選擇使用 Vue 來作為使用者介面程式庫,所以將使用 Vue CLI 來建立 Vue 專案,Vue CLI 使用 WebPack 來做編譯與打包,產生瀏覽器看得懂的部署檔案。

設定專案

首先使用 Vue CLI 建立一個專案:

create Vue Project
1
vue create vue-apollo-sample

接著切換捯 vue-apollo-sample 資料夾並執行 npm run serve 來啟動 app,現在打開流覽器 http://localhost:8080/。 Vue app 已可以運作。

設置 Apollo Client

為了使用 Apollo 工具來建立 GraphQL 用戶端,你必須安裝一些套件。首先,你需要 graphql 套件,它裡面有 GraphQL 語言解析函式。接著需要一個稱為 apollo-boost 的套件。Apollo Boost 包含建立 Apollo Client 以及傳送操作 (operation) 給伺服器所需的 Apollo 套件。最後需要 vue-apollo。Vue Apollo 是一個 NPM 程式庫,可以整合到 Vue 實例中。我們要利用其中的 Vue 元件與 Apollo 來建構使用者介面。

npm install
1
npm install graphql apollo-boost vue-apollo

現在可以開始建立用戶端了。apollo-boost 裡面的 ApolloClient 建構式可以用來建立第一個用戶端。打開 HelloWorld.vue,我們就用 Vue CLI 產生的頁面來測試,刪除一些不需要的訊息,然後加入一些程式碼,首先先使用 ApolloClient,稍後再加入 vue-apollo 套件:

src/components/HelloWorld.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
<template>
<div class="hello">
<h1>{{ msg }}</h1>
</div>
</template>

<script>
import ApolloClient, { gql } from 'apollo-boost'

export default {
name: 'HelloWorld',
props: {
msg: String
},
created() {
const client = new ApolloClient({uri: 'http://localhost:4000/graphql'});

const query = gql`
{
totalUsers
totalPhotos
}
`;

client.query({ query })
.then(({ data }) => console.log(data))
.catch(console.error);
}
}
</script>

第 16 行我們使用 ApolloClient 建構式建立一個新的 client 實例。這個 client 已經可以處裡和 http://localhost:4000/graphql 上的 GraphQL 服務之間的所有網路通訊了。這裡使用 client 傳送 query 來查詢使用者與照片的總數量。gql 函式屬於 graphql-tag 套件,它會隨著 apollo-boost 套件一起被加入,它的用途是將 query 解析成 AST 或抽象語法樹。第 26 行,我們只是將結果顯示在瀏覽器的 console。

如果你的瀏覽器出現如下的錯誤,那是因為 Vue CLI 建立的專案 ESLint 預設禁止使用 console.log

Failed to compile.

./src/components/HelloWorld.vue
  Module Error (from ./node_modules/eslint-loader/index.js):
  error: Unexpected console statement (no-console) at src\components\HelloWorld.vue:26:27:
    24 | 
    25 |     client.query({ query })
  > 26 |       .then(({ data }) => console.log(data))
       |                           ^
    27 |       .catch(console.error);
    28 |   }
    29 | }

可修改 package.json eslintConfig:

"eslintConfig": {
    ...
    "rules": {
      "no-console": "off"
    },
    ...
  }

client.query({ query }) 會用 HTTP 請求 (request) 將 query 送給 GraphQL 服務伺服器,以及解析服務回傳的資料。我們在瀏覽器的主控台應該會有如下的回應:

Chrome console
1
{totalUsers: 3, totalPhotos: 45}

也可以在 HTTP Headers 加入權杖 (token):

ApolloClient
1
2
3
4
5
6
7
8
9
10
const client = new ApolloClient({
uri: 'http://localhost:4000/graphql',
request: operation => {
operation.setContext({
headers: {
Authorization: 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBMb2dpbiI6InNjb3R0IiwibmFtZSI6IuiAgeiZjiBTY290dCIsImlhdCI6MTU3MzYwNDUwOSwiZXhwIjoxNTczNjQ3NzA5fQ.3PEtLsXVZqoamXbIO10KTLgwaYBQAGKB4AH0kJXQ4bg'
},
});
}
});

除了處裡送到 GraphQL 服務的網路請求之外,用戶端也會將回應存到本地記憶體快取中。無論何時,我們都可以藉由呼叫 client.extract( ) 來查看快取:

1
2
3
4
5
6
7
...
console.log('cache', client.extract());

client.query({query})
.then(() => console.log('cache', client.extract()))
.catch(console.error);
...

第 2 行我們在送出之前查看快取,並且在第 5 行 query 被解析之後再度查看快取,你可以看到,現在結果已經被存在一個由用戶端管理的本地物件裡面了:

Client cache
1
2
3
4
5
6
{
ROOT_QUERY: {
totalUsers: 3,
totalPhotos: 45
}
}

下一次我們傳送 query 給用戶端來索取這筆資料時,它會從快取讀出資料,而不是向 GraphQL 服務傳送另一個網路請求。 Apollo Client 可讓我們指定何時以及每隔多久透過網路傳送 HTTP 請求。稍後會討論這些選項。

上面的例子我們只用到 apollo-boost 套件,而且是直接在 Vue 的元件中送出 GraphQL 請求,但是如果你在大部分的 Vue 元件都會使用到 GraphQL 服務,你必須重複產生 ApolloClient 實例,這顯然不是好的方式,你可把 ApolloClient 實例加入 Vue 的全域範圍,但是我們現在要使用一個更強大的套件 Vue Apollo 套件。

這裡我們會建立一個用戶端,並且用一個稱為 apolloProvider 的實例將它加到 Vue 使用者介面。因為我們要將它加入 Vue 的全域範圍,所以要從修改 main.js 開始:

src/main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
import Vue from 'vue'
import App from './App.vue'
import VueApollo from "vue-apollo"
import ApolloClient from "apollo-boost"

Vue.config.productionTip = false;

Vue.use(VueApollo);

const client = new ApolloClient({
uri: "http://localhost:4000/graphql",
request: operation => {
operation.setContext({
headers: {
Authorization:
"Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhcHBMb2dpbiI6InNjb3R0IiwibmFtZSI6IuiAgeiZjiBTY290dCIsImlhdCI6MTU3MzYwNDUwOSwiZXhwIjoxNTczNjQ3NzA5fQ.3PEtLsXVZqoamXbIO10KTLgwaYBQAGKB4AH0kJXQ4bg"
}
});
}
});

const apolloProvider = new VueApollo({
defaultClient: client
});

new Vue({
apolloProvider,
render: h => h(App),
}).$mount('#app')

第 8 行我們調用 Vue.use( ) 全局方法來使用 VueApollo 插件 (Plugin)。第 10 行建立一個客戶端,這與先前的範例沒甚麼不同,接著第 22 行使用 VueApollo 建構式產生一個 apolloProvider 實例,並在第 27 行將它加入 Vue 的全域範圍內,這樣在所有的 Vue 子元件都可以使用這個用戶端,也就是都可以透過 Apollo Client 從 GraphQL 服務接收資料了。

接著我們來看如何從子元件中使用 apolloProvider,我們修改一下 HelloWorld.vue 元件:

src/components/HelloWorld.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { gql } from 'apollo-boost'

export default {
name: 'HelloWorld',
props: {
msg: String
},
created() {
const query = gql`
{
totalUsers
totalPhotos
}
`;

this.$apollo.query({ query })
.then(({ data }) => console.log(data))
.catch(console.error);

this.$apollo.provider.defaultClient.query({ query })
.then(({ data }) => console.log(data))
.catch(console.error);

this.$apollo.getClient().query({ query })
.then(({ data }) => console.log(data))
.catch(console.error);
}
}

這裡我們不再需要產生 ApolloClient 實例,第 16 行我們直接調用 Vue 全域範圍中的 ApolloClient,你也可以使用第 20 行的 this.$apollo.provider.defaultClient 或第 24 行的 this.$apollo.getClient( ) 方法取得 ApolloClient 實例,現在你可以在所有的 Vue 元件中共用這個 ApolloClient 元件。

這裡我們直接在 Vue 元件的 Created( ) Hook 中送出 GraphQL 請求。但它還有更強大的功能,能與 Vue 結合得更緊密:

src/components/HelloWorld.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
<template>
<div class="hello">
<h1>{{ msg }}</h1>
<h1>{{ sayHello }}</h1>
</div>
</template>

<script>
import { gql } from 'apollo-boost'

export default {
name: 'HelloWorld',
props: {
msg: String
},
apollo: {
sayHello: {
query: gql`query hello {
sayHello
}`
}
},
...
}
</script>

第 16 行我們直接在 Vue 元件中加入 apollo 屬性,然後在第 17 行中再加入一個 GraphQL query sayHello 物件,接下來在第 4 行,就可以在 Template 中綁定這個值,這就像使用 Vue 的計算屬性 (computed) 一樣,可以直接把它當成 data 物件的屬性來存取。

但如果 sayHello 屬性需要初始值,也可以明確的定義在 data 物件中:

src/components/HelloWorld.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
props: {
msg: String
},
data() {
return {
sayHello: undefined
}
},
apollo: {
sayHello: {
query: gql`query hello {
sayHello
}`
}
},
...

這裡要注意的是 Vue 的屬性名稱 sayHello 要與 GraphQL 的 Query 同名稱,但如果需要也可以變換名稱,這裡我們將 Vue 的屬性改成 helloTainan:

src/components/HelloWorld.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
...
data() {
return {
helloTainan: undefined
}
},
apollo: {
helloTainan: {
query: gql`query hello {
helloTainan: sayHello
}`
}
},
...

重點在於第 10 行,不要忘記也要修改 Template 的綁定。

使用 GraphQL 的其中一個好處是你可以使用一個 HTTP 請求建構 UI 所需的每一個東西,並且用一個回應來接收所有的資料,現在我們來加一個同時請求多個 GraphQL 的請求:

src/components/HelloWorld.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
...
apollo: {
helloTainan: {
query: gql`query hello {
helloTainan: sayHello
}`
},
allUsers: {
query: gql`query listUsers {
totalUsers
allUsers {
appLogin
name
}
}`
}
},
...

在 apollo 中新增一個屬性 allUsers,對應到 GraphQL 的一個 query 請求,這個請求同時包含有兩個 GraphQL Query,totalUsers 與 allUsers,這裡要注意,只有 allUsers Query 名稱對應到 Vue 的屬性 allUsers,所以 Vue allUsers 的值會自動對應到 GraphQL Query allUsers 的返回值,那如何取得 totalUsers 的值? Apollo 提供了一個 result Hook:

src/components/HelloWorld.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
...
data() {
return {
helloTainan: undefined,
totalUsers: undefined,
loading: true
}
},
apollo: {
...
allUsers: {
query: gql`query listUsers {
totalUsers
allUsers {
appLogin
name
}
}`,
result ({ data, loading }) {
this.totalUsers = data.totalUsers;
this.loading = loading;
}
}
...

這裡我們在第 19 行加入了一個 result Hook,他會接受一個物件當引數,其中 data 屬性就包含所有 GraphQL query 的回應資料,這裡會包含兩個 GraphQL Query totalUsers 與 allUsers 的回應資料,而 loading 屬性則告訴我們操作是否正在載入。

定義在 Vue apollo 屬性中的每個查詢都會被創建成一個智能查詢物件 (SmartQuery),它包含了一些屬性與方法,例如上面的 loading 就是這個物件的一個屬性,還有包含一些方法可被調用:

SmartQuery
1
this.$apollo.queries.helloTainan.refetch();

你可以加入一個按鈕調用它,會將另一個請求送到 GraphQL 端點來重新抓取資料。

輪詢 (polling) 也是 SmartQuery 物件提供的另一個選擇,當我們在 SmartQuery 物件加入輪詢時,就可以自動每隔一段指定的時間自動重複抓取資料:

startPolling and stopPolling
1
2
3
4
5
this.$apollo.queries.helloTainan.startPolling(1000); //ms

setTimeout(() => {
this.$apollo.queries.helloTainan.stopPolling();
}, 5000);

這裡每秒輪詢一次,5 秒鐘後會停止輪詢。

另外你的 GraphQL 服務如果有提供分頁功能,則可以使用 fetchMore( ) 抓取下一頁的資料。