翻译:疯狂的技术宅https://blog.logrocket.com/bu...
未经允许,严禁转载
近一两年来 GraphQL 和 TypeScript 的使用都程爆发式增长,当两者与React结合使用时,它们可以为开发人员提供理想的开发体验。
GraphQL 改变了我们对 API 的思考方式,并利用直观的键/值对匹配,客户端可以请求在网页或移动应用屏幕上显示所需的确切数据。 TypeScript 通过为变量添加静态类型来扩展 JavaScript,从而减少了错误并提高了代码的可读性。
本文将引导你使用 React 和 Apollo 构建客户端应用程序,并调用 SpaceX 的公共 GraphQL API ,来显示有关的发射信息。我们将自动为查询生成 TypeScript 类型,并使用 React Hooks 执行这些查询。
本文假设你对 React,GraphQL 和 TypeScript 有一定的了解,并且正在研究怎样通过把它们集成在一起来构建一个正常运行的程序。如果你需要补充一些基础知识的话,可以关注公众号“前端先锋”。
如果你在学习的过程中遇到困难,可以参考源代码或查看 live app。
为什么选择 GraphQL + TypeScript?
GraphQL API 需要强类型化,数据从单个端点提供。通过在此端点上调用 GET 请求,客户端可以接收后端的完全自我描述的数据,包括所有可用的数据和相应的类型。
通过 GraphQL 代码生成器,我们可以扫描 Web 应用目录中的查询文件,并将它们与 GraphQL API 提供的信息进行匹配,这样以来就可以创建 TypeScript 类型所有请求数据。通过使用 GraphQL,我们可以自动且自由地输入我们的 React 组件的属性。这样可以减少产品上的错误并提高迭代速度。
入门
我们将使用带有 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
文件。
切换到 app 目录:
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
在 React Hook 中包装了 Apollo 查询, 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
设置 codegen 配置执行以下命令:
$(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 查询。你的配置文件应如下所示:
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 出现什么问题,我们需要用 console.log 配合来调试数据。
GraphQL 允许你通过访问 URL 查看完全定义的模式,并在 UI 中执行针对它的请求,从而解决了这个问题。现在访问 https://spacexdata.herokuapp.... 查看你将使用的确切数据。
虽然我们可以获得大量的 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
字符串中。
import gql from 'graphql-tag';
export const QUERY_LAUNCH_LIST = gql`
query LaunchList {
launches {
flight_number
mission_name
launch_year
}
}
`;
其他查询将根据 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 上粘贴前面的查询。
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 接口和类型的 Hook。在终端中执行:
yarn 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,然后将 <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
钩子的函数组件。查询钩子返回 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,IntelliSense 将向你显示可用的值,并提供自动完成列表,因为我们使用的是TypeScript。如果我们使用的数据是 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;
}
为了显示有关发射任务的更多详细信息,还要构建我们的 profile 组件。除了 Profile
查询和组件之外,该组件的代码与 index.tsx
文件大致相同。我们还将一个变量传递给 React 钩子,用于启动时的 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;
现在需要创建我们的演示组件。它将在 UI 顶部显示发射任务的名称和详细信息,然后在描述下方显示发射时的照片。
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 />
转换为函数组件。用函数组件使其更加简单,并允许我们在添加单击功能时使用钩子。
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
组件中创建一个钩子来跟踪班次 ID 并将其传递给 LaunchProfile
组件以重新获取发射数据。
在 src/App.tsx
中,我们将添加 useState
来维护和更新 ID 的状态。当用户从列表中进行选择时,我们还将使用名为 handleIdChange
的 useCallback
作为点击 handler 来更新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
数据。在 LaunchList/index.tsx
文件中,我们将用 useEffect
来管理 React 生命周期,并在 id
更改时触发提取。以下是实现提取所需做的唯一更改:
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
查询。
我鼓励你更深入探索并编写自己的查询,以便巩固这些概念。
本文首发微信公众号:前端先锋
欢迎扫描二维码关注公众号,每天都给你推送新鲜的前端技术文章
欢迎继续阅读本专栏其它高赞文章:
- 深入理解Shadow DOM v1
- 一步步教你用 WebVR 实现虚拟现实游戏
- 13个帮你提高开发效率的现代CSS框架
- 快速上手BootstrapVue
- JavaScript引擎是如何工作的?从调用栈到Promise你需要知道的一切
- WebSocket实战:在 Node 和 React 之间进行实时通信
- 关于 Git 的 20 个面试题
- 深入解析 Node.js 的 console.log
- Node.js 究竟是什么?
- 30分钟用Node.js构建一个API服务器
- Javascript的对象拷贝
- 程序员30岁前月薪达不到30K,该何去何从
- 14个最好的 JavaScript 数据可视化库
- 8 个给前端的顶级 VS Code 扩展插件
- Node.js 多线程完全指南
- 把HTML转成PDF的4个方案及实现
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。