Wojciech Twaróg
Avra Sp. z o.o.
Wojciech TwarógFrontend Developer @ Avra Sp. z o.o.

GraphQL oczami frontend developera: Mockowanie danych

O tym, jak mockować API GraphQL z pomocą Apollo w aplikacji React.
25.06.20187 min
GraphQL oczami frontend developera: Mockowanie danych

GraphQL to język zapytań napisany przez programistów z Facebooka, a obecnie stosowany między innymi w Github i Pinterest. API napisane w tym języku jest alternatywą dla tradycyjnego API REST. Jego główną zaletą jest tylko jeden endpoint, a odpowiednio skonstruowane zapytania - zwane dalej query - dostarczają tylko potrzebnych nam danych. Istnieją również mutacje, które pozwalają na wprowadzanie zmian w modelu, jednak w tym artykule zajmiemy się tylko prezentacją danych w komponentach.

W REST API dostępne byłyby przykładowe endpointy:
api/users/<id> - który zwróciłby podstawowe dane o użytkowniku, albo
api/users/<id>/friends - który zwróciłby listę znajomych. 

Natomiast kiedy używamy GraphQL, dostępny jest tylko jeden endpoint, np. api/graphql

W jednym z projektów w Avrze zespół deweloperski podjął decyzję, aby za pomocą GraphQL realizować komunikację między backendem, a frontendem. Na pewnym etapie warstwa prezentacji tworzona w Reactcie wyprzedzała pracę nad BE i powstała potrzeba mockowania danych dla komponentów, co - w przypadku GraphQL - udało się uzyskać przy niewielkich nakładach sił, a z bardzo dobrym efektem. W artykule pokażę, jak napisać prostą aplikację prezentującą podstawione przez nas odpowiedzi z serwera GraphQL. Gotową aplikację do pobrania znaleźć można pod adresem: https://github.com/blazejpascal/mockedGrapghqlUser

Jeśli jest to Twój pierwszy kontakt z GraphQL, polecam przed przeczytaniem zapoznać się z bardzo dobrą dokumentacją GraphQL i Apollo

Wyzwanie

Postawmy się w sytuacji, kiedy chcemy zapełnić taki oto komponent danymi:

Idealnym rozwiązaniem byłoby otrzymanie z serwera następującego JSON-a z danymi do wyświetlenia w naszym komponencie:

{
 profileInfo {
   "id": 10,
   "name": "Nyah Aufderhar",
   "age": 82,
   "website": "http://wwww.Jacobi.biz/",
   "isFriend": false,
   }
}


Postaram się pokazać, jak uzyskać taki efekt krok po kroku.

Zależności, czyli niezbędne zasoby

Zaczniemy od stworzenia podstawowej aplikacji React, korzystając z create-react-app. Następnie - używając dowolnego package managera - instalujemy poniższe zależności. Opis i ich użycie przedstawiam później.

"dependencies": {
   "apollo-boost": "^0.1.6",
   "apollo-link-schema": "^1.1.0",
   "casual-browserify": "^1.5.19",
   "graphql": "^0.13.2",
   "graphql-tag": "^2.9.2",
   "graphql-tools": "^3.0.0",
   "react": "^16.3.2",
   "react-apollo": "^2.1.4",
   "react-dom": "^16.3.2",
   "react-scripts": "1.1.4"
 }


Do usprawnienia procesu komunikacji z serwerem GraphQL możemy użyć klienta GraphQL. Aplikacja pisana jest z wykorzystaniem Reacta, więc możemy użyć zarówno biblioteki Relay (tylko dla Reacta), jak i Apollo (umożliwia ona połączenie z dowolnym frameworkiem). Biorąc pod uwagę intuicyjność oraz dobrą dokumentację użyjemy biblioteki Apollo.

Time to mock 

Do komunikacji między GraphQL, a naszą aplikacją używamy biblioteki apollo-client. Domyślnie do instancji obiektu podaje się adres URL.

import ApolloClient from "apollo-boost";
 
const client = new ApolloClient({ 
  uri: "https://some-uri" 
});


Co jednak, gdy nie mamy jeszcze wystawionego endpointa? Pierwsze, co należy zrobić, to stworzyć pliki z mockami.


Do wyświetlenia naszych danych w komponencie będziemy musieli:

  1. napisać schemat zapytań (plik schema)
  2. napisać funkcje, które wypełnią nam pliki danymi (plik mocks)
  3. połączyć ze sobą schema i mocki, tworząc SchemaLink - z wykorzystaniem apollo-link-schema (plik createMockLink)
  4. utworzyć instancję ApolloCilenta, apollo-boost (plik core/createApolloClient)
  5. zapewnić aplikacji Apollo Providera - w głównym komponencie aplikacji oraz podpiąć do niego instancję klienta (plik App.js)
  6. napisać query definiujące dane, które chcemy dostarczyć do komponentu
  7. połączyć komponent z query za pomocą komponentu Query i funkcji gql


Do dzieła!

Typy - o co możemy odpytać?

Plik schema.js zawiera obiekt definiujący typy pól, o które możemy odpytać w naszych zapytaniach i mutacjach. W idealnej sytuacji ten fragment kodu (lub z niewielkimi zmianami) powinien odpowiadać temu, co później znajdzie się na serwerze. Umożliwi to zmianę źródła danych na to z serwera - bez konieczności poprawiania kodu po stronie aplikacji.

Obiekt przypisujemy do zmiennej. W GraphQL i Apollo używamy template literals (backticks - `) do wspierania wielowierszowych stringów oraz ich interpolacji. Umożliwi to później łatwiejsze scalenie typów.

const profileInfo = `
   type profileInfo {
       id: ID!
       name: String!
       age: Int!
       website: String!
       isFriend: Boolean!
   }
`

export default [profileInfo]


Poszczególne typy to
:
ID - specjalny typ dostarczany przez GraphQL-a
String, Int, Boolean - to standardowe typy zmiennych
! - świadczy o tym, że wartość jest wymagana

Następnie importujemy i scalamy nasze typy, aby później dostarczyć je jako argumenty do funkcji makeExecutableSchema z graphql-tools.

const RootQuery = `
   type RootQuery {
       profileInfos(size: Int! ): [profileInfo],
   }
   `
const SchemaDefinition = `
   schema {
       query: RootQuery
   }
   `
export default makeExecutableSchema({
   typeDefs: [
       SchemaDefinition,
       RootQuery,
       ...profileInfo,
   ],
   resolvers: {},
})

Mocki - sposoby na wytworzenie danych

Przechodzimy do sedna problemu, a mianowicie do mockowania danych dla naszych pól. Korzystamy z biblioteki casual-browserify, która służy do wytworzenia sztucznych danych. Oczywiście możemy użyć innej dowolnej biblioteki do generowania danych dla JavaScriptu. Piszemy funkcje zwracające nam obiekty z danymi z pól, o które możemy odpytać.

 profileInfo: () => ({
   name: casual.full_name,
   age: casual.integer(10, 100),
   website: casual.url,
   isFriend: casual.coin_flip,
 }


Następnie budujemy zależności między obiektami. Korzystamy z MockList z graphql-tools - umożliwia mockowanie całych tablic bez potrzeby pisania generatorów.

 RootQuery: () => ({
   profileInfos: (root, { size }) => new MockList(size)
 }),


Całość kodu:

import { MockList } from 'graphql-tools'
import casual from 'casual-browserify'

const mocks = {
 RootQuery: () => ({
   profileInfos: (root, { size }) => new MockList(size)
 }),

 profileInfo: () => ({
   name: casual.full_name,
   age: casual.integer(10, 100),
   website: casual.url,
   isFriend: casual.coin_flip,
 })
}

export default mocks


Zachęcam do szerszego zapoznania się z możliwościami bibliotek

Możemy również sami napisać dowolną funkcję, np. wybierającą losowy element z tablicy zaimportowanej z ProfileInfo.mock.

import casual from 'casual-browserify'

const pickRandomFromArray = (array) => array[casual.integer(0, array.length - 1)]

export { pickRandomFromArray }


Do czego można to wykorzystać? Wyobraźmy sobie sytuację, w której ustalamy z testerem skrajne przypadki testowe dla nazwy użytkownika. Możemy w łatwy sposób wygenerować n elementów z różnymi nazwami użytkownika i sprawdzić, czy któryś element nie ma problemów z wyświetlaniem się na różnych przeglądarkach.

const names = [
 'WWWWWWWWWWWWWWWWWWWW',
 '____________________',
 '32165498778965412336',
]

export default names

SchemaLink - czyli jak połączyć mockowane dane z klientem Apollo?

Używamy funkcji addMockFunctionsToSchema, aby połączyć mocki i typy. Następnie, korzystając z apollo-link-schema, tworzymy nowy obiekt SchemaLink, który zostanie później wyeksportowany do instancji klienta Apollo - zamiast tradycyjnego linka do endpointa GraphQL.

function createMockLink() {
 addMockFunctionsToSchema({
   schema,
   mocks,
 })
 return new SchemaLink({ schema })
}

export default createMockLink

Apollo Client - ostatni brakujący element

Kolejny etap prac związanych z generowaniem podstawionych danych to napisanie funkcji zwracającej nowy obiekt Apollo Clienta, który łączy nasz link oraz cache. Cache jest obligatoryjnym składnikiem Apollo Client, który pozwoli nam uniknąć ponownych zapytań o wcześniej pobrane dane. Użyjemy apollo-boost, w którym znajdują się niezbędne zasoby, by zacząć pracę z Apollo.

import { ApolloClient, InMemoryCache } from 'apollo-boost'
import createMockLink from '../mocks/createMockLink'

export default function createApolloClient() {
 const apolloCache = new InMemoryCache()
 const link = createMockLink()
 return new ApolloClient({
   cache: apolloCache,
   link,
 })
}


Inicjujemy instancję Apollo Clienta w głównym komponencie aplikacji - App.js.
Dodajemy komponent Apollo Providera, któremu przypisujemy utworzoną instancję klienta.

import { ApolloProvider } from 'react-apollo'
import createApolloClient from './core/createApolloClient'
import UsersForArticle from './components/UsersForArticle/UsersForArticle'

const client = createApolloClient()

class App extends Component {
 render() {
   return (
     <ApolloProvider client={client}>
       <div className="App">
         <UsersForArticle />
       </div>
     </ApolloProvider>
   );
 }
}
export default App

Struktura aplikacji

By pokazać w pełni działający przykład, wprowadzimy typowy dla Reacta podział na komponenty. Zacznijmy od komponentu, do którego - za pomocą propsów - przekazujemy dane z komponentu rodzica:

const UserData = props => {
 const { name, age, website } = this.props;

 return (
   <div>
     <img src={avatar} alt="avatar" />
     <div>
       <div>`{name} ({age})`</div>
       <div>{website}</div>
     </div>
     <InviteButton isFriend={isFriend} />
   </div>
 );
};

export default UserData;


Komponent pośredniczący - a także pozwalający wykorzystać możliwości płynące z modułowości Reacta:

const UserForArticle = props => {
 const { name, age, website } = props;
 return (
   <div>
     <UserData { name, age, website } />
   </div>
 );
};

export default UserForArticle;


Wraz ze wzrostem skomplikowania relacji między komponentami można wprowadzić managera stanu aplikacji, np. Redux. W projekcie używamy biblioteki Apollo, która również umożliwia nam zarządzanie stanem aplikacji dzięki apollo-state-link. Rolę źródła prawdy dla naszej aplikacji może pełnić Apollo Client Cache. Więcej informacji znajduje się w dokumentacji projektu Apollo

Poniższy komponent wyświetla zadaną liczbę profili. Aby użyć query, należy zaimportować komponent Query z biblioteki react-apollo oraz funkcję gql z graphql-tag. To połączenie pozwala na komunikację między endpointem GraphQL, a naszym komponentem. To, co należy zrobić, to owinąć komponent zestawem Query + gql, a następnie korzystać z obiektu data.

const UsersForArticle = () => (
 <Query
   query={gql`
     {
       profileInfos (size: 4) {
         id
         name
         age
         website
         isFriend
         }
     }
   `}
 >
   {({ data }) => {
        return (
       <div>
         {
           data.profileInfos.map(profile => (
             <div key={profile.id}>
               <UserForArticle {...profile} />
             </div>
           ))}
       </div>
     )
   }
 </Query>
)

export default UsersForArticle

 
W komponencie UsersForArticle przekazujemy zawartość tablicy profileInfos jako propsy do komponentu UserForArticle.

Szczegółową strukturę aplikacji można zobaczyć w projekcie na Githubie.

Wielki finał, czyli efekty naszej pracy

Wpisujemy npm/yarn start i naszym oczom ukazują się cztery komponenty z losowymi danymi, które zmieniają się przy każdym odświeżeniu strony:

Podsumowując

Aby użyć GraphQL tworzymy instancje Apollo Clienta i dodajemy komponent Providera w głównym komponencie aplikacji. W kliencie definiujemy adres URL, pod którym znajduje się endpoint backendu naszej aplikacji. W komponencie, w którym chcemy uzyskać dane, piszemy query. Owijamy je w komponent Query, dostarczany nam przez react-apollo - i gotowe!

Jeśli stroną backendową masz zamiar zająć się później, napisz typy (makeExecutableSchema) i funkcje tworzące dane. Wykorzystaj graphql-tools, aby połączyć mocki z typami (addMockFunctionToSchema). Następnie - korzystając z apollo-link-schema - stwórz obiekt SchemaLink, który używamy zamiast adresu URL w Apollo Client.

Mam nadzieję, że powyższy artykuł wprowadził Cię w tajniki mockowania danych oraz pokazał, że warto ich użyć w oczekiwaniu na BE.

Na sam koniec - porada dla mockujących. Jeśli jakiś typ zawiera inny - wcześniej już zdefiniowany - typ, nie trzeba go definiować ponownie. Np. gdyby komponent wyświetlał listę znajomych użytkownika, nie musimy po raz kolejny mockować imion i nazwisk. Po prostu odpytujemy o nie za pomocą query, a graphql-tools zajmie się za nas resztą.

<p>Loading...</p>