如何使用 GraphQL 构建 TypeScript+React 应用
Posted 前端之巅
tags:
篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何使用 GraphQL 构建 TypeScript+React 应用相关的知识,希望对你有一定的参考价值。
GraphQL 和 TypeScript 的使用率都在爆炸式增长,并且当两者与 React 结合应用时,它们在一起可以创造理想的开发体验。
GraphQL 改变了我们对 API 的思考方式;利用 GrahpQL 直观的键 / 值对匹配,客户端可以精确请求所需的数据来显示在网页或移动应用屏幕上。TypeScript 则向变量添加了静态类型来扩展 javascript,从而减少了错误并提高了可读性。
本文将引导你使用公共的 SpaceX GraphQL API,使用 React 和 Apollo 构建一个客户端应用程序,展示有关火箭发射的信息。我们将自动为查询生成 TypeScript 类型,并使用 React Hooks 执行这些查询。
假定你对 React、GraphQL 和 TypeScript 有所了解,我们将重点介绍如何将它们集成在一起以构建一个正常运作的应用程序。
如果你在哪里卡住了,可以参考源代码 [1] 或查看应用的演示 [2]。
GraphQL API 需要被强类型化,并且从单个端点提供数据。客户端在此端点上调用一个 GET 请求,就可以接收一个后端的完全自注释的表示,以及所有可用数据和相应的类型。
我们可以使用 GraphQL Code Generator[3] 在 Web 应用目录中扫描查询文件,并将它们与 GraphQL API 提供的信息匹配,从而为所有请求数据创建 TypeScript 类型。使用 GraphQL,我们可以免费自动输入 React 组件的 props。这样可以减少错误,并加快产品迭代速度。
npx create-react-app graphql-typescript-react --typescript
// NOTE - you will need Node v8.10.0+ and NPM v5.2+
使用 --typescript 标志,CRA 将生成你的文件以及.ts 和.tsx,并将创建一个 tsconfig.json 文件。
cd graphql-typescript-react
现在我们可以安装其他依赖项。我们的应用将使用 Apollo 来执行 GraphQL API 请求。Apollo 所需的库是 apollo-boost、react-apollo、react-apollo-hooks、graphql-tag 和 graphql。
yarn add apollo-boost react-apollo react-apollo-hooks graphql-tag graphql
yarn add -D @graphql-codegen/cli
$(npm bin)/graphql-codegen init
这将启动 CLI 向导。请执行以下步骤:
-
使用 React 构建应用程序。 -
Schema 位于 https://spacexdata.herokuapp.com/graphql。 -
将你的操作和分片(fragments)位置设置为./src/components/**/*.{ts,tsx}这样它将在我们所有的 TypeScript 文件中搜索查询声明。 -
使用默认插件“TypeScript”“TypeScript Operations”“TypeScript React Apollo”。 -
使用目标 src/Generated/graphql.tsx(react-apollo 插件需要.tsx)。 -
不要生成内省文件。 -
使用默认的 codegen.yml 文件。 -
运行脚本是 codegen。
现在,在 CLI 中运行 yarn 命令,安装 CLI 工具添加到 package.json 中的插件。
overwrite: true
schema: 'https://spacexdata.herokuapp.com/graphql'
documents: './src/components/**/*.ts'
generates:
src/generated/graphql.tsx:
plugins:
- 'typescript'
- 'typescript-operations'
- 'typescript-react-apollo'
config:
withHooks: true
GraphQL 的一大好处是它使用了声明性数据获取。我们能够编写出一些与使用它们的组件并存的查询,并且 UI 能够准确地请求它需要渲染的内容。
使用 REST API 时,我们需要查找处于(或不处于)最新状态的文档。如果 REST 出现任何问题,我们需要针对 API 和 console.log 结果发起请求以调试数据。
GraphQL 允许你在 UI 中访问 URL,查看完全定义的 schema 并针对它执行请求,从而解决了这个问题。请查看要使用的数据 [4]。
尽管我们有大量的 SpaceX 数据可供使用,但我们仅显示有关火箭发射的信息。我们将有两个主要组件:
-
一个 launches 列表,用户可以单击列表以了解有关发射的更多信息。 -
单次 launch 的详细资料。
对于第一个组件,我们将查询 launches 键,并请求 flight_number、mission_name 和 launch_year。我们将这些数据显示在一个列表中,当用户单击其中一个项目时,我们将根据 launch 键查询关于这次火箭发射的更大数据集。下面我们在 GraphQL 游乐场中测试我们的第一个查询。
import gql from 'graphql-tag';
export const QUERY_LAUNCH_LIST = gql`
query LaunchList {
launches {
flight_number
mission_name
launch_year
}
}
`;
我们的其他查询将基于 flight_number,获得有关单次发射的更详细数据。由于这将通过用户交互动态生成,因此我们将需要使用 GraphQL 变量。我们还可以在游乐场上用变量测试查询。
在查询名称旁边指定变量,前面带上 $ 及其类型。然后你就可以在 body 内使用变量了。针对查询,我们通过传递 $id 变量(其类型为 String!)来设置火箭发射的 ID。
我们将 id 作为一个变量传递,该变量对应于 LaunchList 查询中的 flight_number。LaunchProfile 查询还将包含嵌套的对象 / 类型,在这里我们可以在方括号内指定键来获取值。
例如,发射信息包含了一个 rocket 定义(LaunchRocket 类型),我们将要求它提供 rocket_name 和 rocket_type。要了解更多可用于 LaunchRocket 的字段信息,你可以使用侧边的 schema 导航器来了解可用数据。
import gql from 'graphql-tag';
export const QUERY_LAUNCH_PROFILE = gql`
query LaunchProfile($id: String!) {
launch(id: $id) {
flight_number
mission_name
launch_year
launch_success
details
launch_site {
site_name
}
rocket {
rocket_name
rocket_type
}
links {
flickr_images
}
}
}
`;
yarn codegen
在 src/generation/graphql.ts 内部,你将找到定义应用程序所需的所有类型,以及用于获取 GraphQL 端点以检索该数据的对应查询。
这个文件通常会很大,但是充满了有价值的信息。我建议花些时间浏览一下,并了解我们的 codegen 完全基于 GraphQL schema 所创建的所有类型。
比如说检查 type Launch,它是 GraphQL 的 Launch 对象的 TypeScript 表示形式,我们会在游乐场上与之交互。还可以滚动到文件的底部,查看专门为我们将要执行的查询生成的代码——它已创建了组件、HOC、类型化的 props/ 查询和类型化的 hooks。
在 src/index.tsx 中,我们需要初始化 Apollo 客户端,并使用 ApolloProvider 组件将我们的 client 添加到 React 的上下文中。我们还需要 ApolloProviderHooks 组件以在 hooks 中启用上下文。
import React from 'react';
import ReactDOM from 'react-dom';
import ApolloClient from 'apollo-boost';
import { ApolloProvider } from 'react-apollo';
import { ApolloProvider as ApolloHooksProvider } from 'react-apollo-hooks';
import './index.css';
import App from './App';
const client = new ApolloClient({
uri: 'https://spacexdata.herokuapp.com/graphql',
});
ReactDOM.render(
<ApolloProvider client={client}>
<ApolloHooksProvider client={client}>
<App />
</ApolloHooksProvider>
</ApolloProvider>,
document.getElementById('root'),
);
现在我们已经准备好了通过 Apollo 执行 GraphQL 查询所需的一切内容。
在 src/components/LaunchList/index.tsx 内,我们将创建一个函数组件,其使用生成的 useLaunchListQuery hook。查询 hooks 返回 data、loading 和 error 值。我们将检查容器组件中的 loading 和 error,并将 data 传递给我们的演示组件。
我们将此组件用作一个容器 / 智能组件,从而保持关注点的分离;我们还将数据传递给表示 / 哑组件,该组件仅显示给出的内容。我们还将在等待数据时显示基本的加载和错误状态。
import * as React from 'react';
import { useLaunchListQuery } from '../../generated/graphql';
import LaunchList from './LaunchList';
const LaunchListContainer = () => {
const { data, error, loading } = useLaunchListQuery();
if (loading) {
return <div>Loading...</div>;
}
if (error || !data) {
return <div>ERROR</div>;
}
return <LaunchList data={data} />;
};
export default LaunchListContainer;
我们的演示组件将使用我们的类型化 data 对象来构建 UI。我们使用<ol>创建一个有序列表,然后映射到发射信息中,以显示 mission_name 和 launch_year。
import * as React from 'react';
import { LaunchListQuery } from '../../generated/graphql';
import './styles.css';
interface Props {
data: LaunchListQuery;
}
const className = 'LaunchList';
const LaunchList: React.FC<Props> = ({ data }) => (
<div className={className}>
<h3>Launches</h3>
<ol className={`${className}__list`}>
{!!data.launches &&
data.launches.map(
(launch, i) =>
!!launch && (
<li key={i} className={`${className}__item`}>
{launch.mission_name} ({launch.launch_year})
</li>
),
)}
</ol>
</div>
);
export default LaunchList;
如果你使用的是 VS Code,由于我们正在使用 TypeScript,因此 IntelliSense 会准确显示可用的值并提供自动完成列表。它还会警告我们正在使用的数据可以为 null 还是 undefined。
这么神奇?编辑器会自动帮我们编程。另外,如果需要定义类型或函数,可以按 Cmd + t,鼠标指针悬停其上,它将为你提供所有详细信息。
.LaunchList {
height: 100vh;
overflow: hidden auto;
background-color: #ececec;
width: 300px;
padding-left: 20px;
padding-right: 20px;
}
.LaunchList__list {
list-style: none;
margin: 0;
padding: 0;
}
.LaunchList__item {
padding-top: 20px;
padding-bottom: 20px;
border-top: 1px solid #919191;
cursor: pointer;
}
现在我们将构建配置组件,以显示有关火箭发射的更多详细信息。该组件的 index.tsx 文件基本是一样的,只是我们使用的是 Profile 查询和组件。我们还将一个变量传递给我们的 React hook 以获取发射 ID。目前我们将其硬编码为'42',然后在布局好应用后添加动态功能。
import * as React from 'react';
import { useLaunchProfileQuery } from '../../generated/graphql';
import LaunchProfile from './LaunchProfile';
const LaunchProfileContainer = () => {
const { data, error, loading } = useLaunchProfileQuery(
{ variables: { id: '42' } }
);
if (loading) {
return <div>Loading...</div>;
}
if (error) {
return <div>ERROR</div>;
}
if (!data) {
return <div>Select a flight from the panel</div>;
}
return <LaunchProfile data={data} />;
};
export default LaunchProfileContainer;
现在我们需要创建演示组件。它将在用户界面顶部显示火箭发射的名称和详细信息,然后在说明下方显示一个发射图像网格。
import * as React from 'react';
import { LaunchProfileQuery } from '../../generated/graphql';
import './styles.css';
interface Props {
data: LaunchProfileQuery;
}
const className = 'LaunchProfile';
const LaunchProfile: React.FC<Props> = ({ data }) => {
if (!data.launch) {
return <div>No launch available</div>;
}
return (
<div className={className}>
<div className={`${className}__status`}>
<span>Flight {data.launch.flight_number}: </span>
{data.launch.launch_success ? (
<span className={`${className}__success`}>Success</span>
) : (
<span className={`${className}__failed`}>Failed</span>
)}
</div>
<h1 className={`${className}__title`}>
{data.launch.mission_name}
{data.launch.rocket &&
` (${data.launch.rocket.rocket_name} | ${data.launch.rocket.rocket_type})`}
</h1>
<p className={`${className}__description`}>{data.launch.details}</p>
{!!data.launch.links && !!data.launch.links.flickr_images && (
<div className={`${className}__image-list`}>
{data.launch.links.flickr_images.map(image =>
image ? <img src={image} className={`${className}__image`} key={image} /> : null,
)}
</div>
)}
</div>
);
};
export default LaunchProfile;
.LaunchProfile {
height: 100vh;
max-height: 100%;
width: calc(100vw - 300px);
overflow: hidden auto;
padding-left: 20px;
padding-right: 20px;
}
.LaunchProfile__status {
margin-top: 40px;
}
.LaunchProfile__title {
margin-top: 0;
margin-bottom: 4px;
}
.LaunchProfile__success {
color: #2cb84b;
}
.LaunchProfile__failed {
color: #ff695e;
}
.LaunchProfile__image-list {
display: grid;
grid-gap: 20px;
grid-template-columns: repeat(2, 1fr);
margin-top: 40px;
padding-bottom: 100px;
}
.LaunchProfile__image {
width: 100%;
}
import React from 'react';
import LaunchList from './components/LaunchList';
import LaunchProfile from './components/LaunchProfile';
import './App.css';
const App = () => {
return (
<div className="App">
<LaunchList />
<LaunchProfile />
</div>
);
};
export default App;
.App {
display: flex;
width: 100vw;
height: 100vh;
overflow: hidden;
}
在终端中执行 yarn start,在浏览器中转至 http://localhost:3000,你就应该能看到应用的基本版本了!
现在我们需要添加一项功能,以在用户单击面板中的项目时获取完整的火箭发射相关数据。我们将在 App 组件中创建一个 hook 来跟踪火箭 ID,并将其传递给 LaunchProfile 组件以重新获取发射相关数据。
我们在 src/App.tsx 中添加 useState 来维护和更新 ID 的状态。当用户从列表中选择一个 ID 时,我们还将使用名为 handleIdChange 的 useCallback 作为单击处理程序来更新 ID。我们将这个 id 传递给 LaunchProfile,然后将 handleIdChange 传递给
const App = () => {
const [id, setId] = React.useState(42);
const handleIdChange = React.useCallback(newId => {
setId(newId);
}, []);
return (
<div className="App">
<LaunchList handleIdChange={handleIdChange} />
<LaunchProfile id={id} />
</div>
);
};
export interface OwnProps {
handleIdChange: (newId: number) => void;
}
interface Props extends OwnProps {
data: LaunchListQuery;
}
// ...
const LaunchList: React.FC<Props> = ({ data, handleIdChange }) => (
// ...
<li
key={i}
className={`${className}__item`}
onClick={() => handleIdChange(launch.flight_number!)}
>
在 LaunchList/index.tsx 内部,请确保导入 OwnProps 声明以类型化要传递到容器组件的 props,然后将这些 props 散布到<LaunchList data = {data} {... props} />中。
interface OwnProps {
id: number;
}
const LaunchProfileContainer = ({ id }: OwnProps) => {
const { data, error, loading, refetch } = useLaunchProfileQuery({
variables: { id: String(id) },
});
React.useEffect(() => {
refetch();
}, [id]);
配置好应用后,我们可以看到开发速度是非常快的。我们可以轻松构建数据驱动的 UI。GraphQL 允许我们定义组件中所需的数据,并且可以将其无缝用作组件中的 props。生成的 TypeScript 定义为我们编写的代码提供了极高的信心水平。
如果你希望深入研究该项目,那么下一步将是使用 API中的额外字段来添加分页和更多的数据连接。要对火箭发射列表进行分页,你需要获得当前列表的长度,并将 offset 变量传递给 LaunchList 查询。
我鼓励你更深入地研究它并编写自己的查询,以巩固本文提出的概念。
[1] 源代码:https://github.com/treyhuffine/graphql-react-typescript-spacex
[2] 应用的演示:https://spacex-graphql.netlify.com/https://spacex-graphql.netlify.com/
[3]GraphQL Code Generator:https://github.com/dotansimha/graphql-code-generator
[4]https://spacexdata.herokuapp.com/graphql
[5]GraphQL 变量:https://graphql.org/learn/queries/#variables
原文链接:https://levelup.gitconnected.com/build-a-graphql-react-app-with-typescript-9661f908b26
以上是关于如何使用 GraphQL 构建 TypeScript+React 应用的主要内容,如果未能解决你的问题,请参考以下文章
如何使用 GraphQL 构建 TypeScript+React 应用
如何使用 api-platform 构建 GraphQL API?