https://guides.rubyonrails.org/action_mailer_basics.html
寄信
把信寄到對方手中很複雜(複雜的部分在於:郵差是否有正確寄到位置)
是指要不要讓別人在外部機器使用你的伺服器幫他送信,
可能會讓不肖使用者拿伺服器來做亂發廣告信的工具
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
把變數改為字串
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
手冊: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就可以把實體變數拿來用
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>
mailchimp
Mailchimp是美國的營銷自動化平台和電子郵件營銷服務。該平台是其運營商Rocket Science Group的商標名,Rocket Science Group是一家由Ben Chestnut和Mark Armstrong於2001年成立的美國公司,稍後Dan Kurzius加盟
mailgun
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
file:///Users/tingtinghsu/Documents/projects/eddie-file/PPT0817/tmp/letter_opener/1597982884_0915241_e7a5dc0/rich.html
http://localhost:3001/letter_opener
先確認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
這樣暫存在記憶體的工作就不會不見
https://github.com/collectiveidea/delayed_job
gem 'delayed_job_active_record'
其他常見的工作排程gem Ref
檔案上傳功能有很多種解決方案
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
只是包裝器,後面還用到其他套件
imagemagick
、 mini_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
所見即所得 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-content
tag
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>
How to sanitize data (remove html tags) before saving a record?
https://github.com/SortableJS/Sortable
https://github.com/SortableJS/Vue.Draggable
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拉
連線的時候如何找到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
手冊
# app/channels/chat_channel.rb
class ChatChannel < ApplicationCable::Channel
end
# app/channels/appearance_channel.rb
class AppearanceChannel < ApplicationCable::Channel
end
連接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" })
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
參考手冊
// 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" })
收到什麼就廣播出去
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)
不同的瀏覽器都會收到同個廣播