如何使用 GraphQL 构建 TypeScript+React 应用

Posted 前端之巅

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了如何使用 GraphQL 构建 TypeScript+React 应用相关的知识,希望对你有一定的参考价值。

作者 | Trey Huffine
译者 | 王强
GraphQL 和 TypeScript 的使用率都在爆炸式增长,并且当两者与 React 结合应用时,它们在一起可以创造理想的开发体验。本文将引导你使用公共的 SpaceX GraphQL API,使用 React 和 Apollo 构建一个客户端应用程序,展示有关火箭发射的信息。

GraphQL 和 TypeScript 的使用率都在爆炸式增长,并且当两者与 React 结合应用时,它们在一起可以创造理想的开发体验。

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

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

假定你对 React、GraphQL 和 TypeScript 有所了解,我们将重点介绍如何将它们集成在一起以构建一个正常运作的应用程序。

如果你在哪里卡住了,可以参考源代码 [1] 或查看应用的演示 [2]。

​如何使用 GraphQL 构建 TypeScript+React 应用

为什么选择 GraphQL+TypeScript?

GraphQL API 需要被强类型化,并且从单个端点提供数据。客户端在此端点上调用一个 GET 请求,就可以接收一个后端的完全自注释的表示,以及所有可用数据和相应的类型。

我们可以使用 GraphQL Code Generator[3] 在 Web 应用目录中扫描查询文件,并将它们与 GraphQL API 提供的信息匹配,从而为所有请求数据创建 TypeScript 类型。使用 GraphQL,我们可以免费自动输入 React 组件的 props。这样可以减少错误,并加快产品迭代速度。

开始工作
我们将使用带有 TypeScript 设置的 create-react-app 来引导我们的应用程序。执行以下命令来初始化你的应用:
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。

apollo-boost 包含查询 API 和在内存中本地缓存数据所需的工具;react-apollo 为 React 提供绑定;react-apollo-hooks 将 Apollo 查询包装在一个 React Hook 中;graphql-tag 用于构建我们的查询文档;graphql 是一个对等依赖项,提供了 GraphQL 实现的详细信息。
yarn add apollo-boost react-apollo react-apollo-hooks graphql-tag graphql
graphql-code-generator 用于自动执行我们的 TypeScript 工作流程。我们将安装 codegen CLI 以生成所需的配置和插件。
yarn add -D @graphql-codegen/cli
执行以下命令来设置代码生成配置:
$(npm bin)/graphql-codegen init

这将启动 CLI 向导。请执行以下步骤:

  1. 使用 React 构建应用程序。
  2. Schema 位于 https://spacexdata.herokuapp.com/graphql。
  3. 将你的操作和分片(fragments)位置设置为./src/components/**/*.{ts,tsx}这样它将在我们所有的 TypeScript 文件中搜索查询声明。
  4. 使用默认插件“TypeScript”“TypeScript Operations”“TypeScript React Apollo”。
  5. 使用目标 src/Generated/graphql.tsx(react-apollo 插件需要.tsx)。
  6. 不要生成内省文件。
  7. 使用默认的 codegen.yml 文件。
  8. 运行脚本是 codegen。

现在,在 CLI 中运行 yarn 命令,安装 CLI 工具添加到 package.json 中的插件。

我们还将对 codegen.yml 文件进行一次更新,这样它还将添加 withHooks: true 配置选项来生成类型化的 React Hook 查询。你的配置文件应如下所示:
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 查询并生成类型

GraphQL 的一大好处是它使用了声明性数据获取。我们能够编写出一些与使用它们的组件并存的查询,并且 UI 能够准确地请求它需要渲染的内容。

使用 REST API 时,我们需要查找处于(或不处于)最新状态的文档。如果 REST 出现任何问题,我们需要针对 API 和 console.log 结果发起请求以调试数据。

GraphQL 允许你在 UI 中访问 URL,查看完全定义的 schema 并针对它执行请求,从而解决了这个问题。请查看要使用的数据 [4]。

​如何使用 GraphQL 构建 TypeScript+React 应用

尽管我们有大量的 SpaceX 数据可供使用,但我们仅显示有关火箭发射的信息。我们将有两个主要组件:

  1. 一个 launches 列表,用户可以单击列表以了解有关发射的更多信息。
  2. 单次 launch 的详细资料。

对于第一个组件,我们将查询 launches 键,并请求 flight_number、mission_name 和 launch_year。我们将这些数据显示在一个列表中,当用户单击其中一个项目时,我们将根据 launch 键查询关于这次火箭发射的更大数据集。下面我们在 GraphQL 游乐场中测试我们的第一个查询。

​如何使用 GraphQL 构建 TypeScript+React 应用

要编写查询时,我们首先创建一个 src/components 文件夹,然后创建一个 src/components/LaunchList 文件夹。在此文件夹中,创建 index.tsx、LaunchList.tsx、query.ts 和 styles.css 文件。在 query.ts 文件中,我们可以从游乐场传输查询并将其放在一个 gql 字符串中。
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。

​如何使用 GraphQL 构建 TypeScript+React 应用

我们将 id 作为一个变量传递,该变量对应于 LaunchList 查询中的 flight_number。LaunchProfile 查询还将包含嵌套的对象 / 类型,在这里我们可以在方括号内指定键来获取值。

例如,发射信息包含了一个 rocket 定义(LaunchRocket 类型),我们将要求它提供 rocket_name 和 rocket_type。要了解更多可用于 LaunchRocket 的字段信息,你可以使用侧边的 schema 导航器来了解可用数据。

现在将这个查询转移到我们的应用程序中。使用 index.tsx、LaunchProfile.tsx、query.ts 和 styles.css 文件创建 src/components/LaunchProfile 文件夹。在 query.ts 文件中,我们从游乐场粘贴查询。
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
      }
    }
  }
`;
现在我们已经定义了查询,你终于可以生成 TypeScript 接口和类型化的 Hooks。在你的终端中执行:
yarn codegen

在 src/generation/graphql.ts 内部,你将找到定义应用程序所需的所有类型,以及用于获取 GraphQL 端点以检索该数据的对应查询。

这个文件通常会很大,但是充满了有价值的信息。我建议花些时间浏览一下,并了解我们的 codegen 完全基于 GraphQL schema 所创建的所有类型。

比如说检查 type Launch,它是 GraphQL 的 Launch 对象的 TypeScript 表示形式,我们会在游乐场上与之交互。还可以滚动到文件的底部,查看专门为我们将要执行的查询生成的代码——它已创建了组件、HOC、类型化的 props/ 查询和类型化的 hooks。

初始化 Apollo 客户端

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

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

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。

我们的 src/components/LaunchList/LaunchList.tsx 将如下所示:
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,鼠标指针悬停其上,它将为你提供所有详细信息。

我们还将添加一些 CSS 样式,这些样式将显示我们的项目并允许它们在列表溢出时滚动。在 src/components/LaunchList/styles.css 中添加以下代码:
.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',然后在布局好应用后添加动态功能。

在 src/components/LaunchProfile/index.tsx 内添加以下代码:
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;

现在我们需要创建演示组件。它将在用户界面顶部显示火箭发射的名称和详细信息,然后在说明下方显示一个发射图像网格。

src/components/LaunchProfile/LaunchProfile.tsx 组件如下所示:
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;
最后一步是使用 CSS 设置此组件的样式。将以下内容添加到你的 src/components/LaunchProfile/styles.css 文件中:
.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%;
}
现在我们已经完成了组件的静态版本,我们可以在 UI 中查看它们。我们会将组件包含在 src/App.tsx 文件中,还会将 转换为一个函数组件。我们使用函数组件来简化代码,并在添加单击功能时允许使用 hooks。
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;
为了获得想要的样式,我们将 src/App.css 更改为以下内容:
.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 传递给 < LaunchList />。

更新后的<App /> 组件现在应如下所示:
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>
  );
};
在 LaunchList.tsx 组件内部,我们需要为 handleIdChange 创建一个类型并将其添加到 props 解构中。然后在<li> 火箭项目上 我们将在 onClick 回调中执行该函 数。
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} />中。

最后一步是在 id 更改时 refetch 数据。在 LaunchProfile/index.tsx 文件中,我们将使用 useEffect 来管理 React 的生命周期,并在 id 更改时触发一个 fetch。以下是实现 fetch 所需的唯一更改:
interface OwnProps {
  id: number;
}
const LaunchProfileContainer = ({ id }: OwnProps) => {
  const { data, error, loading, refetch } = useLaunchProfileQuery({
    variables: { id: String(id) },
  });
  React.useEffect(() => {
    refetch();
  }, [id]);
由于我们已将演示与数据分离,因此无需对<LaunchProfile /> 组件进行任何更新;我们只需要更新 index.tsx 文件,以便所选的 flight_number 在更改时重新获取完整的火箭发射相关数据。
现在你已经完成了它!如果按照这些步骤操作,应该能做出来一个功能齐全的 GraphQL 应用。如果你迷路了,可以在源代码中找到可行的解决方案。
   小结   

配置好应用后,我们可以看到开发速度是非常快的。我们可以轻松构建数据驱动的 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 应用

如何使用 GraphQL 构建经过身份验证的查询?

如何使用 api-platform 构建 GraphQL API?

使用Vue和GraphQL构建一个CRUD APP

如何使用 GraphQL + parse.com 构建 Web 应用程序? [关闭]

graphql-java:如何在以编程方式生成 graphql 模式时添加自定义标量类型?