Try   HackMD

Action Mailer

https://guides.rubyonrails.org/action_mailer_basics.html

寄信

把信寄到對方手中很複雜(複雜的部分在於:郵差是否有正確寄到位置)

SMTP Relay

是指要不要讓別人在外部機器使用你的伺服器幫他送信,
可能會讓不肖使用者拿伺服器來做亂發廣告信的工具

當新增文章完成後,寄信到作者的信箱

rails g mailer post 

Running via Spring preloader in process 61297
      create  app/mailers/post_mailer.rb
      invoke  erb
      create    app/views/post_mailer
      invoke  test_unit
      create    test/mailers/post_mailer_test.rb
      create    test/mailers/previews/post_mailer_preview.rb

把YML修掉figaro config bug

把變數改為字串

WARNING: Use strings for Figaro configuration. 5 was converted to "5".
WARNING: Use strings for Figaro configuration. 20 was converted to "20".
braintree_merchant_id: 'wpn3n5j7428qnfqp'
braintree_public_key: 'ghcyt9pn2dj5pd5z'
braintree_private_key: '97befac871f6f46225cab9ea7ce492cd'
plan_a_price: "5"
plan_b_price: "20"

觀察application_mailer.rb

信件標題

class ApplicationMailer < ActionMailer::Base
  default from: 'from@example.com'
  layout 'mailer'
end

觀察/layouts/mailer.html.erb

信件樣板

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <style>
      /* Email styles need to be inline */
    </style>
  </head>

  <body>
    <%= yield %>
  </body>
</html>

定義poster方法

class PostMailer < ApplicationMailer
  def poster
    # 這是一個大hash
    mail to: 'tingtinghsu@gmail.com', subject: 'test'
  end
end

新增文章後,寄信

  def create
    @post = @board.posts.new(post_params)

    if @post.save
      # 寄信
      # UserMailer.with(user: @user).welcome_email.deliver_later
      PostMailer.poster.deliver_later      
      redirect_to @board, notice: '文章新增成功'
    else
      render :new
    end
  end

Action Mailer有自己定義的params

手冊:2.4 Mailer Views


class UserMailer < ApplicationMailer
  default from: 'notifications@example.com'
 
  def welcome_email
    @user = params[:user]
    @url  = 'http://example.com/login'
    mail(to: @user.email,
         subject: 'Welcome to My Awesome Site',
         template_path: 'notifications',
         template_name: 'another')
  end
end

按照手冊設定的話:

換成實體變數,view就可以把實體變數拿來用

view下面poster.html.erb

hello!

<%= @post.content %>

把layout改成背景色
layouts/mailer.html.erb

<!DOCTYPE html>
<html>
  <head>
    <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
    <style>
      body {
        background-color: red;
      }
    </style>
  </head>

  <body>
    <%= yield %>
  </body>/Users/tingtinghsu/Documents/projects/eddie-file/PPT0817/app/views/pages/pricing.html.erb
</html>

不用gmail,使用其他方案解決mail server的問題

mailchimp

Mailchimp是美國的營銷自動化平台和電子郵件營銷服務。該平台是其運營商Rocket Science Group的商標名,Rocket Science Group是一家由Ben Chestnut和Mark Armstrong於2001年成立的美國公司,稍後Dan Kurzius加盟

mailgun

https://www.mailgun.com/


config.action_mailer.delivery_method = :smtp
config.action_mailer.smtp_settings = {
  address:              'smtp.gmail.com',
  port:                 587,
  domain:               'example.com',
  user_name:            '<username>',
  password:             '<password>',
  authentication:       'plain',
  enable_starttls_auto: true }

對照mailgun的configue變數

  config.action_mailer.delivery_method = :smtp
  config.action_mailer.smtp_settings = {
    address:              'smtp.mailgun.org',
    port:                 587,
    domain:               '5xruby.tw',
    user_name:            ENV["mailgun_user_name"],
    password:             ENV["mailgun_user_password"],
    authentication:       'plain',
    enable_starttls_auto: true
  }
end

11:39:54 yoyo.1 | [ActiveJob] [ActionMailer::MailDeliveryJob] [5282e305-1148-4c78-9f8b-e886d334b4ab] Date: Fri, 21 Aug 2020 11:39:52 +0800
11:39:54 yoyo.1 | From: from@example.com
11:39:54 yoyo.1 | To: yourmail@gmail.com
11:39:54 yoyo.1 | Message-ID: <5f3f4208207eb_f75d3fe1f7638a3075971@MacBook-Pro.local.mail>
11:39:54 yoyo.1 | Subject: =?UTF-8?Q?=E6=96=B0=E5=A2=9E=E6=96=87=E7=AB=A0=EF=BC=9A=E6=88=91=E6=98=AF=E8=B2=93=E8=B2=93=E8=B2=93?=
11:39:54 yoyo.1 | Mime-Version: 1.0
11:39:54 yoyo.1 | Content-Type: text/html;
11:39:54 yoyo.1 |  charset=UTF-8
11:39:54 yoyo.1 | Content-Transfer-Encoding: 7bit
11:39:54 yoyo.1 | 
11:39:54 yoyo.1 | <!DOCTYPE html>
11:39:54 yoyo.1 | <html>
11:39:54 yoyo.1 |   <head>
11:39:54 yoyo.1 |     <meta http-equiv="Content-Type" content="text/html; charset=utf-8" />
11:39:54 yoyo.1 |     <style>
11:39:54 yoyo.1 |       body {
11:39:54 yoyo.1 |         background-color: red;
11:39:54 yoyo.1 |       }
11:39:54 yoyo.1 |     </style>
11:39:54 yoyo.1 |   </head>
11:39:54 yoyo.1 | 
11:39:54 yoyo.1 |   <body>
11:39:54 yoyo.1 |     hello!
11:39:54 yoyo.1 | 
11:39:54 yoyo.1 | mail gun
11:39:54 yoyo.1 |   </body>
11:39:54 yoyo.1 | </html>
11:39:54 yoyo.1 | 
11:39:54 yoyo.1 | [ActiveJob] [ActionMailer::MailDeliveryJob] [5282e305-1148-4c78-9f8b-e886d334b4ab] Performed ActionMailer::MailDeliveryJob (Job ID: 5282e305-1148-4c78-9f8b-e886d334b4ab) from Async(mailers) in 2544.9ms

letter Opener

file:///Users/tingtinghsu/Documents/projects/eddie-file/PPT0817/tmp/letter_opener/1597982884_0915241_e7a5dc0/rich.html

http://localhost:3001/letter_opener

Active Job

寄出去後會畫面會轉個5秒才寄出,把它延遲加入排程

先確認rails guide
https://guides.rubyonrails.org/active_job_basics.html

 ~/Documents/projects/eddie-file/PPT0817   master  rails g job sendmail
Running via Spring preloader in process 63535
      invoke  test_unit
      create    test/jobs/sendmail_job_test.rb
      create  app/jobs/sendmail_job.rb

/jobs/sendmail_job.rb

class SendmailJob < ApplicationJob
  queue_as :default

# 一顆星:陣列;兩顆星:hash
  def perform(*args)
    # Do something later
  end
end


$ rails generate job guests_cleanup --queue urgent

明明看起來是實體方法,但是用起來是類別方法


class GuestsCleanupJob < ApplicationJob
  queue_as :default
 
  def perform(*guests)
    # Do something later
  end
end

寄信這件事,抽到jobs來做

posts_controller.rb

  def create
    @post = @board.posts.new(post_params)

    if @post.save
      # 寄信
      #UserMailer.with(user: @user).welcome_email.deliver_later
      #PostMailer.with(post: @post).poster.deliver_later   

      # 待會才寄信
      #SendmailJob.perform_later(@post)
      
      # 等十秒才寄信
      SendmailJob.set(wait: 10.seconds).perform_later(@post)

      redirect_to @board, notice: '文章新增成功'
    else
      render :new
    end
  end

sendmail_job.rb

class SendmailJob < ApplicationJob
  queue_as :default

  def perform(post)
    puts "------------------------------"
    puts "寄信囉!!"
    puts "------------------------------"
    PostMailer.with(post: post).poster.deliver_now
  end
end

新增文章後,10秒鐘後寄一封信

12:20:22 yoyo.1 |   Post Load (0.3ms)  SELECT "posts".* FROM "posts" WHERE "posts"."id" = ? LIMIT ?  [["id", 9], ["LIMIT", 1]]
12:20:22 yoyo.1 | [ActiveJob] [SendmailJob] [9aa7f9ab-96fa-42db-9e43-2eb1b47a434d] Performing SendmailJob (Job ID: 9aa7f9ab-96fa-42db-9e43-2eb1b47a434d) from Async(default) enqueued at 2020-08-21T04:20:12Z with arguments: #<GlobalID:0x00007ff101d08c30 @uri=#<URI::GID gid://ppt/Post/9>>
12:20:22 yoyo.1 | ------------------------------
12:20:22 yoyo.1 | 寄信囉!!
12:20:22 yoyo.1 | ------------------------------
12:20:23 yoyo.1 | [ActiveJob] [SendmailJob] [9aa7f9ab-96fa-42db-9e43-2eb1b47a434d]   User Load (0.2ms)  SELECT "users".* FROM "users" WHERE "users"."id" = ? LIMIT ?  [["id", 2], ["LIMIT", 1]]
12:20:23 yoyo.1 | [ActiveJob] [SendmailJob] [9aa7f9ab-96fa-42db-9e43-2eb1b47a434d]   ↳ app/mailers/post_mailer.rb:4:in `poster'
12:20:23 yoyo.1 | [ActiveJob] [SendmailJob] [9aa7f9ab-96fa-42db-9e43-2eb1b47a434d]   Rendering post_mailer/poster.html.erb within layouts/mailer
12:20:23 yoyo.1 | [ActiveJob] [SendmailJob] [9aa7f9ab-96fa-42db-9e43-2eb1b47a434d]   Rendered post_mailer/poster.html.erb within layouts/mailer (Duration: 0.9ms | Allocations: 102)
12:20:23 yoyo.1 | [ActiveJob] [SendmailJob] [9aa7f9ab-96fa-42db-9e43-2eb1b47a434d] PostMailer#poster: processed outbound mail in 8.5ms
12:20:27 yoyo.1 | [ActiveJob] [SendmailJob] [9aa7f9ab-96fa-42db-9e43-2eb1b47a434d] Delivered mail 5f3f4b87d406_1015f3ff87fece3a4636ab@MacBook-Pro.local.mail (4159.8ms)
12:20:27 yoyo.1 | [ActiveJob] [SendmailJob] [9aa7f9ab-96fa-42db-9e43-2eb1b47a434d] Date: Fri, 21 Aug 2020 12:20:23 +0800
12:20:27 yoyo.1 | From: from@example.com
12:20:27 yoyo.1 | To: tingtinghsu.tw@gmail.com
12:20:27 yoyo.1 | Message-ID: <5f3f4b87d406_1015f3ff87fece3a4636ab@MacBook-Pro.local.mail>
12:20:27 yoyo.1 | Subject: =?UTF-8?Q?=E6=96=B0=E5=A2=9E=E6=96=87=E7=AB=A0=EF=BC=9A=E9=87=8D=E5=AF=84=E4=B8=80=E5=B0=81?=
12:20:27 yoyo.1 | Mime-Version: 1.0
12:20:27 yoyo.1 | Content-Type: text/html;
12:20:27 yoyo.1 |  charset=UTF-8
12:20:27 yoyo.1 | Content-Transfer-Encoding: quoted-printable
12:20:27 yoyo.1 | 
12:20:27 yoyo.1 | <!DOCTYPE html>=0D
12:20:27 yoyo.1 | <html>=0D
12:20:27 yoyo.1 |   <head>=0D
12:20:27 yoyo.1 |     <meta http-equiv=3D"Content-Type" content=3D"text/html; charset=3Dutf=
12:20:27 yoyo.1 | -8" />=0D
12:20:27 yoyo.1 |     <style>=0D
12:20:27 yoyo.1 |       body {=0D
12:20:27 yoyo.1 |         background-color: red;=0D
12:20:27 yoyo.1 |       }=0D
12:20:27 yoyo.1 |     </style>=0D
12:20:27 yoyo.1 |   </head>=0D
12:20:27 yoyo.1 | =0D
12:20:27 yoyo.1 |   <body>=0D
12:20:27 yoyo.1 |     hello!=0D
12:20:27 yoyo.1 | =0D
12:20:27 yoyo.1 | 10=E7=A7=92=0D
12:20:27 yoyo.1 |   </body>=0D
12:20:27 yoyo.1 | </html>=0D
12:20:27 yoyo.1 | 
12:20:27 yoyo.1 | [ActiveJob] [SendmailJob] [9aa7f9ab-96fa-42db-9e43-2eb1b47a434d] Performed SendmailJob (Job ID: 9aa7f9ab-96fa-42db-9e43-2eb1b47a434d) from Async(default) in 4217.21ms

delayed job gem: 把工作存放在資料表

這樣暫存在記憶體的工作就不會不見

https://github.com/collectiveidea/delayed_job
gem 'delayed_job_active_record'

其他常見的工作排程gem Ref

Action Storage

檔案上傳功能有很多種解決方案

paperclip
carrierwave
[Ting's筆記Day6] 活用套件carrierwave gem: (1)在Rails實現圖片上傳功能

https://github.com/carrierwaveuploader/carrierwave

來使用Action Storage
多對多model
(缺點:容易產生n+1問題)

https://guides.rubyonrails.org/active_storage_overview.html
rails active_storage:install

Copied migration 20200821055430_create_active_storage_tables.active_storage.rb from active_storage

rails db:migrate            == 20200821055430 CreateActiveStorageTables: migrating ========================
-- create_table(:active_storage_blobs, {})
   -> 0.0044s
-- create_table(:active_storage_attachments, {})
   -> 0.0039s
== 20200821055430 CreateActiveStorageTables: migrated (0.0085s) ===============

參考手冊 3. Attaching Files to Records

class User < ApplicationRecord
  has_one_attached :avatar
end

model
在post.rb增加我們要的虛擬欄位

  has_one_attached :photo
  

修改view

  <div class="fields">
    <%= form.label :photo, "附件" %>
    <%= form.file_field :photo %>
  </div>

記得在controller permit photo欄位

  def post_params
    params.require(:post)
          .permit(:title, :content, :photo)
          .merge(user: current_user)
  end

show.html.erb
上傳好了後找地方放照片


<p>
  <%= @post.content %>
  <%= @post.photo %>
</p>

結果顯示:

上傳 #<ActiveStorage::Attached::One:0x00007fb3b0f0fcb0>

image helper

把剛剛的

  <%= @post.photo %>

改為

if @post.photo.attached? 有圖片才顯示(不然沒有傳檔案的貼文會壞)

  <%= image_tag @post.photo if @post.photo.attached? %>
  
#硬改顯示的大小,但原圖size不變

  <%= image_tag @post.photo, width: 200 if @post.photo.attached? %>

圖片被放在哪裡?

剛剛的migrate做了兩張表,attachement屬於blob

    create_table :active_storage_blobs do |t|
      t.string   :key,        null: false
      t.string   :filename,   null: false
      t.string   :content_type
      t.text     :metadata
      t.bigint   :byte_size,  null: false
      t.string   :checksum,   null: false
      t.datetime :created_at, null: false

      t.index [ :key ], unique: true
    
    
    create_table :active_storage_attachments do |t|
      t.string     :name,     null: false
      t.references :record,   null: false, polymorphic: true, index: false
      t.references :blob,     null: false

      t.datetime :created_at, null: false

      t.index [ :record_type, :record_id, :name, :blob_id ], name: "index_active_storage_attachments_uniqueness", unique: true
      t.foreign_key :active_storage_blobs, column: :blob_id
    end

這兩個表的關聯可能是一對一或一對多

class User < ApplicationRecord
  has_one_attached :avatar, service: :s3
end

如果要多圖上傳的話

class Message < ApplicationRecord
  has_many_attached :images
end

把圖片壓縮 variant

gemfile

# Use Active Storage variant
gem 'image_processing', '~> 1.2'

包裝器

包裝器像是轉接頭一樣

image_processing只是包裝器,後面還用到其他套件
imagemagickmini_magick

pg gem -> 是PostgreSQL的包裝器
sqlite gem -> 是SQLite的包裝器

carrierwave 可以在上傳的時候就把照片壓成三張。

[Ting's筆記Day9] 活用套件Carrierwave gem: (4) 使用Imagemagick修改圖片大小

安裝imagemagick

# (mac) 這個指令會跑的有點久
brew install imagemagick

# (wsl) 這個指令會跑的更久
sudo apt install imagemagick (大部分的電腦)
sudo apt-get install imagemagick
sudo apt-get update

apt-get 通常是對某些套件進行操作,,可能是安裝或移除等等行為。Ref

  <%= image_tag @post.photo.variant(resize_to_limit: [300, 300]) if @post.photo.attached? %>
 # 圖片框框設定300*300,等比例壓縮,長或寬任一邊先撞到邊框就停止壓縮

如果是很大張的圖片,就會變成resize後的大小

第三方登入

# google
https://github.com/zquestz/omniauth-google-oauth2

# FB
https://github.com/heartcombo/devise/wiki/OmniAuth:-Overview

Action Text

所見即所得 WYSIWYG編輯器也有很多選擇:
CKEditor
Redactor
action text 整合Trix編輯器

https://guides.rubyonrails.org/action_text_overview.html
https://trix-editor.org/
https://github.com/basecamp/trix
rails action_text:install

// application.js
require("trix")
require("@rails/actiontext")

post.rb model

  has_rich_text :hello

posts/new.html.erb

  <div class="fields">
    <%= form.label :hello, "編輯器" %>
    <%= form.rich_text_area :hello %>
  </div>

記得要讓post controller可以允許這個欄位通過

  def post_params
    params.require(:post)
          .permit(:title, :content, :photo, :hello)
          .merge(user: current_user)
  end

輸出畫面也要設計
posts/show.html.erb

  <br>
  <%= @post.hello %>
  <br>

檢視html原始碼多了一個trix-contenttag

讓text出現 markdown語法

https://github.com/vmg/redcarpet

board_controller

  def show
    @posts = @board.posts.includes(:user)
    markdown = Redcarpet::Markdown.new(Redcarpet::Render::HTML, autolink: true, tables: true)
    @intro = markdown.render(@board.intro)
  end

board show.html.erb

# 不要做任何跳脫
<div>
  <%= raw @intro %>
  # 或是.html_safe
  <%= @intro.html_safe %>
</div>


語法高亮

pygments
作者

把script標籤移掉

How to sanitize data (remove html tags) before saving a record?

拖拉效果

https://github.com/SortableJS/Sortable
https://github.com/SortableJS/Vue.Draggable

Action Cable

Action Cable實作即時更新的方式,會把WebSocket包起來

WebSocket: 一直處於連線狀態(eg: 抓keydown事件)

WebSocket是一種網路傳輸協定,可在單個TCP連接上進行全雙工通訊,位於OSI模型的應用層。WebSocket協定在2011年由IETF標準化為RFC 6455,後由RFC 7936補充規範。Web IDL中的WebSocket API由W3C標準化。

https://guides.rubyonrails.org/action_cable_overview.html

以投票網的即時更新為例

https://datastore.thenewslens.com/infographic/kao-vote-0815/kao-vote-live/live-format-data/intro-data/detail_data.json?12845
Access to fetch at 'https://google.com/' from origin 'chrome-search://local-ntp' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

前端拉不回來,可以用ruby拉

連線設定 Connection Setup

連線的時候如何找到user?

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    private
    def find_verified_user
      if verified_user = User.find_by(id: cookies.encrypted[:user_id])
        verified_user
      else
        reject_unauthorized_connection
      end
    end
  end
end

把手冊換掉成自己設定的session

module ApplicationCable
  class Connection < ActionCable::Connection::Base
    identified_by :current_user

    private
    def find_verified_user
      if verified_user = User.find_by(id: session[:user_token])
        verified_user
      else
        reject_unauthorized_connection
      end
    end
  end
end

建立頻道 channel

手冊


# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
end
 
# app/channels/appearance_channel.rb
class AppearanceChannel < ApplicationCable::Channel
end

Client-Side Components

連接consumer

// Action Cable provides the framework to deal with WebSockets in Rails.
// You can generate new channels where WebSocket features live using the `rails generate channel` command.

import { createConsumer } from "@rails/actioncable"

export default createConsumer()

font-end建立一個自己的chat_channel

import consumer from "./consumer"
consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" })

# 也可以建立很多房間

// app/javascript/channels/chat_channel.js
import consumer from "./consumer"
 
consumer.subscriptions.create({ channel: "ChatChannel", room: "1st Room" })
consumer.subscriptions.create({ channel: "ChatChannel", room: "2nd Room" })

回到server side

chat_channel.rb

class ChatChannel < ApplicationCable::Channel
  def subscribed
    stream_from "chat_#{params[:room]}"
  end
end

Eg. 手冊裡的這段

# 針對某篇文章做互動留言
class CommentsChannel < ApplicationCable::Channel
  def subscribed
    post = Post.find(params[:id])
    stream_for post
  end
end

Subscriptions

參考手冊


// app/javascript/channels/chat_channel.js
// Assumes you've already requested the right to send web notifications
import consumer from "./consumer"
 
consumer.subscriptions.create({ channel: "ChatChannel", room: "Best Room" }, {
// 收到data
  received(data) {
    this.appendLine(data)
  },
 
  appendLine(data) {
    const html = this.createLine(data)
    const element = document.querySelector("[data-chat-room='Best Room']")
    element.insertAdjacentHTML("beforeend", html)
  },
 
  createLine(data) {
    return `
      <article class="chat-line">
        <span class="speaker">${data["sent_by"]}</span>
        <span class="body">${data["body"]}</span>
      </article>
    `
  }
})

把received(data)這個function加到自己的chat_channel.js

import consumer from "./consumer"
consumer.subscriptions.create(
  {
    channel: "ChatChannel",
    room: "Best Room"
  },
  {
    received(data) {
      console.log(data);
    }
  }
)

// consumer.subscriptions.create({ channel: "ChatChannel", room: "1st Room" })
// consumer.subscriptions.create({ channel: "ChatChannel", room: "2nd Room" })

Reboardcasting

收到什麼就廣播出去

class ChatChannel < ApplicationCable::Channel
  def subscribed
    puts "----------------------------"
    puts "hi"
    puts "----------------------------"
    stream_from "chat_#{params[:room]}"
  end
  
# 為了檢查sever有沒有收到訊息
  def receive(data)
    puts "----------------------------"
    puts data
    puts "----------------------------"
    ActionCable.server.broadcast("chat_#{params[:room]}", data)
  end
end

16:48:14 yoyo.1 | ChatChannel is transmitting the subscription confirmation
16:48:14 yoyo.1 | ChatChannel is streaming from chat_Room1
16:48:14 yoyo.1 | ChatChannel#receive({"sent_by"=>"Paul", "body"=>"This is a cool chat app."})
16:48:14 yoyo.1 | ----------------------------
16:48:14 yoyo.1 | {"sent_by"=>"Paul", "body"=>"This is a cool chat app."}
16:48:14 yoyo.1 | ----------------------------
16:48:14 yoyo.1 | [ActionCable] Broadcasting to chat_Room1: {"sent_by"=>"Paul", "body"=>"This is a cool chat app."}
16:48:14 yoyo.1 | ChatChannel transmitting {"sent_by"=>"Paul", "body"=>"This is a cool chat app."} (via streamed from chat_Room1)
16:48:14 yoyo.1 | Started GET "/cable" for ::1 at 2020-08-21 16:48:14 +0800
16:48:14 yoyo.1 | Started GET "/cable/" [WebSocket] for ::1 at 2020-08-21 16:48:14 +0800
16:48:14 yoyo.1 | Successfully upgraded to WebSocket (REQUEST_METHOD: GET, HTTP_CONNECTION: Upgrade, HTTP_UPGRADE: websocket)
16:48:14 yoyo.1 | ChatChannel is transmitting the subscription confirmation
16:48:14 yoyo.1 | ChatChannel is streaming from chat_Room1
16:48:14 yoyo.1 | ChatChannel#receive({"sent_by"=>"Paul", "body"=>"This is a cool chat app."})
16:48:14 yoyo.1 | ----------------------------
16:48:14 yoyo.1 | {"sent_by"=>"Paul", "body"=>"This is a cool chat app."}
16:48:14 yoyo.1 | ----------------------------
16:48:14 yoyo.1 | [ActionCable] Broadcasting to chat_Room1: {"sent_by"=>"Paul", "body"=>"This is a cool chat app."}
16:48:14 yoyo.1 | ChatChannel transmitting {"sent_by"=>"Paul", "body"=>"This is a cool chat app."} (via streamed from chat_Room1)
16:48:14 yoyo.1 | ChatChannel transmitting {"sent_by"=>"Paul", "body"=>"This is a cool chat app."} (via streamed from chat_Room1)

不同的瀏覽器都會收到同個廣播