Давайте оценим лучшие постеры в нашем любимом субреддите. Все, что нам нужно, это немного Javascript.

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

TL; DR - Где код?

Вот: https://github.com/jonnyk20/reddit-ranking

Что мы будем использовать

Это прекрасная возможность изучить и использовать ES6 и ES7, чтобы сделать наш код красивым и чистым. В частности, я объясню и использую следующее:

  • Стрелочные функции
  • Шаблонные литералы
  • Получить API
  • Асинхронный / Ожидание
  • Сокращение свойств объекта
  • Разрушение объекта
  • Аргументы по умолчанию

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

Перед тем, как мы начнем

  • Я надеюсь, что вы уже знакомы с HTML и CSS, а также с основами Javascript, а также с тем, как работают обещания.
  • Убедитесь, что вы используете последнюю версию Chrome, чтобы открыть этот проект (у меня 68.0.3440.84). С другими браузерами или более старыми версиями Chrome некоторые из используемых нами синтаксисов могут быть несовместимы, и вам пришлось бы перекомпилировать Javascript, чтобы заставить его работать.
  • Выберите свой любимый сабреддит для использования в качестве примера. Я использую «/ r / learnjavascript», но вы можете использовать другой, когда пишете свой.
  • Я дам краткий обзор каждой из концепций ES6 и ES7, которые мы будем использовать. Тем не менее, я также сделаю гиперссылку на каждую тему, чтобы получить ссылку для дальнейшего чтения.

Установка

Наше приложение просто покажет ввод и кнопку и будет работать следующим образом:

  1. Вы вводите имя субреддита во входных данных.
  2. Вы нажимаете ввод или щелкаете кнопку.
  3. Через несколько секунд появится ранжированный список карточек с плакатами, набравшими наибольшее количество баллов.
  4. Каждая карточка пользователя содержит имя пользователя, количество сообщений от этого пользователя и общую оценку этих сообщений.
  5. При нажатии на карточку открывается профиль этого пользователя на Reddit.

Использование довольно простое, как и структура. Все, что нам нужно, это 3 файла. Создайте новую папку для этого проекта и в ней создайте index.html, style.css и script.js.

В index.html создайте простой скелет HTML. В разделе заголовка импортируйте файлы style.css и script.js. Добавьте атрибут defer к тегу скрипта, чтобы он не запускался, пока ваш документ не будет загружен.

В теле добавьте h1 и форму с полем ввода и кнопкой. Под формой создайте div, который будет служить контейнером для результатов. Добавьте описательные атрибуты id в форму, входные данные и контейнер результатов.

<!DOCTYPE html>
<html>
<head>
  <title>Top Redditors</title>
  <link rel="stylesheet" type="text/css" media="screen" href="style.css" />
  <script src="script.js" defer></script>
</head>
<body>
  <h1>Top Redditors</h1>
  <form id="subreddit-select-form">
    <label>r/</label>
    <input id="subreddit" />
    <button type="submit">Rank</button>
  </form>
<div id="results-container">
  </div>
</body>
</html>

В style.css добавьте следующие стили:

body {
  text-align: center;
  color: #0079d3;
}
button {
  background-color: #0079d3;
  color: white;
  border-radius: 2px;
  border: none;
}
#results-container {
  display: flex;
  flex-direction: column;
}
.user {
  text-decoration: none;
  padding: 5px;
  border: solid 1px #0079d3;
  margin: 5px auto;
}

Вы больше не будете вносить никаких изменений ни в один из этих файлов. script.js - это то место, где будет выполняться большая часть работы. В нем для начала можно просто занести лог консоли, чтобы убедиться, что все работает нормально.

console.log('hello from script.js')

Откройте index.html из Chrome, и вы должны увидеть следующую страницу.

Используйте Command+Option+J (mac) или Control+Shift+J (PC), чтобы открыть консоль и увидеть сообщение от script.js.

Reddit API 101

Мы не сможем написать Javascript для взаимодействия с Reddits API, если не разберемся сначала. Давайте посмотрим, как получить JSON от Reddit и как его интерпретировать.

Получение данных JSON

Чтобы получить JSON о ресурсе из API Reddit, все, что нам нужно сделать, это добавить .json в конец URL-адреса, который мы обычно используем для доступа к htm-представлению этого ресурса. Например, чтобы просмотреть все сообщения из / r / learnjavacript, мы обычно набираем https://www.reddit.com/r/learnjavascript, чтобы отправить запрос на получение к этой точке. Чтобы получить данные публикации в формате JSON, нам просто нужно сделать запрос на получение https://www.reddit.com/r/learnjavascript Фак.json.

Попробуйте прямо сейчас, привязав его к адресной строке браузера и нажав клавишу ВВОД. В итоге вы получите что-то вроде этого:

Невозможно читать как есть, поэтому, чтобы увидеть структуру ответа, откройте вкладку «Сеть» в ваших инструментах разработки и на левой вкладке щелкните запрос под названием «learnjavascript.json», чтобы открыть его (обновите страницу, если вы не вижу). Затем нажмите вкладку «Предварительный просмотр» справа, чтобы увидеть возвращаемый объект.

Как использовать параметры для ограничения и разбивки на страницы.

Вы можете добавить параметры в свой запрос, добавив вопросительный знак «?» В конце строки запроса, за которым следуют параметры. Ключ и значения параметра разделяются знаками равенства, а каждый параметр - амперсандом «&».

В нашем конечном продукте запросы будут выглядеть следующим образом:

https://www.reddit.com/r/learnjavascript.json?limit=100&after=d8duc7vds7

Но что означают «ограничение» и «после»?

Лимит легче всего отменить. По умолчанию каждый запрос будет извлекать 25 сообщений, но вы можете увеличить это число до 100, добавив limit=, за которым следует желаемое количество сообщений.

Однако в самых популярных субреддитах содержится более 100 сообщений, поэтому нам нужно будет сделать несколько запросов, чтобы получить больше. Reddit помогает нам, предоставляя в своем ответе свойство «после». Эта строка символов похожа на закладку, которую мы можем использовать, чтобы сообщить Reddit, где мы остановились, если мы хотим пойти и получить больше сообщений.

Если, скажем, в сабреддите 1000 сообщений. Наш первый запрос вернет сообщения 1–100 вместе со свойством «после», например, «123acb». После этого мы можем сделать еще один запрос на Reddit вот так ...

https://www.reddit.com/r/learnjavascript.json?limit=100&after=123abc

... и Reddit api будет автоматически сообщать нам сообщения 101–200 в своем ответе. Во втором ответе он будет включать еще одно свойство «после», например, что-то вроде «xyz987». Мы можем использовать это для получения сообщений 201–300 ... и так далее, и так далее. Этот процесс может продолжаться до тех пор, пока не останется сообщений для выборки, и в этом случае свойство «after» в ответе будет null.

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

Главное событие: Javascript

Все настроено, пора написать сценарий.

Я собираюсь изложить функции, а затем выписать их полностью, подробно объясняя, что все делает. В дополнение к этому я рекомендую вам периодически останавливаться, когда вы следуете (например, каждые 5 минут), обновлять index.html и следить за тем, чтобы результат был таким, каким должен быть на тот момент. Вы можете сделать это одним из двух способов:

  • Поместите console.log операторов в функции и распечатайте переменные
  • Откройте вкладку «Источники» в инструментах разработчика, найдите scrtipt.js и установите точки останова в скрипте перед обновлением страницы, чтобы использовать отладчик и проверить значения переменных.

Структура скрипта

Наш скрипт будет состоять из 4 основных функций:

  • handleSubmit: захватывает имя субреддита из входных данных, а затем вызывает fetchPosts
  • fetchPosts: извлекает сообщение из Reddit API и затем вызывает parseResults
  • parseResults: преобразует объекты ответа API в ранжированный список пользователей и статистику, а затем вызывает displayRankings
  • displayRankings: превращает список пользователей в HTML, отображает его

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

  • postsPerRequest: мы установим максимально допустимое значение, 100
  • maxPostsToFetch: наши запросы будут выполняться последовательно, поэтому нам нужно ограничить их, чтобы наш скрипт не занимал слишком много времени, давайте выберем 500
  • maxRequests: maxPoststoFetch делится на postPerQuest, поэтому в данном случае - 5. После того, как мы достигли этого числа, мы хотим, чтобы наш скрипт больше не отправлял запросы к Reddit API.
  • ответы: массив, который мы будем использовать для хранения ответов на все сделанные запросы.

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

На этом этапе наш сценарий может выглядеть примерно так:

const postsPerRequest = 100;
const maxPostsToFetch = 500;
const maxRequests = maxPostsToFetch / postsPerRequest;
const responses = [];
const handleSubmit = e => {
 // Capture subreddit name from input
};
const fetchPosts = (subreddit, afterParam) => {
  // Fetch from Reddit
  parseResults();
};
const parseResults = responses => {
  // Rank Users by Post Score
  displayRankings();
};
const displayRankings = results => {
  // Attach Rankings to DOM
};
const subRedditSelectForm = document.getElementById('subreddit-select-form');
subRedditSelectForm.addEventListener('submit', handleSubmit);

Если вы не знакомы с ES6, то несколько вещей могут вас смутить:

  • Ключевое слово« const :» Способ написания кода ES6 рекомендует использовать const и let вместо var для объявления переменных. Короче говоря, используйте const для переменных, которые не будут переназначены, и let для тех, которые могут
  • Стрелочные функции: Стрелочные функции (const myFunction = arg => {/* do things */}) работают в основном так же, как и функции, написанные традиционным способом (var myfunction = function(){/* do things */}), но выглядят чище. Функциональные различия стрелочных функций не имеют отношения к этому проекту, но определенно полезны, поэтому я рекомендую вам прочитать о том, как они работают.

Все, что нам нужно сделать сейчас, это выделить наши четыре основные функции.

  1. handleSubmit

Начнем с первого и самого простого, handleSubmit. Для этого нам просто нужно отменить событие отправки по умолчанию (потому что мы не хотим фактически отправлять форму где-либо), нацелить ввод, найти его значение (текст, который был введен), сохранить это значение в переменной а затем вызовите fetchPosts, передав эту переменную в качестве аргумента. У вас должно получиться что-то вроде этого:

const handleSubmit = e => {
  e.preventDefault();
  const subreddit = document.getElementById('subreddit').value;
  fetchPosts(subreddit);
};

2. fetchPosts

Эта функция будет делать запросы к API Reddit, а затем сохранять ответы. Это асинхронное действие, так как запрос вернет обещание, поэтому нам нужно обработать это соответствующим образом. Мы собираемся использовать async / await из ES7, который представляет собой синтаксический сахар, построенный на обещаниях. Мы определяем функцию с ключевым словом async, а затем внутри этой функции вы можете написать await, за которым следует обещание, и код, следующий за этой строкой, не будет выполнен, пока это обещание не будет выполнено.

В основном вместо:

var myfunction = function() { 
 fetchDataFromAPI.then(function(data){
   console.log(data);
  }
 )
};

Мы бы хотели иметь

const myFunction = async () => {
 const data = await fetchDataFromAPI();
 console.log(data)
}

Теперь, когда мы понимаем async / await, есть еще одна вещь, которую я хотел бы представить перед заполнением этой функции, - это литералы шаблонов. В ES6 вместо того, чтобы помещать переменные в такие строки:

var age = 25;
var greeting = 'I am ' + age + ' years old';

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

const age = 25;
const greeting = `I am ${age} and will be ${25 + 1} next year`;

Этот синтаксис будет полезен для написания нашей функции запроса, поскольку URL-адрес будет изменяться в зависимости от переменных. В частности, это будет зависеть от переменных «subreddit» и «afterParam», поэтому убедитесь, что вы записали обе из них в качестве параметров функции (в скобках). Затем поставьте пробел и ключевое слово async после знака равенства в вашем fetchPosts. Ваша функция должна быть такой:

const fetchPosts = async (subreddit, afterParam) => {
  // Fetch from Reddit
  parseResults();
};

Теперь давайте заполним его. Функция должна сделать следующее:

  1. Сделайте запросы к API Reddit на основе желаемого субреддита, ограничения, который мы устанавливаем на количество сообщений для извлечения, и параметра `` после ''. (Примечание: `` после '' будет неопределенным при первом запуске функции, что когда мы делаем наш первый запрос к Reddit API)
  2. Дождитесь ответа от этого запроса, а затем сохраните его в переменной
  3. Прочтите этот ответ и сохраните его как JSON. Этот процесс также является асинхронным, поэтому нам нужно await вызвать функцию, а затем сохранить результат в переменной.
  4. Вставьте ответ JSON в массив responses в верхней части скрипта.

Следующий шаг после этого будет зависеть. Это может произойти двумя способами…

5-A) Если ответ на только что сделанный вами запрос содержит свойство after, которое не является null, И вы не достигли предопределенного количества максимальных запросов (определяемых путем проверки длины вашего массива response), тогда рекурсивно вызовите fetchPosts снова сделайте еще один запрос на получение следующих сообщений. Вы можете сделать это, передав свойство «after» из только что полученного запроса, а затем прикрепив его как параметр к следующему запросу.

5-Б. Если свойство after ответа равно нулю ИЛИ вы достигли максимального количества запросов. Прекратите отправлять запросы и проанализируйте все запрошенные сообщения, вызвав parseResults в массиве responses.

Вот как это выглядит в коде:

const fetchPosts = async (subreddit, afterParam) => {
  const response = await fetch(
    `https://www.reddit.com/r/${subreddit}.json?limit=${postsPerRequest}${
      afterParam ? '&afterParam=' + afterParam : ''
    }`
  );
  responseJSON = await response.json();
  responses.push(responseJSON);
  if (responseJSON.data.afterParam && responses.length < maxRequests) {
    fetchPosts(subreddit, responseJSON.data.afterParam);
    return;
  }
  parseResults(responses);
};

2. parseResults

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

Неявный возврат: Опуская фигурные скобки, стрелочная функция может вернуть простоту значения, то есть без использования ключевого слова return.

var myFunction = function(){ return 5 + 1 };

..имеет тот же результат, что и:

const myFunction = () => 5 + 1

Оператор распространения: Когда вы пишете оператор распространения ... перед объектом, он берет все свойства этого объекта (или элементов этого массива) и распределяет их так, чтобы они интерпретировались как отдельные элементы. . Это полезно, если вы хотите вставить все элементы массива в другой массив, а не вставлять сам массив (в последнем случае получается вложенный массив, который нам не нужен).

Это означает, что если мы хотим объединить два массива, это не сработает:

const arr = [1, 2, 3];
arr.push([4, 5, 6]);
arr // => [1, 2, 3, [4, 5, 6]]

Но это могло бы:

const arr = [1, 2, 3];
arr.push(...[4, 5, 6]);
arr // => [1, 2, 3, 4, 5, 6]

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

Допустим, я передаю в функцию запись о сотруднике, хранящуюся как объект:

const employee = {
 name: 'Jonny',
 shifts: ['Tuesday', 'Thursday'],
 contact: {
  phone: '555 5555',
  email: '[email protected]'
 }
}

Вместо того, чтобы делать это:

const nextShift = (person) => {
 return `${person.name}` is coming in on ${person.shifts[0]}`;
}

Я могу написать свою функцию следующим образом:

const nextShift = ({ name, shifts }) => {
  return `${name}` is coming in on ${shifts[0]}`;
}

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

const getPhoneNumber = ({ name, contactInfo: { phone } }) => {
  return `${name}`'s phone number is ${phone}`;
}

Сокращение свойств объекта: Если вы определяете свойство объекта и присваиваете ему значение с помощью переменной с тем же именем, вы можете очистить его, просто написав имя свойства. Вот простой пример:

Вместо того, чтобы писать:

const age = 25;
const person = {
 age: age
}

Мы просто пишем:

const age = 25;
const person = {
 age
}

Теперь, когда мы разобрались с нашим набором инструментов, давайте сформулируем план. Наш parseResults должен сделать следующее:

  1. Создайте массив с именем allPosts, в котором будут храниться все сообщения.
  2. Прокрутите объекты ответа из массива responses и на каждом из них найдите массив сообщений (response.data.children) и поместите каждое сообщение в allPosts.
  3. Создайте пустой объект для хранения пользовательской статистики и назовите его statsByUser.. В этом объекте каждый ключ будет именем пользователя, а значение этого ключа будет объектом с оценкой пользователя и количеством сообщений.
  4. Просмотрите каждое сообщение из allPosts и добавьте информацию в statsByUser, на каждой итерации может произойти одно из двух:

5-A) Если пользователь этого сообщения еще не существует в statsByUser, создайте его, сделайте счетчик сообщения 1 и сделайте счет баллом текущей итерации (сообщения)

5-B) Если пользователь этого сообщения существует в statsByUser, увеличьте количество сообщений на 1 и прибавьте оценку текущего сообщения к уже существующей оценке.

6. Чтобы упростить сортировку, преобразуйте объект statsByUser в массив, получив ключи объекта, используя Object.keys в качестве массива, а затем сопоставьте этот массив с массивом объектов, каждый с именем пользователя, оценкой и количеством сообщений.

7. Отсортируйте этот массив в порядке убывания по количеству очков.

8. Позвоните displayRankings с отсортированным списком.

Вот как это будет выглядеть в коде:

const parseResults = responses => {
  const allPosts = [];
responses.forEach(response => {
    allPosts.push(...response.data.children);
  });
const statsByUser = {};
allPosts.forEach(({ data: { author, score } }) => {
    statsByUser[author] = !statsByUser[author]
      ? { postCount: 1, score } // score
      : {
          postCount: statsByUser[author].postCount + 1,
          score: statsByUser[author].score + score
        };
  });
const userList = Object.keys(statsByUser).map(username => ({
    username,
    score: statsByUser[username].score,
    postCount: statsByUser[username].postCount
  }));
const sortedList = userList.sort((userA, userB) => userB.score - userA.score);
displayRankings(sortedList);
};

displayResults

Эта последняя функция просто превращает наш отсортированный список в элементы HMTL и вставляет их на нашу страницу.

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

Эта функция должна просто зацикливаться на каждом элементе массива, который ей передается, и на каждой итерации она должна делать следующее:

  1. Используйте индекс, чтобы определить рейтинг этого пользователя (индекс + 1, поскольку массивы начинаются с индекса 0)
  2. Создайте элемент привязки и сохраните его в переменной userCard.
  3. Добавьте свойство href для useCard, которое ссылается на профиль пользователя на Reddit.
  4. Добавьте в useCard текст, содержащий имя пользователя, количество сообщений и общий балл.
  5. Выберите в DOM контейнер результатов по его ID
  6. Добавить userCard в контейнер результатов

Вот как должна выглядеть функция:

const displayRankings = results => {
 const container = document.getElementById('results-container');     results.forEach(({ username, score, postCount }, i) => {
    const rank = i + 1;
    // Text
    const userCard = document.createElement('a');
    userCard.href = `https://www.reddit.com/user/${username}`;
    userCard.classList.add('user');
    userCard.innerText = `${rank}. ${username} - ${postCount} post(s) - (${score}) `;
    container.appendChild(userCard);
  });
};

И все готово!

Попробуйте это сделать, открыв index.html, введя имя субреддита в поле ввода, а затем нажав Enter или кнопку. Дайте ему несколько секунд, чтобы ваши запросы были сделаны, а затем ранжированный список должен появиться под полем ввода. Если вы хотите усложнить задачу, добавьте некоторую проверку формы и обработку ошибок запроса. Совет: try / catch может быть полезен для обработки ошибок с помощью async await и fetch.

Надеюсь, вам понравилось узнавать о некоторых новых необычных методах Javascript, а также о Reddit API. Если у вас есть вопросы, свяжитесь с нами. Ознакомьтесь с моими другими статьями или на моем канале Youtube для получения дополнительных руководств.