Функция для общения с сервером
Сейчас напишем функцию, с помощью которой будем отправлять запросы на сервер (бекенд), и обрабатывать ответы. Эта функция довольно универсальна, её можно будет использовать в разных своих проектах. Итак, открываем в редакторе файл chess.js, и в самое начало добавляем такое:
chess.jsconst createRequest = (options = {}) => {
const method = (options.method === undefined ? 'GET' : options.method.toUpperCase());
let url = options.url;
let formData;
if (options.data) {
if (options.method === 'GET') {
url += url.indexOf('?') >= 0 ? '&' : '?';
for (let key in options.data) {
url += key + '=' + encodeURI(options.data[key])+ '&';
}
url = url.slice(0, -1);
} else {
formData = new FormData();
for (let key in options.data) {
formData.append(key, options.data[key]);
}
}
}
const xhr = new XMLHttpRequest();
try {
xhr.open(method, url);
if (options.headers) {
for (let key in options.headers) {
xhr.setRequestHeader(key, options.headers[key]);
}
}
xhr.responseType = 'json';
if (options.callback) {
xhr.addEventListener('readystatechange', function() {
if (this.readyState == xhr.DONE) {
let response = this.response;
if (this.status == 200 || this.status == 201 || options.no_check_status) {
options.callback(response);
} else if (options.error_callback) {
options.error_callback(response);
} else {
console.log(response);
}
}
});
}
xhr.send(formData);
} catch (e) {
console.log(e);
}
return xhr;
}
Если вас пугает объём, можете пока не углубляться во все детали этого кода, отнеситесь к нему как к библиотеке. Может быть для кого-то это будет новостью, но программисты часто (да считай что всегда) используют чужие библиотеки, не подозревая что там внутри творится.
Но лучше конечно разобраться. Я кратко, "грубыми мазками", без подробностей, опишу что там делается. Если надо детально понять код, разложить его "по полочкам", вот он - перед Вами. Читайте! Очень важно развивать навыки чтения чужого кода.
Если хотите индивидуальных разъяснений, есть какие-то вопросы по коду, логике, алгоритмам, можно спросить здесь или записаться на консультацию
Какие парамеры обрабатывает функция
Вернёмся к коду. Функция createRequest принимает хеш - набор разных опций, который нам нужны. Самый простой вариант использования, когда мы просто шлём GET-запрос по адресу https://someurl.com, и никак не обрабатываем ответ:
createRequest({ url: 'https://someurl.com' });
Если надо сделать POST-запрос по этому адресу, с двумя параметрами, и ответ записать в консоль, вот пример:
createRequest({
method: 'POST',
url: 'https://someurl.com',
data: {
param1: "value1",
param2: "value2"
},
callback: function(response) {
console.log(response);
}
});
В этом примере видны основные используемые опции, и как они используются. Вот список всех опций, обрабатываемых в функции:
- method
- метод запроса
- url
- URL / адрес куда шлём запрос
- data
- данные, которые надо передать
- headers
- заголовки запроса, которые надо отправить
- callback
- функция, которая будет вызываться при "успешном" ответе ("успешный" - значит http-статус равен 200 или 201)
- no_check_status
- логическое значение - проверять ли "успешность ответа" для запуска функции callback
- error_callback
- функция, которая будет вызвана при "неуспешном" ответе
Кратко пройдёмся по функции
Строки 5 - 18 - подготавливаем данные для передачи. Если метод запроса - GET, то данные пихаем конечно в url запроса. Иначе - пользуемся объектом FormData
В строке 19 создаём "объекта запроса" - экземпляр класса XMLHttpRequest - это центральный элемент функции, вокруг которого всё и крутится. Далее настраиваем разные параметры этого объекта запроса: метод запроса, URL (адрес), заголовки, указываем что ожидаем ответ в формате json.
Строки 28 - 41 относятся к обработке ответа. Если во входных опциях функции указан ключ callback, то к объекту запроса мы добавляем "слушателя" события "смены состояния готовности" (readystatechange). Обрабатываем только смену статуса на "полную готовность" (xhr.DONE)
Если код статуса 200 или 201 (т.е. успешный ответ), или мы указали в опциях, что проверять статус не нужно (options.no_check_status), то вызываем функцию, которую мы указали в опции options.callback, передавая ей ответ с сервера (бекенда).
Иначе, если указана функция для обработки ошибки (опция options.error_callback), вызываем её. Если такой фунции не указано, то просто пишем ответ сервера в консоль.
В строке 42 происходит собственно сама отправка запроса, управление передаётся дальше, не дожидаясь ответа сервера. И функция возвращает "объект запроса". При возникновении любой исключительной ситуации (не обработанной ошибки), срабатывает блок try - catch и ответ записывается в консоль.
Делаем запрос на сервер
Помните, в прошлой статье мы объявили javascrit-функцию send_move_to_server? Теперь у нас есть функция для общения с сервером. Используем её:
chess.jsfunction send_move_to_server(cell_index_from, cell_index_to) {
createRequest({
method: 'POST',
url: 'make_move.php',
data: { cell_index_from: cell_index_from, cell_index_to: cell_index_to },
callback: function(response) {
set_game_state(response);
}
});
}
Т.е., мы методом POST шлём запрос по адресу текущий_домен/make_move.php. Передаём в этом запросе параметры cell_index_from, cell_index_to. При успешном ответе вызываем javascript-функцию set_game_state. Этой функции пока нет, очень скоро мы её напишем. Так-же,как и отсутствующий пока php-скрипт make_move.php.
Php - скрипт для обработки хода
Пришла пора сделать уже что-то и на сторне бекенда. Создаём в веб-корне проекта файлик make_move.php, и пишем "верхне-уровнево", что сейчас хотим получить, обращаясь к ещё не написанный классам и функциям:
make_move.php<?php
require_once('engine/chess_game.php');
$cell_index_from = $_POST['cell_index_from'];
$cell_index_to = $_POST['cell_index_to'];
$game = new ChessGame();
$game->makeMove($cell_index_from, $cell_index_to);
$data = $game->getGameState();
header('Content-Type: application/json');
sleep(2);
echo json_encode($data);
Сначала подключаем скрипт, где предполагаем разместить "игровой движок" - engine/chess_game.php. Потом, в строке 5, создаём объект игры класса ChessGame:
$game = new ChessGame();
Класс ChessGame планируем объявить в ненаписанном пока файле engine/chess_game.php. В строке 6 вызываем опять-таки ненаписанный пока метод перемещения фигуры с поля $cell_index_from на поле $cell_index_to. Эти переменные "откуда" и "куда" заполняются в строках 3, 4 - значения берутся из данных, пришедших в POST - запросе.
Далее, в строке 7, получаем данные о состоянии игры (позиции), вызывая несуществующий пока метод getGameState. Этот метод мы напишем специально чтобы он отдавал все нужные данные для клиентской стороны (для javascript- фронтенда), и в нужном виде.
Шлём заголовок, говорящий что данные в теле ответа представлены в формате json. Потом, в строке 9 я поставил задержку на 2 секунды - это просто для демонстрации, чтобы вы успели заметить состояние полей доски после того как мы сделали ход, и до того, как компьютер ответил. Потом мы уберём эту задержку.
И, наконец, в строке 10 отдаём данные $data, закодировав JSON в виде строки.
Создаём класс игры
Создадим всё то, что мы понавызывали в только что описанном файле make_move.php. Пока просто создадим класс с методами - "заглушками", просто чтобы отладить обработку данных от сервера на стороне браузера. Итак, создаём в веб-корне проекта папку engine ("движок"), а в ней - файл chess_game.php:
engine/chess_game.php<?php
class ChessGame {
function makeMove($cell_index_from, $cell_index_to) {
}
function getGameState() {
return [
'is_our_move' => true,
'prev_move_from' => 1,
'prev_move_to' => 18,
'position' => [
6, 0, 8, 4, 2, 8, 10, 6,
12, 12, 12, 12, 12, 12, 12, 12,
0, 0, 10, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
0, 0, 0, 0, 0, 0, 0, 0,
11, 11, 11, 11, 11, 11, 11, 11,
5, 9, 7, 3, 1, 7, 9, 5
],
'available_moves' => [
48 => [40, 32],
49 => [41, 33],
50 => [42, 34],
51 => [43, 35],
52 => [44, 36],
53 => [45, 37],
54 => [46, 38],
55 => [47, 39],
57 => [40, 42],
62 => [45, 47]
]
];
}
}
Мы объявили класс ChessGame с двумя методами.
Метод makeMove пока ничего не делает, он просто объявлен.
Метод getGameState возвращает ассоциативный массив со всей нужной для фронтенда (нашего javascrit - кода) информацией. Пока здесь "захардкоженные" данные. В них мы говорим, что очередь хода - у человека ('is_our_move' => true). Что компьютер сделал ход с поля с индексом 1 на поле 18 ('prev_move_from' => 1, 'prev_move_to' => 18)
В ключе position передаём результирующую позицию. Я её скопировал из файла chess.js, из функции initPosition. Белые фигуры оставил в начальном положении, как будто белые и не делали ход, а чёрного коня передвинул с поля 1 на поле 18:

Да, позиция странная - как будто белые не ходили вовсе. Но это пока заглушка функции, а на этих данных мы на стороне фронтенда научимся обрабатывать ответ сервера.
Ну и допустимые ходы (available_moves) я тоже скопировал из файла chess.js.
Обработка ответа сервера
Возвращаемся к нашему "фронтенду" - chess.js. Что надо сделать? Записать пришедшие данные в javascript - переменные (очередь хода, откуда и куда был предыдущий ход, позицию, и допустимые перемещения). И перерисовать позицию. Всё!
Но когда я сделал полную перерисовку позиции, заметил неприятное "моргание" при перерисовке. Что-ж, будем перерисовывать только фигуры в изменённых полях. Для этого надо изменять переменную position при перемещении фигуры о отправки на сервер. Сделаем небольшие правки и рефакторинг.
Небольшой рефакторинг
1. Отмечаем смену позиции
В функции make_move эту строку
cell_to.appendChild(figure);
уберём под условие существования фигуры, и сразу следом отметим в позиции, что фигура ушла с поля "откуда", и появилась в поле "куда":
if (figure) {
cell_to.appendChild(figure);
}
position[cell_index_to] = position[cell_index_from];
position[cell_index_from] = 0;
2. Убираем выделение преыдущего хода
В большой функции onCellClick ищем строку с комментарием
// кликнули по полю, куда можно переместиться с выделенного поля
и после него добавляем вызов функции, которую сейчас напишем:
deselect_prev_move_cells();
Эта функция будет снимать выделение полей "предыдущего хода". Зачем это здесь надо? Представьте - компьютер сделал ход, и сейчас выделены поля откуда и куда ходила его фигура. Мы выделили свою фигуру, и кликнули по полю, куда ей можно сходить. Т.е. мы сделали ход, и тут надо выделить поля откуда и куда мы пошли. И тут как раз и надо снять "выделение предыдущего хода" противника (компьютера).
Пишем функцию снятия "выделения предыдущего кода", расположив её например после функции deselect_cell:
function deselect_prev_move_cells() {
const prev_cells = Array.from(document.querySelectorAll('.board .cell.prev_move'));
for (let i=0; i < prev_cells.length; i++) {
prev_cells[i].classList.remove('prev_move');
}
}
Тут всё просто - берём все элементы - клетки доски, у которых есть css-класс prev_move, проходим по ним в цикле и убираем этот класс.
3. Установка фигуры на поле
Мы уже устанавливали фигуры на поля в функции showPosition. Нам понадобится в другом месте этот код. Конечно можно каждый раз, когда надо отобразить изменения в позиции, перерисовывать все поля доски в функции showPosition (слегка изменив её для очистки поля, где фигуры нет). Но, как я писал выше, картинка при этом "моргает". Очень быстро, но всё равно заметно. Так что будем вызывать установку фигур "точечно", не всем скопом.
В функции showPosition находим строку
let image = FIGURES[figure];
начиная с неё, и до конца функции убираем всё (можно в буфер обмена). Перенесём этот код в отдельную функцию. А здесь, где мы удалили код, вставляем вызов этой нашей
новой функции:
set_figure_to_cell(figure, i);
Саму новую функцию расположим, к примеру, перед функцией showPosition:
function set_figure_to_cell(figure, cell_index) {
let image = FIGURES[figure];
if (!image) {
return;
}
const figure_cell = document.createElement('div');
const image_tag = document.createElement('img');
image_tag.src = image;
figure_cell.appendChild(image_tag);
figure_cell.classList.add('figure');
cells[cell_index].appendChild(figure_cell);
}
Если вы перенесли текст функции через буфер обмена, не забудте изменить переменную i на cell_index - именно так мы назвали второй входной параметр.
Обработка ответа сервера
Помните, когда мы отсылали наш ход на сервер (на бекенд), мы указали, что при успешном ответе надо вызвать функцию set_game_state? Сейчас мы её напишем. Задача этой функции - обновить информацию о позиции в соответствии с тем, что прислал нам сервер, и показать обновлённое положение фигур на доске. Идём в самый конец файла chess.js, и пишем:
function set_game_state(game_state) {
// снимаем выделение с полей "предыдущего хода"
deselect_prev_move_cells();
// обновляем позицию
for (let i = 0; i < BoardSize**2; i += 1) {
if (position[i] === game_state.position[i]) {
continue;
}
cells[i].textContent = '';
set_figure_to_cell(game_state.position[i], i);
position[i] = game_state.position[i]
}
// обновляю допустимые ходы
available_moves = game_state.available_moves;
// Делаем выделение полей "предыдущего хода"
prev_move_from = game_state.prev_move_from;
prev_move_to = game_state.prev_move_to;
if (prev_move_from !== null) {
cells[prev_move_from].classList.add('prev_move');
}
if (prev_move_to != null) {
cells[prev_move_to].classList.add('prev_move');
}
// передаём очередь хода человеку
is_our_move = game_state.is_our_move;
}
Мы вызывали эту функцию, передавая ей ответ сервера. Здесь мы назвали входной параметр как game_state. Мы ведь ожидаем, что в ответе нам придёт именно состояние игры.
По комментариям в коде понятно что делает функция, она написана достаточно просто. Отдельно наверное стоит пояснить обновление позиции (строки 6 - 13)
Проходим по всем клеткам, от 0 до 63. Что мы сравниваем в строке 7? В массиве position хранится состояние доски на стороне клиента (фронтенда, браузера). Т.е. какие фигуры находились в клетках доски до отправки нашего хода на сервер (бекенд).
В массиве game_state.position сервер нам прислал информацию о том, какие фигуры находятся в полях доски после хода компьютера. Если в клетке до ответа компьютера была та-же фигура, что и после ответа - ничего делать в этой клетке не надо, мы идём на следующую итерацию цикла с помощью continue.
Если же в поле что-то поменялось (фигура ушла с этого поля, встала на это поле), то очищаем содержимое поля, вызываем функцию для установки нужной фигуры в поле, и обновляем информацию об этом поле:
cells[i].textContent = '';
set_figure_to_cell(game_state.position[i], i);
position[i] = game_state.position[i]
Итоги
Мы сделали отправку нашего хода на сервер. Получаем ответный ход компьютера, обновляем позицию, и можем снова ходить. Правда пока ответ компьютера захардкожен - он просто спустя две секунды отдаёт нам позицию, где наши белые фигуры находятся на начальных позициях, а чёрный конь пошёл на поле с6
Посмотреть что получилось, можно на демо - странице №4
Ну и исходные коды можно посмотреть на гитхабе