Создание болтовни - часть 5: разбиение на страницы в GraphQL

Клон WhatsApp с React Native и Apollo

Это пятый блог в серии, состоящей из нескольких частей, в которой мы будем создавать Chatty, клон WhatsApp, используя React Native и Apollo. Посмотреть код этой части серии вы можете здесь.

В этом руководстве мы кратко рассмотрим, как разбивать данные на страницы с помощью GraphQL. Постепенно загружая данные вместо того, чтобы получать их сразу, мы можем значительно улучшить производительность нашего приложения.

Вот что мы сделаем в этом уроке:

  1. Обзор различных стратегий пагинации
  2. Определите лучшую стратегию нумерации страниц для применения в Chatty
  3. Включите разбиение на страницы в схеме и преобразователях на нашем сервере
  4. Включите разбиение на страницы в запросы и макет нашего клиента React Native

Стратегии разбивки на страницы

Давайте посмотрим на 3 распространенные стратегии разбивки на страницы и их основные преимущества и недостатки:

  1. Нумерация страниц
  2. Курсоры
  3. Подключение курсора реле

Для более глубокого чтения о стратегиях пагинации, а также о том, когда и как их использовать, я настоятельно рекомендую проверить Понимание разбивки на страницы: REST, GraphQL и Relay от Сашко Стубайло.

Нумерация страниц

Подумайте о "о" в результатах поиска Goooooogle. Нумерацию страниц в ее наивной форме очень легко реализовать в SQL с помощью limit и offset:

// load the 4th page of messages
SELECT * FROM messages ORDER BY created_at DESC LIMIT 100 OFFSET 300;

Сила нумерации страниц в ее простоте. Это отличная стратегия для работы со статическим или упорядоченным контентом, который вряд ли изменится во время сеанса пользователя.

Слабость нумерации страниц связана с динамическими данными. Когда элементы добавляются или удаляются из нашего набора данных, мы можем пропустить элемент или показать один и тот же элемент дважды. Например, если мы добавили в наш набор данных новый элемент, который принадлежит первому в результатах, разбитых на страницы, при переходе на следующую страницу последний элемент на текущей странице будет отображаться во второй раз. Точно так же, если первый элемент будет удален, переход к следующей странице пропустит то, что было бы первым элементом на новой странице.

Однако, если наши результаты с разбивкой на страницы упорядочены по новейшим элементам, а элементы нельзя удалить, нумерация страниц может быть отличным вариантом для разбивки наших данных на страницы, особенно для бесконечных скроллеров.

Курсоры

Курсоры призваны решить ту самую проблему, которую представляет нумерация страниц. Курсоры во многом похожи на закладку - мы можем прикрепить их с того места, где остановились, и даже если мы случайно засунем в нашу книгу больше листов и вырвем кучу, мы все равно сможем найти то место, где остановились в последний раз.

Допустим, мы пытаемся отобразить список книг с разбивкой на страницы, отсортированный по названию. С курсором $, после которого стоит заголовок последней книги, показанной на текущей странице, мы могли бы получить следующую страницу результатов в SQL следующим образом:

SELECT * FROM books
WHERE title > $after
ORDER BY title LIMIT $page_size;

В GraphQL нам потребуется, чтобы наш ответ на запрос включал курсоры:

booksByTitle(after: "Moby Dick", pageSize: 10) {
  cursors {
    after
  }
  books {
    title
    author {
      firstName
      lastName
    }
  }
}

Курсоры решают проблемы, связанные с нумерацией страниц, но мы можем сделать даже лучше! В этой модели для нашего клиента единственный способ узнать, что он находится на последней странице, - это если мы отправим дополнительный запрос на дополнительные записи и получим пустой ответ. Более того, мы можем представить более сложные сценарии, в которых нам нужно знать курсор для любого заданного элемента в наших результатах, а не только для первого или последнего. Мы также действительно должны стремиться соответствовать стандартизированному ответу на любой запрос с разбивкой на страницы, а не создавать новые по мере продвижения. Enter, реле курсора соединений.

Подключение курсора реле

Соединения ретранслятора курсора определяют стандартный ответ GraphQL Query для данных с разбивкой на страницы. В нашем предыдущем booksByTitle примере это выглядело бы так:

booksByTitle(first:10 after:"Moby Dick") {
  edges {
    node {
      title
        author {
          firstName
          lastName
        }
      }
      cursor
    }
  }
  pageInfo {
    hasPreviousPage
    hasNextPage
  }
}

Вкратце, форма ответа - объект связи - содержит два элемента: edges и pageInfo.

Каждое ребро содержит node, который является самим элементом - в нашем случае книга - и cursor, который представляет курсор для узлового элемента. В идеале курсор должен быть сериализуемым непрозрачным курсором, то есть нам не нужно беспокоиться о его форматировании для работы разбивки на страницы. Итак, чтобы соответствовать спецификации, наш запрос booksByTitle должен выглядеть примерно так:

booksByTitle(first:10 after:"TW9ieSBEaWNr") {
  ...
}

Где «Моби Дик» был закодирован в кодировке base-64. Наша разбивка на страницы на основе курсора должна работать нормально, пока мы можем надежно сериализовать, кодировать и декодировать наш курсор.

Другая половина объекта подключения - pageInfo. pageInfo содержит всего два логических значения hasPreviousPage и hasNextPage, которые точно определяют, что вы ожидаете - доступна ли предыдущая или следующая страница.

С помощью этого объекта подключения мы можем выполнить новый запрос из любого курсора с любым количеством возвращаемых элементов. Мы сэкономим дополнительные обращения к серверу, когда мы дойдем до начала или конца страницы. Теперь у нас также есть стандартизированный способ написания любого запроса с разбивкой на страницы. Милая!

На самом деле самый большой недостаток Relay Cursor Connections - это количество кода и энергия, необходимые для выполнения. Мы также можем получить небольшое снижение производительности по сравнению с другими стратегиями, поскольку преобразователь выполняет немного больше работы на каждый элемент, а ответ немного больше.

Пагинация на сервере

Пора добавить в Chatty пагинацию!

Во-первых, давайте определим некоторые места, в которых разбивка на страницы имеет смысл.

Несомненно, разбивка на страницы крайне необходима для отображения сообщений в группе. Если бы мы сразу показывали каждое последнее сообщение в групповой переписке, все быстро стало бы ужасно. Мы также можем использовать разбиение на страницы для предварительного просмотра самого последнего сообщения для группы, прежде чем пользователь коснется темы группы.

Я не могу себе представить, что будет много случаев, когда пользователь будет принадлежать к сотням групп, поэтому давайте не будем разбивать группы на страницы, пока у нас не будет веской причины.

А как насчет друзей пользователя? Разбиение на страницы здесь может стать немного сложнее, но я собираюсь позвонить руководителю и сказать не сегодня - это неплохая функция, но она не обязательна. У большинства людей нет множества контактов. Даже если звонок станет немного дороже, он, скорее всего, не будет таким дорогим, и уж тем более до тех пор, пока у Chatty не появятся сотни тысяч пользователей. Возможно, мы реализуем это в будущем уроке :)

Во-первых, важно отметить, что нумерация страниц - это вполне допустимое решение для нашего случая использования, и его гораздо проще реализовать, чем Relay Cursor Connections. Наши сообщения всегда будут отсортированы по последним, и мы не планируем в ближайшее время удалять их. WhatsApp только что добавил возможность редактировать и удалять сообщения, и они существуют уже 8 лет. Действительно, в большинстве случаев нумерация страниц может быть покрыта. И когда мы добавим подписки в следующем руководстве, вы увидите, что даже когда данные постоянно добавляются и удаляются, мы все равно можем использовать нумерацию страниц, не сталкиваясь с проблемами.

Тем не менее, Relay Cursor Connections являются золотым стандартом для разбивки на страницы GraphQL, и хотя нумерация страниц нам вполне подходит, мы пойдем по более сложному пути, поэтому мы будем вооружены для более сложных случаев разбивки на страницы. линия.

Давайте запрограммируем это!

Схема подключения курсора реле

Когда мы запрашиваем сообщения для данной группы, мы не используем messages запрос, мы используем group. В настоящее время мы запрашиваем только Messages в контексте Group, и это имеет смысл, потому что маловероятно, что нам нужны только сообщения сами по себе.

Поэтому, если мы запрашиваем Messages внутри Group с формой соединения релейного курсора, он должен выглядеть примерно так:

group(id: 1) {
  id
  name
  # ... other group fields here
  messages(first:10 after:"TW9ieSBEaWNr") {
    edges {
      node { # this is the message!
        id
        text
        # ... other message fields here
      }
      cursor # this is an opaque serializable identifier... a String
    }
    pageInfo {
      hasPreviousPage # boolean
      hasNextPage # boolean
    }
  }
}

Прохладный! Давайте сначала изменим нашу схему, чтобы она соответствовала этой форме.

Нам нужно объявить три новых типа в нашей схеме для соединений релейных курсоров:

  1. MessageConnection - тип оболочки, в которой будут храниться поля edges и pageInfo.
  2. MessageEdge - тип, используемый для edges и содержащий поля node и cursor.
  3. PageInfo - тип, используемый для pageInfo и содержащий поля hasPreviousPage и hasNextPage.

Нам также необходимо изменить поле Group messages, чтобы оно принимало аргументы подключения курсора реле и возвращало MessageConnection вместо массива Messages:

Шаг 5.1: Обновите схему с подключением релейного курсора

Изменено server/data/schema.js

...
  # declare custom scalars
  scalar Date
  type MessageConnection {
    edges: [MessageEdge]
    pageInfo: PageInfo!
  }
  type MessageEdge {
    cursor: String!
    node: Message!
  }
  type PageInfo {
    hasNextPage: Boolean!
    hasPreviousPage: Boolean!
  }
  # a group chat entity
  type Group {
    id: Int! # unique id for the group
    name: String # name of the group
    users: [User]! # users in the group
    messages(first: Int, after: String, last: Int, before: String): MessageConnection # messages sent to the group
  }
  # a user -- keep type really simple for now

Теперь вместо того, чтобы запрашивать все сообщения, когда мы запрашиваем группу или группы, мы будем указывать first n MessageEdges after предоставленный курсор (или last n MessageEdges before предоставленный курсор).

Резольверы подключения курсора реле

Нам нужно обновить наши преобразователи в server/data/resolvers.js, чтобы они соответствовали спецификации, которую мы только что указали.

Нашей первой задачей должно быть определение курсора, который мы будем использовать для отслеживания, какие сообщения нужно получить.

Когда мы создаем новый Messages в SQLite, новый Message id основан на одноатомном увеличивающемся целом числе - причудливый способ сказать, что новые Messages всегда будут иметь более высокие id, чем старые сообщения. Мы можем использовать эту изящную функцию для создания курсора на Message id! Например, если мы запросили первые 10 Messages после Message с id = 25, мы могли бы выполнить следующий запрос продолжения:

Message.findAll({
  where: {
    groupId: 1, // get messages within Group with id = 1
    id: { $lt: 25 }, // get messages before Message #25 -- i.e. message.id < 25
  },
  order: [['id', 'DESC']], // return messages from newest to oldest
  limit: 10,
})

Однако помните, что мы должны использовать сериализуемый непрозрачный курсор, а не целое число. Мы просто преобразуем Message id в строку base64, чтобы соответствовать этой спецификации.

После того, как мы получим Messages из нашего запроса на продолжение, нам все равно нужно преобразовать наши результаты, чтобы они соответствовали нашему типу MessageConnection. Нам нужно будет перебрать наши возвращенные Сообщения и создать edge для каждого, с Message в качестве узла и base64(Message.id) в качестве cursor.

Наконец, нам нужно определить _65 _ / _ 66_. Это можно просто сделать, запросив, есть ли еще Message после / до возвращаемых результатов. Также неплохо сохранить pageInfo запросы как отдельные функции на случай, если клиент не запросит их - небольшое приятное улучшение производительности.

Хорошо, хватит теории - вот код:

Шаг 5.2: Обновите резольверы с подключением релейного курсора

Изменено server/data/resolvers.js

...
import GraphQLDate from 'graphql-date';
import { Group, Message, User } from './connectors';
export const resolvers = {
  Date: GraphQLDate,
  PageInfo: {
    // we will have each connection supply its own hasNextPage/hasPreviousPage functions!
    hasNextPage(connection, args) {
      return connection.hasNextPage();
    },
    hasPreviousPage(connection, args) {
      return connection.hasPreviousPage();
    },
  },
  Query: {
    group(_, args) {
      return Group.find({ where: args });
...
    users(group) {
      return group.getUsers();
    },
    messages(group, { first, last, before, after }) {
      // base query -- get messages from the right group
      const where = { groupId: group.id };
      // because we return messages from newest -> oldest
      // before actually means newer (id > cursor)
      // after actually means older (id < cursor)
      if (before) {
        // convert base-64 to utf8 id
        where.id = { $gt: Buffer.from(before, 'base64').toString() };
      }
      if (after) {
        where.id = { $lt: Buffer.from(after, 'base64').toString() };
      }
      return Message.findAll({
        where,
        order: [['id', 'DESC']],
        limit: first || last,
      }).then((messages) => {
        const edges = messages.map(message => ({
          cursor: Buffer.from(message.id.toString()).toString('base64'), // convert id to cursor
          node: message, // the node is the message itself
        }));
        return {
          edges,
          pageInfo: {
            hasNextPage() {
              if (messages.length < (last || first)) {
                return Promise.resolve(false);
              }
              return Message.findOne({
                where: {
                  groupId: group.id,
                  id: {
                    [before ? '$gt' : '$lt']: messages[messages.length - 1].id,
                  },
                },
                order: [['id', 'DESC']],
              }).then(message => !!message);
            },
            hasPreviousPage() {
              return Message.findOne({
                where: {
                  groupId: group.id,
                  id: where.id,
                },
                order: [['id']],
              }).then(message => !!message);
            },
          },
        };
      });
    },
  },

Быстрый тест в GraphQL Playground показывает, что все в порядке:

Пагинация в React Native

Мы собираемся обновить наш клиент React Native для разбивки сообщений на страницы с помощью бесконечной прокрутки при просмотре групповой цепочки.

FlatList имеет функцию onEndReached, которая срабатывает, когда пользователь прокручивает список ближе к концу (мы можем установить, насколько близко нужно запустить функцию, с помощью onEndReachedThreshold). Однако приложения для обмена сообщениями, подобные нашему, обычно отображают самые новые сообщения внизу списка, что означает, что мы загружаем старые данные вверху. Это противоположно тому, как работает большинство списков, поэтому нам нужно изменить наш FlatList, чтобы он был перевернут, чтобы onEndReached срабатывал, когда мы приближаемся к началу списка, а не к его низу. Мы можем использовать флаг inverted на FlatList, который переворачивает отображение списка с помощью хитрого трюка, просто используя CSS.

Шаг 5.3. Используйте перевернутый FlatList для сообщений

Изменено client/src/screens/messages.screen.js

...

    this.state = {
      usernameColors,
      refreshing: false,
    };
    this.renderItem = this.renderItem.bind(this);
    this.send = this.send.bind(this);
    this.onEndReached = this.onEndReached.bind(this);
  }
  componentWillReceiveProps(nextProps) {
...
    }
  }
  onEndReached() {
    console.log('TODO: onEndReached');
  }
  send(text) {
    this.props.createMessage({
      groupId: this.props.navigation.state.params.groupId,
      userId: 1, // faking the user for now
      text,
    }).then(() => {
      this.flatList.scrollToIndex({ index: 0, animated: true });
    });
  }
...
    const { loading, group } = this.props;
    // render loading placeholder while we fetch messages
    if (loading || !group) {
      return (
        <View style={[styles.loading, styles.container]}>
          <ActivityIndicator />
...
      >
        <FlatList
          ref={(ref) => { this.flatList = ref; }}
          inverted
          data={group.messages}
          keyExtractor={this.keyExtractor}
          renderItem={this.renderItem}
          ListEmptyComponent={<View />}
          onEndReached={this.onEndReached}
        />
        <MessageInput send={this.send} />
      </KeyboardAvoidingView>

Теперь давайте обновим GROUP_QUERY в client/src/graphql/group.query.js в соответствии с нашей последней схемой:

Шаг 5.4: Обновление группового запроса с подключениями релейного курсора

Изменено client/src/graphql/group.query.js

...
import MESSAGE_FRAGMENT from './message.fragment';
const GROUP_QUERY = gql`
  query group($groupId: Int!, $first: Int, $after: String, $last: Int, $before: String) {
    group(id: $groupId) {
      id
      name
...
        id
        username
      }
      messages(first: $first, after: $after, last: $last, before: $before) {
        edges {
          cursor
          node {
            ... MessageFragment
          }
        }
        pageInfo {
          hasNextPage
          hasPreviousPage
        }
      }
    }
  }

Теперь у нас есть возможность передавать переменные first, after, last и before в запрос group, вызываемый нашим Messages компонентом. Эти переменные будут переданы в наше поле messages, где мы получим MessageConnection со всеми необходимыми полями.

Нам нужно указать, как group должен выглядеть при первом запуске и как загрузить больше записей с помощью того же запроса. Модуль graphql в react-apollo предоставляет функцию fetchMore в опоре данных, где мы можем определить, как обновлять наш запрос и наши данные:

Шаг 5.5. Добавьте fetchMore в groupQuery

Изменено client/package.json

...
		"apollo-link-error": "^1.0.7",
		"apollo-link-http": "^1.3.3",
		"apollo-link-redux": "^0.2.1",
		"buffer": "^5.0.8",
		"graphql": "^0.12.3",
		"graphql-tag": "^2.4.2",
		"immutability-helper": "^2.6.4",

Изменено client/src/screens/messages.screen.js

...
import React, { Component } from 'react';
import randomColor from 'randomcolor';
import { graphql, compose } from 'react-apollo';
import update from 'immutability-helper';
import { Buffer } from 'buffer';
import Message from '../components/message.component';
import MessageInput from '../components/message-input.component';
...

    this.state = {
      usernameColors,
    };
    this.renderItem = this.renderItem.bind(this);
...
    });
  }
  keyExtractor = item => item.node.id.toString();
  renderItem = ({ item: edge }) => {
    const message = edge.node;
    return (
      <Message
        color={this.state.usernameColors[message.from.username]}
        isCurrentUser={message.from.id === 1} // for now until we implement auth
        message={message}
      />
    );
  }
  render() {
    const { loading, group } = this.props;
...
        <FlatList
          ref={(ref) => { this.flatList = ref; }}
          inverted
          data={group.messages.edges}
          keyExtractor={this.keyExtractor}
          renderItem={this.renderItem}
          ListEmptyComponent={<View />}
...
    }),
  }),
  group: PropTypes.shape({
    messages: PropTypes.shape({
      edges: PropTypes.arrayOf(PropTypes.shape({
        cursor: PropTypes.string,
        node: PropTypes.object,
      })),
      pageInfo: PropTypes.shape({
        hasNextPage: PropTypes.bool,
        hasPreviousPage: PropTypes.bool,
      }),
    }),
    users: PropTypes.array,
  }),
  loading: PropTypes.bool,
  loadMoreEntries: PropTypes.func,
};
const ITEMS_PER_PAGE = 10;
const groupQuery = graphql(GROUP_QUERY, {
  options: ownProps => ({
    variables: {
      groupId: ownProps.navigation.state.params.groupId,
      first: ITEMS_PER_PAGE,
    },
  }),
  props: ({ data: { fetchMore, loading, group } }) => ({
    loading,
    group,
    loadMoreEntries() {
      return fetchMore({
        // query: ... (you can specify a different query.
        // GROUP_QUERY is used by default)
        variables: {
          // load more queries starting from the cursor of the last (oldest) message
          after: group.messages.edges[group.messages.edges.length - 1].cursor,
        },
        updateQuery: (previousResult, { fetchMoreResult }) => {
          // we will make an extra call to check if no more entries
          if (!fetchMoreResult) { return previousResult; }
          // push results (older messages) to end of messages list
          return update(previousResult, {
            group: {
              messages: {
                edges: { $push: fetchMoreResult.group.messages.edges },
                pageInfo: { $set: fetchMoreResult.group.messages.pageInfo },
              },
            },
          });
        },
      });
    },
  }),
});
...
            query: GROUP_QUERY,
            variables: {
              groupId,
              first: ITEMS_PER_PAGE,
            },
          });
          // Add our message from the mutation to the end.
          groupData.group.messages.edges.unshift({
            __typename: 'MessageEdge',
            node: createMessage,
            cursor: Buffer.from(createMessage.id.toString()).toString('base64'),
          });
          // Write our data back to the cache.
          store.writeQuery({
            query: GROUP_QUERY,
            variables: {
              groupId,
              first: ITEMS_PER_PAGE,
            },
            data: groupData,
          });

Мы указали first: 10 при первом запуске запроса. Когда наш компонент выполняет this.props.loadMoreEntries, мы обновляем after курсор с помощью cursor последнего edge из наших предыдущих результатов, извлекаем еще до 10 сообщений и обновляем состояние нашего приложения, чтобы сдвинуть края до конца нашего набора данных и установить, есть ли там это следующая страница.

Поскольку сейчас мы возвращаем edges, нам нужно обновить наш компонент Messages, чтобы искать group.messages.edges[x].node вместо group.messages[x].

Нам также необходимо изменить функцию update в наших мутациях, чтобы она соответствовала нашим обновленным переменным GROUP_QUERY.

Мы также должны создавать и добавлять edge к нашим кэшированным данным запроса всякий раз, когда мы создаем новый Message. Это означает также получение cursor для созданного нами нового Message.

Наконец, нам нужно обновить компонент Messages для вызова this.props.loadMoreEntries, когда мы вызываем onEndReached:

Шаг 5.6. Примените loadMoreEntries к onEndReached

Изменено client/src/screens/messages.screen.js

...
  }
  onEndReached() {
    if (!this.state.loadingMoreEntries &&
      this.props.group.messages.pageInfo.hasNextPage) {
      this.setState({
        loadingMoreEntries: true,
      });
      this.props.loadMoreEntries().then(() => {
        this.setState({
          loadingMoreEntries: false,
        });
      });
    }
  }
  send(text) {

Загрузите его для разбивки на страницы!

Мы также можем изменить компонент «Группы», чтобы предварительно просмотреть самое последнее сообщение для каждой группы. Используя ту же методологию, сначала обновим USER_QUERY:

Шаг 5.7. Добавьте самое последнее сообщение в каждую группу в USER_QUERY

Изменено client/src/graphql/create-group.mutation.js

...
import gql from 'graphql-tag';
import MESSAGE_FRAGMENT from './message.fragment';
const CREATE_GROUP_MUTATION = gql`
  mutation createGroup($name: String!, $userIds: [Int!], $userId: Int!) {
    createGroup(name: $name, userIds: $userIds, userId: $userId) {
...
      users {
        id
      }
      messages(first: 1) { # we don't need to use variables
        edges {
          cursor
          node {
            ... MessageFragment
          }
        }
      }
    }
  }
  ${MESSAGE_FRAGMENT}
`;
export default CREATE_GROUP_MUTATION;

Изменено client/src/graphql/user.query.js

...
import gql from 'graphql-tag';
import MESSAGE_FRAGMENT from './message.fragment';
// get the user and all user's groups
export const USER_QUERY = gql`
  query user($id: Int) {
...
      groups {
        id
        name
        messages(first: 1) { # we don't need to use variables
          edges {
            cursor
            node {
              ... MessageFragment
            }
          }
        }
      }
      friends {
        id
...
      }
    }
  }
  ${MESSAGE_FRAGMENT}
`;
export default USER_QUERY;

Затем мы обновляем макет компонента элемента списка групп в Groups:

Шаг 5.8: Измените компонент« Группа , чтобы включить в него последнее сообщение»

Изменено client/src/screens/groups.screen.js

...
  FlatList,
  ActivityIndicator,
  Button,
  Image,
  StyleSheet,
  Text,
  TouchableHighlight,
  View,
} from 'react-native';
import { graphql } from 'react-apollo';
import moment from 'moment';
import Icon from 'react-native-vector-icons/FontAwesome';
import { USER_QUERY } from '../graphql/user.query';
...
    fontWeight: 'bold',
    flex: 0.7,
  },
  groupTextContainer: {
    flex: 1,
    flexDirection: 'column',
    paddingLeft: 6,
  },
  groupText: {
    color: '#8c8c8c',
  },
  groupImage: {
    width: 54,
    height: 54,
    borderRadius: 27,
  },
  groupTitleContainer: {
    flexDirection: 'row',
  },
  groupLastUpdated: {
    flex: 0.3,
    color: '#8c8c8c',
    fontSize: 11,
    textAlign: 'right',
  },
  groupUsername: {
    paddingVertical: 4,
  },
  header: {
    alignItems: 'flex-end',
    padding: 6,
...
  },
});
// format createdAt with moment
const formatCreatedAt = createdAt => moment(createdAt).calendar(null, {
  sameDay: '[Today]',
  nextDay: '[Tomorrow]',
  nextWeek: 'dddd',
  lastDay: '[Yesterday]',
  lastWeek: 'dddd',
  sameElse: 'DD/MM/YYYY',
});
const Header = ({ onPress }) => (
  <View style={styles.header}>
    <Button title={'New Group'} onPress={onPress} />
...
  }
  render() {
    const { id, name, messages } = this.props.group;
    return (
      <TouchableHighlight
        key={id}
        onPress={this.goToMessages}
      >
        <View style={styles.groupContainer}>
          <Image
            style={styles.groupImage}
            source={{
              uri: 'https://reactjs.org/logo-og.png',
            }}
          />
          <View style={styles.groupTextContainer}>
            <View style={styles.groupTitleContainer}>
              <Text style={styles.groupName}>{`${name}`}</Text>
              <Text style={styles.groupLastUpdated}>
                {messages.edges.length ?
                  formatCreatedAt(messages.edges[0].node.createdAt) : ''}
              </Text>
            </View>
            <Text style={styles.groupUsername}>
              {messages.edges.length ?
                `${messages.edges[0].node.from.username}:` : ''}
            </Text>
            <Text style={styles.groupText} numberOfLines={1}>
              {messages.edges.length ? messages.edges[0].node.text : ''}
            </Text>
          </View>
          <Icon
            name="angle-right"
            size={24}
            color={'#8c8c8c'}
          />
        </View>
      </TouchableHighlight>
    );
...
  group: PropTypes.shape({
    id: PropTypes.number,
    name: PropTypes.string,
    messages: PropTypes.shape({
      edges: PropTypes.arrayOf(PropTypes.shape({
        cursor: PropTypes.string,
        node: PropTypes.object,
      })),
    }),
  }),
};

Обновление данных

Мы можем применить некоторые из только что изученных приемов, чтобы дать пользователям возможность обновлять данные вручную. В настоящее время, если пользователь отправляет сообщение группе, это новое сообщение не отображается как последнее сообщение на странице групп.

Мы могли бы решить эту проблему, изменив update в sendMessage, чтобы обновить USER_QUERY запрос. Но давайте отложим внедрение этого исправления и воспользуемся этой возможностью, чтобы протестировать обновление вручную.

В дополнение к fetchMore, graphql также предоставляет refetch функцию в свойствах данных. Выполнение этой функции заставит запрос выполнить повторную выборку данных.

Мы можем изменить наш FlatList для использования встроенного RefreshControl компонента через onRefresh. Когда пользователь открывает список, FlatList запускает onRefresh, где мы refetch запрос user.

Нам также необходимо передать параметр refreshing в FlatList, чтобы он знал, когда показывать или скрывать RefreshControl. Мы можем просто установить refreshing для проверки networkStatus нашего запроса. networkStatus === 4 означает, что данные все еще загружаются.

Шаг 5.9: Ручное обновление групп

Изменено client/src/screens/groups.screen.js

...
    super(props);
    this.goToMessages = this.goToMessages.bind(this);
    this.goToNewGroup = this.goToNewGroup.bind(this);
    this.onRefresh = this.onRefresh.bind(this);
  }
  onRefresh() {
    this.props.refetch();
  }
  keyExtractor = item => item.id.toString();
...
  renderItem = ({ item }) => <Group group={item} goToMessages={this.goToMessages} />;
  render() {
    const { loading, user, networkStatus } = this.props;
    // render loading placeholder while we fetch messages
    if (loading || !user) {
...
          keyExtractor={this.keyExtractor}
          renderItem={this.renderItem}
          ListHeaderComponent={() => <Header onPress={this.goToNewGroup} />}
          onRefresh={this.onRefresh}
          refreshing={networkStatus === 4}
        />
      </View>
    );
...
    navigate: PropTypes.func,
  }),
  loading: PropTypes.bool,
  networkStatus: PropTypes.number,
  refetch: PropTypes.func,
  user: PropTypes.shape({
    id: PropTypes.number.isRequired,
    email: PropTypes.string.isRequired,
...

const userQuery = graphql(USER_QUERY, {
  options: () => ({ variables: { id: 1 } }), // fake the user for now
  props: ({ data: { loading, networkStatus, refetch, user } }) => ({
    loading, networkStatus, refetch, user,
  }),
});

Загрузите это!

Теперь, когда мы видим, что ручное обновление работает, давайте исправим update в sendMessage, чтобы обновить USER_QUERY запрос, так что ручное обновление требуется только для странных крайних случаев, а не во всех случаях!

Шаг 5.10. Измените мутацию createMessage, чтобы обновить USER_QUERY

Изменено client/src/screens/messages.screen.js

...
import { graphql, compose } from 'react-apollo';
import update from 'immutability-helper';
import { Buffer } from 'buffer';
import _ from 'lodash';
import moment from 'moment';
import Message from '../components/message.component';
import MessageInput from '../components/message-input.component';
import GROUP_QUERY from '../graphql/group.query';
import CREATE_MESSAGE_MUTATION from '../graphql/create-message.mutation';
import USER_QUERY from '../graphql/user.query';
const styles = StyleSheet.create({
  container: {
...
            },
            data: groupData,
          });
          const userData = store.readQuery({
            query: USER_QUERY,
            variables: {
              id: 1, // faking the user for now
            },
          });
          // check whether the mutation is the latest message and update cache
          const updatedGroup = _.find(userData.user.groups, { id: groupId });
          if (!updatedGroup.messages.edges.length ||
            moment(updatedGroup.messages.edges[0].node.createdAt).isBefore(moment(createMessage.createdAt))) {
            // update the latest message
            updatedGroup.messages.edges[0] = {
              __typename: 'MessageEdge',
              node: createMessage,
              cursor: Buffer.from(createMessage.id.toString()).toString('base64'),
            };
            // Write our data back to the cache.
            store.writeQuery({
              query: USER_QUERY,
              variables: {
                id: 1, // faking the user for now
              },
              data: userData,
            });
          }
        },
      }),

Огромный успех!

Это все для части 5. В следующей части этой серии мы добавим Подписки GraphQL для обмена мгновенными сообщениями в реальном времени!

Как всегда, поделитесь своими мыслями, вопросами, проблемами и открытиями ниже!

Вы можете просмотреть код этого руководства здесь

Продолжить создание болтовни - Часть 6 (Подписки GraphQL)

Полезные ресурсы

Шаги

  1. "Настраивать"
  2. Запросы GraphQL с Express
  3. Запросы GraphQL с React Apollo
  4. Мутации GraphQL и оптимистичный интерфейс
  5. Пагинация GraphQL
  6. Подписки GraphQL
  7. Аутентификация GraphQL
  8. Типы ввода GraphQL