Статьи

Супер-легкая активность с потоком

Stram

Ранее я писал о том, как создавать каналы активности в 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? Рассматриваете ли вы использовать его в будущем? Поделитесь своим мнением в комментариях.
Спасибо, что остаетесь со мной и до скорой встречи!