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

Ранний Интернет реализовал несколько решений в виде java-апплетов, полных программ, которые выполнялись внутри веб-страницы, но также были медленными и требовательными к ресурсам. Затем последовал опрос через javascript, который запрашивал у сервера какие-либо изменения каждые несколько секунд и соответственно обновлялся, но непрактичность была той же. Промышленность использовала несколько решений, таких как DHTML, Microsoft XMLHTTP, который был основой для XMLHttpRequest, и ajax с разной степенью успеха, пока не появились веб-сокеты.

Что такое веб-сокеты?

Websockets - это протокол, который находится поверх TCP / IP, чтобы разрешить одно полнодуплексное соединение. Это означает, что, в отличие от обычного HTTP, где клиент запрашивает, сервер доставляет и закрывает соединение, с веб-сокетами у нас есть соединение, которое остается открытым в ожидании запросов или ответов с любой стороны столько, сколько мы хотим, без выполнять дорогостоящую перезагрузку страницы или прибегать к странным уловкам для представления данных.

Что мы можем делать с веб-сокетами?

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

Круто, как я могу использовать это в Rails?

К счастью, все, что нам нужно для использования Websockets в Rails, легко доступно, и его очень легко настроить. Сначала у нас есть хранилище данных, в нашем случае Redis, которое будет действовать как сервер для отправки и получения сообщений. Затем у нас есть серия каналов, на которые наши пользователи подписываются. Эти подписки и являются фактическими веб-сокетами. Важно отметить, что мы не будем напрямую взаимодействовать с Redis ни для чего другого, кроме начальной настройки, поскольку Rails позаботится обо всем, что находится под капотом. Давайте начнем!

Начальная настройка

Сначала нам нужно установить Redis. Вы можете установить его на Mac через Homebrew, выполнив brew install redis. Если вы работаете в Arch Linux, вы можете использовать sudo pacman -Syu redis, а если в Ubuntu, вы можете запустить sudo apt install redis-server. Пользователи Windows могут попытать счастья с очень устаревшим портом или собственной разработкой Microsoft, которая также устарела, поэтому я бы предложил что-то вроде запуска WSL и установки для Ubuntu. После установки Redis вы можете оставить все в значениях по умолчанию, но если вы хотите посмотреть или что-то изменить, отметьте /etc/redis.conf.

Затем мы создадим новое приложение Rails, запустим наш редактор кода, проверим ./config/cable.yml и изменим значения разработки для adapter и url:

development:
  adapter: redis
    url: redis://localhost:6379/1

Затем нам нужно создать канал, поэтому введите rails g channel general. В результате будет создано несколько файлов, но нас интересуют следующие:

  • ./app/channels/general_channel.rb: Этот файл сообщает Rails, что делать, когда пользователь подписывается на канал и отписывается от него.
  • ./app/javascript/channels/general_channel.js: Здесь мы собираемся определить логику внешнего интерфейса нашего канала. Если вы хотите протестировать свою настройку, вы можете console.log написать сообщение в connected() методе. Учтите, что это не сработает до следующего шага, но должно появиться сразу, когда вы открываете страницу.

Чтобы завершить настройку, мы переходим к ./app/channels/general_channel.rb и меняем метод subscribed, чтобы включить stream_from "general_channel". Итак, наш чат настроен, и теперь осталось просто соединить все вместе.

Сначала наши маршруты, нам нужно всего три из них:

get '/', to: 'chat#index'
  post '/messages', to: 'chat#create'
  get '/messages/new', to: 'chat#new'

Затем нам нужен способ сохранить наши сообщения, поэтому введите rails g model Message content:text Нам также нужен контроллер для наших действий, поэтому введите rails g controller chat и определите следующие методы внутри:

def index
        @messages = Message.all
        @message = Message.new
    end
    def show
        @messages = Message.all
    end
    def create
        @message = Message.create(msg_params)
        if @message.save
            ActionCable.server.broadcast "general_channel", content: @message.content
        end
    end
    def new
        @message = Message.new
    end
    private  def msg_params
        params.require(:message).permit(:content)
    end

Здесь нет ничего нового, кроме нашего метода создания. Первая часть создает новое сообщение и, если оно сохраняется, запускает вторую часть, которая является широковещательной передачей на general канал с телом сообщения в качестве его содержимого.

Теперь не нужно иметь представление, чтобы использовать наши новые сверхспособности, поэтому перейдите к ./views/chat и создайте новый index.html.erb. Внутри мы просто собираемся построить простую форму:

<h1>Chat</h1>
<div id="chat-container">
<ul id="message-list">
<% @messages.each do |message| %>
    <li><%= message.content %></li>
<% end %>
</ul>
</div>
<hr>
<%= form_with model: @message, html: {class: "message-form"} do |f| %>
<%= f.text_field :content, class:'message-input' %>
<%= f.submit "Send message!", class: "send-button" %>
<% end %>

Если вы отправляете сообщение, ничего не происходит. Я знаю, что вы взволнованы, но все, что вы отправили, было сохранено в базе данных! Если вы обновите страницу, вы увидите свое сообщение, поэтому все, что нам нужно сделать сейчас, это обновить наш dom в ./app/javascript/general_channel.js:

consumer.subscriptions.create("GeneralChannel", {
  connected() {
    // Called when the subscription is ready for use on the server
    console.log("Connected to General channel");
  },
  disconnected() {
    // Called when the subscription has been terminated by the server
  },
  received(data) {
    // Called when there's incoming data on the websocket for this channel
    console.log(data);
    addMessage(data.content);
  }
});
function addMessage(message) {
  const messageList = document.getElementById("message-list");
  const containerDiv = document.getElementById("chat-container");
  let newComment = document.createElement("li");
  const messageInput = document.getElementsByClassName("message-input")[0];
  newComment.innerText = message;
  messageList.appendChild(newComment);
  messageInput.value = '';
  containerDiv.scrollTop = containerDiv.scrollHeight;
}

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

body {
    box-sizing: border-box;
}
.chat-container {
    width: 700px;
    margin: 0 auto;
    height: 500px;
    border: 1px solid #ddd;
}
h1 {
    text-align: center;
}
hr {
    border-top: 1px solid #ddd;
    border-bottom: none;
    width: 700px;
    margin: 0 30px 0 30px  auto;
}
#chat-container {
    width: 700px;
    margin: 0 auto;
    height: 500px;
    overflow-y: auto;
    border: 1px solid #ddd;
}
.message-form {
    display: flex;
    flex-flow: row wrap;
    align-items: center;
    width: 700px;
    margin: 0 auto;
}
.message-input {
    vertical-align: middle;
    height: 28px;
    width: 530px;
}
.send-button {
    border: none;
    background-color: #0086c3;
    color: #fff;
    padding: 8px 16px;
    margin-left: 16px;
    // width: 80px;
}
.send-button:hover {
    background-color: #29b6f6;
    cursor: pointer;
}

Вот и все! А теперь иди пообщайся!