Часть про зачётное задание пока в процессе написания
Стандартный способ представления агрегатных данных в 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
).
В заключение подчеркнём две особенности объектов, из-за которых настоятельно не рекомендуется пользоваться объектами как ассоциативными массивами:
__proto__
)У 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