0%

GraphQL 用戶端 (2)

Mutation

Mutation 會更改 Apollo 伺服器服務的數據狀態。基本的運作方式與 query 沒甚麼不同:

在實作 mutation 之前,我們先新加入一個 query 來顯示照片的變化:

src/component/HelloWorld.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
allPhotos: {
query: gql`query listPhotos {
totalPhotos
allPhotos {
id
name
description
}
}`,
result({ data, loading }) {
this.totalPhotos = data.totalPhotos;
this.loading = loading;
}
}
...

現在我們在 Vue 的 methods 增加一個 addPhoto 的操作方法:

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
...
methods: {
addPhoto() {
const newPhoto = {
name: "範例照片 from Vue mutation !",
description: `我們資料庫的範例照片 ${new Date().toISOString()}`
};

this.$apollo.mutate({
mutation: gql`mutation addPhoto($input: PostPhotoInput!) {
postPhoto(input: $input) {
id
name
description
}
}`,
variables: {
input: newPhoto,
}
}).then((data) => {
console.log(data);
this.$apollo.queries.allPhotos.refetch();
}).catch((error) => {
console.error(error);
});
},
},
...
  • 第 4 行的新照片,在實際運作上應該是要由用戶輸入的,這裡讓他單純化,避免失焦。
  • 第 9 行用 this.$apollo.mutate( ) 方法送出 GraphQL mutation,語法與 query 差不多,這裡我們使用一個變數 $input。this.$apollo.mutate( ) 會返回一個 Promise,他返回的值就是你在第 11 行 postPhoto 時所定義的,這裡就是 Photo 的三個屬性 id、name 與 description。
  • 第 22 行我們並重新抓取 allPhotos 的資料,Vue 則會同時更新畫面。

接下來我們要在使用者介面上新增一個按鈕來觸發這個 mutation,並新增一個訊息顯示照片的總數量:

src/components/HelloWorld.vue
1
2
3
4
...
<button @click="addPhoto">Click to add new photo</button>
<h1>Total Users: {{ totalUsers }} Total Photos: {{ totalPhotos }}</h1>
...

現在可以測試看看了。

先前談過 Apollo Client 支援積極 UI 更新 (optimistic UI update),因此 mutation 也提供了一些 Hook,主要的就是 update 與 optimisticResponse Hook。我們不一定要用積極 UI 更新,但了解它可讓我們了解使用者 UI 的技術。

Optimistic UI update

當我們新增或更新資料成功後,伺服器都會返回一個結果,以便我們更新使用者的介面 (UI),但我們為了讓使用者感覺反應快速,就會使用送出的數據,樂觀的當成伺服器更新成功後返回的數據來更新使用者介面,並在伺服器真正返回結果時,再來更新錯誤。

現在修改一下 mutation,加入 update 與 optimisticResponse 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
25
26
27
28
29
30
31
32
...
this.$apollo.mutate({
mutation: gql`mutation addPhoto($input: PostPhotoInput!) {
postPhoto(input: $input) {
id
name
description
}
}`,
variables: {
input: newPhoto,
},
update: (store, { data }) => {
console.log('mutation update begin');
console.log(data);
console.log('mutation update end');
},
optimisticResponse: {
__typename: 'Mutation',
postPhoto: {
__typename: 'Photo',
id: -1,
...newPhoto
}
}
}).then((data) => {
console.log(data);
this.$apollo.queries.allPhotos.refetch();
}).catch((error) => {
console.error(error);
});
...
  • 第 13 行加入了 update Hook,update( ) 函式這裡接受兩個引數 store 與儲存資料的物件,每一個 mutation 它總是會被調用兩次,第一次使用 optimisticResponse 返回的樂觀資料,第二次調用則會是實際返回的資料。這裡我們只是把它顯示出來,沒有做其他的處裡。store 實際上是 Apollo Client 的 cache,因此可以用這調用的時機更新 cache 的資料,可以在第二次調用時回滾 (Rollback) 第一次的樂觀資料用實際返回的資料取代。因此這個功能也必須與 Apollo Client 的快取一起使用。我們將在稍後談到 Apollo Client Cache。
  • 第 18 行 optimisticResponse 在設定樂觀的資料,此時 id 還沒有產生,暫時用 -1。如果這裡沒有設定 optimisticResponse 屬性,則 update( ) 只會被調用一次。

如果你只想在完成 mutation 後執行一些操作,也不需要積極 UI 更新,使用 Promise then 就可以了。

使用快取 Cache

身為開發者,我們應該盡量減少網路請求。我們不希望讓使用者被迫發出無關的請求。為了盡量減少 app 送出的網路請求數量,Apollo Client 提供了 Cache 機制。

抓取策略 Fetch Policy

在預設情況下,Apollo Client 會在一個本地的 JavaScript 變數中儲存資料。每當我們建立用戶端時,Apollo Client 就會為我們建立一個快取。每當我們傳送操作時,回應就會被存到本地快取。fetchPolicy 可告訴 Apollo Client 該去哪裡尋找解析操作所需要的資料,究竟是本地快取還是網路請求。預設的 fetchPolicy 是 cache-first。這代表用戶端會在本地的快取尋找資料來解析操作。如果用戶端可以在不傳送網路請求的情況下解析操作,它就會這樣做。但是,如果解析 query 所需的資料不在快取裡面,用戶端會傳送網路請求給 GraphQL 服務。

另一種類型的 fetchPolicy 是 cache-only。這種作法要求用戶端只在快取中尋找資料,永遠不要傳送網路請求。如果快取裡面沒有滿足 query 的所有資料,就丟出錯誤。

我們修改一下程式,來改變各個 query 的抓取策略:

src/components/HelloWorld.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
allUsers: {
query: gql`query listUsers {
totalUsers
allUsers {
appLogin
name
}
}`,
result ({ data, loading }) {
this.totalUsers = data.totalUsers;
this.loading = loading;
},
fetchPolicy: 'cache-only'
},
...

第 14 行加入了 fetchPolicy,並設定為 cache-only,當你重新整理瀏覽器時應該會看不到 Users 的資料,因為 Apollo Client 只會在快取裡面尋找資料來解析 query,但是 app 啟動時,那些資料並不存在。為了清除這個錯誤,我們將抓取策略改成 cache-and-network。

src/components/HelloWorld.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
...
allUsers: {
query: gql`query listUsers {
totalUsers
allUsers {
appLogin
name
}
}`,
result ({ data, loading }) {
this.totalUsers = data && data.totalUsers;
this.loading = loading;
},
fetchPolicy: 'cache-and-network'
},
...

這個 app 又可以正常運作了。 cache-and-network 策略一定會立刻用快取來解析查詢,同時也會傳送網路請求以取得最新的資料。如果本地快取不存在,例如當 app 啟動時,這個策略只會從網路接收資料。在第 11 行我們也做了一些變更,否則當抓不到快取資料時,data 值會是 undefined,瀏覽器的 console 總是會出現錯誤。這也是一個良好的習慣,當要抓取某個物件中屬性的值時,先檢查這個物件是否存在。

其他的策略包括:

  • network-only 一定會傳送網路請求來解析 query

  • no-cache 一定會傳送網路請求來解析 query,不會將回應結果存放在快取內。

保存快取

在預設情況下,Apollo Client 會在一個本地的 JavaScript 變數中儲存資料。每當我們建立用戶端時,Apollo Client 就會為我們建立一個快取。但我們可以在用戶端儲存快取,這可以完全發揮 cache-first 策略的威力,因為當使用者回到 app 時,永遠都會有快取。在這裡,cache-first 策略會立刻解析既有本地快取的資料,而且完全不會用網路傳送請求。

為了在本地除存快取資料,我們要安裝一個 npm 套件:

npm install
1
npm install apollo-cache-persist --save

apollo-cache-persist 套件有一個函式可以改善快取,它的做法是當快取改變時,就將它存放在本地端。為了實作快取保存,我們要建立自己的 cache 物件,並且在設置 app 時將他加入 client。

src/main.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
import ApolloClient, { InMemoryCache } from 'apollo-boost'
import { persistCache } from 'apollo-cache-persist'
...
const cache = new InMemoryCache();
persistCache({
cache,
storage: localStorage
});
...
const client = new ApolloClient({
cache,

...

});
...

第 5 行我們用 apollo-boost 提供的 InMemoryCache 建構式來建立自己的快取實例,並連同 storage 位置一起將它傳給 persistCache 方法。第 12 行將 cache 加入 ApolloClient。

我們選擇將快取存放在瀏覽器的 localStorage,啟動 app 後,你可以從瀏覽器 console 中檢查它:

或者直接檢查 localStorege:

下一步是在啟動時檢查 localStorege,看看快取是否已經被儲存了。如果有,在建立用戶端之前,我們要用那些資料來初始化本地的 cache:

src/main.js
1
2
3
4
5
6
7
8
9
10
11
12
...
const cache = new InMemoryCache();
persistCache({
cache,
storage: localStorage
});

if (localStorage['apollo-cache-persist']) {
let cacheData = JSON.parse(localStorage['apollo-cache-persist']);
cache.restore(cacheData);
}
...

現在我們的 app 會在啟動前載入任何已被快取的資料。如果我們有資料存放在 Key apollo-cache-persist 之下,就要使用 cache.restore(cacheData) 方法來將它加入 cache 實例。

我們已經用 Apollo Client 的快取成功的將送到 GraphQL 服務的網路請求減到最少了。再來,我們將看看如何將資料直接寫入快取。

更新快取

可以直接從快取讀出資料,這是讓 cache-only 這類的策略得以執行的要素。我們也能夠與 Apollo Cache 直接互動。我們可以從快取讀出目前的資料,或將資料直接寫入快取。每當我們改變快取內的資料時,vue-apollo 就會發現那項改變,並重新喧染所有被影響的元件。我們只要修改快取,UI 就會隨著改變,自動更新。

Apollo Cache 的資料是用 GraphQL query 寫入的,所以也必須用 GraphQL query 讀取,我們來看先前 allUsers 的 GraphQL query:

src/components/HolloWorld.vue
1
2
3
4
5
6
7
8
9
10
...
allUsers: {
query: gql`query listUsers {
totalUsers
allUsers {
appLogin
name
}
}`,
...

這是 allUsers 的 GraphQL query,因此目前 Apollo Cache 的資料就是用這段 GraphQL query 寫入的,我們必須用相同的 GraphQL query 從 cache 讀取。

src/components/HelloWorld.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
...
created() {
const client = this.$apollo.getClient();
const { totalUsers, allUsers } = client.readQuery({
query: gql`query listUsers {
totalUsers
allUsers {
appLogin
name
}
}`
});

console.log(totalUsers ? totalUsers : 0);
console.log(allUsers ? allUsers: []);
}
...

第 3 行我們取得 Apollo Client 實例。
第 4 行用 client.readQuery( ) 讀取資料。

這樣我們就取得存放在快取裡面的 totalUsers 與 allUsers 的值。

我們也可以使用 cache.writeQuery 方法來將資料直接寫入這個 GraphQL query 的 totalUsers 與 allUsers 欄位:

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
...
methods: {
...
clearUsers() {
const client = this.$apollo.getClient();
client.writeQuery({
query: gql`query listUsers {
totalUsers
allUsers {
appLogin
name
}
}`,
data: {
totalUsers: 0,
allUsers: []
}
});
}
},
...

第 6 行使用 client.writeQuery( ) 方法直接更改快取,這裡清除 totalUsers 與 allUsers 欄位。

現在在使用者介面增加一個按鈕來觸發 clearUsers( ):

src/components/HelloWorld.vue
1
2
3
...
<button @click="clearUsers">Click to clear users</button>
...

觸發按鈕修改快取,UI 就會隨著改變,自動更新。

但是這裡要注意的是,因為我們有將 cache 存入 localStorage,當下次重新啟動 app 時,我們之前有在啟動 app 時,將 localStorege 的資料 restore 回 Apollo Cache,所以如果你使用的是預設的 fetchPolicy cache-first 資料將不會更新,會保持在被清除的狀態。所以必須將 fetchPolicy 設定為 cache-and-network。

src/components/HelloTainan.vue
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
 ...
allUsers: {
query: gql`query listUsers {
totalUsers
allUsers {
appLogin
name
}
}`,
result ({ data, loading }) {
this.totalUsers = data && data.totalUsers;
this.loading = loading;
},
fetchPolicy: 'cache-and-network'
},
...

以下是有效的 fetchPolicy 值:

  • cache-first:這是預設值,始終嘗試先從快取中讀取數據。如果滿足查詢所需的所有數據都在快取中,則返回該數據。**僅當沒有快取結果時,Apollo 才會從網絡獲取。 此抓取策略旨在最大程度地減少渲染組件時發送的網絡請求的數量。

  • cache-and-network:此抓取策略將使 Apollo 首先嘗試從快取中讀取數據。如果滿足查詢所需的所有數據都在快取中,則返回該數據。但是,無論快取中是否有完整數據,該 fetchPolicy 始終會通過網絡執行查詢,這與 cache-first 不同,後者僅在查詢數據不在快取中時才執行查詢。 此抓取策略優化了用戶的響應速度,同時還嘗試使快取的數據與服務伺服器器數據保持一致,但以額外的網絡請求為代價。

  • network-only:此抓取策略將永遠不會從快取中返回初始數據。 相反,它將始終使用網絡向服務器發出請求。這種抓取策略可優化與服務器的數據一致性,但是會以對用戶的即時響應為代價。

  • cache-only:此抓取策略將永遠不會使用網絡執行查詢。 相反,它將始終嘗試從快取中讀取。如果用於查詢的數據在=快取中不存在,則將引發錯誤。此抓取策略僅允許與本地客戶端快取中的數據進行交互,而不會發出任何的網絡請求,以保持客戶端的組件保持快速的運行,但是這意味著本地數據可能與服務器上的數據不一致。如果只對與 ApolloClient 快取中的數據進行交互感興趣,可使用 ApolloClient 實例的 readQuery( )和 writeQuery( ) 方法。

  • no-cache:此抓取策略將永遠不會從快取中返回初始數據。相反,它將始終使用網絡向服務器發出請求。network-only 策略不同,查詢完成後,它也不會將任何數據寫入快取

之前我們實作 mutation 時談到了 Optimistic UI update,現在我們可以搭配快取來實作,看看如何改善客戶端的響應。

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
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
...
methods: {
addPhoto() {
const newPhoto = {
name: "範例照片 from Vue mutation !",
description: `我們資料庫的範例照片 ${new Date().toISOString()}`
};

this.$apollo.mutate({
mutation: gql`mutation addPhoto($input: PostPhotoInput!) {
postPhoto(input: $input) {
id
name
description
}
}`,
variables: {
input: newPhoto,
},
update: (store, { data: { postPhoto }}) => {
const LISTPHOTOS_QUERY = gql`query listPhotos {
totalPhotos
allPhotos {
id
name
description
}
}`;

const cacheData = store.readQuery({
query: LISTPHOTOS_QUERY
});

cacheData.totalPhotos += 1;
cacheData.allPhotos.push(postPhoto);

store.writeQuery({
query: LISTPHOTOS_QUERY,
data: cacheData
});

console.log(postPhoto);
console.log(cacheData);
},
optimisticResponse: {
__typename: 'Mutation',
postPhoto: {
__typename: 'Photo',
id: -1,
...newPhoto
}
}
}).then((data) => {
console.log(data);
//this.$apollo.queries.allPhotos.refetch();
}).catch((error) => {
console.error(error);
});
},
...
}
...

這裡需要花一些時間理解。我們在 this.$apollo.mutate( ) 方法中加入了兩個 hook,分別是 update 與 optimisticResponse。

第 20 行 update 是一個函式,第一個引數 store 就是 Apollo Cache 實例,第二個引數則是執行 mutation 時被回傳的資料。
第 30 行從快取中讀取資料,34、35 直接修改讀出來的資料。
第 37 行把修改後的資料寫回 Apollo Cache 快取中。
第 42、43 把它顯示在 console,我們可以來觀察它們的操作。
第 45 行 optimisticResponse 用來提供樂觀的資料。
第 55 行我們不再需要從伺服器抓取資料來刷新。

現在可以新增一張新照片,然後從瀏覽器的 console 觀察看看:

之前談過,如果 update 與 optimisticResponse 一起用,則 update 會被調用兩次,但是再第二次調用時,第一次調用的資料會被回滾 (rollback),這裡我們做了兩次 cache 的變動,但第一次的資料自動被回滾了!