跳至主要內容
版本:v18.0.0

持久化查詢

Relay 編譯器支援持久化查詢。這很有用,因為

  • 客戶端操作文字只會變成一個 md5 雜湊值,通常比實際的查詢字串短。這節省了從客戶端上傳到伺服器的位元組。

  • 伺服器現在可以允許查詢,這透過限制客戶端可以執行的操作來提高安全性。

在客戶端上的用法

persistConfig 選項

在您的 package.jsonrelay 配置區段中,您需要指定 "persistConfig"。

"scripts": {
"relay": "relay-compiler",
"relay-persisting": "node relayLocalPersisting.js"
},
"relay": {
"src": "./src",
"schema": "./schema.graphql",
"persistConfig": {
"url": "https://#:2999",
"params": {}
}
}

在配置中指定 persistConfig 將會執行以下操作

  1. 它會將所有查詢和變更操作文字轉換為 md5 雜湊值。

    例如,如果沒有 persistConfig,產生的 ConcreteRequest 可能如下所示

    const node/*: ConcreteRequest*/ = (function(){
    //... excluded for brevity
    return {
    "kind": "Request",
    "operationKind": "query",
    "name": "TodoItemRefetchQuery",
    "id": null, // NOTE: id is null
    "text": "query TodoItemRefetchQuery(\n $itemID: ID!\n) {\n node(id: $itemID) {\n ...TodoItem_item_2FOrhs\n }\n}\n\nfragment TodoItem_item_2FOrhs on Todo {\n text\n isComplete\n}\n",
    //... excluded for brevity
    };
    })();

    有了 persistConfig,它會變成這樣

    const node/*: ConcreteRequest*/ = (function(){
    //... excluded for brevity
    return {
    "kind": "Request",
    "operationKind": "query",
    "name": "TodoItemRefetchQuery",
    "id": "3be4abb81fa595e25eb725b2c6a87508", // NOTE: id is now an md5 hash
    // of the query text
    "text": null, // NOTE: text is null now
    //... excluded for brevity
    };
    })();

  2. 它會向指定的 url 發送帶有 text 參數的 HTTP POST 請求。您也可以透過 params 選項添加其他請求主體參數。

"scripts": {
"relay": "relay-compiler"
},
"relay": {
"src": "./src",
"schema": "./schema.graphql",
"persistConfig": {
"url": "https://#:2999",
"params": {}
}
}

本地持久化查詢

使用以下配置,您可以生成一個本地 JSON 檔案,其中包含 operation_id => 完整操作文字 的對應關係。

"scripts": {
"relay": "relay-compiler"
},
"relay": {
"src": "./src",
"schema": "./schema.graphql",
"persistConfig": {
"file": "./persisted_queries.json",
"algorithm": "MD5" // this can be one of MD5, SHA256, SHA1
}
}

理想情況下,您會將此檔案傳送到您的伺服器,以便在部署時伺服器知道它可能接收到的所有查詢。如果您不想這樣做,則必須實作 自動持久化查詢握手

權衡

  • ✅ 如果您的伺服器持久化查詢資料儲存被清除,您可以透過客戶端的請求自動恢復。
  • ❌ 當快取未命中時,您需要額外一次往返伺服器的時間。
  • ❌ 您必須將 persisted_queries.json 檔案傳送到瀏覽器,這將增加您的 bundle 大小。

relayLocalPersisting.js 的範例實作

這是一個簡單持久化伺服器的範例,它會將查詢文字儲存到 queryMap.json 檔案中。

const http = require('http');
const crypto = require('crypto');
const fs = require('fs');

function md5(input) {
return crypto.createHash('md5').update(input).digest('hex');
}

class QueryMap {
constructor(fileMapName) {
this._fileMapName = fileMapName;
this._queryMap = new Map(JSON.parse(fs.readFileSync(this._fileMapName)));
}

_flush() {
const data = JSON.stringify(Array.from(this._queryMap.entries()));
fs.writeFileSync(this._fileMapName, data);
}

saveQuery(text) {
const id = md5(text);
this._queryMap.set(id, text);
this._flush();
return id;
}
}

const queryMap = new QueryMap('./queryMap.json');

async function requestListener(req, res) {
if (req.method === 'POST') {
const buffers = [];
for await (const chunk of req) {
buffers.push(chunk);
}
const data = Buffer.concat(buffers).toString();
res.writeHead(200, {
'Content-Type': 'application/json'
});
try {
if (req.headers['content-type'] !== 'application/x-www-form-urlencoded') {
throw new Error(
'Only "application/x-www-form-urlencoded" requests are supported.'
);
}
const text = new URLSearchParams(data).get('text');
if (text == null) {
throw new Error('Expected to have `text` parameter in the POST.');
}
const id = queryMap.saveQuery(text);
res.end(JSON.stringify({"id": id}));
} catch (e) {
console.error(e);
res.writeHead(400);
res.end(`Unable to save query: ${e}.`);
}
} else {
res.writeHead(400);
res.end("Request is not supported.")
}
}

const PORT = 2999;
const server = http.createServer(requestListener);
server.listen(PORT);

console.log(`Relay persisting server listening on ${PORT} port.`);

上面的範例將完整的查詢對應檔案寫入到 ./queryMap.json。若要使用此檔案,您需要更新 package.json

"scripts": {
"persist-server": "node ./relayLocalPersisting.js",
"relay": "relay-compiler"
}

網路層變更

您需要修改您的網路層擷取實作,以便在 POST 主體中傳遞 ID 參數 (例如,doc_id),而不是查詢參數

function fetchQuery(operation, variables) {
return fetch('/graphql', {
method: 'POST',
headers: {
'content-type': 'application/json'
},
body: JSON.stringify({
doc_id: operation.id, // NOTE: pass md5 hash to the server
// query: operation.text, // this is now obsolete because text is null
variables,
}),
}).then(response => {
return response.json();
});
}

在伺服器上執行持久化查詢

若要執行發送持久化查詢而不是查詢文字的客戶端請求,您的伺服器需要能夠查閱與每個 ID 對應的查詢文字。通常這會涉及到將 queryMap.json JSON 檔案的輸出儲存到資料庫或其他儲存機制中,並檢索客戶端指定的 ID 對應的文字。

此外,您的 relayLocalPersisting.js 實作可以直接將查詢儲存到資料庫或其他儲存中。

對於客戶端和伺服器程式碼位於同一個專案中的通用應用程式,這不是問題,因為您可以將查詢對應檔案放在客戶端和伺服器都可以存取的共用位置。

編譯時推送

對於客戶端和伺服器專案是分開的應用程式,一種選擇是在編譯時使用額外的 npm 執行指令碼將查詢對應推送到您的伺服器可存取的位置

"scripts": {
"push-queries": "node ./pushQueries.js",
"persist-server": "node ./relayLocalPersisting.js",
"relay": "relay-compiler && npm run push-queries"
}

您可以在 ./pushQueries.js 中執行的一些可能性

  • git push 到您的伺服器儲存庫。

  • 將查詢對應儲存到資料庫。

執行時推送

第二種更複雜的選擇是在執行時將您的查詢對應推送到伺服器,而伺服器一開始不知道查詢 ID。客戶端樂觀地向伺服器發送查詢 ID,而伺服器沒有查詢對應。然後,伺服器反過來向客戶端請求完整的查詢文字,以便它可以快取查詢對應以供後續請求使用。這是一種更複雜的方法,需要客戶端和伺服器互動以交換查詢對應。

簡單的伺服器範例

一旦您的伺服器可以存取查詢對應,您就可以執行對應。解決方案因您使用的伺服器和資料庫技術而異,因此我們在這裡只涵蓋最常見和基本的範例。

如果您使用 express-graphql 並且可以存取查詢對應檔案,您可以直接匯入它並使用 express-graphql-persisted-queriespersistedQueries 中介軟體執行匹配。

import express from 'express';
import {graphqlHTTP} from 'express-graphql';
import {persistedQueries} from 'express-graphql-persisted-queries';
import queryMap from './path/to/queryMap.json';

const app = express();

app.use(
'/graphql',
persistedQueries({
queryMap,
queryIdKey: 'doc_id',
}),
graphqlHTTP({schema}),
);

使用 persistConfig--watch

可以透過同時使用 persistConfig--watch 選項來持續產生查詢對應檔案。這只對通用應用程式有意義,也就是說,如果您的客戶端和伺服器程式碼位於單個專案中,並且您在開發期間在 localhost 上一起執行它們。此外,為了讓伺服器取得 queryMap.json 的變更,您需要設定伺服器端熱重載。有關如何設定此選項的詳細資訊不在本文檔的範圍內。


此頁面是否有用?

請透過以下方式協助我們讓網站變得更好 回答幾個快速問題.