用TS+GraphQL查询SpaceX火箭发射数据[每日前端夜话0x81]

疯狂的技术宅 前端先锋

每日前端夜话0x81 每日前端夜话,陪你聊前端。 每天晚上18:00准时推送。 正文共:4126 字 预计阅读时间: 11 分钟 翻译:疯狂的技术宅 来源:logrocket

近一两年来 GraphQL 和 TypeScript 的使用都程爆发式增长,当两者与React结合使用时,它们可以为开发人员提供理想的开发体验。

GraphQL 改变了我们对 API 的思考方式,并利用直观的键/值对匹配,客户端可以请求在网页或移动应用屏幕上显示所需的确切数据。 TypeScript 通过为变量添加静态类型来扩展 JavaScript,从而减少了错误并提高了代码的可读性。

本文将引导你使用 React 和 Apollo 构建客户端应用程序,并调用 SpaceX 的公共 GraphQL API ,来显示有关的发射信息。我们将自动为查询生成 TypeScript 类型,并使用 React Hooks 执行这些查询。

本文假设你对 React,GraphQL 和 TypeScript 有一定的了解,并且正在研究怎样通过把它们集成在一起来构建一个正常运行的程序。如果你需要补充一些基础知识的话,可以关注公众号“前端先锋”。

如果你在学习的过程中遇到困难,可以参考源代码(https://github.com/treyhuffine/graphql-react-typescript-spacex)或查看 live app(https://spacex-graphql.netlify.com/)。 SpaceX Launch App

为什么选择 GraphQL + TypeScript?

GraphQL API 需要强类型化,数据从单个端点提供。通过在此端点上调用 GET 请求,客户端可以接收后端的完全自我描述的数据,包括所有可用的数据和相应的类型。

通过 GraphQL 代码生成器(https://github.com/dotansimha/graphql-code-generator),我们可以扫描 Web 应用目录中的查询文件,并将它们与 GraphQL API 提供的信息进行匹配,这样以来就可以创建 TypeScript 类型所有请求数据。通过使用 GraphQL,我们可以自动且自由地输入我们的 React 组件的属性。这样可以减少产品上的错误并提高迭代速度。

入门

我们将使用带有 TypeScript 配置的 create-react-app 来创建程序。首先执行以下命令:


1npx create-react-app graphql-typescript-react --typescript
2// NOTE - you will need Node v8.10.0+ and NPM v5.2+

通过使用 --typescript 标志,CRA 将为你生成项目文件和 .ts 和 .tsx,它将创建一个 tsconfig.json 文件。

切换到 app 目录:


1cd graphql-typescript-react

现在安装附加依赖项。我们的程序用 Apollo 来执行 GraphQL API 请求。 Apollo 所需的库是 apollo-boost,react-apollo,react-apollo-hooks,graphql-tag和graphql。

apollo-boost 包含了查询 API 和在内存中缓存数据所需的工具, react-apollo 为React提供绑定, react-apollo-hooks 在 React Hook 中包装了 Apollo 查询, graphql-tag 用于构建我们的查询文档, graphql 是一个对等依赖项,它提供了 GraphQL 实现的细节。


1yarn add apollo-boost react-apollo react-apollo-hooks graphql-tag graphql

graphql-code-generator 用于自动化 TypeScript 的工作流程。接下来安装 codegen CLI 来生成我们需要的配置和插件。


1yarn add -D @graphql-codegen/cli

设置 codegen 配置执行以下命令:


1$(npm bin)/graphql-codegen init

这将启动CLI向导,并执行以下步骤:

  1. 要用 React 构建的程序。
  2. schema 位于 https://spacexdata.herokuapp.com/graphql。
  3. 将你的操作和代码位置设置为 ./src/components/*/.{ts,tsx} ,以便它能够搜索到所有的 TypeScript 文件以进行查询声明。
  4. 使用默认插件 “TypeScript”,“TypeScript Operations”,“TypeScript React Apollo”。
  5. 将生成的目标文件夹更新为 src/generated/graphql.tsx (react-apollo 插件需要 .tsx)。
  6. 不要生成 introspection file。
  7. 使用默认的 codegen.yml 文件。
  8. 制作你的运行脚本 codegen。 在 CLI 中运行 yarn 命令安装 CLI 工具的插件并添加到 package.json 。

我们还将对 codegen.yml 文件进行一次更新,通过在其中添加 withHooks:true 配置选项来生成类型化的 React Hook 查询。你的配置文件应如下所示:


 1overwrite: true
 2schema: 'https://spacexdata.herokuapp.com/graphql'
 3documents: './src/components/**/*.ts'
 4generates:
 5  src/generated/graphql.tsx:
 6    plugins:
 7      - 'typescript'
 8      - 'typescript-operations'
 9      - 'typescript-react-apollo'
10    config:
11      withHooks: true

编写 GraphQL 查询并生成类型

GraphQL 最主要的好处是它利用声明性数据进行提取。我们能够编写与使用它们的组件并存的查询,并且 UI 能够准确地请求它要呈现的内容。

在使用 REST API 时,我们所能找的的文档有可能不是最新的。如果 REST 出现什么问题,我们需要用 console.log 配合来调试数据。

GraphQL 允许你通过访问 URL 查看完全定义的模式,并在 UI 中执行针对它的请求,从而解决了这个问题。现在访问 https://spacexdata.herokuapp.com/graphql 查看你将使用的确切数据。

SpaceX发布数据 虽然我们可以获得大量的 SpaceX 数据,但我们只会显示有关发射任务的信息。我们有两个主要组成部分:

  1. 用户可以通过单击“发射”任务列表来查看有关它们的更多信息。
  2. 单个发射任务的详细资料。 对于第一个组件,我们将查询 launchs 并请求 flight_number,mission_name 和 launch_year。我们将在列表中显示这些数据,当用户点击其中一个项目时,查询 launch 来获取该火箭的更多数据。让我们在 GraphQL playground 中测试第一个查询。

在GraphQL Playground 中进行第一个查询 要编写我们的查询,首先需要创建一个 src/components 文件夹,然后创建 src/components/LaunchList 文件夹。在此文件夹中,创建 index.tsx,LaunchList.tsx,query.ts 和 styles.css 文件。在 query.ts 文件中,可以从 playground 中发送查询并将其放在 gql 字符串中。


 1import gql from 'graphql-tag';
 2
 3export const QUERY_LAUNCH_LIST = gql`
 4  query LaunchList {
 5    launches {
 6      flight_number
 7      mission_name
 8      launch_year
 9    }
10  }
11`;

其他查询将根据 flight_number 获得更详细的单次发射数据。由于这将通过用户交互动态生成,所以需要用到 GraphQL 变量。我们还可以在 playground 上测试带变量的查询。

在查询名的后面,你可以通过使用前缀为$及类型去指定变量,然后在查询体中,你可以使用该变量。对于我们的查询,通过传递 $id 变量来设置启动的id,该变量的类型为String!。

将ID作为查询变量传递 我们传入 id 作为变量,它对应于 LaunchList 查询中的 flight_number。 LaunchProfile 查询还会包含嵌套对象或类型,可以通过指定括号内的键来获取对应的值。

例如,launch 内包含一个 rocket 定义(类型 LaunchRocket),其内部包含 rocket_name 和 rocket_type。为了更好地理解可用于 LaunchRocket 的字段,你可以通过侧面的模式导航器来了解可用数据。

现在将此查询转移到我们的程序。创建 src/components/LaunchProfile 文件夹及 index.tsx,LaunchProfile.tsx,query.ts和styles.css 文件。在 query.ts 文件中,我们从 playground 上粘贴前面的查询。


 1import gql from 'graphql-tag';
 2
 3export const QUERY_LAUNCH_PROFILE = gql`
 4  query LaunchProfile($id: String!) {
 5    launch(id: $id) {
 6      flight_number
 7      mission_name
 8      launch_year
 9      launch_success
10      details
11      launch_site {
12        site_name
13      }
14      rocket {
15        rocket_name
16        rocket_type
17      }
18      links {
19        flickr_images
20      }
21    }
22  }
23`;

现在我们已经定义了查询,你终于可以生成 TypeScript 接口和类型的 Hook。在终端中执行:


1yarn codegen

在 src/generated/graphql.ts 中,你将会找到定义程序所需的所有类型,以及获取 GraphQL 端点以检索该数据的相应查询。

这个文件往往很大,但里面的信息非常有价值。我建议花点时间研究它,并理解我们的 codegen 基于 GraphQL 架构创建的所有类型。

例如,检查 type Launch,这是我们在 playground 上与 GraphQL 交互的 Launch 对象的 TypeScript 表示。还可以滚动到文件的底部,查看专门为我们将要执行的查询生成的代码 —— 它创建了组件、HOC、类型化props或查询,还有类型化的 hook。

初始化Apollo客户端

在 src/index.tsx 中,我们需要初始化 Apollo 客户端并用 ApolloProvider 组件将 client 添加到 React 的上下文中。另外还需要 ApolloProviderHooks 组件来启用 hook 中的上下文。

我们初始化一个新的 ApolloClient 并给它 GraphQL API 的 URI,然后将 <App/> 组件包装在上下文提供程序中。你的索引文件应如下所示:


 1import React from 'react';
 2import ReactDOM from 'react-dom';
 3import ApolloClient from 'apollo-boost';
 4import { ApolloProvider } from 'react-apollo';
 5import { ApolloProvider as ApolloHooksProvider } from 'react-apollo-hooks';
 6import './index.css';
 7import App from './App';
 8
 9const client = new ApolloClient({
10  uri: 'https://spacexdata.herokuapp.com/graphql',
11});
12
13ReactDOM.render(
14  <ApolloProvider client={client}>
15    <ApolloHooksProvider client={client}>
16      <App />
17    </ApolloHooksProvider>
18  </ApolloProvider>,
19  document.getElementById('root'),
20);

构建组件

现在我们已经具备了通过 Apollo 执行 GraphQL 查询所需的一切条件。

在 src/components/LaunchList/index.tsx 中,创建一个使用生成的 useLaunchListQuery 钩子的函数组件。查询钩子返回 data,loading 和 error 的值。我们将在容器组件中检查 loading 和 error,并将 data 传递给表示组件。

我们将用这个组件作为智能组件来保持关注点的分离,并且将数据传给只显示给定内容的表示组件。我们还将在等待数据时显示基本的加载和错误状态。

你的容器组件应如下所示:


 1import * as React from 'react';
 2import { useLaunchListQuery } from '../../generated/graphql';
 3import LaunchList from './LaunchList';
 4
 5const LaunchListContainer = () => {
 6  const { data, error, loading } = useLaunchListQuery();
 7
 8  if (loading) {
 9    return <div>Loading...</div>;
10  }
11
12  if (error || !data) {
13    return <div>ERROR</div>;
14  }
15
16  return <LaunchList data={data} />;
17};
18
19export default LaunchListContainer;

表示组件将用 data 对象来构建 UI。我们用 <ol> 创建一个有序列表,然后通过映射来显示 mission_name 和launch_year。

src/components/LaunchList/LaunchList.tsx将如下所示:


 1import * as React from 'react';
 2import { LaunchListQuery } from '../../generated/graphql';
 3import './styles.css';
 4
 5interface Props {
 6  data: LaunchListQuery;
 7}
 8
 9const className = 'LaunchList';
10
11const LaunchList: React.FC<Props> = ({ data }) => (
12  <div className={className}>
13    <h3>Launches</h3>
14    <ol className={`${className}__list`}>
15      {!!data.launches &&
16        data.launches.map(
17          (launch, i) =>
18            !!launch && (
19              <li key={i} className={`${className}__item`}>
20                {launch.mission_name} ({launch.launch_year})
21              </li>
22            ),
23        )}
24    </ol>
25  </div>
26);
27
28export default LaunchList;

如果你使用的是 VS Code,IntelliSense 将向你显示可用的值,并提供自动完成列表,因为我们使用的是TypeScript。如果我们使用的数据是 null 或 undefined,它也会警告我们。

VS代码中自动完成的值列表 真是太棒了!编辑将帮助我们进行编码。此外,如果你需要一个类型或函数的定义,可以通过 Cmd + t 快捷键,或用鼠标悬停在它上面,这样会给出所有的细节。

另外还需要添加一些 CSS 样式,它将显示我们的项目,并允许它们在列表高度不够时滚动。在 src/components/LaunchList/styles.css 里,添加以下代码:


 1.LaunchList {
 2  height: 100vh;
 3  overflow: hidden auto;
 4  background-color: #ececec;
 5  width: 300px;
 6  padding-left: 20px;
 7  padding-right: 20px;
 8}
 9
10.LaunchList__list {
11  list-style: none;
12  margin: 0;
13  padding: 0;
14}
15
16.LaunchList__item {
17  padding-top: 20px;
18  padding-bottom: 20px;
19  border-top: 1px solid #919191;
20  cursor: pointer;
21}

为了显示有关发射任务的更多详细信息,还要构建我们的 profile 组件。除了 Profile 查询和组件之外,该组件的代码与 index.tsx 文件大致相同。我们还将一个变量传递给 React 钩子,用于启动时的 id。现在先把它硬编码为42,然后在完成程序布局之后再添加动态功能。

在 src/components/LaunchProfile/index.tsx 中添加以下代码:


 1import * as React from 'react';
 2import { useLaunchProfileQuery } from '../../generated/graphql';
 3import LaunchProfile from './LaunchProfile';
 4
 5const LaunchProfileContainer = () => {
 6  const { data, error, loading } = useLaunchProfileQuery({ variables: { id: '42' } });
 7
 8  if (loading) {
 9    return <div>Loading...</div>;
10  }
11
12  if (error) {
13    return <div>ERROR</div>;
14  }
15
16  if (!data) {
17    return <div>Select a flight from the panel</div>;
18  }
19
20  return <LaunchProfile data={data} />;
21};
22
23export default LaunchProfileContainer;

现在需要创建我们的演示组件。它将在 UI 顶部显示发射任务的名称和详细信息,然后在描述下方显示发射时的照片。

src/components/LaunchProfile/LaunchProfile.tsx 组件如下所示:


 1import * as React from 'react';
 2import { LaunchProfileQuery } from '../../generated/graphql';
 3import './styles.css';
 4
 5interface Props {
 6  data: LaunchProfileQuery;
 7}
 8
 9const className = 'LaunchProfile';
10
11const LaunchProfile: React.FC<Props> = ({ data }) => {
12  if (!data.launch) {
13    return <div>No launch available</div>;
14  }
15
16  return (
17    <div className={className}>
18      <div className={`${className}__status`}>
19        <span>Flight {data.launch.flight_number}: </span>
20        {data.launch.launch_success ? (
21          <span className={`${className}__success`}>Success</span>
22        ) : (
23          <span className={`${className}__failed`}>Failed</span>
24        )}
25      </div>
26      
27        {data.launch.mission_name}
28        {data.launch.rocket &&
29          ` (${data.launch.rocket.rocket_name} | ${data.launch.rocket.rocket_type})`}
30      
31      <p className={`${className}__description`}>{data.launch.details}</p>
32      {!!data.launch.links && !!data.launch.links.flickr_images && (
33        <div className={`${className}__image-list`}>
34          {data.launch.links.flickr_images.map(image =>
35            image ? <img src={image} className={`${className}__image`} key={image} /> : null,
36          )}
37        </div>
38      )}
39    </div>
40  );
41};
42
43export default LaunchProfile;

最后一步是用 CSS 设置这个组件的样式。将以下内容添加到 src/components/LaunchProfile/styles.css 文件中:


 1.LaunchProfile {
 2  height: 100vh;
 3  max-height: 100%;
 4  width: calc(100vw - 300px);
 5  overflow: hidden auto;
 6  padding-left: 20px;
 7  padding-right: 20px;
 8}
 9
10.LaunchProfile__status {
11  margin-top: 40px;
12}
13
14.LaunchProfile__title {
15  margin-top: 0;
16  margin-bottom: 4px;
17}
18
19.LaunchProfile__success {
20  color: #2cb84b;
21}
22
23.LaunchProfile__failed {
24  color: #ff695e;
25}
26
27.LaunchProfile__image-list {
28  display: grid;
29  grid-gap: 20px;
30  grid-template-columns: repeat(2, 1fr);
31  margin-top: 40px;
32  padding-bottom: 100px;
33}
34
35.LaunchProfile__image {
36  width: 100%;
37}

现在完成了组件的静态版本,可以在 UI 中查看它们。我们将在 src/App.tsx 文件中包含这些组件,并将 <App /> 转换为函数组件。用函数组件使其更加简单,并允许我们在添加单击功能时使用钩子。


 1import React from 'react';
 2import LaunchList from './components/LaunchList';
 3import LaunchProfile from './components/LaunchProfile';
 4
 5import './App.css';
 6
 7const App = () => {
 8  return (
 9    <div className="App">
10      <LaunchList />
11      <LaunchProfile />
12    </div>
13  );
14};
15
16export default App;

为了得到我们想要的样式,将 src/App.css 改为以下内容:


1.App {
2  display: flex;
3  width: 100vw;
4  height: 100vh;
5  overflow: hidden;
6}

在终端执行 yarn start,然后在你的浏览器中打开 http://localhost:3000,你应该看到自己程序最基本的版本!

添加用户交互

现在需要添加当用户点击面板中的项目时获取完整发射数据的功能。我们将在 App 组件中创建一个钩子来跟踪班次 ID 并将其传递给 LaunchProfile 组件以重新获取发射数据。

在 src/App.tsx 中,我们将添加 useState 来维护和更新 ID 的状态。当用户从列表中进行选择时,我们还将使用名为 handleIdChange 的 useCallback 作为点击 handler 来更新ID。我们需要将 id 传递给 LaunchProfile,然后将 handleIdChange 传递给 <LaunchList />。

更新后的 <App/> 组件应如下所示:


 1const App = () => {
 2  const [id, setId] = React.useState(42);
 3  const handleIdChange = React.useCallback(newId => {
 4    setId(newId);
 5  }, []);
 6
 7  return (
 8    <div className="App">
 9      <LaunchList handleIdChange={handleIdChange} />
10      <LaunchProfile id={id} />
11    </div>
12  );
13};

在 LaunchList.tsx 组件中,我们需要为 handleIdChange 创建一个类型并将其添加到 props 的解构中 。然后,在 <li> 班次项目中的 onClick 回调中执行该函数。


 1export interface OwnProps {
 2  handleIdChange: (newId: number) => void;
 3}
 4
 5interface Props extends OwnProps {
 6  data: LaunchListQuery;
 7}
 8
 9// ...
10const LaunchList: React.FC<Props> = ({ data, handleIdChange }) => (
11
12// ...
13<li
14  key={i}
15  className={`${className}__item`}
16  onClick={() => handleIdChange(launch.flight_number!)}
17>

在 LaunchList/index.tsx 里面,一定要导入 OwnProps 声明来输入传递给容器组件的 props,然后将 props 传播到 <LaunchList data={data} {...props} />。

最后一步是在 id 改变时 refetch 数据。在 LaunchList/index.tsx 文件中,我们将用 useEffect 来管理 React 生命周期,并在 id 更改时触发提取。以下是实现提取所需做的唯一更改:


 1interface OwnProps {
 2  id: number;
 3}
 4
 5const LaunchProfileContainer = ({ id }: OwnProps) => {
 6  const { data, error, loading, refetch } = useLaunchProfileQuery({
 7    variables: { id: String(id) },
 8  });
 9  React.useEffect(() => {
10    refetch();
11  }, [id]);

由于我们已经把表示与数据分开,因此不需要对 <LaunchProfile /> 组件进行任何更新,只需更新 index.tsx 文件即可,这样在选定的 flight_number 更改时能够重新获取完整的发射数据。

好了,如果你按照以上步骤进行操作,现在就应该有了一个功能齐全的 GraphQL 程序了。如果你对什么地方还不清楚,可以在源代码中(https://github.com/treyhuffine/graphql-react-typescript-spacex)找到一个可行的解决方案。

总结

我们可以看到一旦配置好了程序之后,开发速度是非常快的。我们可以轻松构建数据驱动的 UI。 GraphQL 允许我们在组件中定义所需要的数据,并且可以无缝地将其用于组件中的 props。生成的 TypeScript 定义使我们编写的代码具有极高的稳定性。

如果你希望深入了解该项目,接下来的步骤将是使用 API 中的其他字段添加分页和更多的数据关联。要对发射任务列表进行分页,你将获取当前列表的长度并将 offset 变量传递给 LaunchList 查询。

我鼓励你更深入探索并编写自己的查询,以便巩固这些概念。

原文:https://blog.logrocket.com/build-a-graphql-react-app-with-typescript/