W dzisiejszych czasach rozproszone architektury oprogramowania stały się bardziej popularne, a wraz z tym trendem zespoły programistyczne stosują podejście API-first do tworzenia produktów.
Z drugiej strony, architekci i deweloperzy coraz częściej decydują się na korzystanie z GraphQL API zamiast REST (ale REST API są nadal dobre)
Nie będę tutaj przytaczał dobrze znanych zalet i wad GraphQL , ale chcę zwrócić uwagę na tę, która nie jest dobrze znana, ale jest dość ważna dla programistów:
Jedną z najważniejszych zalet korzystania z GraphQL jest to, że programista frontendowy może szybko zamockować przykładowe dane i przełączyć się na prawdziwe dane, gdy backend jest gotowy.
Czym dokładnie są mockupy danych?
Załóżmy, że jakaś funkcjonalność potrzebuje danych z backendu lub w jakiś sposób musi komunikować się z API, a dane te nie są dostępne lub API nie zostało jeszcze wykonane. W takim przypadku programista Front-end musi przygotować przykładowe dane. Spójrz na poniższe przykłady:
1. Wyświetlanie dodatkowych informacji o produkcie
Zespół programistów pracuje nad wyświetlaniem dodatkowych informacji o produkcie na stronie produktu. Dane te nazywane są „kluczowymi cechami” i składają się z obrazu, nazwy i opisu. Dane te będą pochodzić z backendu. Zespół backendowy nie rozpoczął jeszcze pracy nad tą funkcjonalnością, więc programista frontendowy decyduje się na makietę tych danych i wyświetlenie tej makiety na frontendzie. Gdy backend zostanie ukończony, dane makiety zostaną zastąpione prawdziwymi danymi.
2. Wysyłanie wiadomości do sprzedawcy
Ta funkcja umożliwia klientom wysyłanie wiadomości do sprzedawcy. Klient wypełnia formularz. Wpisuje swoje imię, nazwisko, adres e-mail i wiadomość. Ponadto muszą zaakceptować zgodę na przetwarzanie danych osobowych. Programista frontendowy zbudował już formularz i walidację i jest na etapie wysyłania formularza do backendu. Backend musi odebrać formularz i zwrócić komunikat o powodzeniu lub błędzie, który zostanie wyświetlony użytkownikowi. Część backendowa nie jest jeszcze gotowa, więc programista Frontend musi zamockować interakcję z backendem.
Podsumowując, gdy niektóre dane, takie jak pola lub nawet kolekcje danych, nie zostały jeszcze zaimplementowane w backendzie, programiści używają fałszywych wartości (mock data) i zastępują je prawdziwymi danymi, gdy backend jest gotowy.
W tym artykule pokażę, jak to zrobić:
- pojedyncze pola
- zapytania
- mutacja
Na koniec pokażę ci, jak łatwo zastąpić pozorowane dane prawdziwymi danymi z backendu.
Dzięki tej wiedzy będziesz w stanie pracować nad front-endem bardziej efektywnie, nawet jeśli back-end pozostaje daleko w tyle.
Wymagania wstępne
Komputer z edytorem tekstu, NodeJS, połączenie internetowe, podstawowe umiejętności JavaScript, React i GraphQL .
Tworzenie aplikacji react
Używam create-react-app, aby stworzyć rusztowanie dla nowego projektu:
$ npx create-react-app graphql-mocks
$ cd graphql-mocks
$ npm start
Instalacja klienta Apollo
Następnie używam wiersza poleceń do zainstalowania klienta apollo:
$ npm install @apollo/client graphql
Klient Apollo umożliwia łączenie się z serwerem GraphQL i wykonywanie operacji GraphQL, takich jak zapytania i mutacje, dzięki niestandardowym hakom React , takim jak useQuery lub useMutation.
Inicjalizacja klienta Apollo
Po zainstalowaniu klienta mogę połączyć mój front-end z GraphQL API. Tym razem wykorzystam publicznie dostępne API GraphQL SpaceX.
Najpierw importuję Apolo Client, InMemoryCache i ApolloProvider z @apollo/client.
import { ApolloClient, InMemoryCache, ApolloProvider } from '@apollo/client';
Po drugie, inicjalizuję klienta:
const client = new ApolloClient({
uri: 'https://api.spacex.land/graphql/',
cache: new InMemoryCache(),
});
Jest to minimalna konfiguracja potrzebna do uruchomienia klienta Apollo – adres URL do API i zainicjowana pamięć podręczna w pamięci.
Po trzecie, opakowuję komponent App przez ApolloProvider:
<ApolloProvider client={client}>
<App />
</ApolloProvider>
ApolloProvided przyjmuje jeden argument: klienta, którego już zainicjowaliśmy. Po owinięciu komponentu App za pomocą ApolloProvider, mogę używać klienta w komponencie App i każdym elemencie podrzędnym komponentu App.
Jak makietować pojedyncze pola
W tym przykładzie pokażę, jak pobrać istniejące pola z API i jak dodać pole, które nie istnieje w odpowiedzi.
Tworzenie komponentu missions
Na początku tworzę komponent Missions, który będzie odpowiedzialny za wyświetlanie misji.
Utwórz plik components \ missions \ Missions.jsx:
export const Missions = () => {
return <>
<h1>Missions</h1>
Missions will be listed here
</>
}
Utwórz plik components \ missions \ index.js:
export { Missions } from './Missions';
Utwórz plik icomponents \ index.js:
export { Missions } from './missions';
Zaimportuj komponent Missions w pliku App.js:
import { Missions } from "./components";
Renderowanie komponentu:
function App() {
return <main className="container">
<Missions/>
</main>
}
Dodaj style do App.css:
.container {
max-width: 1200px;
margin: 0 auto;
}
Komponent w przeglądarce powinien wyglądać następująco:
Zapytanie o dane
Chcę pobierać misje z serwera graphql. Aby to zrobić, muszę zdefiniować zapytanie.
Dodaję plik components \ missions \ missions.gql.js:
import {gql} from "@apollo/client";
export default gql`{
missions(limit: 10) {
description
id
manufacturers
name
twitter
website
wikipedia
}
}
`;
Importuję zapytanie i hook useQuery w komponentach \ Missions \ missions.jsx:
import {useQuery} from "@apollo/client";
import MISSIONS_QUERY from './missions.gql.js'
Używam hooka useQuery do pobierania danych graphql:
const {loading, error, data} = useQuery(MISSIONS_QUERY);
Dodaję trochę logiki do obsługi stanu ładowania i błędów:
if (loading) return null;
if (error) return `Error! ${error}`
if (!data?.missions?.length) {
return 'No missions found';
}
const {missions} = data;
Na koniec renderuję dane zwrócone z API:
{missions.map(mission => {
return <div className="mission" key={mission.id}>
<h2>{mission.name}</h2>
<p>{mission.description}</p>
<h3>Manufacturers:</h3>
<ol>
{mission.manufacturers?.map(manufacturer => <li key={`${mission.id}-${manufacturer}`}>{manufacturer}</li>)}
</ol>
<h3>Links:</h3>
<ul>
{mission.twitter?.length && <li><a href={mission.twitter}>{mission.twitter}</a></li> }
{mission.website?.length && <li><a href={mission.website}>{mission.website}</a></li> }
{mission.wikipedia?.length && <li><a href={mission.wikipedia}>{mission.wikipedia}</a></li> }
</ul>
</div>
})}
Kompletny kod komponentu wygląda następująco:
import {useQuery} from "@apollo/client";
import MISSIONS_QUERY from './missions.gql.js'
export const Missions = () => {
const {loading, error, data} = useQuery(MISSIONS_QUERY);
if (loading) return null;
if (error) return `Error! ${error}`
if (!data?.missions?.length) {
return 'No missions found';
}
const {missions} = data;
return <>
<h1>Missions</h1>
{missions.map(mission => {
return <div className="mission" key={mission.id}>
<h2>{mission.name}</h2>
<p>{mission.description}</p>
<h3>Manufacturers:</h3>
<ol>
{mission.manufacturers?.map(manufacturer => <li key={`${mission.id}-${manufacturer}`}>{manufacturer}</li>)}
</ol>
<h3>Links:</h3>
<ul>
{mission.twitter?.length && <li><a href={mission.twitter}>{mission.twitter}</a></li> }
{mission.website?.length && <li><a href={mission.website}>{mission.website}</a></li> }
{mission.wikipedia?.length && <li><a href={mission.wikipedia}>{mission.wikipedia}</a></li> }
</ul>
</div>
})}
</>
}
W przeglądarce powinny pojawić się wyrenderowane dane:
Rozszerzenie schematu GraphQL po stronie klienta
Tutaj zaczyna się główny temat artykułu! Komponent Missions renderuje niektóre dane o misjach z przodu, takie jak opis, nazwa, linki internetowe itp:
Chcę zamockować jedno pole o nazwie: sponsor. Pole to jest tablicą. Aby to zrobić, tworzę plik graphql-type-defs.js w katalogu głównym projektu z następującą zawartością:
import {gql} from "@apollo/client";
export default gql`
extend type Mission {
sponsors: [String]
}
`;
Ta definicja schematu rozszerza typ Mission i dodaje do niego pole sponsors.
Teraz importuję definicję typu w pliku index.js:
import typeDefs from './graphql-type-defs';
I przekazuję go do konstruktora ApolloClient:
const client = new ApolloClient({
uri: 'https://api.spacex.land/graphql/',
cache: new InMemoryCache(),
typeDefs
});
Definiowanie funkcji odczytu z makietą danych
Następnym krokiem jest zdefiniowanie niestandardowej funkcji odczytu, która wygeneruje dla nas mockowane dane.
Zanim to zrobię, instaluję FakerJS. jest to biblioteka (generator fałszywych danych), która pomaga tworzyć fałszywe i losowe dane.
$ npm install @faker-js/faker --save-dev
Następnie przekazuję konfigurację z politykami typów obiektów do konstruktora InMemoryCache:
cache: new InMemoryCache({
typePolicies: {
Mission: {
fields: {
sponsors: {
read() {
return [...faker.random.words(faker.datatype.number({
'min': 1,
'max': 5
})).split(' ')]
}
},
},
},
},
}),
Kod ten definiuje funkcję read() dla pola sponsorów typu Mission. Funkcja read() zwraca fałszywe obiekty. W tym przypadku zwraca nową tablicę zawierającą od jednego do pięciu elementów. Elementami tej tablicy są losowe słowa.
Zapytanie z dyrektywą @client i wyświetlenie danych
Aby pobrać pole mock, muszę dodać je do zapytania. Aby to zadziałało, muszę użyć dyrektywy @client. Zapoznaj się ze zaktualizowanym zapytaniem o misje:
export default gql`{
missions(limit: 10) {
description
id
manufacturers
name
twitter
website
wikipedia
sponsors @client // here you go
}
}
`;
Wreszcie mogę wyrenderować pole sponsorów na interfejsie użytkownika. Dodaję ten kod do funkcji renderowania komponentu misji:
<h3>Sponsors:</h3>
<ol>
{mission.sponsors?.map(sponsor => <li key={`${mission.id}-${sponsor}`}>{sponsor}</li>)}
</ol>
Wyniki w przeglądarce:
Jak wykonać makietę całego zapytania
Mockowanie pojedynczych pól jest bardzo przydatne. Co więcej, czasami deweloperzy chcą zakpić z zapytania lub mutacji, która nie istnieje w backendzie. Zacznijmy od wyśmiewania zapytania.
Dodanie nowego zapytania do schematu
Dodajmy zapytanie publications, które zwraca tablicę publikacji (nazwa publikacji i adres URL).
Rozszerzam plik graphql-type-defs.js dodając nowe typy:
type Query {
publications: [Publication]
}
type Publication {
name: String!
url: String!
}
Definiowanie resolvera
Następnie muszę zdefiniować resolver, który będzie generował fałszywe dane dla zapytania o publikacje.
Tworzę plik graphql-resolvers.js:
import {faker} from "@faker-js/faker";
export default {
Query: {
publications: () => {
const publications = [];
const publicationLength = faker.datatype.number({
'min': 1,
'max': 5
})
for (let i = 0; i < publicationLength; i++) {
publications.push({
name: faker.lorem.sentence(),
url: faker.internet.url()
})
}
return publications;
},
}
}
Zdefiniowałem funkcję publications, która zwraca nową tablicę fałszywych publikacji.
Rejestrowanie resolvera
Aby zarejestrować resolver, należy przekazać go do konstruktora ApolloClient:
import resolvers from './graphql-resolvers';
const client = new ApolloClient({
uri: 'https://api.spacex.land/graphql/',
cache: new InMemoryCache({
typePolicies: {
Mission: {
fields: {
sponsors: {
read() {
return [...faker.random.words(faker.datatype.number({
'min': 1,
'max': 5
})).split(' ')]
}
},
},
},
},
}),
typeDefs,
resolvers // here you go
});
Używanie makiety zapytania w aplikacji
Stwórzmy komponent publikacji, który wyświetla wyśmiewane dane.
components \ publications \ Publications.jsx
import {useQuery} from "@apollo/client";
// 1. here is imported the publications query
import PUBLICATIONS_QUERY from './publications.gql.js'
export const Publications = () => {
// 2. here the query is used
const {loading, error, data} = useQuery(PUBLICATIONS_QUERY);
if (loading) return null;
if (error) return `Error! ${error}`;
if (!data?.publications?.length) {
return 'No publications found';
}
const {publications} = data;
return <>
<h1>Publications</h1>
<ol>
{publications?.map(publication => <li key={publication.name}><a href={publication.url}>{publication.name}</a></li>)}
</ol>
</>
}
(1.) W to miejsce zaimportowałem zapytanie o publikacje, a tutaj (2.) użyłem w nim haka useQuery.
Jak widać z perspektywy komponentu i hooka useQuery, nie ma znaczenia, czy używany kamieniołom jest fałszywy czy prawdziwy. Jest przejrzysty i działa w ten sam sposób.
components \ publications \ publications.gql.js:
import {gql} from "@apollo/client";
export default gql`
{
publications @client {
name
url
}
}
`;
Dyrektywa @client umożliwia definiowanie nie tylko pól, jak w poprzednim przykładzie, ale także zapytań i mutacji.
components \ publications \ index.js:
export {Publications} from './Publications'
components \ index.js:
export { Publications } from './publications'; // added this import
export { Missions } from './missions';
Dodaj komponent publikacji w pliku App.js:
import './App.css';
import {Missions, Publications} from "./components"; // added import
function App() {
return <main className="container">
<Missions/>
<Publications/> !<-- added component -->
</main>
}
export default App;
Wyniki w przeglądarce:
Jak zamockować mutację GraphQL
Ostatnim przykładem, który chcę pokazać, jest sposób wyśmiewania mutacji graphql. Zaimplementujmy prosty formularz, który pozwoli użytkownikom przesłać nową publikację. Formularz zawiera dwa dane wejściowe: tytuł publikacji i jej adres URL.
Komponent PublicationForm
Utwórz plik components \ PublicationForm \ PublicationForm.jsx
import {useCallback, useState} from "react";
export const PublicationForm = () => {
const [ title, setTitle ] = useState('');
const [ url, setUrl ] = useState('');
const submitForm = useCallback((e) => {
e.preventDefault();
console.log(title, url);
}, [title, url]);
return <form onSubmit={submitForm}>
<legend>Submit a new publication:</legend>
<input type="text" placeholder="Publication title" value={title} onChange={(e) => setTitle(e.target.value)}/>
<input type="text" placeholder="Publication URL" value={url} onChange={(e) => setUrl(e.target.value)}/>
<button type="submit">Submit</button>
</form>
}
W formularzu są więc dwa pola i przycisk przesyłania. Gdy użytkownik kliknie przycisk submit, wywoływana jest funkcja submitForm. Na razie loguje tylko do konsoli.
Utwórz plik components \ publicationForm \ index.js
export { PublicationForm } from './PublicationForm';
Ponownie wyeksportuj komponent w components/index.js:
export { Publications } from './publications';
export { Missions } from './missions';
export { PublicationForm } from './publicationForm'; // added here
Dodaj komponent do funkcji renderowania komponentu aplikacji:
import './App.css';
import {Missions, Publications, PublicationForm} from "./components";
function App() {
return <main className="container">
<Missions/>
<Publications/>
<PublicationForm/>
</main>
}
export default App;
Dodanie mutacji do schematu
Zdefiniujmy nową mutację w naszym schemacie graphql:
type Mutation {
addPublication(name: String!, url: String!): String
}
Ostateczna wersja graphql-type-defs wygląda następująco:
import {gql} from "@apollo/client";
export default gql`
extend type Mission {
sponsors: [String]
}
type Query {
publications: [Publication]
}
type Mutation {
addPublication(name: String!, url: String!): String
}
type Publication {
name: String!
url: String!
}
`;
Definiowanie resolvera dla mutacji
Teraz dodam resolver mutacji addPublication do pliku graphql-resolvers.js:
import {faker} from "@faker-js/faker";
export default {
Query: {
// query resolvers
},
Mutation: {
addPublication: (parent, args, context, info) => {
console.log(parent, args, context, info);
return 'Your publication has been submitted, thank you!'
}
}
}
Zdefiniowałem mutację i zwraca ona ciąg znaków. Oczywiście, jeśli potrzebujesz bardziej zaawansowanego testowania wyśmiewanej mutacji, możesz dodać kod tutaj.
Jak używać wyśmiewanej mutacji w aplikacji
Dodaj plik components \ publicationForm \ addPublication.gql.js:
import {gql} from "@apollo/client";
export default gql`
mutation addPublication(
$name: String!,
$url: String!
) {
addPublication(
name: $name,
url: $url
) @client
}
`;
Jak widać, tutaj również użyłem dyrektywy @client do zdefiniowania mock mutacji
Zaktualizuj kod komponentu publicationForm.jsx:
import {useCallback, useMemo, useState} from "react";
import {useMutation} from "@apollo/client";
import ADD_PUBLICATION_MUTATION from './addPublication.gql';
export const PublicationForm = () => {
const [ name, setName ] = useState('');
const [ url, setUrl ] = useState('');
const [addPublication, {data, loading, error}] = useMutation(ADD_PUBLICATION_MUTATION)
const submitForm = useCallback((e) => {
e.preventDefault();
addPublication({variables: {name, url >;
}, [name, url, addPublication]);
const results = useMemo(() => {
return data?.addPublication
}, [data]);
if (loading) return null;
if (error) return `Error! ${error}`;
return <form onSubmit={submitForm}>
<legend>Submit a new publication:</legend>
<input type="text" placeholder="Publication title" value={name} onChange={(e) => setName(e.target.value)}/>
<input type="text" placeholder="Publication URL" value={url} onChange={(e) => setUrl(e.target.value)}/>
<button type="submit">Submit</button>
<div>{results}</div>
</form>
}
Tutaj dodałem logikę odpowiedzialną za wykonywanie mutacji. Jeśli chodzi o zapytania – działają one tak samo z wyśmiewanymi mutacjami, jak z prawdziwymi.
Wyniki w przeglądarce:
Jak korzystać z danych na żywo, gdy są gotowe
Ok, więc zakpiliśmy z niektórych pól, zapytań i mutacji, i możesz zapytać, co powinieneś zrobić, gdy zespół backendowy zaimplementuje wszystkie żądane pola i operacje w API.
To całkiem proste. Powinieneś:
- 1. usuwanie adnotacji @client – gdy określone pole lub operacja jest gotowa, wystarczy usunąć dyrektywę @client z zapytania/mutacji.
- 2. usuń resolvery klienta – usuń resolvery, ponieważ nie są już potrzebne, gdy dane są wypełniane z API
- 3. usunięcie definicji typu klienta – to samo tutaj, schemat powinien być zaimplementowany po stronie backendu, więc nie jest już potrzebny.
Podsumowanie
W tym artykule pokazałem, jak kpić z zapytań i mutacji GraphQL. W porównaniu do API REST, wyśmiewanie zapytań GraphQL jest znacznie łatwiejsze. Późniejsze przejście na rzeczywiste dane wiąże się jedynie ze zmianą zapytań GraphQL i usunięciem resolverów. Moim zdaniem mockowanie danych w GraphQL jest znacznie łatwiejsze niż w REST, co jest niewątpliwie korzystne dla wszystkich.
Jeśli chcesz zamockować niekte pola lub operacje po stronie klienta za pomocą klienta Apollo, wykonaj następujące kroki:
- Stwórz schemat GraphQL po stronie klienta
- Zdefiniuj niestandardowe funkcje resolver/read
- Użyj dyrektywy @client w zapytaniach/mutacjach
Mam nadzieję, że spodobał Ci się ten artykuł. Dzięki za przeczytanie!