用TS+GraphQL查询SpaceX火箭发射数据[每日前端夜话0x81]
近一两年来 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/)。
为什么选择 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向导,并执行以下步骤:
-
要用 React 构建的程序。
-
schema 位于 https://spacexdata.herokuapp.com/graphql。
-
将你的操作和代码位置设置为
./src/components/*/.{ts,tsx}
,以便它能够搜索到所有的 TypeScript 文件以进行查询声明。 -
使用默认插件 “TypeScript”,“TypeScript Operations”,“TypeScript React Apollo”。
-
将生成的目标文件夹更新为
src/generated/graphql.tsx
(react-apollo 插件需要 .tsx)。 -
不要生成 introspection file。
-
使用默认的
codegen.yml
文件。 -
制作你的运行脚本
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 数据,但我们只会显示有关发射任务的信息。我们有两个主要组成部分:
-
用户可以通过单击“发射”任务列表来查看有关它们的更多信息。
-
单个发射任务的详细资料。
对于第一个组件,我们将查询 launchs
并请求 flight_number
, mission_name
和 launch_year
。我们将在列表中显示这些数据,当用户点击其中一个项目时,查询 launch
来获取该火箭的更多数据。让我们在 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
作为变量,它对应于 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,然后将 组件包装在上下文提供程序中。你的索引文件应如下所示:
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 15 16 17 18 , 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 returnLoading...; 10 } 11 12 if (error || !data) { 13 returnERROR; 14 } 15 16 return ; 17}; 18 19export default LaunchListContainer;
表示组件将用 data
对象来构建 UI。我们用
- 创建一个有序列表,然后通过映射来显示
- 20 {launch.mission_name} ({launch.launch_year}) 21 22 ), 23 )} 24
- 班次项目中的
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 = ({ data, handleIdChange }) => ( 11 12// ... 13
- handleIdChange(launch.flight_number!)}
17>
在
LaunchList/index.tsx
里面,一定要导入OwnProps
声明来输入传递给容器组件的props
,然后将 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]);
由于我们已经把表示与数据分开,因此不需要对
组件进行任何更新,只需更新
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/
图书推荐
下面夹杂一些私货:也许你和高薪之间只差这一张图
2019年京程一灯课程体系上新,这是我们第一次将全部课程列表对外开放。
愿你有个好前程,愿你月薪30K。我们是认真的 !
在公众号内回复“体系”查看高清大图
长按二维码,加大鹏老师微信好友
拉你加入前端技术交流群
唠一唠怎样才能拿高薪
往期精选
小手一抖,资料全有。长按二维码关注 前端先锋 ,阅读更多技术文章和业界动态。
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 = ({ data }) => ( 121326); 27 28export default LaunchList;Launches
1415 {!!data.launches && 16 data.launches.map( 17 (launch, i) => 18 !!launch && ( 19
25
如果你使用的是 VS Code,IntelliSense 将向你显示可用的值,并提供自动完成列表,因为我们使用的是TypeScript。如果我们使用的数据是 null
或 undefined
,它也会警告我们。
真是太棒了!编辑将帮助我们进行编码。此外,如果你需要一个类型或函数的定义,可以通过 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 returnLoading...; 10 } 11 12 if (error) { 13 returnERROR; 14 } 15 16 if (!data) { 17 returnSelect a flight from the panel; 18 } 19 20 return ; 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 = ({ data }) => { 12 if (!data.launch) { 13 returnNo launch available; 14 } 15 16 return ( 171840 ); 41}; 42 43export default LaunchProfile;19 Flight {data.launch.flight_number}: 20 {data.launch.launch_success ? ( 21 Success 22 ) : ( 23 Failed 24 )} 252627 {data.launch.mission_name} 28 {data.launch.rocket && 29 ` (${data.launch.rocket.rocket_name} | ${data.launch.rocket.rocket_type})`} 30
31{data.launch.details} 32 {!!data.launch.links && !!data.launch.links.flickr_images && ( 33
34 {data.launch.links.flickr_images.map(image => 35 image ? : null, 36 )} 3738 )} 39
最后一步是用 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
文件中包含这些组件,并将 转换为函数组件。用函数组件使其更加简单,并允许我们在添加单击功能时使用钩子。
1import React from 'react'; 2import LaunchList from './components/LaunchList'; 3import LaunchProfile from './components/LaunchProfile'; 4 5import './App.css'; 6 7const App = () => { 8 return ( 910 11 1213 ); 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
传递给 。
更新后的 组件应如下所示:
1const App = () => { 2 const [id, setId] = React.useState(42); 3 const handleIdChange = React.useCallback(newId => { 4 setId(newId); 5 }, []); 6 7 return ( 89 10 1112 ); 13};
在 LaunchList.tsx
组件中,我们需要为 handleIdChange
创建一个类型并将其添加到 props 的解构中 。然后,在