От идеи до готового приложения

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

Определение спецификации

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

Второе направление - викторина. quiz-class отслеживает прогресс игрока и решает, отображать ли следующий вопрос на последнем экране.

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

Мы будем использовать Open Trivia DB API для вопросов, чтобы нам не приходилось придумывать свои вопросы.

Как вы уже могли догадаться, поскольку я много говорю о классах, для реализации этого приложения-викторины мы будем использовать объектно-ориентированное программирование, а не функциональное программирование. Если вас интересует разница между этими двумя парадигмами, прочтите мою статью Функциональное программирование против ООП в JavaScript.

Предпосылки

Прежде чем мы сможем приступить к реализации викторины, нам нужно создать нашу структуру папок, а также HTML и CSS. В этой статье мы сосредоточимся на части приложения, связанной с JavaScript. Поэтому в этом разделе я предоставлю необходимый HTML и CSS. Начнем с создания такой структуры папок:

$ mkdir vanilla-quiz
$ cd ./vanilla-quiz
$ mkdir quiz
$ touch index.html index.js styles.css

Скопируйте и вставьте index.html и styles.css из этих источников:

Теперь мы готовы приступить к работе над нашим приложением. Первый класс, над которым мы будем работать, - это Настройки.

Как получить вопросы?

Цель settings-class - дать игроку возможность выбрать категорию, сложность и количество вопросов, на которые он хочет ответить. Нам нужно создать запрос к API Open Trivia DB из этих трех параметров, чтобы получить вопросы для прохождения игрока.

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

import Settings from ‘./quiz/settings.js’;
new Settings();

Это приведет к ошибке, поскольку файл settings.js еще не существует, поэтому давайте создадим его.

$ touch ./quiz/settings.js

Затем мы создаем скелет для нашего settings-class. Для этого нам понадобится класс с конструктором и startQuiz-method плюс export-statement. Без export-statement мы не смогли бы импортировать класс в index.js. Вот как это должно выглядеть:

class Settings {
  constructor() {
  }
  
  startQuiz() {
  }
}
export default Settings;

В конструкторе мы хотим получить все элементы DOM, необходимые для запуска викторины. Для этого нам нужно взять два блока div, quiz и settings, чтобы переключить их видимость, когда игрок хочет начать викторину. Далее нам понадобятся все параметры, чтобы мы могли создать запрос на получение вопросов. И последнее, но не менее важное: нам нужно получить кнопку для добавления startQuiz-method к событию клика.

constructor() {
  this.quizElement = document.querySelector('.quiz');
  this.settingsElement = document.querySelector('.settings');
  this.category = document.querySelector('#category');
  this.numberOfQuestions = document.querySelector('#questions');
  this.difficulty = [
    document.querySelector('#easy'),
    document.querySelector('#medium'),
    document.querySelector('#hard'),
  ];
  this.startButton = document.querySelector('#start');
  
  this.quiz = { };
  this.startButton.addEventListener('click', this.startQuiz.bind(this));
}

В первом сегменте мы получаем все элементы DOM, обратите внимание, что мы сохраняем элементы сложности в массиве, чтобы впоследствии их отфильтровать. После этого мы инициализируем свойство quiz и добавляем startQuiz-method к startButton. Обязательно привяжите this к методу startQuiz. Если вы этого не сделаете, у вас не будет this внутри метода.

Для запуска викторины нам нужно собрать все параметры и динамически создать запрос. Поскольку мы имеем дело с вызовом API, я решил использовать async / await для обработки асинхронного вызова. Чтобы избежать потери ошибок, мы заключим весь вызов в try-catch-block. Итак, startQuiz-method должен выглядеть примерно так:

async startQuiz() {
  try {
    const amount = this.getAmount();
    const categoryId = this.category.value;
    const difficulty = this.getCurrentDifficulty();
 
    const url = `https://opentdb.com/api.php?amount=${amount}&category=${categoryId}&difficulty=${difficulty}&type=multiple`;
    let data = await this.fetchData(url);
    this.toggleVisibility();
    this.quiz = new Quiz(this.quizElement, amount, data.results);
  } catch (error) {
    alert(error);
  }
}

Что мы здесь делаем?

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

После этого мы создаем URL с только что полученными параметрами. Этот URL-адрес передается в методе fetchData, который отправляет запрос и возвращает данные. После этого мы вызываем toggleVisibility и инициализируем новый quiz-object, передавая result, amount и quizElement.

Если в какой-то момент возникает ошибка, мы ее поймаем и отобразим с помощью alert-method.

Окончательный класс настроек должен выглядеть так:

Оба метода getAmount и getCurrentDifficulty возвращают ошибку, если игрок ничего не выбрал или выбранное значение выходит за границы (для количества вопросов). Мы также добавили import-statement для quiz-class вверху этого файла. Два других метода (fetchData и toggleVisibility) делают именно то, что предполагают их названия. Теперь мы можем сосредоточиться на викторине.

Пришло время викторины!

Прежде чем мы начнем думать о quiz-class, нам нужно создать файл, который будет его содержать.

$ touch ./quiz/quiz.js

Начнем так же, как и с settings.js, с создания скелета.

class Quiz {
  constructor(quizElement, amount, questions) {
    this.quizElement = quizElement;
    this.totalAmount = amount;
    this.questions = this.setQuestions(questions);
  }
  setQuestions(questions) {
    return questions.map(question => new Question(question));
  }
  nextQuestion() {
  }
  endQuiz() {
  }
}
export default Settings;

На этот раз у нас есть некоторые аргументы, переданные объектом settings-object, с которыми нам нужно иметь дело. Для вопросов мы создаем один вопрос-объект для каждого вопроса, переданного с помощью settings-object. Конструктор нуждается в дополнительной настройке, поэтому мы добавим еще несколько DOM-элементов и прослушиватель событий в nextButton. Так что давай, сделаем это!

constructor(quizElement, amount, questions) {
  this.quizElement = quizElement;
  this.currentElement = document.querySelector('.current');
  this.totalElement = document.querySelector('.total');
  this.nextButton = document.querySelector('#next');
  this.finalElement = document.querySelector('.final')
  this.totalAmount = amount;
  this.answeredAmount = 0;
  this.questions = this.setQuestions(questions);
  this.nextButton.addEventListener('click',
  this.nextQuestion.bind(this));
  this.renderQuestion();
}

Как видите, он почти похож на конструктор в settings.js. Одно из основных отличий - это вызов renderQuestion в конце. Целью этого звонка является то, что мы хотим немедленно задать первый вопрос.

Между setQuestions и nextQuestion мы создаем метод renderQuestion и реализуем его следующим образом:

renderQuestion() {
  this.questions[this.answeredAmount].render();
  this.currentElement.innerHTML = this.answeredAmount;
  this.totalElement.innerHTML = this.totalAmount;
}

В начале викторины answerAmount равно 0, поэтому мы вызываем render-method для первого вопроса в questions-array. После этого выставляем текущий прогресс игрока. Поскольку мы еще не реализовали question-class, этот код выдает ошибку, но мы скоро это исправим.

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

nextQuestion() {
  const checkedElement = this.questions[this.answeredAmount].answerElements.filter(el => el.firstChild.checked);
  if (checkedElement.length === 0) {
    alert(‘You need to select an answer’);
  } else {
    this.questions[this.answeredAmount].answer(checkedElement)
    this.showResult();
    this.answeredAmount++;
    (this.answeredAmount < this.totalAmount) ? this.renderQuestion() : this.endQuiz();
  }
}

Единственные методы, отсутствующие в этом классе, - это showResult, endQuiz и метод для суммирования всех правильных ответов на экране результатов. Окончательный вариант quiz.js должен выглядеть так:

Мы добавили два импорта вверху для question.js и final.js. Кроме того, мы реализовали showResult, проверяя, правильно ли был дан ответ на вопрос с помощью тернарного оператора.

endQuiz-method немного похож на toggleVisibility-method из нашего settings.js, за исключением того, что он суммирует все правильные ответы вызов calculateCorrectAnswers, а затем передача его новому экземпляру final-class (нам все еще нужно реализовать этот класс).

Отображение вопроса и результата

Наш викторина в данный момент не работает, потому что еще не существует двух зависимостей. Давайте изменим это, добавив два файла следующим образом:

$ touch ./quiz/question.js ./quiz/final.js

Начнем с реализации question-class. Первым делом мы добавляем в файл скелет следующим образом:

class Question {
  constructor(question) {
    this.correctAnswer = question.correct_answer;
    this.question = question.question;
    this.answers = this.shuffleAnswers([
      question.correct_answer,
      ...question.incorrect_answers
    ]);
  }
  shuffleAnswers(answers) {
  }
  
  answer(checkedElement) {
  }
  render() {
  }
}
export default Question;

Итак, что мы здесь сделали?

Мы сохраняем вопрос, правильный ответ и массив ответов, которые мы перемешиваем перед сохранением.

Следующим шагом является реализация методов shuffleAnswers, answer и render. Для перетасовки массива воспользуемся алгоритмом Fisher-Yates-Shuffle-Algorithm.

answer-method просто сравнит выбор игрока со свойством rightAnswer, а метод render отобразит вопрос и все возможные ответы. . Чтобы это сработало, нам нужно получить соответствующие элементы DOM и получить этот question.js:

Теперь не хватает только final-class. Этот класс действительно прост, нам просто нужно заставить элементы DOM отображать конечный результат для игрока. Для удобства мы можем добавить кнопку снова, которая перезагружает страницу, чтобы игрок мог начать заново. Вот как это должно выглядеть:

Заключение

Приложение-викторина готово. Мы реализовали это с помощью простого старого JavaScript и использовали концепцию объектно-ориентированного программирования. Надеюсь, вам понравилось, и, как всегда, вы можете найти код на моем GitHub.

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

При запуске вопросы с несколькими вариантами ответов выглядят следующим образом.

И в конце всех вопросов вы увидите эту последнюю страницу.

Надеюсь, вам было весело следить за мной. Попробуйте улучшить эту версию, если хотите.