На главную Назад Вперёд

JSON

Часть про зачётное задание пока в процессе написания

Объекты в Javascript

Стандартный способ представления агрегатных данных в Javascript – объекты. В первом приближении они ведут себя как ассоциативные массивы с текстовыми ключами (более того, стандартные массивы в Javascript реализованы в рамках этой же концепции). Литералы для объектов выглядят так:

{ foo: 1, bar: 2, 21: 3, "baz": 4 }

Никакой разницы между foo и "foo" в качестве ключа нет. Как и между 21 и "21".

Доступ к значению объекта foo по ключу (или, как ещё говорят в Javascript-сообществе, свойству) bar осуществляется при помощи синтаксиса foo.bar или foo["bar"]. В первом случае bar должен быть корректным идентификатором (в частности, не начинаться с цифры). Если ключ не хранится в объекте, то результатом доступа по этому ключу является undefined. Отметим также, что глобальные переменные являются свойствами т.н. глобального объекта (для браузеров – window, для nodejs – global).

В заключение подчеркнём две особенности объектов, из-за которых настоятельно не рекомендуется пользоваться объектами как ассоциативными массивами:

JSON как универсальный текстовый формат

У Javascript-объектов существует текстовое представление JSON (от JavaScript Object Notation). Для преобразования между объектами и JSON можно использовать функции JSON.stringify и JSON.parse. Гарантируется, что все свойства, считанные при помощи JSON.parse из JSON, будут у результирующего объекта с правильными значениями (даже служебные, типа __proto__).

Если JSON.parse не удаётся считать JSON, происходит ошибка. Её можно отловить блоком try .. catch (подробности см. MDN).

JSON де-факто является одним из наиболее распространённых протоколов обмена информацией между различными приложениями. У него есть два важных достоинства:

К сожалению, JSON плохо поддерживает передачу бинарных данных: нетекстовые символы требуют экранирования (типичный экран выглядит как \u00XX, где XX – шестнадцатиричное представление экранируемого байта). Относительно экономным способом включения бинарных данных внутрь текстовых является Base64, кодирующий каждые три байта четырьмя буквами. В Javascript есть функции atob и btoa для перехода от Base64 к бинарным данным и наоборот.

Асинхронные запросы и функция fetch

В то время, когда браузеры всё ещё были в первую очередь средством обмена данными с веб-серверами (а не интерпретаторами языков CSS и Javascript, как сейчас), всё общение с веб-сервером происходило синхронно: пользователь вводил данные в адресную строку браузера или в форму на веб-странице, нажимал на кнопку, после чего ожидал от браузера результат (который браузер предоставлял в виде новой веб-страницы).

Потом появилась технология AJAX (Asynchronous Javascript And Xml), позволяющая программным образом отправлять запросы на сервер без перезагрузки страницы. Стоит отметить, что наличие слова Xml в названии технологии довольно случайно: xml как формат обмена данных не выдержал испытание временем, а функция XMLHttpRequest позволяет отправить запрос в совершенно произвольном формате.

С приходом в Javascript монады обещаний, произошедшим несколько лет назад, громоздкий интерфейс XMLHttpRequest сменился существенно более простой функцией fetch, возвращающей обещание. Самая простая форма fetch выглядит так:

fetch("http://yandex.ru")

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

Чтобы ответ не был заблокирован, веб-сервер обязан включить в этот ответ заголовок Access-Control-Allow-Origin со списком разрешённых имён серверов. При получении ответа браузер сравнивает этот список с именем сервера, на котором расположена текущая открытая страница.

Чтобы отправить при помощи fetch запрос типа POST, используется следующая конструкция:

fetch(адрес, { method: 'POST', body: тело_запроса })

Телом запроса может являться любой текст.

Как было сказано выше, fetch возвращает обещание. Результатом этого обещания является ответ (точнее, его заголовок). От него можно получить код состояния (хранится в свойсте status), успешность запроса (хранится в свойстве ok; является истинным, если код состояния в пределах от 200 до 299), а также – обещание тела ответа (при помощи нулярных методов blob, json или text в зависимости от требуемого типа результата).

Учебный чат-сервер

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

Сервер принимает только POST-запросы с телом в формате JSON. В случае успешного выполнения запроса ответ имеет код состояния 200, а содержание его тела запросозависимо.

В случае ошибки ответ имеет код состояния 500, а в теле ответа содержится текст с человекочитаемым описанием возникшей проблемы (как-то специфически обрабатывать ошибки не требуется; достаточно куда-нибудь выводить описание проблемы).

Далее следует описание запросов, поддерживаемых сервером.

Вход пользователя

{ action: "login", name: имя_пользователя }

Ответом является текст с идентификатором сессии (именуемым в дальнейшем токен). Рекомендуется запихнуть этот токен в локальное хранилище, иначе он потеряется при обновлении страницы или закрытии вкладки.

Ожидание

В любое свободное время клиент должен ждать ответа на запрос

{ action: "wait", token: токен }

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

Возможные ответы (имеют формат JSON):

{ event: "invalid_token", reason: причина }
{ event: "message",       message: сообщение }
{ event: "refresh_token", token: токен}

В первом случае нужно завершить сессию, информировав пользователя о причине завершения.

Во втором случае нужно вывести полученное сообщение. Формат сообщения таков:

{ name: имя_пользователя, timestamp: время_отправки_сообщения, content: содержимое_сообщения }

В третьем случае нужно обновить текущий токен.

ОПИСАТЬ ФОРМАТ ВРЕМЕНИ

Отправка сообщения

{ action: "post_message", token: токен, content: содержимое_сообщения }

На запрос приходит пустой ответ с кодом состояния 200. Если ответ какой-то другой или произошла ошибка, сообщение нужно переотправить или же информировать пользователя о том, что отправить сообщение не удалось.

Получение истории сообщений

{ action: "get_old_messages", token: токен }

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

{ messages: массив_сообщений, more: есть_ли_ещё_сообщения? }

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

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

Обновление истории сообщений

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

{ action: "relogin", token: токен }

Он приводит к изменению хранимого на сервере времени последнего входа пользователя. А от get_old_messages можно заново получить всю историю сообщений до момента логина.

Завершение сессии

{ action: "leave", token: токен }

Не забудьте сделать кнопку «завершить сессию».

Получение аватара

Для получения отметки «отлично» за задание требуется реализовать поддержку «аватаров» – в настоящем контексте так называется картинка, символизирующая собой пользователя.

Чтобы получить аватар пользователя, нужно сделать запрос

{ action: "get_avatar", token: токен, name: имя_пользователя }

Если у указанного пользователя нет аватара, ответ будет содержать JSON:

{ avatar_status: "no_avatar" }

В случае наличия аватара он будет передан в кодировке Base64:

{ avatar_status: "ok", avatar: картинка }

Чтобы вставить картинку в img элемент, нужно выставить в его свойство src так называемую объектную ссылку, которую можно получить из объекта типа Blob (тип для неструктурированных бинарных данных) методом URL.createObjectURL. Объект типа Blob можно получить из Base64-кодировки функцией

function getBlobFromBase64(base64) {
    var binaryText = atob(base64)
    
    var codes = []
    for (var i = 0; i < binaryText.length; i++) {
        codes.push(binaryText.charCodeAt(i))
    }

    var byteArray = new Uint8Array(codes)
    
    return new Blob([byteArray])
}

Сохранение аватара

{ action: "save_avatar", token: токен, avatar: картинка }

Картинка должна быть передана в формате Base64. Чтобы преобразовать содержимое файла в Base64, проще всего воспользоваться методом readAsDataURL у FileReader-объекта. Результатом будет т.н. Data URL, в котором после служебной информации идёт запятая, сразу за которой находится Base64-кодировка содержимого файла.

На запрос приходит пустой ответ с кодом состояния 200. Если произошла ошибка обработки запроса, нужно информировать пользователя о ней. Переотправлять аватар не нужно.

Пример клиента

По ВСТАВИТЬ ССЫЛКУ доступен пример того, как может выглядеть и работать клиент. Смотреть исходники довольно бесполезно: он написан не на Javascript, поэтому результирующий код читать весьма сложно. ;)

@ 2016 arbrk1, all rights reversed