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嚮導。請執行以下步驟:
- 使用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字符串中。
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