Подключаем логгер к проектам на JavaScript
medium

Подключаем логгер к проектам на JavaScript

Следим за каждым шагом программы

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

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

Сегодня мы попробуем логгер в деле — подключим его к цветному арканоиду на JavaScript и посмотрим, что будет в логах после игры.

js-Logger — простой логгер для браузеров

Если нам нужен простой логгер для консоли браузера с делением на уровни сообщений, то проще всего использовать js-Logger. Он подключается как обычный скрипт на HTML-странице:   

<script src="https://github.com/jonnyreeves/js-logger/blob/master/src/logger.js"></script>

Сразу после этого его можно использовать для логирования разных сообщений:

// ставим настройки по умолчанию
Logger.useDefaults();

// и выводим тестовые сообщения разных уровней
Logger.debug("Это обычное сообщение о каком-то событии, например для отладки");
Logger.info("Информационное сообщение, его можно привязать к разовому событию");
Logger.warn("А вот это уже серьёзно, тут логгер нас о чём-то предупреждает. Ничего критичного, но присмотреться стоит");
Logger.error("Ошибка — что-то в программе пошло не так");
Подключаем логгер к проектам на JavaScript

Смотрим уровни логирования

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

Подключаем логгер к проектам на JavaScript

Если нам нужны только предупреждения и ошибки, то ставим галочку на Warnings (предупреждения) и Errors (ошибки). Браузер отфильтрует эти события и скроет всё остальное, но при этом сами логи никуда не исчезнут — мы просто перестанем их видеть. Это работает с любым логгером, который поддерживает разные уровни сообщений:

Подключаем логгер к проектам на JavaScript

Добавляем логгер в проект

Теперь расставим сообщения логгера в разных местах в коде арканоида и посмотрим, что будет в консоли, если мы немного в неё поиграем. Мы не будем комментировать каждый шаг, а добавим вызов логгера в ключевых местах: при касании платформы, нажатии игроком на кнопки, потере или добавлении жизней и при вылете шарика за пределы поля. Чтобы было интереснее, сделаем события на разных уровнях. Единственное, чего у нас не будет, — сообщений об ошибках, потому что мы не обрабатывали их в исключениях.

Поначалу кажется, что всё нормально и с игрой всё в порядке: мы видим каждое нажатие клавиши и реакцию игры на все события:

Подключаем логгер к проектам на JavaScript

Но как только мы потратим последнюю жизнь, то увидим странное: начнётся бесконечный повтор событий «Шарик улетел за границы поля» → «Потеряли жизнь». При этом внешне игра остановится штатно: появится чёрный экран с надписью «Game over»:

Подключаем логгер к проектам на JavaScript

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

<!-- Лицензия CC0 1.0 Universal-->
<!-- оригинал — https://gist.github.com/straker/98a2aed6a7686d26c04810f08bfaf66b -->
<!DOCTYPE html>
<html>

<head>
	<meta charset="utf-8">
  <title>Arkanoid</title>
  <style>
	/* стили для всей страницы */
	html, body {
	  height: 100%;
	  margin: 0;
	}
	/* отдельные параметры для фона */
	body {
	  background: black;
	  display: flex;
	  align-items: center;
	  justify-content: center;
	}
  </style>
</head>

<body>
  <!-- рисуем игровое поле -->
  <canvas width="400" height="500" id="game"></canvas>
  <!-- сам скрипт с игрой -->
   <script src="https://github.com/jonnyreeves/js-logger/blob/master/src/logger.js"></script>


  <script>

  // ставим настройки по умолчанию
  Logger.useDefaults();

  // и выводим тестовые сообщения разных уровней
  Logger.debug("Это обычное сообщение о каком-то событии, например для отладки");
	Logger.info("Информационное сообщение, его можно привязать к разовому событию");
	Logger.warn("А вот это уже серьёзно, тут логгер нас о чём-то предупреждает. Ничего критичного, но присмотреться стоит");
	Logger.error("Ошибка — что-то в программе пошло не так");


  // переменная для работы с холстом, на котором будет нарисована игра
  const canvas = document.getElementById('game');
  const context = canvas.getContext('2d');

  // каждый ряд состоит из 14 кирпичей. На уровне будут 6 пустых рядов, а затем 8 рядов с кирпичами
  // цвета кирпичей: красный, оранжевый, зелёный и жёлтый
  // буква в массиве означает цвет кирпича
  const level1 = [
		[],
		[],
		[],
		[],
		[],
		[],
		['R','R','R','R','R','R','R','R','R','R','R','R','R','R'],
		['R','R','R','R','R','R','R','R','R','R','R','R','R','R'],
		['O','O','O','O','O','O','O','O','O','O','O','O','O','O'],
		['O','O','O','O','O','O','O','O','O','O','O','O','O','O'],
		['G','G','G','G','G','G','G','G','G','G','G','G','G','G'],
		['G','G','G','G','G','G','G','G','G','G','G','G','G','G'],
		['Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y'],
		['Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y','Y']
  ];

  // сопоставляем буквы (R, O, G, Y) с цветами
  const colorMap = {
		'R': 'red',
		'O': 'orange',
		'G': 'green',
		'Y': 'yellow'
  };

  // делаем зазор в 2 пикселя между кирпичами, чтобы отделить их друг от друга
  const brickGap = 2;
  // размеры каждого кирпича
  const brickWidth = 25;
  const brickHeight = 12;

  // ширина стены должна занимать оставшееся место на холсте с каждой стороны
  // у нас 14 кирпичей по 25 пикселей и 13 промежутков по 2 пикселя, а общая ширина холста — 400 пикселей
  // получаем общую ширину стен: 400 - (14 * 25 + 2 * 13) = 24px. 
  // разделим пополам, чтобы получить ширину каждой стены, и получим  12px
  const wallSize = 12;
  // основной массив для игры
  const bricks = [];

  // количество набранных очков за одну попытку
  score = 0;

  // количество жизней на старте
  lives = 3;

  Logger.info("Объявили все переменные")

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

// в эту функцию мы поместим всё, что связано с касанием кирпичей
function touchdown(t_brick) {
	Logger.debug('Коснулись кирпича')

	// начисляем очки в зависимости от цвета кирпича
	switch(t_brick.color) {
		case "yellow" : score += 1; break;
		case "green"  : score += 2; break;
		case "orange" : score += 3; break;
		case "red"    : score += 4; 
	}

	// за каждые 25 очков — увеличиваем размер платформы на 2 пикселя
	if (score % 25 == 0) {
		paddle.width += 2;
	}

	// а за каждые 100 очков в одной попытке — прибавляем ещё одну жизнь
	if (score % 100 == 0){
		lives += 1;
		Logger.info('Получили ещё одну жизнь')
		// и усложняем игру — увеличиваем скорость шарика
		ball.speed += 1;
	}

}

  function lost() {
  	// уменьшаем количество жизней
  	lives = lives - 1;
  	Logger.info('Потеряли жизнь')

  	// обнуляем набранные очки
  	score = 0;

  }

  Logger.debug('Переходим к обработке массива с уровнем')
  // пока у нас есть необработанные элементы в массиве с уровнем — обрабатываем их
  for (let row = 0; row < level1.length; row++) {
		for (let col = 0; col < level1[row].length; col++) {

		  // находим цвет кирпича
		  const colorCode = level1[row][col];

		  // создаём новый элемент игрового массива — с координатами кирпича, цветом, шириной и высотой кирпича
		  bricks.push({
				x: wallSize + (brickWidth + brickGap) * col,
				y: wallSize + (brickHeight + brickGap) * row,
				color: colorMap[colorCode],
				width: brickWidth,
				height: brickHeight
		  });
		}
  }

  // платформа, которой управляет игрок
  const paddle = {
		// ставим её внизу по центру поля
		x: canvas.width / 2 - brickWidth / 2,
		y: 440,
		// делаем её размером с кирпич
		width: brickWidth,
		height: brickHeight,

		// пока платформа никуда не движется, поэтому направление движения равно нулю
		dx: 0
  };

  // шарик, который отскакивает от платформы и уничтожает кирпичи
  ball = {
		// стартовые координаты
		x: 130,
		y: 260,
		// высота и ширина (для простоты это будет квадратный шарик)
		width: 5,
		height: 5,

		// скорость шарика по обоим координатам
		speed: 2,

		// на старте шарик пока никуда не смещается
		dx: 0,
		dy: 0
  };

	// проверка на пересечение объектов
	// взяли отсюда: https://developer.mozilla.org/en-US/docs/Games/Techniques/2D_collision_detection
	function collides(obj1, obj2) {
	  return obj1.x < obj2.x + obj2.width &&
			 obj1.x + obj1.width > obj2.x &&
			 obj1.y < obj2.y + obj2.height &&
			 obj1.y + obj1.height > obj2.y;
	}

	Logger.info('Входим в главный цикл игры')
  // главный цикл игры
  function loop() {
		// на каждом кадре — очищаем поле и рисуем всё заново
		requestAnimationFrame(loop);
		context.clearRect(0,0,canvas.width,canvas.height);

		// двигаем платформу с нужной скоростью 
		paddle.x += paddle.dx;

		// при этом смотрим, чтобы она не уехала за стены
		if (paddle.x < wallSize) {
		  paddle.x = wallSize
		}
		else if (paddle.x + paddle.width > canvas.width - wallSize) {
		  paddle.x = canvas.width - wallSize - paddle.width;
		}

		// шарик тоже двигается со своей скоростью
		ball.x += ball.dx;
		ball.y += ball.dy;

		// и его тоже нужно постоянно проверять, чтобы он не улетел за границы стен
		// смотрим левую и правую стенки
		if (ball.x < wallSize) {
		  ball.x = wallSize;
		  ball.dx *= -1;
		}
		else if (ball.x + ball.width > canvas.width - wallSize) {
		  ball.x = canvas.width - wallSize - ball.width;
		  ball.dx *= -1;
		}
		// проверяем верхнюю границу
		if (ball.y < wallSize) {
		  ball.y = wallSize;
		  ball.dy *= -1;
		}

		// перезагружаем шарик, если он улетел вниз, за край игрового поля
		if (ball.y > canvas.height) {
			Logger.warn('Шарик улетел за границы поля')
			// обрабатываем падение шарика
			lost();

			if (lives <= 0){ 
				// рисуем чёрный прямоугольник посередине поля
				context.fillStyle = 'black';
				context.globalAlpha = 0.75;
				context.fillRect(0, canvas.height / 2 - 30, canvas.width, 60);
				// пишем надпись белым моноширинным шрифтом по центру
				context.globalAlpha = 1;
				context.fillStyle = 'white';
				context.font = '36px monospace';
				context.textAlign = 'center';
				context.textBaseline = 'middle';
				context.fillText('GAME OVER!', canvas.width / 2, canvas.height / 2);

				return; };
			console.log(lives);

		  ball.x = 130;
		  ball.y = 260;
		  ball.dx = 0;
		  ball.dy = 0;
		}

		// проверяем, коснулся ли шарик платформы, которой управляет игрок. Если коснулся — меняем направление движения по y на противоположное
		if (collides(ball, paddle)) {
		  ball.dy *= -1;

		  // сдвигаем шарик выше платформы, чтобы на следующем кадре это снова не засчиталось за столкновение
		  ball.y = paddle.y - ball.height;
		}

		// проверяем, коснулся ли шарик цветного кирпича 
		// если коснулся — меняем направление движения шарика в зависимости от стенки касания
		// для этого в цикле проверяем каждый кирпич на касание
		for (let i = 0; i < bricks.length; i++) {
		  // берём очередной кирпич
		  const brick = bricks[i];

		  // если было касание
		  if (collides(ball, brick)) {
		  	Logger.debug('Касание')

				touchdown(brick);

				// убираем кирпич из массива
				bricks.splice(i, 1);

				// если шарик коснулся кирпича сверху или снизу — меняем направление движения шарика по y
				if (ball.y + ball.height - ball.speed <= brick.y ||
					ball.y >= brick.y + brick.height - ball.speed) {
				  ball.dy *= -1;
				}
				// в противном случае меняем направление движения шарика по x
				else {
				  ball.dx *= -1;
				}
				// как нашли касание — сразу выходим из цикла проверки
				break;
		  }
		}

		// рисуем стены
		context.fillStyle = 'lightgrey';
		context.fillRect(0, 0, canvas.width, wallSize);
		context.fillRect(0, 0, wallSize, canvas.height);
		context.fillRect(canvas.width - wallSize, 0, wallSize, canvas.height);

		// если шарик в движении — рисуем его
		if (ball.dx || ball.dy) {
		  context.fillRect(ball.x, ball.y, ball.width, ball.height);
		}

		// рисуем кирпичи
		bricks.forEach(function(brick) {
		  context.fillStyle = brick.color;
		  context.fillRect(brick.x, brick.y, brick.width, brick.height);
		});

		// рисуем платформу
		context.fillStyle = 'cyan';
		context.fillRect(paddle.x, paddle.y, paddle.width, paddle.height);

		// Цвет текста — серый    
		context.fillStyle = "#777777";    
		// Задаём размер и шрифт    
		context.font = "20pt monospace";    
		// Сначала выводим рекорд    
		context.fillText('Очки: ' + score, 50, 490);    
		// Затем — набранные очки    
		context.fillText('Жизни:'+ lives, 250, 490);
  }

  // отслеживаем нажатия игрока на клавиши
  document.addEventListener('keydown', function(e) {
  	Logger.info('Игрок нажал на кнопку')
		// стрелка влево
		if (e.which === 37) {
		  paddle.dx = -3;
		}
		// стрелка вправо
		else if (e.which === 39) {
		  paddle.dx = 3;
		}

		// обрабатываем нажатие на пробел
		// если шарик не запущен — запускаем его из начальной точки, сверху вниз
		if (ball.dx === 0 && ball.dy === 0 && e.which === 32) {
		  ball.dx = ball.speed;
		  ball.dy = ball.speed;
		}
  });

  // как только игрок перестал нажимать клавиши со стрелками — останавливаем платформу
  document.addEventListener('keyup', function(e) {
		if (e.which === 37 || e.which === 39) {
		  paddle.dx = 0;
		}
  });

  // запускаем игру
  requestAnimationFrame(loop);
  </script>
</body>
</html>

log4javascript — продвинутый логгер для сложных задач

Если понадобится логгер посложнее, можно посмотреть на log4javascript — он также подключается как внешний скрипт и не требует установленной платформы Node.js (как это делают многие логгеры).

Особенность этого логгера в том, что он может не просто выводить сообщения в консоль, но и создать отдельное окно для логов:

Подключаем логгер к проектам на JavaScript

Чтобы подключить логгер, его нужно скачать себе на компьютер, а потом указать полный путь к файлу, например так:

<script type="text/javascript" src="/Users/mike/Downloads/log4javascript-1.4.13/log4javascript.js"></script>

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

// настраиваем лог на вывод сообщений в отдельное окно
	var log = log4javascript.getLogger();
var popUpAppender = new log4javascript.PopUpAppender();
var popUpLayout = new log4javascript.PatternLayout("%d{HH:mm:ss} %-5p - %m%n");
popUpAppender.setLayout(popUpLayout);
log.addAppender(popUpAppender);
var ajaxAppender = new log4javascript.AjaxAppender("myloggingservlet.do");
ajaxAppender.setThreshold(log4javascript.Level.ERROR);
log.addAppender(ajaxAppender);
// выводим тестовые сообщения
log.debug('Простое сообщение отладчика');
log.info('Информационное сообщение');
log.warn('Предупреждение, на которое нужно обратить внимание')
log.error('Ошибка! В программе что-то случилось.');

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

Что дальше

В следующий раз добавим логгер в проект на Python — там больше возможностей для отладки и настройки таких сообщений.

Вы фронтендер? Гляньте сюда ↓
У нас есть курсы по прокачке навыков до уровня middle и далее. Начать можно бесплатно. Посмотрите, интересно ли вам это.
Начать бесплатно
Вы фронтендер? Гляньте сюда ↓ Вы фронтендер? Гляньте сюда ↓ Вы фронтендер? Гляньте сюда ↓ Вы фронтендер? Гляньте сюда ↓
Получите ИТ-профессию
В «Яндекс Практикуме» можно стать разработчиком, тестировщиком, аналитиком и менеджером цифровых продуктов. Первая часть обучения всегда бесплатная, чтобы попробовать и найти то, что вам по душе. Дальше — программы трудоустройства.
Начать карьеру в ИТ
Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию Получите ИТ-профессию
Еще по теме
medium