Slide 1

Slide 1 text

Um clone do Twitter com Phoenix e Vue.js @philipsampaio

Slide 2

Slide 2 text

Sobre Twitter

Slide 3

Slide 3 text

Sobre Vue.js

Slide 4

Slide 4 text

Sobre Phoenix

Slide 5

Slide 5 text

Philip Sampaio @philipsampaio @philss

Slide 6

Slide 6 text

github.com/philss/floki

Slide 7

Slide 7 text

magnetis.com.br/dev

Slide 8

Slide 8 text

No content

Slide 9

Slide 9 text

Twitter

Slide 10

Slide 10 text

No content

Slide 11

Slide 11 text

Twitter

Slide 12

Slide 12 text

No content

Slide 13

Slide 13 text

Twitter é importante!

Slide 14

Slide 14 text

No content

Slide 15

Slide 15 text

Não podemos correr esse risco

Slide 16

Slide 16 text

Precisamos de uma alternativa

Slide 17

Slide 17 text

Será feito, aqui e agora

Slide 18

Slide 18 text

Um MVP do Twitter

Slide 19

Slide 19 text

MVP que eu possa twittar

Slide 20

Slide 20 text

MVP que eu possa seguir

Slide 21

Slide 21 text

MVP que eu possa ver tweets

Slide 22

Slide 22 text

Um nome

Slide 23

Slide 23 text

sabiah

Slide 24

Slide 24 text

Falando em pássaros

Slide 25

Slide 25 text

No content

Slide 26

Slide 26 text

Poderoso

Slide 27

Slide 27 text

Poderoso Herda os superpoderes da Elixir

Slide 28

Slide 28 text

Produtivo

Slide 29

Slide 29 text

Produtivo Geradores, guides, metaprogramação

Slide 30

Slide 30 text

Produtivo Um pouco parecido com Rails

Slide 31

Slide 31 text

sabiah

Slide 32

Slide 32 text

sabiah ⏳ 1. tweetar 2. seguir 3. ver timeline com tweets de quem sigo

Slide 33

Slide 33 text

mix phoenix.new sabiah

Slide 34

Slide 34 text

No content

Slide 35

Slide 35 text

web/

Slide 36

Slide 36 text

lib/

Slide 37

Slide 37 text

sabiah ⏳ 1. tweetar 2. seguir 3. ver timeline com tweets de quem sigo

Slide 38

Slide 38 text

Uma timeline estática Tweetar

Slide 39

Slide 39 text

Timeline é onde vejo novos tweets de quem sigo
 (também os meus tweets)

Slide 40

Slide 40 text

Começa pelo router.ex

Slide 41

Slide 41 text

scope "/", Sabiah do pipe_through :browser # Use the default browser stack get "/", TimelineController, :index end web/router.ex

Slide 42

Slide 42 text

TimelineController

Slide 43

Slide 43 text

defmodule Sabiah.TimelineController do use Sabiah.Web, :controller def index(conn, _params) do render conn, "index.html" end end web/controllers/timeline_controller.ex

Slide 44

Slide 44 text

defmodule Sabiah.TimelineController do use Sabiah.Web, :controller def index(conn, _params) do render conn, "index.html" end end web/controllers/timeline_controller.ex

Slide 45

Slide 45 text

defmodule Sabiah.TimelineView do use Sabiah.Web, :view end web/views/timeline_view.ex

Slide 46

Slide 46 text

Timeline

Tweet
  • {{ tweet }}
web/templates/timeline/index.html.eex

Slide 47

Slide 47 text

{{ }} ?

Slide 48

Slide 48 text

No content

Slide 49

Slide 49 text

No content

Slide 50

Slide 50 text

{{ message }}
new Vue({ el: '#app', data: { message: 'Hello world' } }); priv/examples/vue.html

Slide 51

Slide 51 text

No content

Slide 52

Slide 52 text

new Vue({ el: '#app', data: { message: 'Hello world' } }); priv/examples/vue.html

Slide 53

Slide 53 text

No content

Slide 54

Slide 54 text

{{ message }}
priv/examples/vue.html

Slide 55

Slide 55 text

No content

Slide 56

Slide 56 text

Timeline

Slide 57

Slide 57 text

Timeline

Tweet
  • {{ tweet }}
web/templates/timeline/index.html.eex

Slide 58

Slide 58 text

Precisamos da app Vue.js

Slide 59

Slide 59 text

npm install --save vue

Slide 60

Slide 60 text

Zero de configuração

Slide 61

Slide 61 text

No content

Slide 62

Slide 62 text

import 'phoenix_html'; import Vue from 'vue/dist/vue'; new Vue({ el: '#timeline-app', data: { newTweet: '', tweets: [] }, methods: { postTweet: function() { this.tweets.unshift(this.newTweet); this.newTweet = ''; } } }); web/static/js/app.js

Slide 63

Slide 63 text

Timeline

Tweet
  • {{ tweet.content }}
web/templates/timeline/index.html.eex

Slide 64

Slide 64 text

Temos uma timeline

Slide 65

Slide 65 text

No content

Slide 66

Slide 66 text

No content

Slide 67

Slide 67 text

No content

Slide 68

Slide 68 text

Enviando tweets Tweetar

Slide 69

Slide 69 text

Web Sockets

Slide 70

Slide 70 text

Phoenix Channels

Slide 71

Slide 71 text

Um canal

Slide 72

Slide 72 text

Uma timeline por usuário

Slide 73

Slide 73 text

mix phoenix.gen.channel Timeline

Slide 74

Slide 74 text

No content

Slide 75

Slide 75 text

$ novo tweet timeline:42 Philip, id 42

Slide 76

Slide 76 text

defmodule Sabiah.UserSocket do use Phoenix.Socket channel "timeline:*", Sabiah.TimelineChannel transport :websocket, Phoenix.Transports.WebSocket end web/channels/user_socket.ex

Slide 77

Slide 77 text

defmodule Sabiah.UserSocket do use Phoenix.Socket channel "timeline:*", Sabiah.TimelineChannel transport :websocket, Phoenix.Transports.WebSocket end web/channels/user_socket.ex

Slide 78

Slide 78 text

conecta timeline:42 Philip, id 42 $

Slide 79

Slide 79 text

Conectamos pelo cliente JS

Slide 80

Slide 80 text

import socket from './socket'; const userId = 42; let channel = socket.channel(`timeline:${userId}`, {}); channel.join() .receive('ok', resp => { console.log('Joined successfully', resp) }) .receive('error', resp => { console.log('Unable to join', resp) }); // The app ... web/static/js/app.js

Slide 81

Slide 81 text

No Phoenix, aceitamos ou não

Slide 82

Slide 82 text

defmodule Sabiah.TimelineChannel do use Sabiah.Web, :channel def join("timeline:", _payload, _socket) do {:error, %{reason: "User id is missing."}} end def join("timeline:" <> user_id, _payload, socket) do socket = assign(socket, :user_id, user_id) {:ok, socket} end def handle_in("new_tweet", payload, socket) do broadcast socket, "new_tweet", payload {:noreply, socket} end end web/channels/timeline_channel.ex

Slide 83

Slide 83 text

Nosso canal aceita tweets

Slide 84

Slide 84 text

defmodule Sabiah.TimelineChannel do use Sabiah.Web, :channel def join("timeline:", _payload, _socket) do {:error, %{reason: "User id is missing."}} end def join("timeline:" <> user_id, _payload, socket) do socket = assign(socket, :user_id, user_id) {:ok, socket} end def handle_in("new_tweet", payload, socket) do broadcast socket, "new_tweet", payload {:noreply, socket} end end web/channels/timeline_channel.ex

Slide 85

Slide 85 text

novo tweet timeline:42 Philip, id 42 $

Slide 86

Slide 86 text

let app = new Vue({ el: '#timeline-app', data: { newTweet: '', tweets: [] }, methods: { postTweet: function() { channel.push('new_tweet', { tweet: this.newTweet }); this.newTweet = ''; } } }); channel.on('new_tweet', (payload) => { app.tweets.unshift(payload.tweet); }); web/static/js/app.js

Slide 87

Slide 87 text

O Phoenix envia a resposta

Slide 88

Slide 88 text

defmodule Sabiah.TimelineChannel do use Sabiah.Web, :channel def join("timeline:", _payload, _socket) do {:error, %{reason: "User id is missing."}} end def join("timeline:" <> user_id, _payload, socket) do socket = assign(socket, :user_id, user_id) {:ok, socket} end def handle_in("new_tweet", payload, socket) do broadcast socket, "new_tweet", payload {:noreply, socket} end end web/channels/timeline_channel.ex

Slide 89

Slide 89 text

novo tweet timeline:42 Philip, id 42 tweet $

Slide 90

Slide 90 text

let app = new Vue({ el: '#timeline-app', data: { newTweet: '', tweets: [] }, methods: { postTweet: function() { channel.push('new_tweet', { tweet: this.newTweet }); this.newTweet = ''; } } }); channel.on('new_tweet', (payload) => { app.tweets.unshift(payload.tweet); }); web/static/js/app.js

Slide 91

Slide 91 text

No content

Slide 92

Slide 92 text

Salvando Tweets Tweetar

Slide 93

Slide 93 text

É necessário um model

Slide 94

Slide 94 text

Tweet

Slide 95

Slide 95 text

Tweet user_id, content

Slide 96

Slide 96 text

mix phoenix.gen.model Tweet tweets user_id:references:users content

Slide 97

Slide 97 text

No content

Slide 98

Slide 98 text

defmodule Sabiah.Tweet do use Sabiah.Web, :model schema "tweets" do field :content, :string belongs_to :user, Sabiah.User timestamps() end @doc """ Builds a changeset based on the `struct` and `params`. """ def changeset(struct, params \\ %{}) do struct |> cast(params, [:user_id, :content]) |> validate_required([:user_id, :content]) end end web/models/tweet.ex

Slide 99

Slide 99 text

Precisamos salvar esse tweet

Slide 100

Slide 100 text

alias Sabiah.Tweet def handle_in("new_tweet", %{ "content" => content } = payload, socket) do user_id = socket.assigns[:user_id] tweet_changeset = Tweet.changeset(%Tweet{}, %{user_id: user_id, content: content}) Repo.insert!(tweet_changeset) broadcast socket, "new_tweet", payload {:noreply, socket} end web/channels/timeline_channel.ex

Slide 101

Slide 101 text

Pronto. Salvando tweets!

Slide 102

Slide 102 text

Mas o que é um changeset?

Slide 103

Slide 103 text

alias Sabiah.Tweet def handle_in("new_tweet", %{ "content" => content } = payload, socket) do user_id = socket.assigns[:user_id] tweet_changeset = Tweet.changeset(%Tweet{}, %{user_id: user_id, content: content}) Repo.insert!(tweet_changeset) broadcast socket, "new_tweet", payload {:noreply, socket} end web/channels/timeline_channel.ex

Slide 104

Slide 104 text

– Ecto.Changeset - https://hexdocs.pm/ecto/Ecto.Changeset.html “Changesets allow filtering, casting, validation and definition of constraints when manipulating structs.”

Slide 105

Slide 105 text

def changeset(struct, params \\ %{}) do struct |> cast(params, [:user_id, :content]) |> validate_required([:user_id, :content]) end web/models/tweet.ex

Slide 106

Slide 106 text

Transforma e valida

Slide 107

Slide 107 text

Pronto. Salvando tweets!

Slide 108

Slide 108 text

sabiah ⏳ 1. tweetar ✅ 2. seguir 3. ver timeline com tweets de quem sigo

Slide 109

Slide 109 text

Tweets para seguidores Seguir

Slide 110

Slide 110 text

Seguidores

Slide 111

Slide 111 text

No content

Slide 112

Slide 112 text

Um model

Slide 113

Slide 113 text

Follower

Slide 114

Slide 114 text

Follower user_id, followed_user_id

Slide 115

Slide 115 text

E o botão “Seguir”?

Slide 116

Slide 116 text

No content

Slide 117

Slide 117 text

No template, temos forms

Slide 118

Slide 118 text

<%= for user <- @users do %> <%= user.name %> <%= user.username %> <%= form_for follower_changeset(user.id),
 follower_path(@conn, :create), fn f -> %> <%= hidden_input f, :followed_user_id, value: user.id %> <%= submit "+ follow", class: "button" %> <% end %> <% end %> web/templates/user/index.html.eex

Slide 119

Slide 119 text

O form recebe um changeset

Slide 120

Slide 120 text

defmodule Sabiah.UserView do use Sabiah.Web, :view alias Sabiah.Follower def follower_changeset(user_id) do Follower.changeset(%Follower{followed_user_id: user_id}) end end web/views/user_view.ex

Slide 121

Slide 121 text

Só falta o controller

Slide 122

Slide 122 text

def create(conn, %{"follower" => params}) do user_id = get_session(conn, :user_id) params = Map.put(params, "user_id", user_id) changeset = Follower.changeset(%Follower{}, params) case Repo.insert(changeset) do {:ok, follow} -> conn |> put_flash(:info, "You are now following #{follow.followed_user_id}") |> redirect(to: user_path(conn, :index)) {:error, changeset} -> IO.inspect(changeset) conn |> put_flash(:error, "You seems to be logged out. Create an user to continue.") |> redirect(to: user_path(conn, :new)) end end web/views/user_view.ex

Slide 123

Slide 123 text

&

Slide 124

Slide 124 text

sabiah ⏳ 1. tweetar ✅ 2. seguir ✅ 3. ver timeline com tweets de quem sigo

Slide 125

Slide 125 text

( ) $ * novo tweet ??? $

Slide 126

Slide 126 text

Broadcaster

Slide 127

Slide 127 text

def handle_in("new_tweet", %{"content" => content}, socket) do user_id = socket.assigns[:user_id] tweet = save_tweet!(user_id, content) response_payload = decorate_payload(user_id, %{"content" => content}) broadcast socket, "new_tweet", response_payload TweetBroadcaster.broadcast_to_followers(tweet, response_payload) {:noreply, socket} end web/channels/timeline_channel.ex

Slide 128

Slide 128 text

def handle_in("new_tweet", %{"content" => content}, socket) do user_id = socket.assigns[:user_id] tweet = save_tweet!(user_id, content) response_payload = decorate_payload(user_id, %{"content" => content}) broadcast socket, "new_tweet", response_payload TweetBroadcaster.broadcast_to_followers(tweet, response_payload) {:noreply, socket} end web/channels/timeline_channel.ex

Slide 129

Slide 129 text

( ) $ * novo tweet broadcast! $

Slide 130

Slide 130 text

defmodule Sabiah.TweetBroadcaster do alias Sabiah.{Endpoint, Repo} import Ecto.Query def broadcast_to_followers(tweet, payload) do followers_ids_query = from f in "followers", where: f.followed_user_id == ^tweet.user_id, select: f.user_id followers_ids = Repo.all(followers_ids_query) Enum.map(followers_ids, fn(user_id) -> Endpoint.broadcast!("timeline:#{user_id}", "new_tweet", payload) end) end end web/channels/tweet_broadcaster.ex

Slide 131

Slide 131 text

( ) $ * novo tweet broadcast! $

Slide 132

Slide 132 text

sabiah ⏳ 1. tweetar ✅ 2. seguir ✅ 3. ver timeline com tweets de quem sigo

Slide 133

Slide 133 text

Há um detalhe faltando

Slide 134

Slide 134 text

Salvar a timeline de cada usuário

Slide 135

Slide 135 text

No content

Slide 136

Slide 136 text

Veremos ao vivo

Slide 137

Slide 137 text

[Perigo iminente]

Slide 138

Slide 138 text

No content

Slide 139

Slide 139 text

Conclusão

Slide 140

Slide 140 text

Phoenix

Slide 141

Slide 141 text

Vue.js

Slide 142

Slide 142 text

Escolha a ferramenta

Slide 143

Slide 143 text

Divirta-se!

Slide 144

Slide 144 text

github.com/philss/sabiah

Slide 145

Slide 145 text

• ElixirSchool - https://elixirschool.com/pt/ • Phoenix Guides - http://www.phoenixframework.org/docs/overview • Vue.js - http://vuejs.org/

Slide 146

Slide 146 text

No content

Slide 147

Slide 147 text

Obrigado! @philipsampaio @philss