9月7日-8日,相约 2023 腾讯全球数字生态大会!聚焦产业未来发展新趋势! 了解详情
写点什么

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

  • 2019-12-23
  • 本文字数:8088 字

    阅读完需:约 27 分钟

如何使用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: trueschema: '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变量。我们还可以在游乐场上用变量测试查询。


在查询名称旁边指定变量,前面带上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


活动推荐:

2023年9月3-5日,「QCon全球软件开发大会·北京站」 将在北京•富力万丽酒店举办。此次大会以「启航·AIGC软件工程变革」为主题,策划了大前端融合提效、大模型应用落地、面向 AI 的存储、AIGC 浪潮下的研发效能提升、LLMOps、异构算力、微服务架构治理、业务安全技术、构建未来软件的编程语言、FinOps 等近30个精彩专题。咨询购票可联系票务经理 18514549229(微信同手机号)。

2019-12-23 18:413662

评论 1 条评论

发布
用户头像
都9102年了为什么react+graphql的文章还是能到首页?
2019-12-24 16:10
回复
没有更多了
发现更多内容

Java注解-一文就懂,mysql注入攻击原理

Java 程序员 后端

Java的四大面向对象编程概念,程序员必须要了解的知识点

Java 程序员 后端

Java并发(三),java程序设计教程雍俊海第三版答案

Java 程序员 后端

Java应用日志如何与Jaeger的trace关联,腾讯、字节跳动面经已发

Java 程序员 后端

java教程——泛型,java零基础教学视频

Java 程序员 后端

Java程序员经典面试题集大全 (二),最全SpringBoot学习教程

Java 程序员 后端

Java开发必备 Git 分支开发:规范指南及完全学会Git的24堂课笔记

Java 程序员 后端

Java模板工具类实现解析简单的表达式,kafka实战项目

Java 程序员 后端

java注解解析,10天拿到字节跳动Java岗位offer

Java 程序员 后端

Java程序员拿80W年薪很难吗?这套阿里P7的进阶路线让我惭愧了

Java 程序员 后端

Java并发关键字-final,36套java架构师百度云

Java 程序员 后端

Java日志体系(二) log4j 配置文件详解 缓存问题,mybatis基本工作原理

Java 程序员 后端

Java状态模式(State),马哥linux2020全套视频下载

Java 程序员 后端

Java并发(五),大厂程序员35岁后的职业出路在哪

Java 程序员 后端

Java框架总结,mysql教程视频

Java 程序员 后端

Java的Io模型你了解多少?RPC的通信Netty-Netty的底层是Nio-

Java 程序员 后端

Java注解(1),headfirstjavapdf百度云

Java 程序员 后端

Java流程控制语句-分支结构(选择结构),java技术专家面试题

Java 程序员 后端

Java的新未来:逐渐“Kotlin化,java接口菜鸟教程

Java 程序员 后端

java教程——线程,整合springboot集成实现动态刷新配置

Java 程序员 后端

java整理,springboot2精髓百度云

Java 程序员 后端

Java毕设项目-校园失物招领管理系统,HR的话扎心了

Java 程序员 后端

Java并发(二),redis深度笔记

Java 程序员 后端

Java开发五年裸辞美团,八个月后跳槽阿里涨薪20w!,大学java课程视频

Java 程序员 后端

Java数组的拷贝 优化冒泡排序 二分查找,神策数据java面试题

Java 程序员 后端

Java最新高频大厂面试集锦(附答案),看完老板哭着让我留下来

Java 程序员 后端

Java注解,mysql教程入门到精通

Java 程序员 后端

Java市场饱和了吗?现在转行学习Java会不会太晚了?,linux操作系统基础

Java 程序员 后端

Java并发包源码学习系列:LBD双端阻塞队列源码解析,linux内核架构与底层原理

Java 程序员 后端

Java并发工具AbstractQueuedSynchronizer实现详解,如何保证高可用

Java 程序员 后端

Java开发两年备战金三银四:多线程+IO,zookeeper面试题总结

Java 程序员 后端

  • 扫码添加小助手
    领取最新资料包
如何使用GraphQL构建TypeScript+React应用_语言 & 开发_Trey Huffine_InfoQ精选文章