В статье мы попытаемся описать два совершенно не связанных с собой аспекта децентрализованных одностраничных приложений. Это соединение двух пользователей и сохранение паролей в одностраничном приложении при помощи браузера.
Информация об используемой технологии WebRTC — webrtc.org. В браузере весь смысл общения завязан на этой технологии, а точнее на WebRTC API которое доступно для Front-end разработчика.
Одной из целей было создать децентрализованный чат, чат без использования сервера. Однако, сервер оказался просто необходим для реализации следующих возможностей:
Рассмотрим детально схему соединения для двух пользователей. Для этого выделим основные шаги в обычной схеме соединения Клиента А и Клиента Б:
Выделенные слова в трех случаях как раз показывают зачем нужен сервер на начальном этапе соединения. Для обмена данными через сервер.
В нашем чате оба клиента одновременно могут выступать инициаторами соединения, но в результате мы все равно хотим получить лишь одно соединение RTCDataChannel между двумя клиентами. Сигнальный сервер конечно может указать какому клиенту выступать инициатором — а какому ждать предложение. Но для минимизации ошибки при соединении мы запускаем механизм в котором каждый из клиентов является инициирующем и принимающем одновременно. В результате оставляем то соединение клиент-клиент, которое образуется быстрее. Рассмотрим более детально этот аспект.
Ниже представлена схема соединения двух инициирующих/принимающих клиентов.
Таким образом при инициировании соединения двумя клиентами, между ними будет всего один RTCDataChannel, а не два.
При работы с приложением каждый пользователь проходит следующие этапы:
Для этого в приложении были предусмотрены три страницы, каждая из которых предназначена для соответствующего этапа (структура сайта):
Но страницы генерируются на клиенте, а для перехода между ними используется HistoryAPI. Важным моментом для удобства использования приложения, является сохранение авторизационных данных пользователя в браузере (пароля), используя при этом поведение браузера по умолчанию.
В нашем случае, введенные данные пользователя должны предлагаться к сохранению для формы логина при смене url с "/login" на "/chat", а при смене "/login" на "/register" не должны предлагаться. На практике это оказалось невозможно реализовать. Сохранение предлагалось и при переходе с "/login" на "/register" и с "/register" на "/login". Таким образом задача заключалась в отмене сохранения данных в браузере для определенных случаев.
Для решения данного вопроса были использованы различные методы, которые представлены ниже.
Чтобы отключить автозаполнение на странице регистрации, устанавливаем для всей формы атрибут 'autocomplete' со значением 'off'.
Разметка для данного варианта:
Ссылка на данный вариант решения stackoverflow.com/a/468295.
Пробовали отключить автозаполнение на странице регистрации установив атрибут 'autocomplete' со значением 'off' для полей input, то есть имени и пароля пользователя.
Разметка для данного варианта:
В предыдущей ссылке обсуждается и этот вариант.
Данный вариант предполагает добавление в разметку скрытого элемента div, который включает в себя поля имени и пароля пользователя, которые всегда будут оставаться пустыми. Разметка для данного варианта:
Ссылка на данный вариант решения stackoverflow.com/a/25111774. Аналогичное решение предложено и в данном источнике.
В данном варианте прибегаем к помощи js. При клике пользователя на кнопку «Войти» по событию submit выполняем обнуление полей имени и пароля.
Программный код:
Обнуление данных выполнялось как сразу после клика, так и через некоторый интервал времени ( SetTimeout() ).
Последний из вариантов заключается в создании новой кнопки в форме, которая бы выполняла роль кнопки submit.
Разметка данного варианта:
Программный код:
Все вышеприведенные способы не позволяли при одном переходе сохранять пароль, а при другом не сохранять. Можно было либо сохранять всегда, либо не сохранять никогда.
Отсюда конечно о наболевшем, что давно пора для одностраничных приложений сделать JS API для браузера о сохранении пароля:
Что-то вроде этого было бы замечательно.
В настоящее время в качестве решения рассматриваемого вопроса в приложении была изменена структура сайта на следующую:
Где страница /account имеет два внутренних состояния login и register, в зависимости от этого состояния мы и генерируем ту или иную разметку. Только так мы смогли решить проблему с контролируемым сохранением пароля в браузере.
SPACHAT (Single Page Application Chat) — веб приложение для обмена сообщениями между клиентами с помощью технологии WebRTC, которое мы и реализовываем. Реализованное приложение или просто чат можно посмотреть на сайте spachat.net.
Непосредственно сам код доступны по ссылке github.com/volodalexey/spachat.
Информация об используемой технологии WebRTC — webrtc.org. В браузере весь смысл общения завязан на этой технологии, а точнее на WebRTC API которое доступно для Front-end разработчика.
Одной из целей было создать децентрализованный чат, чат без использования сервера. Однако, сервер оказался просто необходим для реализации следующих возможностей:
- Генерация уникальных id для идентификации подключенных устройств (девайсов)
- Обмен SDP (session description protocol) для инициализации соединения Клиент — Клиент; для сигнализации клиентам использовался websocket протокол
Создание соединения
Рассмотрим детально схему соединения для двух пользователей. Для этого выделим основные шаги в обычной схеме соединения Клиента А и Клиента Б:
- Клиент А является инициатором соединения и генерирует предложение (Offer), т.е sdp-предложение, которое содержит все доступные кодеки, на которых может общаться клиент и другую информацию. Клиент А отправляет свое sdp-предложение Клиенту Б.
- Клиент Б принимает sdp-предложение и генерирует ответ (Answer), т.е sdp-ответ. На данном этапе Клиент Б генерирует у себя все доступные кодеки и выбирает те, которые доступны обоим клиентам, добавляет свою информацию и отправляет его Клиенту А.
- Клиент А получает sdp-ответ от Клиента Б. Далее Клиент А уже напрямую связывается с Клиентом Б на основе полученного ответа и информации в нём. После этого между ними образовался RTCDataChannel — канал обмена данными.
Выделенные слова в трех случаях как раз показывают зачем нужен сервер на начальном этапе соединения. Для обмена данными через сервер.
В нашем чате оба клиента одновременно могут выступать инициаторами соединения, но в результате мы все равно хотим получить лишь одно соединение RTCDataChannel между двумя клиентами. Сигнальный сервер конечно может указать какому клиенту выступать инициатором — а какому ждать предложение. Но для минимизации ошибки при соединении мы запускаем механизм в котором каждый из клиентов является инициирующем и принимающем одновременно. В результате оставляем то соединение клиент-клиент, которое образуется быстрее. Рассмотрим более детально этот аспект.
Ниже представлена схема соединения двух инициирующих/принимающих клиентов.
- Одновременно оба клиента получают оповещение по вебсокет-соединению о том, что они в одном чате. Клиент А и Клиент Б инициируют соединение — каждый генерирует своё sdp-предложение и отсылает его на сервер.
- Сервер в свою очередь отправляет полученные предложения адресатам (предложение от Клиента А отправляет Клиенту Б, предложение от Клиента Б отправляет Клиенту А).
- Каждый клиент после получения sdp-предложения генерирует sdp-ответ на него. Оба клиента посылают свои sdp-ответы на сервер.
- Сервер в свою очередь отправляет полученные sdp-ответы их адресатам (ответ от Клиента А отправляет Клиенту Б, ответ от Клиента Б отправляет Клиенту А).
- Каждый клиент получает sdp-ответ и принимает его. Важное условие что у каждого клиента есть логика, что с каждым другим клиентом не может быть более одного соединения. Соответственно, если у Клиента Б есть открытый RTCDataChannel с Клиентом А, то он не создает новый RTCDataChannel с ним, можно сказать, все остальное просто игнорируется. Таким образом то соединение, которое создастся быстрее и выиграет.
Таким образом при инициировании соединения двумя клиентами, между ними будет всего один RTCDataChannel, а не два.
Регистрация пользователя и Browser History
При работы с приложением каждый пользователь проходит следующие этапы:
- Регистрация — для каждого нового пользователя.
- Авторизация — для каждого ранее зарегистрированного пользователя.
- Непосредственно работа с чатом.
Для этого в приложении были предусмотрены три страницы, каждая из которых предназначена для соответствующего этапа (структура сайта):
- /register — страница регистрации
- /login — страница логина
- /chat — страница чата
Но страницы генерируются на клиенте, а для перехода между ними используется HistoryAPI. Важным моментом для удобства использования приложения, является сохранение авторизационных данных пользователя в браузере (пароля), используя при этом поведение браузера по умолчанию.
В нашем случае, введенные данные пользователя должны предлагаться к сохранению для формы логина при смене url с "/login" на "/chat", а при смене "/login" на "/register" не должны предлагаться. На практике это оказалось невозможно реализовать. Сохранение предлагалось и при переходе с "/login" на "/register" и с "/register" на "/login". Таким образом задача заключалась в отмене сохранения данных в браузере для определенных случаев.
Для решения данного вопроса были использованы различные методы, которые представлены ниже.
Автозаполнение для формы
Чтобы отключить автозаполнение на странице регистрации, устанавливаем для всей формы атрибут 'autocomplete' со значением 'off'.
Разметка для данного варианта:
<form autocomplete="off" data-role="loginForm">
<label>Имя пользователя:</label>
<input type="text" name="userName">
<label>Пароль пользователя:</label>
<input type="password" name="userPassword">
<button type="submit">Войти</button>
</form>
Ссылка на данный вариант решения stackoverflow.com/a/468295.
Автозаполнение для конкретных полей
Пробовали отключить автозаполнение на странице регистрации установив атрибут 'autocomplete' со значением 'off' для полей input, то есть имени и пароля пользователя.
Разметка для данного варианта:
<form data-role="loginForm">
<label>Имя пользователя:</label>
<input type="text" name="userName" autocomplete="off">
<label>Пароль пользователя:</label>
<input type="password" name="userPassword" autocomplete="off">
<button type="submit">Войти</button>
</form>
В предыдущей ссылке обсуждается и этот вариант.
Скрытие полей
Данный вариант предполагает добавление в разметку скрытого элемента div, который включает в себя поля имени и пароля пользователя, которые всегда будут оставаться пустыми. Разметка для данного варианта:
<form data-role="loginForm">
<div style={{display:'none'}}>
<input type="text" />
<input type="password" />
</div>
<label>Имя пользователя:</label>
<input type="text" name="userName" autocomplete="off">
<label>Пароль пользователя:</label>
<input type="password" name="userPassword" autocomplete="off">
<button type="submit">Войти</button>
</form>
Ссылка на данный вариант решения stackoverflow.com/a/25111774. Аналогичное решение предложено и в данном источнике.
Обнуление полей при submit
В данном варианте прибегаем к помощи js. При клике пользователя на кнопку «Войти» по событию submit выполняем обнуление полей имени и пароля.
Программный код:
handleSubmit: function(event) {
this.loginForm = document.querySelector('[data-role="loginForm"]');
event.preventDefault();
this.loginForm.elements.userName.value = '';
this.loginForm.elements.userPassword.value = '';
}
Обнуление данных выполнялось как сразу после клика, так и через некоторый интервал времени ( SetTimeout() ).
Программный переход через HistoryAPI
Последний из вариантов заключается в создании новой кнопки в форме, которая бы выполняла роль кнопки submit.
Разметка данного варианта:
<form data-role="loginForm">
<label>Имя пользователя:</label>
<input type="text" name="userName" autocomplete="off">
<label>Пароль пользователя:</label>
<input type="password" name="userPassword" autocomplete="off">
<button type="button" data-action="submit" onclick="handleClick">Войти</button>
</form>
Программный код:
handleClick(event) {
if (event.target.dataset.action === 'submit'){
history.pushState({foo: 'bar'}, 'Title', '/chat')
}
}
Все вышеприведенные способы не позволяли при одном переходе сохранять пароль, а при другом не сохранять. Можно было либо сохранять всегда, либо не сохранять никогда.
Отсюда конечно о наболевшем, что давно пора для одностраничных приложений сделать JS API для браузера о сохранении пароля:
window.navigator.promtPasswordSave()
Что-то вроде этого было бы замечательно.
В настоящее время в качестве решения рассматриваемого вопроса в приложении была изменена структура сайта на следующую:
- /account — страница регистрации и логина
- /chat — страница чата
Где страница /account имеет два внутренних состояния login и register, в зависимости от этого состояния мы и генерируем ту или иную разметку. Только так мы смогли решить проблему с контролируемым сохранением пароля в браузере.
SPACHAT (Single Page Application Chat) — веб приложение для обмена сообщениями между клиентами с помощью технологии WebRTC, которое мы и реализовываем. Реализованное приложение или просто чат можно посмотреть на сайте spachat.net.
Непосредственно сам код доступны по ссылке github.com/volodalexey/spachat.