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

GraphQL改变了我们对API的思考方式。在GraphQL迅猛发展的今天,本文将使用GraphQL构建一个客户端应用程序,通过实例介绍如何将React、GraphQL和TypeScript集成在一起以构建一个应用程序。——本文将引导你使用公共的SpaceX GraphQL API,使用React和Apollo构建一个客户端应用程序,展示有关火箭发射的信息。

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

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

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

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

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

为什么选择GraphQL+TypeScript?

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

我们可以使用GraphQL Code Generator(https://github.com/dotansimha/graphql-code-generator)在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并针对它执行请求,从而解决了这个问题。请访问https://spacexdata.herokuapp.com/graphql,查看要使用的数据。

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

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

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

要编写查询时,我们首先创建一个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。

我们将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文件中,还会将< App />转换为一个函数组件。我们使用函数组件来简化代码,并在添加单击功能时允许使用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查询。

我鼓励你更深入地研究它并编写自己的查询,以巩固本文提出的概念。

原文链接https://levelup.gitconnected.com/build-a-graphql-react-app-with-typescript-9661f908b26

  • 发表于:
  • 本文为 InfoQ 中文站特供稿件
  • 首发地址https://www.infoq.cn/article/3UgYciBMWcudepLp42mW

扫码关注云+社区

领取腾讯云代金券