Advertisement

Phoenix for Rails Devs

Diacode
Oct. 16, 2016
Advertisement

More Related Content

Advertisement
Advertisement

Phoenix for Rails Devs

  1. PHOENIX FOR RAILS DEVS Conferencia Rails Madrid 15/10/2016
  2. If you're having talk problems, I feel bad for you, son. I got 61 problems, but a slide ain't one; hit me!
  3. About me: Javier Cuevas @javier_dev RUBY ON RAILS SHOP WHO EMBRACED ELIXIR AIRBNB FOR DOGS “MAJESTIC” RAILS 3.2 MONOLITH
  4. GUESTS OF THE DAY
  5. José Valim Former Rails Core Team member. He was trying to make Rails really thread safe but... ended up creating a new programming language (Elixir). Oops! PerformanceProductivity
  6. Chris McCord Author of render_sync a Ruby gem to have real-time partials in Rails (before ActionCable). It got complicated and... he ended up creating a new web framework (Phoenix). Oops!
  7. WHAT IS ELIXIR?
  8. Elixir is a dynamic, functional language designed for building scalable and maintainable applications. Elixir leverages the Erlang VM, known for running low-latency, distributed and fault-tolerant systems.
  9. WHAT IS PHOENIX?
  10. Phoenix is a productive web framework for Elixir that does not compromise speed and maintainability.
  11. PHOENIX = PRODUCTIVITY + PERFORMANCE
  12. PERFORMANCE
  13. I don’t care about performance. * that much
  14. PRODUCTIVITY SHORT TERM LONG TERM
  15. SHORT TERM PRODUCTIVITY
  16. The Phoenix Backpack • Mix (generators, tasks, etc.) • Erlang libraries + Hex.pm • ES6 out of the box • Live reload • Nice error pages • Concurrent test tools + DocTests • Great docs (for real) • Channels + Presence • OTP: humongous set of libraries for distributed computing • Erlang observer • ....
  17. Remember the “15 min blog” by DHH? That was productivity! Let’s try build the “15 min real time Twitter” (or something close to).
  18. https://github.com/javiercr/conferencia_ror_demo
  19. LET’S GET STARTED
  20. rails new twitter_demo mix phoenix.new twitter_demo
  21. !"" twitter_demo #"" app $   #"" assets $   #"" channels $   #"" controllers $   #"" helpers $   #"" jobs $   #"" mailers $   #"" models $   !"" views #"" bin #"" config #"" db #"" lib #"" log #"" public #"" test #"" tmp !"" vendor !"" twitter_demo #"" config #"" deps #"" lib #"" node_modules #"" priv #"" test !"" web #"" channels #"" controllers #"" models #"" static #"" templates !"" views
  22. $ cd twitter_demo $ bundle install $ rake db:create $ rails server $ cd twitter_demo $ mix deps.get && npm install $ mix ecto.create $ mix phoenix.server
  23. ROUTES
  24. # /web/router.ex defmodule TwitterDemo.Router do use TwitterDemo.Web, :router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers end pipeline :api do plug :accepts, ["json"] end scope "/", TwitterDemo do pipe_through :browser # Use the default browser stack get "/", PageController, :index get "/timeline", PageController, :timeline end # Other scopes may use custom stacks. # scope "/api", TwitterDemo do # pipe_through :api # end end # /config/routes.rb Rails.application.routes.draw do root 'page#index' get '/timeline' => 'page#timeline' end
  25. # /web/router.ex defmodule TwitterDemo.Router do use TwitterDemo.Web, :router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers end pipeline :api do plug :accepts, ["json"] end scope "/", TwitterDemo do pipe_through :browser # Use the default browser stack get "/", PageController, :index get "/timeline", PageController, :timeline end # Other scopes may use custom stacks. # scope "/api", TwitterDemo do # pipe_through :api # end end # /config/routes.rb Rails.application.routes.draw do root 'page#index' get '/timeline' => 'page#timeline' end
  26. Plug It’s an Elixir library that tries to solve the same problem than Rack does for Ruby. A plug is a function or module which always receives and returns a connection, doing some data transformations in the middle. When we compose multiple plugs we form a pipeline.
  27. CONTROLLER
  28. # /web/router.ex defmodule TwitterDemo.Router do use TwitterDemo.Web, :router pipeline :browser do plug :accepts, ["html"] plug :fetch_session plug :fetch_flash plug :protect_from_forgery plug :put_secure_browser_headers end pipeline :api do plug :accepts, ["json"] end scope "/", TwitterDemo do pipe_through :browser # Use the default browser stack get "/", PageController, :index get "/timeline", PageController, :timeline end # Other scopes may use custom stacks. # scope "/api", TwitterDemo do # pipe_through :api # end end $ rails g controller Page index timeline
  29. # /app/controllers/page_controller.rb class PageController < ApplicationController def index end def timeline end end # /web/controllers/page_controller.ex defmodule TwitterDemo.PageController do use TwitterDemo.Web, :controller def index(conn, _params) do render conn, "index.html" end def timeline(conn, params) do conn |> assign(:nickname, params["nickname"]) |> render("timeline.html") end end
  30. # /app/controllers/page_controller.rb class PageController < ApplicationController def index end def timeline end end # /web/controllers/page_controller.ex defmodule TwitterDemo.PageController do use TwitterDemo.Web, :controller def index(conn, _params) do render conn, "index.html" end def timeline(conn, params) do conn |> assign(:nickname, params["nickname"]) |> render("timeline.html") end end
  31. Typical code in OOP / imperative programming: people = DB.find_customers orders = Orders.for_customers(people) tax = sales_tax(orders, 2013) filing = prepare_filing(tax) We could rewrite it as... filing = prepare_filing( sales_tax(Orders.for_customers( DB.find_customers), 2013)) Pipe Operator |>
  32. Pipe Operator |> With Elixir pipe operator we can do just filing = DB.find_customers |> Orders.for_customers |> sales_tax(2013) |> prepare_filing “|>” passes the result from the left expression as the first argument to the right expression. Kinda like the Unix pipe “|”. It’s just useful syntax sugar.
  33. VIEWS / TEMPLATES
  34. <!-- /app/views/page/index.html.erb --> <h1>Welcome to TwitterDemo!</h1> <%= form_tag timeline_path, method: "get" do %> <label for="nickname">Nickname</label>: <input type="text" name="nickname"></input> <button>Connect!</button> <% end %> <!-- /web/templates/page/index.html.eex --> <h1>Welcome to TwitterDemo!</h1> <%= form_tag(page_path(@conn, :timeline), method: "get") do %> <label for="nickname">Nickname</label>: <input type="text" name="nickname"></input> <button>Connect!</button> <% end %>
  35. <!-- /app/views/page/timeline.html.erb --> <script>window.nickname = "<%= @nickname %>";</script> <div id="messages"></div> <input id="chat-input" type="text"></input> <!-- /web/templates/page/timeline.html.eex --> <script>window.nickname = "<%= @nickname %>";</script> <div id="messages"></div> <input id="chat-input" type="text"></input>
  36. MODEL
  37. $ rails g model Message author:string content:text $ rake db:migrate $ mix phoenix.gen.model Message messages author:string content:text $ mix ecto.create && mix ecto.migrate
  38. # /web/models/message.ex defmodule TwitterDemo.Message do use TwitterDemo.Web, :model @derive {Poison.Encoder, only: [:author, :content, :inserted_at]} schema "messages" do field :author, :string field :content, :string timestamps() end @doc """ Builds a changeset based on the `struct` and `params`. """ def changeset(struct, params %{}) do struct |> cast(params, [:author, :content]) |> validate_required([:author, :content]) end end # /app/models/message.rb class Message < ApplicationRecord validates_presence_of :author, :content end
  39. # /web/models/message.ex defmodule TwitterDemo.Message do use TwitterDemo.Web, :model @derive {Poison.Encoder, only: [:author, :content, :inserted_at]} schema "messages" do field :author, :string field :content, :string timestamps() end @doc """ Builds a changeset based on the `struct` and `params`. """ def changeset(struct, params %{}) do struct |> cast(params, [:author, :content]) |> validate_required([:author, :content]) end end # /app/models/message.rb class Message < ApplicationRecord validates_presence_of :author, :content end
  40. Ecto You could think about Ecto as “the ActiveRecord of Elixir”. But better don’t. It’s not even an ORM (in its purest definition). It’s a database wrapper and it’s main target it’s PostgreSQL. Other database are supported too. Main concepts behind Ecto are: Schemas: each Model defines a struct with its schema. Changesets: define a pipeline of transformations (casting, validation & filtering) over our data before it hits the database.
  41. CHANNEL
  42. $ rails g channel Timeline new_msg $ mix phoenix.gen.channel Timeline
  43. # /app/channels/timeline_channel.rb # Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading. class TimelineChannel < ApplicationCable::Channel def subscribed @nickname = params[:nickname] stream_from "timeline" end def unsubscribed # Any cleanup needed when channel is unsubscribed end def new_msg(payload) # Careful with creating the record here # http://www.akitaonrails.com/2015/12/28/fixing-dhh-s- rails-5-chat-demo message = Message.create!(content: payload['content'], author: @nickname) # DHH suggests doing this in a background job instead, I’m not sure why? ActionCable.server.broadcast 'timeline', message: message end end $ mix phoenix.gen.channel Timeline
  44. # /web/channels/user_socket.ex defmodule TwitterDemo.UserSocket do use Phoenix.Socket ## Channels channel "timeline:lobby", TwitterDemo.TimelineChannel ## Transports transport :websocket, Phoenix.Transports.WebSocket # transport :longpoll, Phoenix.Transports.LongPoll # Socket params are passed from the client and can # be used to verify and authenticate a user. After # verification, you can put default assigns into # the socket that will be set for all channels, ie # # {:ok, assign(socket, :user_id, verified_user_id)} # # To deny connection, return `:error`. # # ... def connect(params, socket) do {:ok, assign(socket, :nickname, params["nickname"])} end # .... def id(_socket), do: nil end # /app/channels/timeline_channel.rb # Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading. class TimelineChannel < ApplicationCable::Channel def subscribed @nickname = params[:nickname] stream_from "timeline" end def unsubscribed # Any cleanup needed when channel is unsubscribed end def new_msg(payload) # Careful with creating the record here # http://www.akitaonrails.com/2015/12/28/fixing-dhh-s- rails-5-chat-demo message = Message.create!(content: payload['content'], author: @nickname) # DHH suggests doing this in a background job instead ActionCable.server.broadcast 'timeline', message: message end end
  45. # /web/channels/timeline_channel.ex defmodule TwitterDemo.TimelineChannel do use TwitterDemo.Web, :channel alias TwitterDemo.Message def join("timeline:lobby", payload, socket) do # Add authorization logic here as required. {:ok, socket} end def handle_in("new_msg", %{"content" => content,}, socket) do changeset = Message.changeset(%Message{}, %{ content: content, author: socket.assigns.nickname }) case Repo.insert(changeset) do {:ok, message} -> broadcast! socket, "new_msg", %{message: message} {:noreply, socket} {:error, _changeset} -> {:reply, {:error, %{error: "Error saving the message"}}, socket} end end def handle_out("new_msg", payload, socket) do push socket, "new_msg", payload {:noreply, socket} end end # /app/channels/timeline_channel.rb # Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading. class TimelineChannel < ApplicationCable::Channel def subscribed @nickname = params[:nickname] stream_from "timeline" end def unsubscribed # Any cleanup needed when channel is unsubscribed end def new_msg(payload) # Careful with creating the record here # http://www.akitaonrails.com/2015/12/28/fixing-dhh-s- rails-5-chat-demo message = Message.create!(content: payload['content'], author: @nickname) # DHH suggests doing this in a background job instead ActionCable.server.broadcast 'timeline', message: message end end
  46. # /web/channels/timeline_channel.ex defmodule TwitterDemo.TimelineChannel do use TwitterDemo.Web, :channel alias TwitterDemo.Message def join("timeline:lobby", payload, socket) do # Add authorization logic here as required. {:ok, socket} end def handle_in("new_msg", %{"content" => content,}, socket) do changeset = Message.changeset(%Message{}, %{ content: content, author: socket.assigns.nickname }) case Repo.insert(changeset) do {:ok, message} -> broadcast! socket, "new_msg", %{message: message} {:noreply, socket} {:error, _changeset} -> {:reply, {:error, %{error: "Error saving the message"}}, socket} end end def handle_out("new_msg", payload, socket) do push socket, "new_msg", payload {:noreply, socket} end end # /app/channels/timeline_channel.rb # Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading. class TimelineChannel < ApplicationCable::Channel def subscribed @nickname = params[:nickname] stream_from "timeline" end def unsubscribed # Any cleanup needed when channel is unsubscribed end def new_msg(payload) # Careful with creating the record here # http://www.akitaonrails.com/2015/12/28/fixing-dhh-s- rails-5-chat-demo message = Message.create!(content: payload['content'], author: @nickname) # DHH suggests doing this in a background job instead ActionCable.server.broadcast 'timeline', message: message end end
  47. Pattern Matching In Elixir: a = 1 does not mean we are assigning 1 to the variable a. Instead of assigning a variable, in Elixir we talk about binding a variable . The equal signs means we are asserting that the left hand side (LHS) is equal to the right one (RHS). It’s like basic algebra. iex> a = 1 1 iex> 1 = a 1 iex> [1, a, 3] = [1, 2, 3] [1, 2, 3] iex> a 2
  48. Pattern Matching Function signatures use pattern matching. Therefore we can have more than one signature. defmodule Factorial do def of(0), do: 1 def of(x), do: x * of(x-1) end look mum! programming without if - else
  49. # /web/channels/timeline_channel.ex defmodule TwitterDemo.TimelineChannel do use TwitterDemo.Web, :channel alias TwitterDemo.Message def join("timeline:lobby", payload, socket) do # Add authorization logic here as required. {:ok, socket} end def handle_in("new_msg", %{"content" => content,}, socket) do changeset = Message.changeset(%Message{}, %{ content: content, author: socket.assigns.nickname }) case Repo.insert(changeset) do {:ok, message} -> broadcast! socket, "new_msg", %{message: message} {:noreply, socket} {:error, _changeset} -> {:reply, {:error, %{error: "Error saving the message"}}, socket} end end def handle_out("new_msg", payload, socket) do push socket, "new_msg", payload {:noreply, socket} end end # /app/channels/timeline_channel.rb # Be sure to restart your server when you modify this file. Action Cable runs in a loop that does not support auto reloading. class TimelineChannel < ApplicationCable::Channel def subscribed @nickname = params[:nickname] stream_from "timeline" end def unsubscribed # Any cleanup needed when channel is unsubscribed end def new_msg(payload) # Careful with creating the record here # http://www.akitaonrails.com/2015/12/28/fixing-dhh-s- rails-5-chat-demo message = Message.create!(content: payload['content'], author: @nickname) # DHH suggests doing this in a background job instead ActionCable.server.broadcast 'timeline', message: message end end Jose, Don’t forget to mention OTP!
  50. JAVASCRIPT
  51. # /app/assets/javascripts/channels/timeline.coffee App.timeline = App.cable.subscriptions.create {channel: "TimelineChannel", nickname: window.nickname}, connected: -> # Called when the subscription is ready for use on the server chatInput = document.querySelector("#chat-input") chatInput.addEventListener "keypress", (event) => if event.keyCode == 13 @new_msg chatInput.value chatInput.value = "" received: (payload) -> @_renderdMessage(payload.message) new_msg: (message) -> @perform 'new_msg', {content: message} _renderdMessage: (message) -> # [...] messagesContainer.appendChild(messageItem) // /web/static/js/socket.js import {Socket} from "phoenix" let socket = new Socket("/socket", { params: { token: window.userToken, nickname: window.nickname }}) socket.connect() let channel = socket.channel("timeline:lobby", {}) let chatInput = document.querySelector("#chat-input") let renderMessage = (message) => { // [...] messagesContainer.appendChild(messageItem) } chatInput.addEventListener("keypress", event => { if(event.keyCode === 13){ channel.push("new_msg", {content: chatInput.value}) chatInput.value = "" } }) channel.on("new_msg", payload => { renderMessage(payload.message) }) channel.join() export default socket
  52. HOMEWORK (for you)
  53. 1. Send history of messages when connecting to channel. 2. Add Presence module (to display who is online). 3. Create a startup with this, become a unicorn and profit! * Only 1. & 2. are solved here https://github.com/diacode/talkex/tree/feature/message-db-persistence hint: it takes 10 minutes with phoenix v1.2
  54. tl;dr $ rails new my_project $ mix phoenix.new my_project $ rails g [x] $ mix phoenix.gen.[x] $ bundle install $ mix deps.get $ rake db:migrate $ mix ecto.migrate $ rails server $ mix phoenix.server $ rails console $ iex -S mix bundle + rake Mix RubyGems Hex.pm Rack Plug Minitest / RSpec ExUnit ActiveRecord Ecto ActionCable Channels + Presence sprockets Brunch (npm based) Redis / Sidekiq / Resque OTP
  55. LONG TERM PRODUCTIVY
  56. EXPLICIT > IMPLICIT or at least some reasonable balance
  57. “Functional Programming is about making the complex parts of your program explicit” – José Valim
  58. Next steps (for you) • Watch every talk by José Valim & Chris McCord Really, you won’t regret. • Books: Programming Elixir – Dave Thomas Programming Phoenix – Chris McCord, Bruce Tate & José Valim. • Elixir Getting Started Guide (really good!) http://elixir-lang.org/getting-started/introduction.html • Phoenix Guide (really good!) http://www.phoenixframework.org/docs/overview • Elixir Radar (newsletter) http://plataformatec.com.br/elixir-radar • Madrid |> Elixir MeetUp http://www.meetup.com/es-ES/Madrid-Elixir/
  59. THANK YOU Questions? Special thanks go to Diacode’s former team: Victor Viruete, Ricardo García, Artur Chruszcz & Bruno Bayón <3 [ now you can blink again ]
Advertisement