Ранее я писал о том, как создавать каналы активности в Rails, используя гем public_activity . Сегодня я собираюсь представить вам Stream , платформу, предоставляющую API для простого создания сложных масштабируемых каналов. Вместе мы создадим демонстрационное приложение и интегрируем его с Stream.
Поток бесплатен до 3 миллионов обновлений в месяц. Он имеет поддержку нескольких регионов и обновления в режиме реального времени. Есть клиенты для Ruby, Python, Javascript, PHP, Java, C #, Scala и Go, а также интеграции для фреймворков Rails, Django и Laravel.
В настоящее время эта платформа активно развивается ( недавно было привлечено 1,75 миллиона долларов), и вам, безусловно, стоит попробовать.
Рабочая демоверсия доступна по адресу sitepoint-stream.herokuapp.com .
Исходный код этого поста можно найти на GitHub .
Особая благодарность Tommaso Barbugli за предоставление ценной информации для этой статьи.
Поток: прошлое и будущее
Платформа Stream была создана Тьерри Шелленбахом и Томмазо Барбугли, разработчиками Stream-Framework с открытым исходным кодом, библиотеки Python для создания масштабируемых новостных лент и потоков активности. Они отметили, что даже при использовании этой библиотеки многим разработчикам сложно подготовить необходимую инфраструктуру (требуются Redis или Cassandra, RabbitMQ и Python Celery). Кроме того, многие люди просили HTTP-уровень API, чтобы они могли использовать его с другими языками программирования. Так родилась идея создания Stream. После шести месяцев тестирования была выпущена первая бета-версия.
В настоящее время команда состоит из Тьерри, Томмазо и трех инженеров (Stream нанимает , если вам интересно). В последнее время они получили немало инвестиций, поэтому будущее этой компании выглядит действительно светлым. В ближайшем будущем команда планирует упростить добавление персонализированных каналов в приложения. Подход заключается в том, чтобы объединить ориентированную на пользователя аналитическую платформу с машинным обучением.
Если вы хотите понять основную идею Stream, потратьте немного времени и поиграйте с этим интерактивным учебником . Кроме того, уже есть демонстрационное приложение Rails, которое вы можете использовать в качестве примера.
Подготовка демо-приложения
Как я уже сказал, Stream обеспечивает интеграцию для трех фреймворков. Конечно, мы собираемся выбрать наш любимый Rails и использовать драгоценный камень stream-rails . В этой статье я буду использовать Rails 4, но stream-rails работает и с ActiveRecord 3.
Прежде чем продолжить, зарегистрируйтесь здесь — это бесплатно (до 3 миллионов обновлений в месяц), однако доступны и платные планы .
Давайте создадим новое приложение Rails без набора тестов по умолчанию:
$ rails new FeedMe -T Оставьте следующие драгоценные камни:
Gemfile
 [...] gem 'stream_rails' gem 'devise' gem 'bootstrap-sass' [...] 
и беги
 $ bundle install 
Я собираюсь использовать Devise для быстрого создания аутентификации, но вы можете использовать другое решение, если вы так склонны. (Недавно я рассмотрел некоторые из них , поэтому у вас есть много вариантов, из которых можно выбирать).
Давайте немного стилизовать приложение с нашим старым другом Bootstrap:
application.scss
 @import 'bootstrap-sprockets'; @import 'bootstrap'; 
макеты / application.html.erb
 [...] <nav class="navbar navbar-inverse"> <div class="container"> <div class="navbar-header"> <%= link_to 'FeedMe', root_path, class: 'navbar-brand' %> </div> <div id="navbar"> <ul class="nav navbar-nav"> </ul> </div> </div> </nav> <div class="container"> <% flash.each do |key, value| %> <div class="alert alert-<%= key %>"> <%= value %> </div> <% end %> <%= yield %> </div> [...] 
Аутентификация
  Запустите генераторы Devise, чтобы установить все необходимые файлы и создать модель User : 
 $ rails g devise:install $ rails g devise User 
Давайте дадим каждому пользователю имя:
 $ rails g migration add_name_to_users name:string 
Теперь запустите миграцию:
 $ rake db:migrate 
  Если вы используете защищенные атрибуты, атрибут name должен быть разрешен при регистрации: 
application_controller.rb
 [...] before_action :configure_permitted_parameters, if: :devise_controller? protected def configure_permitted_parameters devise_parameter_sanitizer.for(:sign_up) << :name end [...] 
Скопируйте представления Devise в папку представлений для настройки:
 $ rails g devise:views 
  Настройте вид регистрации, чтобы включить поле name : 
просмотров / Завещание / регистрация / new.html.erb
 [...] <div class="field"> <%= f.label :name %><br /> <%= f.text_field :name %> </div> [...] 
Пока мы работаем с представлениями, давайте также обновим макет, чтобы пользователи могли выйти:
макеты / application.html.erb
 [...] <ul class="nav navbar-nav"> <% if user_signed_in? %> <li> <a href="#" class="dropdown-toggle" data-toggle="dropdown"> <i class="glyphicon"></i> <%= current_user.name %><b class="caret"></b> </a> <ul class="dropdown-menu"> <li><%= link_to 'Log out', destroy_user_session_path, method: :delete %></li> </ul> </li> <% end %> </ul> [...] 
Корневая страница
Теперь создайте контроллер статических страниц вместе с представлением для корневой страницы и соответствующим маршрутом:
pages_controller.rb
 class PagesController < ApplicationController before_action :authenticate_user! def index end end 
 authenticate_user!  это метод, предоставляемый Devise, который перенаправляет пользователей на страницу входа, если они не прошли проверку подлинности. 
конфиг / routes.rb
 [...] root to: 'pages#index' [...] 
просмотров / страниц / index.html.erb
 <div class="page-header"><h1>Welcome!</h1></div> 
Отлично, теперь пришло время добавить некоторые «элементы» на нашу страницу, которые наши пользователи смогут прикрепить, как в Pinterest.
Добавление предметов
На самом деле не имеет значения, как выглядят наши элементы, поэтому для демо они будут иметь заголовок и сообщение. После этого каждый пользователь сможет закреплять и откреплять их — информация об этих действиях будет отображаться в ленте активности.
  Прежде всего, создайте модель Item : 
 $ rails g model Item user:references title:string message:text $ rake db:migrate 
Установите связь один-ко-многим на стороне пользователя:
модели / user.rb
 [...] has_many :items [...] 
Теперь контроллер:
items_controller.rb
 class ItemsController < ApplicationController before_action :authenticate_user! def new @item = Item.new end def create @item = Item.new(item_params) @item.save flash[:success] = "Item created!" redirect_to root_path end private def item_params params.require(:item).permit(:message, :title) end end 
Маршруты:
routes.rb
 [...] resources :items, only: [:new, :create] [...] 
И мнение:
просмотров / пункты / new.html.erb
 <div class="page-header"><h1>New item</h1></div> <%= form_for @item do |f| %> <div class="form-group"> <%= f.label :title %> <%= f.text_field :title, class: 'form-control' %> </div> <div class="form-group"> <%= f.label :message %> <%= f.text_area :message, class: 'form-control' %> </div> <%= f.submit 'Create', class: 'btn btn-primary' %> <% end %> 
Давайте также отобразим список элементов на корневой странице
pages_controller.rb
 [...] def index @items = Item.order('created_at DESC') end [...] 
просмотров / страниц / index.html.erb
 <div class="page-header"><h1>Welcome!</h1></div> <%= render @items %> 
просмотров / пункты / _item.html.erb
 <div class="well well-sm"> <small class="text-muted"><%= time_ago_in_words item.created_at %> ago</small> <h3><%= item.title %></h3> <p><%= item.message %></p> </div> 
  Brilliant!  Вы можете либо создать несколько предметов вручную, либо использовать seed.rb для автоматизации этой задачи. 
  (что является предпочтительным способом, конечно). 
Интегрирующий поток
Интеграция Stream действительно проста. Создайте новый файл инициализатора:
конфиг / Инициализаторы / stream_rails.rb
 require 'stream_rails' StreamRails.configure do |config| config.api_key = ENV["STREAM_KEY"] config.api_secret = ENV["STREAM_SECRET"] config.timeout = 30 end 
Чтобы получить пару ключей, вы должны авторизоваться на веб-сайте getstream.io и открыть панель инструментов. Здесь для вас будет создано приложение (убедитесь, что для статуса установлено значение «Вкл.»), И это место, где вы можете получить ключ и секрет.
  Я храню пару ключей в переменных среды, но вы можете использовать другой метод, просто убедитесь, что 
  эти ключи не являются общедоступными. 
Закрепление и открепление предметов
Stream сохраняет модели ActiveRecord в ленте как действия. Действия — это объекты, которые в основном сообщают, кто выполнил какое действие с каким объектом. В простейшем случае действие состоит из актера , глагола и объекта . Stream требует, чтобы модели реагировали на следующие методы:
-   activity_object— возвращает объект действия (например, модель ActiveRecord).
-   activity_actor— возвращает субъекта, выполняющего действие (по умолчаниюself.user).
-   activity_verb— возвращает строковое представление глагола (по умолчанию используется имя класса модели).
  Прежде всего, мы хотим позволить пользователям прикреплять и откреплять элементы.  Для этого нам понадобится отдельная модель Pin 
  оснащен методами Stream: 
 $ rails g model Pin user:references item:references $ rake db:migrate 
Настроить модель:
модели / pin.rb
 [...] belongs_to :user belongs_to :item validates :item, presence: true, uniqueness: {scope: :user} validates :user, presence: true, uniqueness: {scope: :item} include StreamRails::Activity as_activity def activity_object self.item end [...] 
  include StreamRails::Activity и as_activity Pin функциональностью Stream.  Обратите внимание, что я не использую self.user потому что значение по умолчанию ( self.user ) подходит нам идеально.  Мы также оставляем activity_verb на значение по умолчанию Pin . 
Теперь давайте добавим кнопки закрепления и открепления к частичному элементу:
просмотров / пункты / _item.html.erb
 <div class="well well-sm"> <small class="text-muted"><%= time_ago_in_words item.created_at %> ago</small> <h3><%= item.title %></h3> <p><%= item.message %></p> <%= render "pins/form", item: item %> </div> 
Вот частичная форма булавки:
просмотров / контакты / _form.html.erb
 <% if item.user_pin(current_user) %> <%= button_to "Unpin", pin_path(item.user_pin(current_user)), method: :delete, class: "btn btn-primary btn-sm btn-danger" %> <% else %> <%= form_for :pin, url: pins_path do |f| %> <input class="btn btn-primary btn-sm" type="submit" value="Pin"> <%= f.hidden_field :item_id, value: item.id %> <% end %> <% end %> 
  Как вы можете видеть, закрепление просто означает создание новой записи в таблице pins при предоставлении элемента и 
  идентификатор пользователя  Открепление означает, конечно, удаление этой записи.  Вот соответствующий контроллер: 
pins_controller.rb
 class PinsController < ApplicationController before_action :authenticate_user! def create @pin = Pin.new(pin_params) @pin.user = current_user @pin.save! flash[:success] = "Pinned!" redirect_to root_path end def destroy @pin = Pin.find(params[:id]) @pin.destroy flash[:success] = "Unpinned!" redirect_to root_path end private def pin_params params.require(:pin).permit(:item_id) end end 
Пока мы настроили модель, эти действия будут отслеживаться Stream.
Наконец, маршруты:
routes.rb
 [...] resources :pins, only: [:create, :destroy] [...] 
  Наш следующий шаг — позволить пользователям следовать друг другу.  Будут отображены только действия, выполненные подписчиками 
  в персонализированной подаче. 
Подписка и отмена подписки
Следовать, еще раз, означает создание простой записи, которая сообщает, за кем последовал пользователь:
 $ rails g model Follow target_id:integer user_id:integer $ rake db:migrate 
Измените миграцию следующим образом:
дб / Миграция / xxx_create_follows.rb
 class CreateFollows < ActiveRecord::Migration def change create_table :follows do |t| t.integer :target_id, index: true t.integer :user_id, index: true t.timestamps null: false end add_index :follows, [:target_id, :user_id], unique: true end end 
  Настройте модель Follow чтобы добавить функциональность Stream и установить ассоциации: 
модели / follow.rb
 [...] belongs_to :user belongs_to :target, class_name: "User" validates :target_id, presence: true validates :user_id, presence: true include StreamRails::Activity as_activity def activity_notify [StreamRails.feed_manager.get_notification_feed(self.target_id)] end def activity_object self.target end [...] 
Этот метод
 def activity_notify [StreamRails.feed_manager.get_notification_feed(self.target_id)] end 
используется для создания канала уведомлений . Этот тип канала полезен для уведомления определенных пользователей о действии. В нашем случае мы уведомляем пользователя о том, что кто-то последовал за ним.
Не забудьте установить ассоциацию на другой стороне:
модели / user.rb
 [...] has_many :follows def followed_by(user = nil) user.follows.find_by(target_id: id) end [...] 
  followed_by — метод проверки, следует ли пользователь за кем-то.  Мы собираемся использовать его в ближайшее время. 
  Нам нужно отобразить список пользователей и предоставить кнопку подписки / отписки.  Вот пользовательский UserController : 
users_controller.rb
 class UsersController < ApplicationController def index @users = User.all end end 
Маршруты:
routes.rb
 [...] resources :users, only: [:index] [...] 
И мнения:
просмотров / пользователей / index.html.erb
 <div class="page-header"><h1>Users</h1></div> <%= render @users %> 
просмотров / пользователей / _user.html.erb
 <h3><%= user.name %></h3> <% if user.followed_by(current_user) %> <%= button_to "Unfollow", follow_path(user.followed_by(current_user)), method: :delete, :class => "btn btn-primary btn-sm btn-danger" %> <% else %> <%= form_for :follow, url: follows_path do |f| %> <%= f.hidden_field :target_id, value: user.id %> <input class="btn btn-primary btn-sm btn-default" type="submit" value="Follow"> <% end %> <% end %> 
Это очень похоже на то, что мы имели с функциональностью pin / unpin.
Давайте обновим верхнее меню:
макеты / application.html.erb
 [...] <li><%= link_to 'Users', users_path %></li> [...] 
Конечно, нам также потребуется контроллер для управления следующим:
follows_controller.rb
 class FollowsController < ApplicationController before_action :authenticate_user! def create follow = Follow.new(follow_params) follow.user = current_user if follow.save StreamRails.feed_manager.follow_user(follow.user_id, follow.target_id) end flash[:success] = 'Followed!' redirect_to users_path end def destroy follow = Follow.find(params[:id]) if follow.user_id == current_user.id follow.destroy! StreamRails.feed_manager.unfollow_user(follow.user_id, follow.target_id) end flash[:success] = 'Unfollowed!' redirect_to users_path end private def follow_params params.require(:follow).permit(:target_id) end end 
  StreamRails.feed_manager.follow_user(follow.user_id, follow.target_id) следует за пользователем;  должны быть предоставлены идентификаторы актера и цели.  unfollow_user работает точно так же. 
Не забудьте настроить маршруты:
routes.rb
 [...] resources :follows, only: [:create, :destroy] [...] 
Отлично, теперь убедитесь, что все работает правильно. Последний шаг отображает фактическую подачу.
Рендеринг Ленты
Мы собираемся сделать три канала:
- Личный канал пользователя . Этот канал, как следует из названия, отображает все действия для определенного пользователя.
- Плоские новостные ленты показывают, что произошло недавно. Плоские каналы отображают действия без какой-либо группировки, и это тип каналов по умолчанию в Stream.
- Агрегированные новостные ленты позволяют пользователю указать формат агрегации. Мы собираемся отображать булавки и следует отдельно, используя этот канал.
- Фид уведомлений пользователя аналогичен агрегированным фидам, однако уведомления могут быть помечены как прочитанные, и вы можете получить счетчик количества невидимых и непрочитанных уведомлений.
  Для этих каналов FeedsController .  Я собираюсь начать с личного канала пользователя: 
feeds_controller.rb
 class FeedsController < ApplicationController before_action :authenticate_user! before_action :create_enricher def user @user = User.find(params[:id]) feed = StreamRails.feed_manager.get_user_feed(@user.id) results = feed.get['results'] @activities = @enricher.enrich_activities(results) end private def create_enricher @enricher = StreamRails::Enrich.new end end 
  Используя этот feed = StreamRails.feed_manager.get_user_feed(@user.id строки feed = StreamRails.feed_manager.get_user_feed(@user.id мы получаем доступ к feed = StreamRails.feed_manager.get_user_feed(@user.id пользователя. 
  Что это create_enricher метод create_enricher ?  Необработанные данные, прочитанные из ленты, выглядят так: 
 {"actor": "User:1", "verb": "like", "object": "Item:42"} 
  Этот формат не готов к использованию в шаблонах.  Поэтому механизм обогащения подготавливает данные, загруженные из канала, для использования в шаблонах.  Внутри create_enricher мы создаем экземпляр класса StreamRails::Enrich а затем просто используем enrich_activities для подготовки наших данных. 
Вот маршрут:
routes.rb
 [...] scope path: '/feeds', controller: :feeds, as: 'feed' do get 'user/:id', to: :user, as: :user end [...] 
И мнение:
просмотров / каналы / user.html.erb
 <div class="page-header"><h1>My feed</h1></div> <% for activity in @activities %> <%= render_activity activity %> <% end %> 
  render_activity — это еще один специальный метод, который будет использоваться в шаблонах.  Этот метод ожидает получения обогащенных данных и будет искать партиалы в папках действий (для плоских каналов) или в агрегированном_активности (для агрегированных каналов).  Частицы должны быть названы в честь глагола действия (pin, follow, etc.) 
просмотров / деятельность / _follow.html.erb
 <div class="well well-sm"> <p><small class="text-muted"><%= time_ago_in_words activity['time'] %> ago</small></p> <p><strong><%= activity['object'].name %></strong> and <strong><%= activity['actor'].name %></strong> are now friends</p> </div> 
  render_activity автоматически отправляет activity в локальную область render_activity .  Здесь мы просто получаем доступ к именам объекта и цели.  Мы можем вызвать activity['object'].name потому что activity['object'] возвращает экземпляр класса User . 
просмотров / деятельность / _pin.html.erb
 <div class="well well-sm"> <p><small class="text-muted"><%= time_ago_in_words activity['time'] %> ago</small></p> <p> <strong><%= activity['actor'].name %></strong> pinned <strong><%= activity['object'].title %></strong> </p> </div> 
Здесь процесс тот же: мы показываем, кто прикрепил какой элемент.
Теперь предоставьте ссылку в верхнем меню для доступа к этому недавно созданному каналу:
макеты / application.html.erb
 [...] <ul class="dropdown-menu"> <li><%= link_to 'My feed', feed_user_path(current_user) %></li> [...] </ul> [...] 
Также измените частичку пользователя:
просмотров / пользователей / _user.html.erb
 <h3><%= link_to user.name, feed_user_path(user) %></h3> [...] 
Теперь давайте добавим плоский канал:
feeds_controller.rb
 [...] def flat feed = StreamRails.feed_manager.get_news_feeds(current_user.id)[:flat] results = feed.get['results'] @activities = @enricher.enrich_activities(results) end [...] 
  feed = StreamRails.feed_manager.get_news_feeds(current_user.id) обращается к новостной ленте, а [:flat] указывает, что плоская лента должна быть получена. 
просмотров / каналы / flat.html.erb
 <div class="page-header"><h1>Flat feed</h1></div> <% for activity in @activities %> <%= render_activity activity %> <% end %> 
  Еще раз, мы передаем обогащенные данные в render_activity .  Пока мы уже создали папку действий и части внутри, мы можем перейти к агрегированному каналу. 
feeds_controller.rb
 [...] def aggregated feed = StreamRails.feed_manager.get_news_feeds(current_user.id)[:aggregated] results = feed.get['results'] @activities = @enricher.enrich_aggregated_activities(results) end [...] 
  На этот раз это [:aggregated] вместо [:flat] . 
просмотров / каналы / aggregated.html.erb
 <div class="page-header"><h1>Aggregated feed</h1></div> <% for activity in @activities %> <%= render_activity activity %> <% end %> 
Здесь требуются отдельные партиалы внутри папки aggregated_activity :
просмотров / aggregated_activity / _pin.html.erb
 <% if activity['actor_count'] == 1 %> <%= activity['activities'][0]['actor'].name %> pinned <%= pluralize(activity['activity_count'], 'item') %> <% elsif activity['actor_count'] == 2 %> <%= activity['activities'][0]['actor'].name %> and <%= activity['activities'][1]['actor'].name %> pinned <%= activity['activity_count'] %> items <% else %> <%= activity['activities'][0]['actor'].name %>, <%= activity['activities'][1]['actor'].name %> and <%= activity['actor_count'].name - 2 %> more pinned <%= activity['activity_count'] %> items <% end %> <div class="pull-right"> <i class="glyphicon glyphicon-time"></i> <%= time_ago_in_words(activity['updated_at']) %> ago </div> <% for activity in activity['activities'] %> <%= render_activity activity %> <% end %> 
  for activity in activity['activities'] render_activity activity for activity in activity['activities'] render_activity activity for activity in activity['activities'] выполняет каждое действие одно за другим, а действие render_activity activity использует те же партиалы внутри папки действий, которую мы недавно создали.  Обратите внимание, что вы можете передать дополнительные аргументы этому методу, чтобы выбрать другие партиалы, например: 
 <%= render_activity activity, :prefix => "aggregated_" %> 
  Это будет искать партиалы с префиксом aggregated_ . 
просмотров / aggregated_activity / _follow.html.erb
 <i class="glyphicon glyphicon-time"></i> <%= time_ago_in_words(activity['updated_at']) %> ago <% for activity in activity['activities'] %> <%= render_activity activity %> <% end %> 
Наконец добавьте фид уведомлений:
feeds_controller.rb
 [...] def notification feed = StreamRails.feed_manager.get_notification_feed(current_user.id) results = feed.get['results'] @activities = @enricher.enrich_aggregated_activities(results) end [...] 
просмотров / каналы / notification.html.erb
 <div class="page-header"><h1>Your notification feed</h1></div> <% for activity in @activities %> <%= render_activity activity %> <% end %> 
Настройте маршруты:
routes.rb
 [...] scope path: '/feeds', controller: :feeds, as: 'feed' do get 'me', to: :user get 'flat', to: :flat get 'aggregated', to: :aggregated get 'notification', to: :notification end [...] 
Также обновите верхнее меню:
макеты / application.html.erb
 [...] <li><%= link_to 'Flat feed', feed_flat_path %></li> <li><%= link_to 'Aggregated feed', feed_aggregated_path %></li> <li><%= link_to 'Notification feed', feed_notification_path %></li> [...] 
Теперь загрузите ваш сервер и проверьте, как все это работает! Только не забывайте, что вы должны следить за пользователем, чтобы просмотреть его действия (вы также можете следить за собой).
Вывод
  В этой статье мы обсудили Stream, платформу для простого создания масштабируемых каналов активности.  Не стесняйтесь 
  просмотреть документацию и поэкспериментировать с ней. 
  Вы когда-нибудь пробовали использовать Stream?  Рассматриваете ли вы использовать его в будущем?  Поделитесь своим мнением в комментариях. 
  Спасибо, что остаетесь со мной и до скорой встречи! 
