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

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

Эта тема довольно плотная, поэтому, если вам сложно следить за ней, я рекомендую вам выполнить форк и клонировать этот репозиторий. Теперь, без лишних слов, давайте разберемся!

Фон: что такое разбиение на страницы?

Задумайтесь на мгновение о том, что происходит, когда пользователь просматривает контент в типичном веб-приложении. Пользователь прокручивает список объектов, но поначалу этот список довольно короткий - скажем, для этого примера он состоит из пятидесяти элементов. Когда пользователь прокручивает объект № 50, приложение приостанавливает загрузку следующего набора из пятидесяти объектов для просмотра; на объекте № 100 приложение снова приостанавливается для загрузки следующего набора и так далее.

Что происходит за занавеской? Для каждого набора объектов приложение отправляет запрос веб-API, который содержит информацию о характере запроса. Сервер, на котором размещен API, отвечает с отображаемыми объектами вместе с информацией о контексте запроса. С каждым запросом документ становится все длиннее и длиннее, поскольку приложение отображает результат каждого запроса.

Эээ, ладно… так?

JavaScript работает быстро - настолько быстро, что ни один человек не может понять, что он запущен! HTTP-запросы, с другой стороны, по сравнению с ними медленны, как патока. При запросе пятидесяти объектов задержка не очень заметна, но как насчет запроса пятидесяти тысяч или пятидесяти миллионов объектов?

Twitter API, например, содержит миллиарды и миллиарды твитов. Даже если вы не собираете каждый твит в Twitterverse для отображения единственного профиля, запрос API для твитов отдельного пользователя замедлит ваше приложение до сканирования, если в этом профиле их десятки тысяч. Это огромная проблема, когда вы доставляете контент непостоянному, придирчивому пользователю, у которого есть дела и места. Одно недавнее исследование показало, что пользователи теряют интерес к веб-сайту всего через десять секунд ожидания.

Введите нумерацию страниц. Практически каждый веб-API позволяет (или заставляет) инженера ограничивать запрос до определенного размера. Затем API может предоставить инженеру инструменты для разбивки на страницы через API с этим ограничением - точно так же, как листать страницы в книге. Каждая страница представляет собой фрагмент данных API ограниченного размера, поэтому в нашем типичном приложении, приведенном выше, где ограничение составляет пятьдесят объектов, страница 1 включает объекты 1–50, страница 2 включает объекты 51–100 и т. Д. Ограничения с разбивкой на страницы дают инженерам всю гибкость и мощь огромного API со встроенной стратегией, позволяющей избежать чрезмерной нагрузки HTTP-запроса с большим объемом данных.

Запуск json-сервера

настоящий веб-API находится на сервере где-то в пустыне в Калифорнии или Израиле, а нет на компьютере Джоша в его квартире в Адской кухне… но действительно забавные стоят деньги , и они часто не предоставляют нам полноценные функции CRUD. Итак, чтобы изучить и протестировать, мы будем использовать пакет под названием json-server, модуль узла (расширение базового кода JavaScript), который имитирует / имитирует поведение внешнего API.

Документация для json-server не очень полезна для новичков, но поскольку веб-API полустандартизированы, маршруты / ответы, которые мы можем использовать, выглядят довольно согласованными с тем, что мы получаем от внешнего API. Вы можете сами убедиться в этом в репозитории этого сообщения в блоге, в котором есть db.json файл с данными о населении округов США. Скопируйте и клонируйте репозиторий, затем установите json-server через терминал с помощью команды npm install -g json-server. После установки введите json-server db.json, и вы увидите сообщение, показывающее, какой порт вы слушаете:

\{^_^}/ hi!
Loading db.json
  Done
Resources
  http://localhost:3000/us-counties
Home
  http://localhost:3000
Type s + enter at any time to create a snapshot of the database

Теперь в вашем любимом браузере вы можете перейти к http://localhost:3000/us-counties и увидеть гигантский список округов с их населением на момент последней переписи:

{
   "us-counties": [
   {
      "population": 55200,
      "state": "Alabama",
      "name": "Autauga",
      "type": "County"
   },
   {
      "population": 208107,
      "state": "Alabama",
      "name": "Baldwin",
      "type": "County"
   },
...

Быстрый просмотр json-server документации показывает нам маршруты, которые мы можем использовать и комбинировать, следуя знакомым соглашениям RESTful. Параметры маршрута начинаются с ? и могут быть связаны с & - так, например, маршрут http://localhost:3000/us-counties?state=New%20York&_sort=name дает нам список 62 округов штата Нью-Йорк в алфавитном порядке.

Обратите особое внимание на наши маршруты нумерации страниц, _page и _limit. Эти маршруты позволяют нам _limit размер нашего вызова API до _page, что соответствует размеру нашего _limit. Таким образом, маршрут http://localhost:3000/us-counties?state=New%20York&_sort=population&_order=desc&_limit=10&_page=2 покажет нам с десятого по двадцатый по численности населения округ в штате Нью-Йорк.

Изучение заголовка ссылки

Помните, я сказал выше, что API-интерфейсы отвечают на запросы данными - гораздо большим количеством данных, чем просто запрошенные объекты. Google Chrome упрощает изучение этих данных, предоставляя нам вкладку Сеть в консоли браузера:

Обратите особое внимание на заголовок Ссылка, который отображается в маршрутах пагинации. Давайте продолжим разбивать страницы по округам Нью-Йорка и изучим заголовки ссылок на &_page=2 маршрута ?state=New%20York&_sort=population:

<http://localhost:3000/us-counties?state=New%20York&_limit=10&_page=1>; rel="first", <http://localhost:3000/us-counties?state=New%20York&_limit=10&_page=2>; rel="prev", <http://localhost:3000/us-counties?state=New%20York&_limit=10&_page=4>; rel="next", <http://localhost:3000/us-counties?state=New%20York&_limit=10&_page=7>; rel="last"

Как удобно! Заголовок ссылки дает нам URL-адреса для первой, предыдущей, следующей и последней страниц нашего API в соответствии с текущим _limit! Более того, ссылки prev и next не появляются на первой и последней страницах соответственно. Мы воспользуемся этим, чтобы убедиться, что пользователь не может перейти на несуществующую страницу. А для получения заголовков ссылок и работы с ними легко использовать обычный, знакомый, готовый к работе JavaScript .fetch()!

fetch( currentUrl ).then( response => response.headers.get( "Link" ) ).then(...);

Разбор заголовка ссылки

Пока что звучит просто, правда? К сожалению, как и все в JavaScript, здесь есть одна большая загвоздка - на самом деле две из них…

  1. Заголовок ссылки отображается только в ответе _limit ed с несколькими _page s. Если вы попытаетесь вызвать response.headers.get( "Link" ) на маршруте только с одной страницей, вы получите null объект.
  2. Заголовок ссылки представляет собой строку - не массив или объект -, и вам придется проанализировать ее, чтобы Object сделать с ней что-нибудь полезное. .

У нас есть несколько вариантов синтаксического анализа заголовка ссылки, ни один из которых не упадет легко:

  • Вы можете использовать отличный бесплатный пакет под названием parse-link-header… но он должен быть установлен отдельно с npm install parse-link-header, и он не очень хорошо работает с большинством браузеров, особенно с браузерами мобильных устройств;
  • Вы можете использовать регулярное выражение, например эту мозговую бомбу: /^(?:(?:(([^:\/#\?]+:)?(?:(?:\/\/)(?:(?:(?:([^:@\/#\?]+)(?:\:([^:@\/#\?]*))?)@)?(([^:\/#\?\]\[]+|\[[^\/\]@#?]+\])(?:\:([0-9]+))?))?)?)?((?:\/?(?:[^\/\?#]+\/+)*)(?:[^\?#]*)))?(\?[^#]+)?)(#.*)?/… но даже если вы не читали мои предыдущие сообщения в блоге, вы можете сказать, что работать с регулярными выражениями так же весело, как промывать глаза скипидаром;
  • Итак, в этом сообщении блога, чтобы продемонстрировать и объяснить, я воспользуюсь наивным подходом и использую свой верный набор ножей для гинсу JavaScipt для .split(), .map(), .replace() и .slice() заголовка ссылки в пятизвездочном гурмане Object:
function parseLinkHeader( linkHeader ) {
   return Object.fromEntries( linkHeader.split( ", " ).map( header => header.split( "; " ) ).map( header => [ header[1].replace( /"/g, "" ).replace( "rel=", "" ), header[0].slice( 1, -1 ) ] ) );
}

Поскольку этот метод представляет собой гноящуюся раковую опухоль, которая нарушает все принципы хорошей практики программирования, вот идентичная версия, разделенная на несколько строк, чтобы мы могли понять, что происходит с нашим (относительно) психическим здоровьем:

function parseLinkHeader( linkHeader ) {
   const linkHeadersArray = linkHeader.split( ", " ).map( header => header.split( "; " ) );
   const linkHeadersMap = linkHeadersArray.map( header => {
      const thisHeaderRel = header[1].replace( /"/g, "" ).replace( "rel=", "" );
      const thisHeaderUrl = header[0].slice( 1, -1 );
      return [ thisHeaderRel, thisHeaderUrl ]
   } );
   return Object.fromEntries( linkHeadersMap );
}

Вот построчная разбивка на примере из Нью-Йорка выше:

  • Сначала давайте создадим переменную linkHeadersArray, .split() поместив отдельные маршруты и rel в linkHeader запятыми, а затем разделив каждую пару маршрутов и rel точками с запятой с .map(), чтобы получить следующий вложенный массив:
[
   [ '<http://localhost:3000/us-counties?state=New%20York&_limit=10&_page=1>', 'rel="first"' ],
   [ '<http://localhost:3000/us-counties?state=New%20York&_limit=10&_page=2>', 'rel="prev"' ],
   [ '<http://localhost:3000/us-counties?state=New%20York&_limit=10&_page=4>', 'rel="next"' ],
   [ '<http://localhost:3000/us-counties?state=New%20York&_limit=10&_page=7>', 'rel="last"' ]
]
  • Затем давайте превратим linkHeadersArray в linkHeadersMap с помощью .map() функции, которая удаляет караты из URL с помощью .slice( 1, -1 ), удаляет все, кроме значения атрибута из rel с помощью .replace(), и меняет их местами;
  • Наконец, верните Object.fromEntries() из linkHeadersMap, что, наконец, даст нам идеально скроенный по индивидуальному заказу Object, который выглядит следующим образом:
{
   first: 'http://localhost:3000/us-counties?state=New%20York&_limit=10&_page=1',
   prev: 'http://localhost:3000/us-counties?state=New%20York&_limit=10&_page=2',
   next: 'http://localhost:3000/us-counties?state=New%20York&_limit=10&_page=4',
   last: 'http://localhost:3000/us-counties?state=New%20York&_limit=10&_page=7'
}

Использование заголовка ссылки

Теперь, когда мы принесли в жертву богам белоснежного быка в полночь при полной луне, чтобы написать parseLinkHeader() метод, мы можем использовать его в методе обратного вызова paginate( direction ). Теперь мы можем прослушивать 'click' на кнопке с addEventListener(), которая разбивает на страницы любую из наших четырех возможных целевых страниц - страницу "first", "prev", "next" или "last":

let currentUrl = "http://localhost:3000/us-counties?_limit=20&_page=1"
function paginate( direction ) {
   fetch( currentUrl ).then( response => {
      let linkHeaders = parseLinkHeader( response.headers.get( "Link" ) );
      if ( !!linkHeaders[ direction ] ) {
         currentUrl = linkHeaders[ direction ];
         fetchCounties( linkHeaders[ direction ] );
      }
   } );
}

Помните, я сказал выше, что мы воспользуемся тем фактом, что наш проанализированный заголовок ссылки дает нам только "prev" или "next" _page, если мы не на странице "first" или "last" соответственно. Это упрощает предотвращение перехода нашего пользователя на страницу до конца вселенной при нажатии на API нашего приложения. Все, что нам нужно сделать, это обернуть наш метод для получения следующей страницы условным запросом if ( !!linkHeaders[ direction ] ), который возвращает true и выполняет блок только, если заголовок нашей ссылки содержит направление, которое мы ищем. Таким образом, наш пользователь не сможет прервать работу нашего приложения, щелкнув предыдущую страницу, находясь на первой странице, или следующую страницу на последней странице.

Вывод: вот и я, снова играю звезду - вот и я, переверните страницу ...

Еще одна вещь, которую я указал выше, чтобы запомнить: заголовок ссылки появляется только на маршруте с разбивкой на страницы, и если вы попытаетесь разобрать заголовок ссылки null, вы утонете в яростном потоке Uncaught (in promise) ReferenceErrors. Вам нужно будет реализовать дополнительную логику, чтобы этого никогда не произошло, и вы можете увидеть именно такие примеры в репозитории этого сообщения в блоге. Большая часть этой обработки крайнего случая происходит в простой функции фильтра / поиска, которую я построил, потому что некоторые возможные поисковые запросы короткие - в Делавэре всего три округа!

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