Одна из самых больших функций в Rails 5 — это WebSockets. Похоже, что эта функция была частично вдохновлена Phoenix, веб-фреймворком Elixir. В этой статье мы рассмотрим создание тепловой карты потока Twitter в реальном времени с использованием Elixir и Phoenix.
Идея состоит в том, чтобы подключиться к примеру потока Twitter и нанести твиты на карту мира. Я не буду описывать инструкции по установке, потому что официальные руководства Phoenix делают намного лучше.
Вот для чего мы снимаем:
Настройка проекта Феникс
Давайте начнем! Сначала создайте новый проект Phoenix:
% mix phoenix.new heetweet && cd heetweet
Чтобы убедиться, что все настроено правильно, откройте http://localhost:4000
в вашем браузере, и вы должны приветствовать страницу по умолчанию Phoenix:
Для связи с API Twitter нам нужно установить ExTwitter , клиентскую библиотеку Twitter.
Мы будем указывать эту зависимость в mix.exs
, эквивалентном mix.exs
в Rails. Найдите функцию deps
и добавьте ExTwitter
и OAuth
, на который опирается первый:
defmodule Heetweet.Mixfile do use Mix.Project # ... defp deps do [{:phoenix, "~> 1.1.4"}, {:postgrex, ">= 0.0.0"}, {:phoenix_ecto, "~> 2.0"}, {:phoenix_html, "~> 2.4"}, {:phoenix_live_reload, "~> 1.0", only: :dev}, {:gettext, "~> 0.9"}, {:cowboy, "~> 1.0"}, {:oauth, github: "tim/erlang-oauth"}, {:extwitter, "~> 0.7.1"}] end end
Эрланг и Эликсир могут счастливо взаимодействовать!
Вы могли заметить, что
OAuth
— это библиотека Erlang , но ExTwitter зависит от нее. Оказывается, Эликсир и Эрланг могут счастливо взаимодействовать друг с другом. Это означает, что библиотеки из обоих языков могут использоваться на любом языке.
Затем вам нужно будет сказать Phoenix запустить приложение ExTwitter. Приложение в Elixir — это, по сути, автономная библиотека, которая может использоваться другими приложениями. Фактически, многие зависимости, указанные в функции deps
являются приложениями.
Возможно, вы слышали о деревьях наблюдения в Эликсире / Эрланге. Приложения могут поставляться с их собственными деревьями супервизора, которые могут самоизлечиться, если что-то пойдет не так.
Взгляните на функцию applications
указанную в том же файле mix.exs
:
defmodule Heetweet.Mixfile do use Mix.Project # ... def application do [mod: {Heetweet, []}, applications:[:phoenix, :phoenix_html, :cowboy, :logger, :gettext, :phoenix_ecto, :postgrex]] end end
Нам нужно добавить ExTwitter в список applications
:
def application do [mod: {Heetweet, []}, applications:[:phoenix, :phoenix_html, :cowboy, :logger, :gettext, :phoenix_ecto, :postgrex, :extwitter]] end
Настройка Twitter
Прежде чем мы сможем использовать ExTwitter, нам нужно перейти на https://apps.twitter.com/
, войти в систему и нажать «Создать новое приложение».
Вот что я заполнил:
- Имя : Хитвит
- Описание : Heetweet отображает твиты на тепловой карте
- Веб-сайт : http://www.example.com
Примечание. Вам нужно будет указать номер своего мобильного телефона в разделе профиля в разделе «Мобильный телефон».
После выполнения этих шагов у вас будет доступ к этой странице, содержащей ключ и секретный ключ потребителя, токен доступа и секретный токен доступа:
Нам нужно добавить их в config/config.exs
и добавить в config/config.exs
файла:
config :extwitter, :oauth, [ consumer_key: System.get_env("TWITTER_CONSUMER_KEY"), consumer_secret: System.get_env("TWITTER_CONSUMER_SECRET"), access_token: System.get_env("TWITTER_ACCESS_TOKEN"), access_token_secret: System.get_env("TWITTER_ACCESS_SECRET") ]
Конечно, вы никогда не захотите хранить конфиденциальные учетные данные в виде простого текста и передавать их в систему контроля версий. Поэтому вам нужно будет указать их в ~/.bashrc
или что-то эквивалентное (в зависимости от вашей оболочки):
export TWITTER_CONSUMER_KEY="xxxxxxxxxxxxxxxxxxxxxxxxx" export TWITTER_CONSUMER_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" export TWITTER_ACCESS_TOKEN="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" export TWITTER_ACCESS_SECRET="xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
После этого убедитесь, что вы либо открыли новое окно терминала, либо вручную запустили source ~/.bashrc
(или эквивалентный в зависимости от вашей оболочки).
Принимая Феникс за спин
Давайте удостоверимся, что мы все настроили правильно. Мы откроем Elixir REPL (read-eval-print-loop) и загрузим наше приложение Phoenix следующим образом:
% iex -S mix phoenix.server
Давайте проверим, правильно ли мы настроили ExTwitter, запустив запрос, который гарантированно даст результаты:
iex> ExTwitter.search("bieber", [count: 5]) \ |> Enum.map(fn(tweet) -> tweet.text end) \ |> Enum.join("\n---\n") \ |> IO.puts
Примечание: если вы копируете вставленный выше фрагмент, делайте это построчно, а не весь кусок.
Давайте пройдемся по тому, что делает этот код. Первая строка ищет «bieber» с ограничением в пять результатов. Результаты возвращаются в списке ExTwitter.Model.Tweet
s. Поскольку нас интересует только содержимое твита, мы запускаем функцию Enum.map
для извлечения text
каждого твита. Каждый твит представлен внутри так:
%ExTwitter.Model.Tweet{id: 724195286688645120, lang: "en", user: %ExTwitter.Model.User{contributors_enabled: false, id: 714513423896342530, screen_name: "asloveme4", lang: "pl", name: "ohai", url: "https://t.co/A8YqMw5YPh", description: "@justinbieber So take my hand and walk with me,\n Show me what to be,\n I need you to set me free", profile_image_url: "http://pbs.twimg.com/profile_images/722497035136413696/4xAxtZvN_normal.jpg", statuses_count: 1186, friends_count: 177, favourites_count: 80, show_all_inline_media: nil, utc_offset: nil, followers_count: 93, profile_banner_url: "https://pbs.twimg.com/profile_banners/714513423896342530/1460233025", default_profile_image: false, following: false, location: "Poland", protected: false, profile_background_image_url_https: "https://abs.twimg.com/images/themes/theme1/bg.png", quoted_status: nil, text: "RT @hot_or_not_pll: #43 Justin Bieber\nRT- hot \nFav- not https://t.co/3IpoxYL6qZ", entities: %{hashtags: [], media: [%{display_url: "pic.twitter.com/3IpoxYL6qZ", expanded_url: "http://twitter.com/hot_or_not_pll/status/723558996532187137/photo/1", user_mentions: [%{id: 723238093705367552, id_str: "723238093705367552", , favorite_count: 28, favorited: false, geo: nil}
Обратите внимание, что модель Tweet
имеет соответствующую модель %ExTwitter.Model.User
. Это будет полезно для нас при попытке определить страну пользователя.
Настройка карты
Мы будем использовать Leaflet , библиотеку Javascript, которая дает нам интерактивные карты. Откройте web/templates/layout/app.html.eex
. Мы собираемся очистить большую часть HTML и добавить CSS и Javascript, необходимые для Leaflet. Вот результат:
<!DOCTYPE html> <html lang="en"> <head> <meta charset="utf-8"> <meta http-equiv="X-UA-Compatible" content="IE=edge"> <meta name="viewport" content="width=device-width, initial-scale=1"> <meta name="description" content=""> <meta name="author" content=""> <title>Hello Heetweet!</title> <link rel="stylesheet" href="<%= static_path(@conn, "/css/app.css") %>"> <link rel="stylesheet" href="http://cdn.leafletjs.com/leaflet/v0.7.7/leaflet.css" /> </head> <body> <%= render @view_module, @view_template, assigns %> <script src="<%= static_path(@conn, "/js/app.js") %>"></script> </body> </html>
Настройка библиотеки Heatmap
К счастью, Leaflet поставляется с удобной библиотекой Heatmap, которая называется Leaflet.heat .
Я просто скачал библиотеку и поместил файл в web/static/vendor
.
WebSockets
Давайте доберемся до совершенства WebSocket! Во-первых, нам нужно включить клиентскую часть WebSockets в Phoenix. Откройте app.js
и раскомментируйте следующую строку:
import socket from "./socket"
Теперь web/static/js/socket.js
к web/static/js/socket.js
. Перейдите в конец файла:
let channel = socket.channel("tweets:lobby", {}) channel.join() .receive("ok", resp => { console.log("Joined successfully", resp) }) .receive("error", resp => { console.log("Unable to join", resp) })
Мы в основном указываем, что мы хотим, чтобы сокет подключался к теме «твиты: лобби» через канал. Сейчас мы оставим console.log
без изменений.
Настройка бэкэнда каналов
Давайте создадим канал Феникс. Phoenix поставляется с удобным генератором только для этого:
% mix phoenix.gen.channel Room tweets * creating web/channels/room_channel.ex * creating test/channels/room_channel_test.exs Add the channel to your `web/channels/user_socket.ex` handler, for example: channel "tweets:lobby", Heetweet.RoomChannel
Давайте последуем совету генератора и добавим канал в user_socket.ex
:
defmodule Heetweet.UserSocket do use Phoenix.Socket channel "tweets:lobby", Heetweet.RoomChannel # ... end
Когда вы направляетесь к своему приложению Phoenix, вы должны видеть
Joined successfully Object {}
в консоли браузера. Если вы достигли этой стадии, вы успешно подключили WebSockets в Фениксе. Теперь самое интересное!
Все вместе
Теперь, когда браузер может успешно подключиться к бэкэнду, что дальше? Что ж, нам нужно сообщить нашему бэкэнду, что он может начать отправлять нам данные. Чтобы сделать это, мы start_stream
в канал:
channel.join() .receive("ok", resp => { console.log("Joined successfully", resp) channel.push("start_stream", {}) // <--- }) .receive("error", resp => { console.log("Unable to join", resp) })
Затем нам нужно обработать сообщение start_stream
в start_stream
. web/channels/room_channel.ex
на web/channels/room_channel.ex
. Напомним, что мы ранее создали это с mix phoenix.channel
генератора mix phoenix.channel
. Вот функция, где происходит вся магия:
defmodule Heetweet.RoomChannel do use Heetweet.Web, :channel # ... def handle_in("start_stream", payload, socket) do stream = ExTwitter.stream_filter(locations: ['-180,-90,180,90']) |> Stream.map(fn x -> x.coordinates end) # Twitter gives us coordinates in reverse order (long first, then lat) for %{coordinates: [lng, lat]} <- stream do push socket, "new_tweet", %{lat: lat, lng: lng} end {:reply, {:ok, payload}, socket} end # ... end
Мы делаем здесь немало, поэтому давайте немного распакуем вещи.
Во-первых, мы должны получить поток твитов, которые содержат координаты. Мы хотим, чтобы твиты охватывали всю планету, и библиотека ExTwitter имеет функцию для этого:
ExTwitter.stream_filter(locations: ['-180,-90,180,90'])
Вот лакомый кусочек эликсира: вышеприведенная функция возвращает поток . В Ruby это похоже на перечисление lazy. Он не генерирует никаких значений, пока мы явно не запросим его. Фактически, мы можем связать другие потоковые операции, чтобы манипулировать каждым выдаваемым значением, и оно все равно не будет возвращать никакого значения. Таким образом, мы получаем поток координат из твитов:
stream = ExTwitter.stream_filter(locations: ['-180,-90,180,90']) |> Stream.map(fn x -> x.coordinates end)
Наконец, мы можем перебрать (потенциально бесконечный) поток координат с помощью for
понимания . Хотя for
понимания здесь выглядит как нормальный цикл, есть больше, чем кажется на первый взгляд. Эликсир for
понимания помогает отбросить нулевые значения. Это чрезвычайно полезно, поскольку некоторые твиты не имеют прикрепленных координат. В теле for
понимания мы можем вытолкнуть координаты в подключенный сокет:
for %{coordinates: [lng, lat]} <- stream do push socket, "new_tweet", %{lat: lat, lng: lng} end
Теперь вернемся к клиентской части. Откройте web/static/js/socket.js
. Сначала мы настроим карту и добавим пустой слой тепловой карты:
var tiles = L.tileLayer('http://{s}.tile.osm.org/{z}/{x}/{y}.png', { attribution: '© <a href="http://osm.org/copyright">OpenStreetMap</a> contributors', }).addTo(map); var heat = L.heatLayer([]).addTo(map);
Далее нам нужно обработать new_tweet
и его полезную нагрузку (координаты):
channel.on("new_tweet", payload => { // artificially add more points to get the heatmap effect for (var i = 0; i < 100; i++) { heat.addLatLng([payload.lat, payload.lng]) } })
Обратите внимание, что у нас есть цикл, который добавляет координаты к слою тепловой карты сто раз для данной координаты. Причина в том, что я слишком ленив, чтобы появился хороший эффект тепловой карты, и в интересах мгновенного удовлетворения я решил немного ускорить процесс.
Это оно! Вот Heetweet во всей своей красе:
Резюме
Надеюсь, вам понравилась эта статья и, что более важно, вы узнали что-то новое! Я призываю вас попробовать Phoenix, особенно если вам не терпелось поэкспериментировать с WebSockets.