如何使用 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 来引导我们的应用程序。执行以下命令来初始化你的应用:
复制代码
npxcreate-react-appgraphql-typescript-react--typescript //NOTE-youwillneedNodev8.10.0+andNPMv5.2+
使用–typescript 标志,CRA 将生成你的文件以及.ts 和.tsx,并将创建一个 tsconfig.json 文件。
导航到应用目录:
复制代码
cdgraphql-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-taggraphql
graphql-code-generator 用于自动执行我们的 TypeScript 工作流程。我们将安装 codegen CLI 以生成所需的配置和插件。
复制代码
yarnadd-D@graphql-codegen/cli
执行以下命令来设置代码生成配置:
复制代码
$(npmbin)/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 中的插件。
我们还将对 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 数据可供使用,但我们仅显示有关火箭发射的信息。我们将有两个主要组件:
- 一个 launches 列表,用户可以单击列表以了解有关发射的更多信息。
- 单次 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 字符串中。
复制代码
importgqlfrom'graphql-tag'; exportconstQUERY_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 文件中,我们从游乐场粘贴查询。
复制代码
importgqlfrom'graphql-tag'; exportconstQUERY_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,然后将 组件包装在上下文提供程序中。你的索引文件应如下所示:
复制代码
importReactfrom'react'; importReactDOMfrom'react-dom'; importApolloClientfrom'apollo-boost'; import{ ApolloProvider }from'react-apollo'; import{ ApolloProviderasApolloHooksProvider }from'react-apollo-hooks'; import'./index.css'; importAppfrom'./App'; constclient =newApolloClient({ uri:'https://spacexdata.herokuapp.com/graphql', }); ReactDOM.render( , document.getElementById('root'), );
构建我们的组件
现在我们已经准备好了通过 Apollo 执行 GraphQL 查询所需的一切内容。
在 src/components/LaunchList/index.tsx 内,我们将创建一个函数组件,其使用生成的 useLaunchListQuery hook。查询 hooks 返回 data、loading 和 error 值。我们将检查容器组件中的 loading 和 error,并将 data 传递给我们的演示组件。
我们将此组件用作一个容器 / 智能组件,从而保持关注点的分离;我们还将数据传递给表示 / 哑组件,该组件仅显示给出的内容。我们还将在等待数据时显示基本的加载和错误状态。
你的容器组件应如下所示:
复制代码
import*asReactfrom'react'; import{ useLaunchListQuery }from'../../generated/graphql'; importLaunchListfrom'./LaunchList'; constLaunchListContainer =()=>{ const{ data, error, loading } = useLaunchListQuery(); if(loading) { returnLoading...; } if(error || !data) { returnERROR; } return; }; export default LaunchListContainer;
我们的演示组件将使用我们的类型化 data 对象来构建 UI。我们使用
- 创建一个有序列表,然后映射到发射信息中,以显示 mission_name 和 launch_year。
我们的 src/components/LaunchList/LaunchList.tsx 将如下所示:
复制代码
import*asReactfrom'react'; import{ LaunchListQuery }from'../../generated/graphql'; import'./styles.css'; interface Props { data: LaunchListQuery; } constclassName ='LaunchList'; constLaunchList: React.FC =({ data }) =>(Launches
{!!data.launches && data.launches.map( (launch, i) => !!launch && ( {launch.mission_name} ({launch.launch_year}) ), )}
); exportdefaultLaunchList;
如果你使用的是 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:1pxsolid#919191; cursor: pointer; }
现在我们将构建配置组件,以显示有关火箭发射的更多详细信息。该组件的 index.tsx 文件基本是一样的,只是我们使用的是 Profile 查询和组件。我们还将一个变量传递给我们的 React hook 以获取发射 ID。目前我们将其硬编码为’42’,然后在布局好应用后添加动态功能。
在 src/components/LaunchProfile/index.tsx 内添加以下代码:
复制代码
import*asReactfrom'react'; import{ useLaunchProfileQuery }from'../../generated/graphql'; importLaunchProfilefrom'./LaunchProfile'; constLaunchProfileContainer =()=>{ const{ data, error, loading } = useLaunchProfileQuery( {variables: {id:'42'} } ); if(loading) { returnLoading...; } if(error) { returnERROR; } if(!data) { returnSelect a flight from the panel; } return; }; export default LaunchProfileContainer;
现在我们需要创建演示组件。它将在用户界面顶部显示火箭发射的名称和详细信息,然后在说明下方显示一个发射图像网格。
src/components/LaunchProfile/LaunchProfile.tsx 组件如下所示:
复制代码
import*asReactfrom'react'; import{ LaunchProfileQuery }from'../../generated/graphql'; import'./styles.css'; interface Props { data: LaunchProfileQuery; } constclassName ='LaunchProfile'; constLaunchProfile: React.FC =({ data }) =>{ if(!data.launch) { returnNo launch available; } return( Flight {data.launch.flight_number}: {data.launch.launch_success ? ( Success ) : ( Failed )}
最后一步是使用 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。
复制代码
importReactfrom'react'; importLaunchListfrom'./components/LaunchList'; importLaunchProfilefrom'./components/LaunchProfile'; import'./App.css'; constApp =()=>{ return(