59. GraphQL с Angular
62. GraphQL с Angular 🔮
Заголовок раздела «62. GraphQL с Angular 🔮»Привет! Яша здесь. REST API — хорошо, но GraphQL даёт тебе точно те данные, что нужны, не больше и не меньше. Apollo Angular — лучший способ интегрировать GraphQL в Angular. Разберём всё от запросов до оптимистичных обновлений 🚀
Установка Apollo Angular
Заголовок раздела «Установка Apollo Angular»ng add apollo-angular# - Установит apollo-angular, @apollo/client, graphql# - Добавит GraphQLModule в AppModule# - Попросит URL GraphQL сервера// graphql.module.ts — ручная настройкаimport { NgModule } from '@angular/core';import { ApolloModule, APOLLO_OPTIONS } from 'apollo-angular';import { ApolloClientOptions, InMemoryCache, ApolloLink } from '@apollo/client/core';import { HttpLink } from 'apollo-angular/http';import { onError } from '@apollo/client/link/error';
export function createApollo(httpLink: HttpLink): ApolloClientOptions<any> { const http = httpLink.create({ uri: 'https://api.example.com/graphql' });
// Error link — глобальная обработка ошибок const errorLink = onError(({ graphQLErrors, networkError, operation }) => { if (graphQLErrors) { graphQLErrors.forEach(({ message, locations, path }) => { console.error(`GraphQL Error: ${message}`, { locations, path }); }); } if (networkError) { console.error('Network error:', networkError); } });
// Auth link — добавляем JWT токен const authLink = new ApolloLink((operation, forward) => { const token = localStorage.getItem('token'); operation.setContext({ headers: { Authorization: token ? \`Bearer \${token}\` : '' } }); return forward(operation); });
return { link: ApolloLink.from([errorLink, authLink, http]), cache: new InMemoryCache({ typePolicies: { User: { keyFields: ['id'] }, // ← кэш нормализация по id Product: { keyFields: ['id'] }, } }), defaultOptions: { watchQuery: { fetchPolicy: 'cache-and-network' }, // сначала кэш, потом сеть query: { fetchPolicy: 'network-only' }, }, };}
@NgModule({ imports: [ApolloModule], providers: [{ provide: APOLLO_OPTIONS, useFactory: createApollo, deps: [HttpLink], }],})export class GraphQLModule {}Queries: watchQuery и query
Заголовок раздела «Queries: watchQuery и query»import { gql } from '@apollo/client/core';import { Apollo } from 'apollo-angular';import { Observable } from 'rxjs';import { map } from 'rxjs/operators';
// Типизацияexport const GET_USERS = gql` query GetUsers($page: Int, $limit: Int) { users(page: $page, limit: $limit) { id name email role createdAt } }`;
export const GET_USER = gql` query GetUser($id: ID!) { user(id: $id) { id name email role posts { id title createdAt } } }`;
@Injectable({ providedIn: 'root' })export class UserService { constructor(private apollo: Apollo) {}
// watchQuery — возвращает Observable, обновляется при изменении кэша getUsers(page = 1, limit = 10): Observable<User[]> { return this.apollo.watchQuery<{ users: User[] }>({ query: GET_USERS, variables: { page, limit }, }).valueChanges.pipe( map(result => result.data.users) ); }
// query — одноразовый запрос getUserById(id: string): Observable<User> { return this.apollo.query<{ user: User }>({ query: GET_USER, variables: { id }, }).pipe( map(result => result.data.user) ); }}Mutations: изменение данных
Заголовок раздела «Mutations: изменение данных»const CREATE_USER = gql` mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id name email role } }`;
const UPDATE_USER = gql` mutation UpdateUser($id: ID!, $input: UpdateUserInput!) { updateUser(id: $id, input: $input) { id name email } }`;
const DELETE_USER = gql` mutation DeleteUser($id: ID!) { deleteUser(id: $id) { success message } }`;
@Injectable({ providedIn: 'root' })export class UserMutationService { constructor(private apollo: Apollo) {}
createUser(input: CreateUserInput): Observable<User> { return this.apollo.mutate<{ createUser: User }>({ mutation: CREATE_USER, variables: { input }, // Обновление кэша после мутации update: (cache, { data }) => { const existing = cache.readQuery<{ users: User[] }>({ query: GET_USERS }); if (existing && data?.createUser) { cache.writeQuery({ query: GET_USERS, data: { users: [...existing.users, data.createUser] }, }); } }, }).pipe( map(result => result.data!.createUser) ); }
deleteUser(id: string): Observable<boolean> { return this.apollo.mutate<{ deleteUser: { success: boolean } }>({ mutation: DELETE_USER, variables: { id }, // evict удаляет объект из кэша update: (cache) => { cache.evict({ id: cache.identify({ __typename: 'User', id }) }); cache.gc(); // garbage collect }, }).pipe( map(result => result.data!.deleteUser.success) ); }}Оптимистичные обновления
Заголовок раздела «Оптимистичные обновления»updateUserName(id: string, name: string): Observable<User> { return this.apollo.mutate<{ updateUser: User }>({ mutation: UPDATE_USER, variables: { id, input: { name } },
// Показываем результат НЕМЕДЛЕННО, не дожидаясь ответа сервера optimisticResponse: { __typename: 'Mutation', updateUser: { __typename: 'User', id, name, email: '', // заглушка — обновится когда придёт реальный ответ }, }, }).pipe( map(result => result.data!.updateUser) );}Subscriptions: реальное время
Заголовок раздела «Subscriptions: реальное время»const USER_UPDATED_SUBSCRIPTION = gql` subscription OnUserUpdated { userUpdated { id name email updatedAt } }`;
// Настройка WebSocket linkimport { split } from '@apollo/client/core';import { GraphQLWsLink } from '@apollo/client/link/subscriptions';import { createClient } from 'graphql-ws';import { getMainDefinition } from '@apollo/client/utilities';
const wsLink = new GraphQLWsLink(createClient({ url: 'wss://api.example.com/graphql', connectionParams: { authToken: localStorage.getItem('token'), },}));
// HTTP для queries/mutations, WS для subscriptionsconst splitLink = split( ({ query }) => { const definition = getMainDefinition(query); return definition.kind === 'OperationDefinition' && definition.operation === 'subscription'; }, wsLink, // ← subscriptions httpLink, // ← queries & mutations);
// Использование@Component({...})export class UsersComponent implements OnInit, OnDestroy { users$ = this.userService.getUsers();
constructor(private apollo: Apollo) {}
ngOnInit(): void { // Подписка обновляет кэш — компонент автоматически перерисуется! this.apollo.subscribe<{ userUpdated: User }>({ query: USER_UPDATED_SUBSCRIPTION, }).subscribe(({ data }) => { // Apollo автоматически обновит кэш через keyFields: ['id'] console.log('Пользователь обновлён:', data?.userUpdated); }); }}Code Generation с graphql-codegen
Заголовок раздела «Code Generation с graphql-codegen»npm install --save-dev @graphql-codegen/cli @graphql-codegen/typescript @graphql-codegen/typescript-operations @graphql-codegen/typescript-apollo-angularschema: https://api.example.com/graphqldocuments: 'src/**/*.graphql'generates: src/app/graphql/generated.ts: plugins: - typescript - typescript-operations - typescript-apollo-angular config: addExplicitOverride: true apolloAngularVersion: 3query GetUsers($page: Int, $limit: Int) { users(page: $page, limit: $limit) { id name email role }}
mutation CreateUser($input: CreateUserInput!) { createUser(input: $input) { id name email }}# Генерация типов и сервисовnpm run codegen
# Результат — типизированные Angular сервисы!# GetUsersGQL — query сервис# CreateUserGQL — mutation сервис// Использование сгенерированных сервисовimport { GetUsersGQL, CreateUserGQL, GetUsersQuery } from './graphql/generated';
@Component({...})export class UsersComponent { users$ = this.getUsersGQL.watch({ page: 1, limit: 10 }) .valueChanges.pipe( map(result => result.data.users) );
constructor( private getUsersGQL: GetUsersGQL, private createUserGQL: CreateUserGQL, ) {}
createUser(name: string, email: string): void { this.createUserGQL.mutate({ input: { name, email } }) .subscribe(result => console.log('Создан:', result.data?.createUser)); }}Нормализация кэша и Fragment
Заголовок раздела «Нормализация кэша и Fragment»// Fragments — переиспользуемые части запросовconst USER_FRAGMENT = gql` fragment UserFields on User { id name email role avatar }`;
const GET_USERS_WITH_FRAGMENT = gql` ${USER_FRAGMENT} query GetUsersDetailed { users { ...UserFields posts { id title } } }`;
// Чтение/запись отдельных объектов в кэшcache.writeFragment({ id: cache.identify({ __typename: 'User', id: '1' }), fragment: USER_FRAGMENT, data: { id: '1', name: 'Обновлённое имя', role: 'admin', avatar: null, },});Playground 🎮
Заголовок раздела «Playground 🎮»Симулятор GraphQL запросов и мутаций: