--- title: Astro課程 0821 - Rails (Day10) tags: astro, rails --- # 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 ``` 按照手冊設定的話: ![](https://i.imgur.com/n7DCsz7.png) 換成實體變數,view就可以把實體變數拿來用 ![](https://i.imgur.com/AU0zmMH.png) ## 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/ ![](https://i.imgur.com/E3Sr0Ka.png) ``` 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 ``` ![](https://i.imgur.com/LiQQBHc.png) ## letter Opener ``` file:///Users/tingtinghsu/Documents/projects/eddie-file/PPT0817/tmp/letter_opener/1597982884_0915241_e7a5dc0/rich.html ``` http://localhost:3001/letter_opener ![](https://i.imgur.com/8AxCbpE.png) # 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`來做 ![](https://i.imgur.com/LLV3Isp.png) posts_controller.rb ```ruby 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://) https://github.com/collectiveidea/delayed_job ``` ``` gem 'delayed_job_active_record' ``` 其他常見的工作排程gem [Ref](https://rails-hosting.com/2020/) ![](https://i.imgur.com/KhOkyfZ.png) # Action Storage 檔案上傳功能有很多種解決方案 paperclip carrierwave [[Ting's筆記Day6] 活用套件carrierwave gem: (1)在Rails實現圖片上傳功能](https://ithelp.ithome.com.tw/articles/10199035) ``` 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> ``` ![](https://i.imgur.com/YmVHXeX.png) 記得在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? %> ``` ![](https://i.imgur.com/jv7usgT.jpg) ## 圖片被放在哪裡? ![](https://i.imgur.com/19Ali9b.png) 剛剛的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`只是包裝器,後面還用到其他套件 `imagemagick` 、 `mini_magick` ``` pg gem -> 是PostgreSQL的包裝器 sqlite gem -> 是SQLite的包裝器 ``` carrierwave 可以在上傳的時候就把照片壓成三張。 [[Ting's筆記Day9] 活用套件Carrierwave gem: (4) 使用Imagemagick修改圖片大小](https://ithelp.ithome.com.tw/articles/10199131) ## 安裝`imagemagick` ``` # (mac) 這個指令會跑的有點久 brew install imagemagick # (wsl) 這個指令會跑的更久 sudo apt install imagemagick (大部分的電腦) sudo apt-get install imagemagick sudo apt-get update ``` > apt-get 通常是對某些套件進行操作,,可能是安裝或移除等等行為。[Ref](https://b9532026.wordpress.com/2010/03/30/apt-get-%E6%8C%87%E4%BB%A4%E4%B8%80%E8%A6%BD-2/) ``` <%= image_tag @post.photo.variant(resize_to_limit: [300, 300]) if @post.photo.attached? %> # 圖片框框設定300*300,等比例壓縮,長或寬任一邊先撞到邊框就停止壓縮 ``` 如果是很大張的圖片,就會變成`resize`後的大小 ![](https://i.imgur.com/ADpB9yp.png) ## 第三方登入 ``` # google https://github.com/zquestz/omniauth-google-oauth2 # FB https://github.com/heartcombo/devise/wiki/OmniAuth:-Overview ``` # Action Text `所見即所得 WYSIWYG`編輯器也有很多選擇: [CKEditor](https://ckeditor.com/) [Redactor](https://imperavi.com/redactor) action text 整合[Trix編輯器](https://trix-editor.org/) ``` 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-content`tag ![](https://i.imgur.com/YXByMHF.png) ## 讓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](https://github.com/tmm1/pygments.rb) [作者](https://github.com/tmm1?language=objective-c&tab=stars) ## 把script標籤移掉 [How to sanitize data (remove html tags) before saving a record? ](https://stackoverflow.com/questions/6954258/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://i.imgur.com/sNBnFmv.png) ``` 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 ![](https://i.imgur.com/xxpELMB.png) 連線的時候如何找到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 參考手冊 ```javascript // 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` ```javascript 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 ``` ![](https://i.imgur.com/ZSZlC6Z.png) ``` 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) ``` 不同的瀏覽器都會收到同個廣播 ![](https://i.imgur.com/IBLUT1o.png)