跳至主要内容
版本: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": "http://localhost: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. 它将发送一个带有 text 参数的 HTTP POST 请求到指定的 url。您还可以通过 params 选项添加其他请求体参数。

"scripts": {
"relay": "relay-compiler"
},
"relay": {
"src": "./src",
"schema": "./schema.graphql",
"persistConfig": {
"url": "http://localhost: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 文件发送到浏览器,这会增加您的包大小。

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 选项来持续生成查询映射文件。这只有对通用应用程序有意义,即如果您的客户端和服务器代码位于同一个项目中,并且您在开发期间将它们都一起运行在本地主机上。此外,为了让服务器获取对 queryMap.json 的更改,您需要设置服务器端热重载。有关如何设置它的详细信息超出了本文档的范围。


此页面是否有用?

通过以下方式帮助我们改善网站 回答几个简单的问题.