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, alboapi/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:
- napisać schemat zapytań (plik schema)
- napisać funkcje, które wypełnią nam pliki danymi (plik mocks)
- połączyć ze sobą schema i mocki, tworząc SchemaLink - z wykorzystaniem apollo-link-schema (plik createMockLink)
- utworzyć instancję ApolloCilenta, apollo-boost (plik core/createApolloClient)
- zapewnić aplikacji Apollo Providera - w głównym komponencie aplikacji oraz podpiąć do niego instancję klienta (plik App.js)
- napisać query definiujące dane, które chcemy dostarczyć do komponentu
- 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-aString
, 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ą.