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