翻译 | 《JavaScript Everywhere》第14章 应用Apollo Client

写在最后面

大家好呀,我是毛小悠,是一位前端开发工程师。正在翻译一本英文技术书籍。

为了进步大家的浏览体验,对语句的构造和内容略有调整。如果发现本文中有存在瑕疵的中央,或者你有任何意见或者倡议,能够在评论区留言,或者加我的微信:code_maomao,欢送互相沟通交流学习。

(゚∀゚)..:*☆哎哟不错哦

第14章 应用Apollo Client

我分明地记得我的第一个互联网连贯。我用计算机的调制解调器拨号上网,而后我能够收费上网。过后感觉如此神奇,与咱们明天应用的始终在线的连贯相距甚远。
流程如下所示:

  1. 坐在我的电脑上,而后关上ISP软件。
  2. 单击“连贯”,而后期待调制解调器拨打该号码。
  3. 如果连贯胜利,请听到“调制解调器声音”。

如果不是这样,例如在顶峰时段,此时线路可能会过载和忙碌,请重试。

  1. 建设连贯后,你将收到胜利告诉,并浏览所有充斥GIF90年代辉煌的网页。

这个周期看起来很艰巨,然而它依然代表了服务彼此之间进行通信的形式:它们申请一个连贯,建设该连贯,发送一个申请,并失去一些回应。咱们的客户应用程序将以雷同的形式工作。咱们将首先连贯到服务器API应用程序,如果胜利,将向该服务器发出请求。

在本章中,咱们将应用Apollo Client连贯到咱们的API。连贯之后,咱们将编写一个GraphQL查问,该查问将用于在页面上显示数据。咱们还将在API查问和接口组件中引入分页。

在本地运行API

Web客户端应用程序的开发将须要拜访咱们API的本地实例。如果你始终在浏览本书,那么你可能曾经在计算机上启动了Notedly API及其数据库。如果不是这样,我曾经在附录A增加了无关如何获取API并应用一些样本数据一起运行的正本。如果你曾经在运行API,然而心愿应用其余数据,请从API我的项目目录的根目录运行npm run seed

设置Apollo客户端

Apollo服务器十分类似,Apollo客户端提供了许多有用的性能来简化在JavaScript UI应用程序中应用GraphQL的工作。Apollo客户端提供了用于将Web客户端连贯到API、本地缓存、GraphQL语法、本地状态治理等的库。咱们还将在React应用程序中应用Apollo Client,但Apollo还提供了VueAngularMeteorEmberWeb Components的库。

首先,咱们要确保.env文件蕴含了对咱们本地API URI的援用,这将使咱们可能在开发中应用本地API实例,同时在将应用程序公布到公共Web服务器时指向咱们的产品API。在咱们的.env文件,咱们应该有一个API_URI变量保留咱们本地API服务器的地址:

API_URI=http://localhost:4000/api

咱们的代码捆绑程序Parcel设置为主动解决.env文件。

任何时候咱们想在咱们的代码中援用一个.env变量,咱们能够应用process.env.VARIABLE_NAME。这将使咱们可能在本地开发、生产以及咱们可能须要的任何其余环境(例如暂存或继续集成)中应用特有的变量值。

应用存储在环境变量中的地址,咱们能够将Web客户端连贯到API服务器。在src/App.js文件中工作,首先,咱们须要导入将要应用的Apollo软件包:

// import Apollo Client librariesimport { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';

导入这些内容后,咱们能够配置一个新的Apollo Client实例,将其传递给API URI,启动缓存,并启用本地Apollo开发者工具:

// configure our API URI & cacheconst uri = process.env.API_URI;const cache = new InMemoryCache();// configure Apollo Clientconst client = new ApolloClient({uri,cache,connectToDevTools: true});

最初,咱们能够将React应用程序打包到一个ApolloProvider。咱们将用替换空标签,并将咱们的客户端包含在内:

const App = () => {return (<ApolloProvider client={client}><GlobalStyle /><Pages /></ApolloProvider>);};

总体而言,咱们的src/App.js文件当初将如下所示:

import React from 'react';import ReactDOM from 'react-dom';// import Apollo Client librariesimport { ApolloClient, ApolloProvider, InMemoryCache } from '@apollo/client';// global stylesimport GlobalStyle from '/components/GlobalStyle';// import our routesimport Pages from '/pages';// configure our API URI & cacheconst uri = process.env.API_URI;const cache = new InMemoryCache();// configure Apollo Clientconst client = new ApolloClient({uri,cache,connectToDevTools: true});const App = () => (<ApolloProvider client={client}><GlobalStyle /><Pages /></ApolloProvider>);ReactDOM.render(<App />, document.getElementById('root'));

通过将客户端连贯到API服务器,咱们当初能够将GraphQL查问和批改集成到咱们的应用程序中。

查问API

当咱们查问API时,咱们是在申请数据。在UI客户端中,咱们心愿可能查问该数据并将其显示给用户。Apollo使咱们可能编写查问以获取数据。而后,咱们能够更新React组件以将数据显示给最终用户。咱们能够通过编写noteFeed查问来摸索查问的用法,该查问将向用户返回最新笔记的摘要,并将其显示在应用程序的主页上。

当我第一次编写查问时,发现以下过程很有用:

  1. 思考查问须要返回什么数据。
  2. GraphQL Playground中编写查问。
  3. 将查问集成到客户端应用程序。

让咱们在构思查问时遵循此过程。如果你持续浏览本书的API局部,你可能会记得,noteFeed查问返回一个蕴含10个笔记的列表,以及一个游标(批示最初返回的笔记的地位)和hasNextPage布尔值,该布尔值容许咱们确定是否有其余笔记要加载。咱们能够在GraphQL Playground中查看咱们的构造,从而能够查看所有可用的数据选项。对于咱们的查问,咱们很可能须要以下信息:

{cursorhasNextPagenotes {idcreatedAtcontentfavoriteCountauthor {idusernameavatar}}}

当初,在咱们的GraphQLPlayground中,咱们能够将其填充到GraphQL查问中。咱们将比服务器章节中的查问更加具体,通过命名查问并提供一个可选的名为cursor的变量,要应用GraphQL Playground,请首先确保API服务器正在运行,而后拜访http://localhost:4000/api。在GraphQL Playground中,增加以下查问:

query noteFeed($cursor: String) {noteFeed(cursor: $cursor) {cursorhasNextPagenotes {idcreatedAtcontentfavoriteCountauthor {usernameidavatar}}}}

GraphQL Playground中,增加一个“query variable”以测试该变量的应用:

{"cursor": ""}

要测试此变量,请将空字符串替换为数据库中任何笔记的ID值(图14-1)。

14-1 咱们在GraphQL Playground中的noteFeed查问

当初咱们晓得查问已正确编写,咱们能够释怀地将其集成到Web应用程序中。在src/pages/home.js文件中,通过@apollo/client中的gql库导入useQuery库以及GraphQL语法:

// import the required librariesimport { useQuery, gql } from '@apollo/client';// our GraphQL query, stored as a variableconst GET_NOTES = gql`query NoteFeed($cursor: String) {noteFeed(cursor: $cursor) {cursorhasNextPagenotes {idcreatedAtcontentfavoriteCountauthor {usernameidavatar}}}}`;

当初咱们能够将查问集成到咱们的React应用程序中。为此,咱们将GraphQL查问字符串传递给ApollouseQueryReact钩子。咱们的钩子将返回一个蕴含以下值之一的对象:

  • data数据

查问返回的数据(如果胜利)。

  • loading

加载状态,在获取数据时将其设置为true。这使咱们能够向用户显示加载指示器。

  • error

如果无奈获取咱们的数据,则会将谬误返回到咱们的应用程序。

咱们能够更新Home组件包含咱们的查问:

const Home = () => {// query hookconst { data, loading, error, fetchMore } = useQuery(GET_NOTES);// if the data is loading, display a loading messageif (loading) return <p>Loading...</p>;// if there is an error fetching the data, display an error messageif (error) return <p>Error!</p>;// if the data is successful, display the data in our UIreturn (<div>{console.log(data)}The data loaded!</div>);};export default Home;

如果胜利实现所有操作,则应该看到“数据已加载!”音讯呈现在应用程序主页上(图14-2)。咱们还包含一个console.log语句,它将打印咱们的数据到浏览器控制台。将数据集成到应用程序中时,查看数据后果的构造是有用的领导。

14-2。如果咱们的数据已胜利获取,则咱们的组件将显示“数据已加载!”音讯,数据将打印到控制台。

当初,让咱们将接管到的数据集成到应用程序中。为此,咱们将应用数据中返回的笔记数组。React要求为每个后果调配一个惟一的键,为此咱们将应用单个笔记的ID。首先,咱们将为每个笔记显示作者的用户名:

const Home = () => {// query hookconst { data, loading, error, fetchMore } = useQuery(GET_NOTES);// if the data is loading, display a loading messageif (loading) return <p>Loading...</p>;// if there is an error fetching the data, display an error messageif (error) return <p>Error!</p>;// if the data is successful, display the data in our UIreturn (<div>{data.noteFeed.notes.map(note => (<div key={note.id}>{note.author.username}</div>))}</div>);};

应用JavaScript的map()办法

如果你以前没有应用过JavaScriptmap()办法,那么语法仿佛有些生疏。map()办法,能够在数组中执行遍历动作。当你解决从API返回的数据时,这十分有用,它容许你执行操作,例如以某种形式在模板中显示每个我的项目。要理解无关map()的更多信息,倡议浏览MDN Web文档指南。

如果咱们的数据库中有数据,你当初应该在页面上看到用户名列表(图14-3)。

14-3来自咱们数据的用户名,打印到屏幕上。

当初咱们曾经胜利输入了咱们的数据,咱们能够编写其余的组件了。因为咱们的笔记是用Markdown编写的,因而咱们导入一个库,该库将容许咱们将Markdown出现到页面上。

src/pages/home.js中:

import ReactMarkdown from 'react-markdown';

当初,咱们能够更新UI,包含作者的头像、作者的用户名、笔记的创立日期,笔记具备的收藏夹数量以及笔记本身的内容。

src/pages/home.js中:

// if the data is successful, display the data in our UIreturn (<div>{data.noteFeed.notes.map(note => (<article key={note.id}><img src={note.author.avatar} alt={`${note.author.username} avatar`} height="50px" />{' '}{note.author.username} {note.createdAt} {note.favoriteCount}{' '}<ReactMarkdown source={note.content} /></article>))}</div>);

React中的空格

React会去除在新行上的元素之间的空格。在咱们的标记中应用{''}是一种手动增加空格的办法。

当初,你应该在浏览器中看到残缺的笔记列表。然而,在持续进行款式设计之前,能够进行一些小的重构。这是咱们显示笔记的第一页,然而咱们晓得咱们还会做更多事件。在其余页面上,咱们将须要显示单个笔记以及其余笔记类型的性能(例如“我的笔记”和“收藏夹”)。让咱们持续创立两个新组件:src/components/Note.jssrc/components/NoteFeed.js

src/components/Note.js中,咱们将包含单个笔记的标记。为此,咱们将为每个组件函数传递一个蕴含内容的属性。

import React from 'react'; import ReactMarkdown from 'react-markdown';const Note = ({ note }) => { return (<article><imgsrc={note.author.avatar}alt="{note.author.username} avatar" height="50px" />{' '}{note.author.username} {note.createdAt} {note.favoriteCount}{' '}<ReactMarkdown source={note.content} /></article>);}; export default Note;

当初,对于src/components/NoteFeed.js组件:

import React from 'react';import Note from './Note';const NoteFeed = ({ notes }) => {return (<div>{notes.map(note => (<div key={note.id}><Note note={note} /></div>))}</div>);};export default NoteFeed;

最初,咱们能够更新src/pages/home.js组件援用咱们的NoteFeed

import React from 'react';import { useQuery, gql } from '@apollo/client';import Button from '../components/Button';import NoteFeed from '../components/NoteFeed';const GET_NOTES = gql`query NoteFeed($cursor: String) {noteFeed(cursor: $cursor) {cursorhasNextPagenotes {idcreatedAtcontentfavoriteCountauthor {usernameidavatar}}}}`;const Home = () => {// query hookconst { data, loading, error, fetchMore } = useQuery(GET_NOTES);// if the data is loading, display a loading messageif (loading) return <p>Loading...</p>;// if there is an error fetching the data, display an error messageif (error) return <p>Error!</p>;// if the data is successful, display the data in our UIreturn <NoteFeed notes={data.noteFeed.notes} />;};export default Home;

通过这种重构,咱们当初能够轻松地在咱们的应用程序中从新创立笔记和笔记摘要的实例。

一些格调

当初咱们曾经编写了组件并能够查看数据,咱们能够增加一些款式。最显著的改良机会之一就是咱们的“创立日期”显示方式。
为了解决这个问题,咱们将应用date-fns库,它提供了一些小的组件来解决JavaScript中的日期。在src/components/Note.js中,导入库并更新日期标记以利用转换,如下所示:

// import the format utility from `date-fns`import { format } from 'date-fns';// update the date markup to format it as Month, Day, and Year{format(note.createdAt, 'MMM Do YYYY')} Favorites:{' '}

格式化日期后,咱们能够应用款式化组件库来更新笔记布局:

import React from 'react';import ReactMarkdown from 'react-markdown';import { format } from 'date-fns';import styled from 'styled-components';// Keep notes from extending wider than 800pxconst StyledNote = styled.article` max-width: 800px;margin: 0 auto; `;// Style the note metadataconst MetaData = styled.div` @media (min-width: 500px) { display: flex;align-items: top;} `;// add some space between the avatar and meta infoconst MetaInfo = styled.div` padding-right: 1em; `;// align 'UserActions' to the right on large screensconst UserActions = styled.div` margin-left: auto; `;const Note = ({ note }) => {return (<StyledNote><MetaData><MetaInfo><imgsrc={note.author.avatar}alt="{note.author.username} avatar"height="50px"/></MetaInfo><MetaInfo><em>by</em> {note.author.username} <br />{format(note.createdAt, 'MMM Do YYYY')}</MetaInfo><UserActions><em>Favorites:</em> {note.favoriteCount}</UserActions></MetaData><ReactMarkdown source={note.content} /></StyledNote>);};export default Note;

咱们还能够在NoteFeed.js组件中的笔记之间增加一些空间和浅边框:

import React from 'react';import styled from 'styled-components';const NoteWrapper = styled.div`max-width: 800px;margin: 0 auto;margin-bottom: 2em;padding-bottom: 2em;border-bottom: 1px solid #f5f4f0;`;import Note from './Note';const NoteFeed = ({ notes }) => {return (<div>{notes.map(note => (<NoteWrapper key={note.id}><Note note={note} /></NoteWrapper>))}</div>);};export default NoteFeed;

通过这些更新,咱们将布局款式引入了咱们的应用程序。

动静查问

以后,咱们的应用程序蕴含三个路由,每个路由都是动态的。这些路由位于动态URL,并且将始终收回雷同的数据申请。然而,应用程序通常须要动静路由和基于这些路由的查问。例如,Twitter.com上的每条推文调配了惟一的URL :twitter.com//status/.这使用户能够在Twitter生态系统内以及网络上的任何中央链接和共享单个推文。

以后,在咱们的利用笔记中只能在摘要中拜访,然而咱们心愿容许用户查看并链接到各个笔记。为此,咱们将在React应用程序中设置动静路由以及GraphQL查问单个笔记。咱们的指标是使用户可能拜访/note/处的路由。

首先,咱们将在src/pages/note.js中创立一个新的页面组件。咱们将把props(属性)对象传递给组件,该对象包含通过React Routermatch属性。此属性蕴含无关路由门路如何与URL匹配的信息。这将使咱们可能通过以下形式拜访URL参数。

import React from 'react';const NotePage = props => {return (<div><p>ID: {props.match.params.id}</p></div>);};export default NotePage;

当初,咱们能够向src/pages/index.js文件增加相应的路由。该路由将蕴含一个ID参数,以:id示意:

// import React and routing dependenciesimport React from 'react';import { BrowserRouter as Router, Route } from 'react-router-dom';// import shared layout componentimport Layout from '../components/Layout';// import routesimport Home from './home';import MyNotes from './mynotes';import Favorites from './favorites';import NotePage from './note';// define routesconst Pages = () => {return (<Router><Layout><Route exact path="/" component={Home} /><Route path="/mynotes" component={MyNotes} /><Route path="/favorites" component={Favorites} /><Route path="/note/:id" component={NotePage} /></Layout></Router>);};export default Pages;

当初,拜访 http://localhost:1234/note/123将在咱们的页面上打印ID:123。要对其进行测试,请应用你抉择的任何内容替换ID参数,例如/note/pizza或/note/GONNAPARTYLIKE1999

这很酷,但不是很有用。让咱们更新咱们的src/pages/note.js组件,以对在URL中找到ID的笔记进行GraphQL查问。为此,咱们将应用APINoteReact组件中的note查问:

import React from 'react';// import GraphQL dependenciesimport { useQuery, gql } from '@apollo/client';// import the Note componentimport Note from '../components/Note';// the note query, which accepts an ID variableconst GET_NOTE = gql`query note($id: ID!) {note(id: $id) {idcreatedAtcontentfavoriteCountauthor {usernameidavatar}}}`;const NotePage = props => {// store the id found in the url as a variableconst id = props.match.params.id;// query hook, passing the id value as a variableconst { loading, error, data } = useQuery(GET_NOTE, { variables: { id } });// if the data is loading, display a loading messageif (loading) return <p>Loading...</p>;// if there is an error fetching the data, display an error messageif (error) return <p>Error! Note not found</p>;// if the data is successful, display the data in our UIreturn <Note note={data.note} />;};export default NotePage;

当初,导航到带有ID参数的URL将出现相应的笔记或谬误音讯。最初,让咱们更新src/components/NoteFeed.js组件,以在UI中显示指向单个笔记的链接。

首先,在文件顶部,从React Router导入{Link}:

import { Link } from 'react-router-dom';

而后,更新JSX,使其蕴含指向笔记页面的链接,如下所示:

<NoteWrapper key={note.id}><Note note={note} /><Link to={`note/${note.id}`}>Permalink</Link></NoteWrapper>

这样,咱们就在应用程序中应用了动静路由,并使用户可能查看各个笔记。

分页

目前,咱们仅检索应用程序主页中的10个最新笔记。如果要显示其余笔记,则须要启用分页。你可能会从本章的结尾以及咱们的API服务器的开发中回想起,咱们的API返回了游标,这是后果页面中返回的最初一个笔记的ID。此外,API返回hasNextPage布尔值,如果在咱们的数据库中找到其余笔记,则为true。向咱们的API发出请求时,咱们能够向其传递一个游标参数,该参数将返回接下来的10个我的项目。

换句话说,如果咱们有25个对象的列表(对应的ID125),则在收回初始申请时,它将返回我的项目110,游标值10hasNextPagetrue。如果咱们发送一个游标值为10的申请,咱们将收到11-20,游标值为20hasNextPage值为true。最初,如果咱们做的第三申请,传递一个指针的20,咱们会在收到物品21-25,用游标的值25hasNextPage的值为false。这正是咱们将在noteFeed查问中实现的逻辑。

为此,让咱们更新src/pages/home.js文件以进行分页查问。在咱们的用户界面中,当用户单击“查看更多”按钮时,接下来的10条笔记应加载到页面上。咱们心愿这种状况下不会刷新任何页面。为此,咱们须要在查问组件中蕴含fetchMore参数,并仅在hasNextPagetrue时显示Button组件。

当初,咱们将其间接写到咱们的主页组件中,能够很容易地将其隔离到本人的组件中或成为NoteFeed组件的一部分。

// if the data is successful, display the data in our UIreturn (// add a <React.Fragment> element to provide a parent element<React.Fragment><NoteFeed notes={data.noteFeed.notes} />{/* Only display the Load More button if hasNextPage is true */}{data.noteFeed.hasNextPage && (<Button>Load more</Button>)}</React.Fragment>);

React中的条件

在后面的示例中,咱们应用带有&&运算符的内联if语句有条件地显示“加载更多”按钮。如果hasNextPagetrue,则显示按钮。你能够在官网React文档中浏览无关条件渲染的更多信息。

当初咱们能够应用onClick处理程序更新组件。

当用户单击按钮时,咱们将要应用fetchMore办法进行附加查问,并将返回的数据附加到咱们的页面。

{data.noteFeed.hasNextPage && (// onClick peform a query, passing the current cursor as a variable<ButtononClick={() =>fetchMore({variables: {cursor: data.noteFeed.cursor},updateQuery: (previousResult, { fetchMoreResult }) => {return {noteFeed: {cursor: fetchMoreResult.noteFeed.cursor,hasNextPage: fetchMoreResult.noteFeed.hasNextPage,// combine the new results and the oldnotes: [...previousResult.noteFeed.notes,...fetchMoreResult.noteFeed.notes],__typename: 'noteFeed'}};}})}>Load more</Button>)}

先前的代码可能看起来有些毛糙,所以让咱们对其进行合成。咱们的组件包含一个onClick处理程序。单击该按钮时,将应用fetchMore办法执行新查问,并传递前一个查问中返回的游标值。返回后,updateQuery被执行,它更新咱们的光标和hasNextPage值,并将后果组合到一个数组中。

该__typename是查问名称,它蕴含在Apollo的后果中。

进行此更改后,咱们便能够从笔记摘要中查看所有笔记。滚动到笔记底部,尝试一下。如果你的数据库蕴含10个以上的笔记,则该按钮将可见。单击“加载更多”将下一个noteFeed后果增加到页面。

论断

在本章中,咱们曾经介绍了很多基础知识。咱们曾经设置了Apollo Client来与咱们的React应用程序一起应用,并将多个GraphQL查问集成到咱们的UI中。GraphQL的弱小能力体现在编写单个查问的能力,这些查问恰好返回UI所需的数据。在下一章中,咱们将用户身份验证集成到咱们的应用程序中,容许用户登录并查看其笔记和收藏夹。

如果有了解不到位的中央,欢送大家纠错。如果感觉还能够,麻烦您点赞珍藏或者分享一下,心愿能够帮到更多人。