remix-graphql
Remix和GraphQL可以和谐共存❤️ 本包含有基本的实用函数,可以帮助你实现这一点。
更具体地说,最新版本的remix-graphql
可以帮助你完成以下任务:
- 使用GraphQL查询和变更处理loader和action请求
- 你可以定义本地schema和解析器来处理请求
- 你也可以对远程API执行GraphQL请求
- 将GraphQL API设置为资源路由
以下是一些未来可能实现的酷点子:
- 将多个loader的查询批处理为单个API请求
目录
- 安装
- 定义你的schema
- 使用GraphQL处理loader和action请求
- [向远程GraphQL API发送请求](#向远程GraphQL API发送请求)
- [在Remix应用中设置GraphQL API](#在Remix应用中设置GraphQL API)
- 上下文
安装
你可以使用你喜欢的包管理器安装remix-graphql
。它依赖于graphql
包,所以请确保也安装了该包。
# 使用`npm`
npm install graphql remix-graphql
# 或使用`yarn`
yarn add graphql remix-graphql
它还列出了一些Remix包作为对等依赖。(如果你使用Remix CLI设置项目,你很可能已经安装了它们。)如果遇到意外错误,请再次检查是否安装了以下包:
@remix-run/dev
@remix-run/react
@remix-run/serve
remix
如何从remix-graphql
导入
此模块不适用于浏览器环境,它仅在服务器上工作。你可以通过从扩展名为.server.js
(或.server.ts
)的文件中导入,强制Remix编译器永远不会在客户端包中包含remix-graphql
的内容。
// 这样做不行,实际上会抛出错误:
import { anything } from "remix-graphql";
// 应该这样做:
import { anything } from "remix-graphql/index.server";
定义你的schema
remix-graphql
保持简单,让你自己决定定义GraphQL schema的最佳方式。在所有需要"将schema传递给remix-graphql
"的地方,相应的函数都期望一个GraphQLSchema
对象。
这意味着以下所有方法都可以用来定义schema:
- 使用
graphql
包中的GraphQLSchema
类(显然...) - 使用SDL定义schema,在对象中定义解析器函数,并使用
makeExecutableSchema
(来自@graphql-tools/schema
)合并两者 - 使用
nexus
和makeSchema
我们建议从一个文件中导出schema,例如app/graphql/schema.server.ts
。通过使用.server.ts
扩展名,你可以确保这些代码不会被发送到浏览器。(这是对Remix编译器的提示,在构建浏览器包时应忽略此模块。)
使用GraphQL处理loader和action请求
loaders
和actions
都只是简单的函数,给定一个Request
返回一个Response
。使用remix-graphql
,你可以使用GraphQL来处理这个请求!以下是一个完整且可工作的示例,展示了它是如何工作的:
// app/routes/index.tsx
import type { GraphQLError } from "graphql";
import { Form } from "remix";
import type { ActionFunction, LoaderFunction } from "@remix-run/node";;
import { processRequestWithGraphQL } from "remix-graphql/index.server";
// 从你导出schema的地方导入
import { schema } from "~/graphql/schema";
const ALL_POSTS_QUERY = /* GraphQL */ `
query Posts($limit: Int) {
posts(limit: $limit) {
id
title
likes
author {
name
}
}
}
`;
export const loader: LoaderFunction = (args) =>
processRequestWithGraphQL({
// 传递Remix传给loader函数的参数。
args,
// 提供你的schema。
schema,
// 提供应该执行的GraphQL操作。这也可以是一个mutation,
// 它被命名为`query`是为了与通过HTTP发送GraphQL请求的常见命名保持一致。
query: ALL_POSTS_QUERY,
// 可选地提供执行操作时应使用的变量。如果不传递,`remix-graphql`将从以下位置派生变量:
// - ...路由参数。
// - ...提交的`formData`(如果存在)。
variables: { limit: 10 },
// 可选地传递一个对象,其属性应包含在执行上下文中。
context: {},
// 可选地传递一个函数,为成功执行的操作派生自定义HTTP状态码。
deriveStatusCode(
// 执行的结果。
executionResult: ExecutionResult,
// 默认情况下会返回的状态码,即如果不传递`deriveStatusCode`函数。
defaultStatusCode: number
) {
return defaultStatusCode;
},
});
const LIKE_POST_MUTATION = /* GraphQL */ `
mutation LikePost($id: ID!) {
likePost(id: $id) {
id
likes
}
}
`;
// `processRequestWithGraphQL`函数可以用于loader和action!
export const action: ActionFunction = (args) =>
processRequestWithGraphQL({ args, schema, query: LIKE_POST_MUTATION });
export default function IndexRoute() {
const { data } = useLoaderData<LoaderData>();
if (!data) {
return "哎呀,出了点问题 :(";
}
return (
<main>
<h1>博客文章</h1>
<ul>
{data.posts.map((post) => (
<li key={post.id}>
{post.title}(作者:{post.author.name})
<br />
{post.likes} 个赞
<Form method="post">
{/* `remix-graphql`会自动将所有提交的表单数据
转换为GraphQL操作的同名变量 */}
<input hidden name="id" value={post.id} />
<button type="submit">点赞</button>
</Form>
</li>
))}
</ul>
</main>
);
}
type LoaderData = {
data?: {
posts: {
id: string;
title: string;
likes: number;
author: { name: string };
}[];
};
errors?: GraphQLError[];
};
自动类型生成
在上面示例的末尾,你可以看到从loader函数返回的数据类型必须手动定义。由于GraphQL是强类型的,如果你想的话,可以自动化这个过程!
首先,你需要从你的schema生成内省数据作为JSON,并将其存储在本地文件中。为此,你可以创建一个简单的脚本,如下所示:
// app/graphql/introspection.{js,ts}
import fs from "fs";
import { introspectionFromSchema } from "graphql";
import path from "path";
import { schema } from "./schema";
fs.writeFileSync(
path.join(__dirname, "introspection.json"),
JSON.stringify(introspectionFromSchema(schema))
);
通常,你不希望将生成的JSON文件提交到版本控制中,所以我们建议将其添加到你的.gitignore
文件中。
为了更方便地运行这个脚本,在你的package.json
中创建一个简单的NPM脚本:
{
"scripts": {
// 如果你用JavaScript创建了脚本
"introspection": "node app/graphql/introspection.js",
// 如果你用TypeScript创建了脚本(确保在这种情况下安装
// `esbuild-register`作为开发依赖)
"introspection": "node --require esbuild-register app/graphql/introspection.ts"
}
}
要实际从你的查询和变更生成类型,我们推荐使用GraphQL Code Generator。为此,你需要安装几个依赖:
# 使用`npm`
npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
# 或使用`yarn`
yarn add -D @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations
快完成了!现在在你项目的根目录创建一个名为codegen.yml
的配置文件,内容如下:
overwrite: true
# 之前生成的内省数据存储的路径
schema: "app/graphql/introspection.json"
# 匹配所有包含操作定义的文件的glob
documents: "app/routes/**/*.{ts,tsx}"
generates:
# 这是生成的类型将被存储的路径
app/graphql/types.ts:
plugins:
- "typescript"
- "typescript-operations"
config:
skipTypename: true
现在你终于可以生成类型了!为了方便起见,再添加一个NPM脚本:
{
"scripts": {
"introspection": "node --require esbuild-register app/graphql/introspection.ts",
"codegen": "npm run introspection && graphql-codegen --config codegen.yml"
}
}
运行npm run codegen
(或yarn codegen
)现在将自动为所有查询和变更创建返回数据的类型。(附注:这也是验证所有操作是否对你的schema有效的好方法!)
**还有一件事:**注意到我们在上面的示例中包含查询和变更的字符串之前的/* GraphQL */
注释了吗?这很重要!它是对@graphql-codegen
的提示,表明这个字符串应该被解析为GraphQL。没有它,你将无法获得字符串中定义的操作的任何类型。
现在可以像这样修改上面的示例:
// 添加这个导入...
import type { PostsQuery } from "~/graphql/types";
// ...并像这样更改`LoaderData`类型
type LoaderData = { data?: PostsQuery; errors?: GraphQLError[] };
向远程GraphQL API发送请求
也许你不想把GraphQL API写在Remix应用中,或者你想使用第三方GraphQL API,比如GitHub的公共API。在这两种情况下,remix-graphql
都可以帮到你!
// app/routes/$username.tsx
import type { GraphQLError } from "graphql";
import type { LoaderFunction } from "@remix-run/node";
import { sendGraphQLRequest } from "remix-graphql/index.server";
const LOAD_USER_QUERY = /* GraphQL */ `
query LoadUser($username: String!) {
user(login: $username) {
name
}
}
`;
export const loader: LoaderFunction = (args) =>
sendGraphQLRequest({
// 传递Remix传给loader函数的参数。
args,
// 提供远程GraphQL API的端点。
endpoint: "https://api.github.com/graphql",
// 可选地为请求添加头部。
headers: { authorization: `Bearer ${process.env.GITHUB_TOKEN}` },
// 提供要发送到远程API的GraphQL操作。
query: LOAD_USER_QUERY,
// 可选地提供执行操作时应使用的变量。如果不传递,`remix-graphql`将从以下位置派生变量:
// - ...路由参数。
// - ...提交的`formData`(如果存在)。
// 这意味着以下是默认值,也可以省略。
variables: args.params,
});
export default function UserRoute() {
const { data } = useLoaderData<LoaderData>();
if (!data) {
return "哎呀,出了点问题 :(";
}
if (!data.user) {
return "找不到用户 :(";
}
return <h1>{data.user.name}</h1>;
}
type LoaderData = {
data?: {
user: {
name: string | null;
} | null;
};
errors?: GraphQLError[];
};
如果你想在加载器中执行比单个 GraphQL 查询更多的操作,完全可以做到!函数 sendGraphQLRequest
会返回对远程 API 进行获取请求的 Response
对象,因此你可以在加载器中根据需要对其进行任何操作。
import { json } from "remix";
import type { LoaderFunction } from "@remix-run/node";
import { sendGraphQLRequest } from "remix-graphql/index.server";
const LOAD_USER_QUERY = /* GraphQL */ `
query LoadUser($username: String!) {
user(login: $username) {
name
}
}
`;
export const loader: LoaderFunction = (args) => {
try {
const loadUserRes = await sendGraphQLRequest({
args,
endpoint: "https://api.github.com/graphql",
headers: { authorization: `Bearer ${process.env.GITHUB_TOKEN}` },
query: LOAD_USER_QUERY,
}).then((res) => res.json());
/* 你可以在这里执行任何额外的操作... */
const otherStuff = 42;
return json({ username: loadUserRes.data.user.name, otherStuff });
} catch {
throw new Response("加载数据时出现问题 :(");
}
};
在 Remix 应用中设置 GraphQL API
你可以使用 Remix 的资源路由为你的 GraphQL API 创建一个专用端点。你只需创建一个路由(例如 app/routes/graphql.ts
)并粘贴以下代码。通过同时使用加载器和操作,你的端点可以支持 GET 和 POST 请求!
// app/routes/graphql.ts
import {
getActionFunction,
getLoaderFunction,
} from "remix-graphql/index.server";
import type { DeriveStatusCodeFunction } from "remix-graphql/index.server";
// 从你导出的地方导入 schema
import { schema } from "~/graphql/schema";
// 处理 GET 请求
export const loader = getLoaderFunction({
// 提供你的 schema
schema,
// 可选:传递一个对象,其属性应包含在执行上下文中
context: {},
// 可选:传递一个函数,为成功执行的操作派生自定义 HTTP 状态码
deriveStatusCode,
});
// 处理 POST 请求
export const action = getActionFunction({
// 提供你的 schema
schema,
// 可选:传递一个对象,其属性应包含在执行上下文中
context: {},
// 可选:传递一个函数,为成功执行的操作派生自定义 HTTP 状态码
deriveStatusCode,
});
// 此函数等同于默认行为
const deriveStatusCode: DeriveStatusCodeFunction = (
// 执行的结果
executionResult,
// 默认情况下返回的状态码,即不传递 `deriveStatusCode` 函数时的状态码
defaultStatusCode
) => defaultStatusCode;
上下文
在定义 schema 和编写解析器时,通常会提供一个上下文对象。remix-graphql
导出的所有函数都接受一个可选的 context
属性作为参数对象。如果传递,它必须是一个对象。它的所有属性都将包含在传递给解析器的上下文对象中。
remix-graphql
还导出了一个 Context
类型,包含了添加到执行上下文对象的所有属性。这个类型接受一个可选的泛型,你可以通过它向上下文对象添加任何自定义属性。
import type { PrismaClient } from "@prisma/client";
import type { Context } from "remix-graphql/index.server";
type ContextWithDatabase = Context<{ db: PrismaClient }>;
以下小节突出显示了 remix-graphql
添加到上下文对象的所有属性。
request
这是传递给 Remix 中加载器或操作函数的 Request
对象。它始终是上下文对象的一部分。
redirect
在处理 UI 路由的加载器或操作时,Remix 中的一个常见模式是重定向。(Remix 甚至提供了一个 redirect
实用函数,可以从任何加载器或操作函数返回。)在 remix-graphql
中,你可以通过使用上下文对象中提供的 redirect
函数来实现这一点。
这个函数的签名如下:
function redirect(
// 重定向的 URL
url: string,
// 可选:包含在 HTTP 响应中的头部值
headers?: HeadersInit
): void;
注意,这个函数只在处理 UI 路由中的 GraphQL 请求时才是上下文对象的一部分,即使用 processRequestWithGraphQL
时。在处理资源路由中的 GraphQL 请求时,即使用 getActionFunction
或 getLoaderFunction
时,它不是上下文对象的一部分。