Alex Liang

[Rails] 重構controllers

上一篇我們介紹使用sidekiq和ActionMailer處理寄信功能。
原本要寄信時還得手動按下按鈕且controller寫得不夠乾淨,這次選擇重構controller

原來的controller

app/controllers/goals_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class GoalsController < ApplicationController
before_action :find_params, only: [:edit, :update, :destroy, :complete, :notify_friend]
...略

def notify_friend
mail_list = {}
count = 0
for email in @goal.shared_mails
mail_list[count.to_s] = email.mail_addr
count+=1
end

h = JSON.generate({ 'owner' => @goal.owner.name,
'goal' => @goal.title,
'complete_date' => @goal.complete_date,
'email' => mail_list })
SendEmailJob.perform_now(h)
redirect_to goals_path
end

private

def find_params
@goal = current_user.goals.find(params[:id])
end
end

使用after_action,在更新目標的分享對象後發信。並且將產生json格式分出來讓notify_friend功能單一

app/controllers/goals_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class GoalsController < ApplicationController
before_action :authenticate_user!, only: [:new, :edit, :create, :update, :destroy]
before_action :find_params, only: [:edit, :update, :destroy, :complete]
+ after_action :notify_friend, only: [:update]

def notify_friend
SendEmailJob.perform_now(generate_json_params)
end

private
...略

def generate_json_params
mail_list = {}
count = 0
for email in @goal.shared_mails
mail_list[count.to_s] = email.mail_addr
count+=1
end

h = JSON.generate({ 'owner' => @goal.owner.name,
'goal' => @goal.title,
'complete_date' => @goal.complete_date,
'email' => mail_list })
end
end

[Rails] 使用sidekiq處理mailer寄信功能

上一篇完成動態新增email欄位後,我們使用sidekiq和mailer完成非同步寄信功能
sidekiq官方建議使用JSON作為active job的參數,此篇也記錄將Model的參數轉換成需要的json格式

先從gem的基本安裝開始,此範例使用’letter_opener’和’sidekiq’
letter_opener可讓我們即時檢視信件,而不必真的寄信到信箱裡
sidekiq則是幫助我們將寄信的動作放到背景執行

Gem Install

Gemfile
1
2
gem 'letter_opener', group: :development
gem 'sidekiq'

使用bundle安裝gem

接著安裝Redis,sidekiq需要它才能運行

1
brew install redis

Mailer

1
rails g mailer UserMailer

設定config

config/environments/development.rb
1
2
config.action_mailer.default_url_options = { host: "http://localhost:3000" }
config.action_mailer.delivery_method = :letter_opener

設定發信的信箱位址

app/mailers/application_mailer.rb
1
2
3
4
class ApplicationMailer < ActionMailer::Base
default from: "[email protected]"
layout 'mailer'
end

設定寄信的收件人和主旨,這裡我們還需要目標的擁有者和目標的標題。share_goal會在之後的ActiveJob使用

app/mailers/user_mailer.rb
1
2
3
4
5
6
7
8
9
10
class UserMailer < ApplicationMailer
def share_goal(owner, goal, complete_date, email)
@owner = owner
@goal = goal
@complete_date = complete_date
@email = email

mail(to: @email, subject: "[ResolutionTracker] #{@owner}分享目標")
end
end

接著設定信件的內容,這裡會使用share_goal傳進來的參數

app/views/user_mailer/share_goal.html.erb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
<h1>ResolutionTracker</h1>

<p>
Hi
</p>

<p>
以下是<%= @owner %>的目標:
<ul>
<li>目標:<%= @goal%></li>
<li>預計完成日期:<%= @complete_date %></li>
</ul>
目標連結:<%= link_to @goal, "#" %>
</p>

<p>
請好好督促 <%= @owner %> 喔!
</p>

Controller

在controller中需要增加一個action將active record轉成json格式再傳給ActiveJob

app/controllers/goals_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
class GoalsController < ApplicationController
before_action :find_params, only: [:edit, :update, :destroy, :complete, :notify_friend]
...略

def notify_friend
mail_list = {}
count = 0
for email in @goal.shared_mails
mail_list[count.to_s] = email.mail_addr
count+=1
end

h = JSON.generate({ 'owner' => @goal.owner.name,
'goal' => @goal.title,
'complete_date' => @goal.complete_date,
'email' => mail_list })
SendEmailJob.perform_now(h)
redirect_to goals_path
end

private

def find_params
@goal = current_user.goals.find(params[:id])
end
end

由於一個目標可能會分享給多位朋友,我們創立一個空的hash table儲存各email address
接著使用ruby的JSON.generate產生active job參數。
這裡提到為什麼使用簡單的格式而不是整個物件當參數,原因是這會讓Redis serializing,加重系統負擔

Active Job

設定active job初始化

config/initializers/active_job.rb
1
2
3
4
5
if Rails.env.test?
ActiveJob::Base.queue_adapter = :inline
else
ActiveJob::Base.queue_adapter = :sidekiq
end

在app資料夾底下新增jobs資料夾,並新增send_email_job.rb

app/jobs/send_email_job.rb
1
2
3
4
5
6
7
8
9
class SendEmailJob < ActiveJob::Base
queue_as :default

def perform(h)
t = JSON.load(h)
t['email'].each_value {|value|
UserMailer.share_goal(t['owner'], t['goal'], t['complete_date'], value).deliver}
end
end

這裡接收controller傳來的json參數後,將其轉換成hash table,並對每一筆email位址發送信件

驗證

要驗證前得分別啟動redis server和sidekiq

1
redis server

1
bundle exec sidekiq

螢幕快照 2016-04-14 下午3.27.10.png

參考來源:
Action Mailer Basics
blog參考文章
sidekiq github

[Rails] 使用nested_form動態新增輸入欄位

實作專案ResolutionTracker時,想增加分享目標給朋友的功能。
在填入email的頁面需要動態增加輸入欄位,讓使用者一次存入多筆資料
這裡介紹nested_form解決問題

Install Gem

Gemfile
1
gem 'nested_form'

然後使用bundle install安裝並重開server

Model

我們有二個model: goal, shared_mail,分別負責處理目標和使用者存入的mail address
在goal.rb需要能接受nested_attribute

app/models/goal.rb
1
2
3
4
5
6
7
8
class Goal < ActiveRecord::Base
validates :title, presence: true

has_many :shared_mails
belongs_to :owner, class_name: "User", foreign_key: :user_id

accepts_nested_attributes_for :shared_mails, allow_destroy: true
end
app/models/shared_mail.rb
1
2
3
class SharedMail < ActiveRecord::Base
belongs_to :goal
end

Controller

controller接受strong parameter時,得允許傳入shared_mails_attribute

app/controllers/goals_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
class GoalsController < ApplicationController
#略...

def create
@goal = current_user.goals.create(goal_params)

if @goal.save
redirect_to goals_path
else
render 'new'
end
end

def update
if @goal.update(goal_params)
redirect_to goal_path
else
render 'edit'
end
end

private

def goal_params
params.require(:goal).permit(:title, :comment, :is_shareable,
:complete_date,
shared_mails_attributes: [:id, :mail_addr, :_destroy])
end
end

View

view的部分,將原來的form_for(或simple_form_for)改成nested_form_for

app/views/shared_mails/new.html.erb
1
2
3
4
5
6
7
8
9
10
11
<h2>分享目標</h2>

<%= nested_form_for @goal do |f| %>
<%= f.fields_for :shared_mails do |mail_form| %>
<%= mail_form.text_field :mail_addr %>
<%= mail_form.link_to_remove "Remove this mail address" %>
<% end %>

<%= f.link_to_add "Add a mail address", :shared_mails %>
<%= f.submit "提交", disable_with: "Submiting...", class: "btn btn-primary" %>
<% end %>

Archetecture the Lost Years by Uncle Bob演講筆記

最近看了Uncle Bob在Ruby Midwest 2011的演講(影片連結)介紹軟體架構。

Archetecture is about intent

一開始Uncle Bob列出幾種建築物的結構圖,藉此比喻好的架構設計能讓工程師迅速了解系統
從這裡再帶出web framework常採用的MVC架構
為何會有MVC的出現? 為了讓系統耦合度降低 讓前端的設計不需要跟後端的資料庫或商業邏輯綁在一起

演講從一個user case講起,再畫一個UML圖解釋架構。
他使用類似MVC架構的系統(interactor, entity, boundary)來處理這個問題
例如: interactor負責application特有的商業邏輯,並且和entity有連結的關係,可以視為controller
entity則是處理資料庫存取,屬於一般application共有的行為
boundary把interactor產生的資訊秀出來給使用者,整體架構如下圖所示:
螢幕快照 2016-04-01 下午4.44.26.png
Uncle Bob解釋這種設計能將商業邏輯和使用者介面切開,二者之間少了相依性
而右半邊的interactor和entity可封裝成gem或dll(看是哪種application)
最左邊的delivery machanism負責將user input分配給boundary,可封裝成plug-in的型態,方便測試

What about MVC?

講到MVC這章,先從發明人Trygve Reenskaug開始介紹
延申到web application時會出現view和controller相依model的現象,如下圖
螢幕快照 2016-04-04 下午2.27.14.png
從測試的角度來看,view只要把資訊編排顯示正確即可,不需要關心後端的資料如何產生
對於商業邏輯的正確才是每次測試的重點,這點他比較偏好Model-View-Presenter
螢幕快照 2016-04-04 下午3.06.06.png

接下來Uncle Bob問觀眾做一次測試要多久?測了幾項?

4分半測1000多項 –> That’s good
1小時半 –> That’s bad >”<

對工程師來說,測試如果花太久的時間,之後每次修改都會猶豫要不要測試。
這也增加專案爆炸的機率…

Good Architeture

Uncle Bob最後提到早期一個wiki-like project。
從一開始他們就決定晚一點連結資料庫,先把application實作完
為了滅少測試時間,把wiki page存在memory中,雖然不能存檔,但至少能測試整個專案
後來他的同事實作FileSystemPage測試persistence,架構如下
螢幕快照 2016-04-04 下午3.30.17.png
而這時專案還沒有真正的database

A good architecture allows major decisions to be deferred
A good architecture maximizes the number of decisions not made

到最後客戶要求需要database,而他們只需要一天就把專案完成了

好的架構可推遲重大的決定;好的架構能滅少做決定的次數。

[前端筆記] CSS3 Cross Country筆記

把Code School CSS3課程做個筆記

Inheritance & Specificity

CSS3對selector和class有優先權的設定
有個數列能做參考:

, , ,
由左至右為權限高到低,舉例來說

1
2
3
4
<section id="content">
<p class="product">Coffee brewer</p>
<p>It's the best way to enjoy coffee</p>
</section>

1
2
3
4
5
6
7
8
9
#content p {		# specificity: 0,1,0,1
color: #000;
}
.product { # specificity: 0,0,1,0 優先權較低,不會生效
color: #555;
}
#content .product { # specificity: 0,1,1,0 加上ID selector才會生效
color: #555;
}

The Box Model

當我們需要計算某個樣式的寬度時,需套入以下公式:
total calculated box width = content width + padding width + border width
例如:

1
2
3
4
5
6
.form {
border: 5px solid #fff;
padding-left: 10px;
padding-right: 5px;
width: 120px;
}

此樣式總寬度為: 120(content)+15(padding)+10(border)=145px

Positioning

CSS的樣式位置預設是static,總共有4種設定:static/relative/absolute/fixed
除了static之外,其它3種設定可分別調整元素上、下、左、右位置,例如:

1
2
3
<article>
<h2>Alex's Playground<sup>2</sup></h2>
</article>

1
2
3
4
5
6
sup {
font-size: 60%;
vertical-align: baseline;
position: relative;
top: -0.5em;
}

螢幕快照 2016-04-01 下午2.17.56.png

參考來源:
Positioning

[Rails] paperclip上傳檔案及圖片

如果網站需要上傳檔案或圖片的功能,除了carrierwave之外,也可以使用paperclip實現

以下介紹這個好用的gem

環境要求

要使用paperclip,ruby的版本必須是2.0以上,rails則是3.2之後

imagemagick也得先安裝,在OS X下,使用Homebrew安裝

1
brew install imagemagick

安裝

Gemfile
1
gem "paperclip", "~> 4.3"
1
> bundle install

Model

設定model,假如有一個Pic model負責管理圖片

pic.rb
1
2
3
4
class Pic < ActiveRecord::Base
has_attached_file :avatar, styles: { medium: "300x300>", thumb: "100x100>" }, default_url: "/images/:style/missing.png"
validates_attachment_content_type :avatar, content_type: /\Aimage\/.*\Z/
end

Migration

1
> rails generate paperclip pic avatar

Controller

pics_controller.rb
1
2
3
4
5
6
7
8
9
def create
@pic = Pic.create(pic_params)
end

private

def pic_params
params.require(:pic).permit(:avatar)
end

View

1
2
3
<%= image_tag @pic.avatar.url %>
<%= image_tag @pic.avatar.url(:medium) %>
<%= image_tag @pic.avatar.url(:thumb) %>

參考來源:
paperclip github repo

[Rails] HAML介紹

這二天接觸到haml這個輕量級語言,在嘗試後發覺真的是很方便的語言,可以用更優雅的方式撰寫前端的template

以下做個整理

首先得安裝gem

1
2
3
gem "haml"

> bundle install

HAML必須將.erb檔案改為.haml,如app/views/docs/index.html.erb改為app/views/docs/index.html.haml

基本的轉換規則如下:

1. html的tag改用%代替,例如<header>改為%header,不需要close tage
2. class attribute改用.,例如<div class="nav">改為.nav
3. id attribute改用#,例如<div class="banner" id="message">改為.banner#message
4. 遇到<%= ...>的代碼改用=,例如<= f.input>改為= f.input

舉例:

show.html.erb
1
2
3
4
5
6
7
8
9
10
11
<div class="wrapper_with_padding>
<div id="doc_show">
<h1><%= @doc.title %></h1>
<p><%= simple_format(@doc.content) %>

<div class="buttons">
<%= link_to "Fix Doc", edit_doc_path(@doc), class: "button" %>
<%= link_to "Delete Doc", doc_path(@doc), method: :delete, data: {confirm: "Are you sure?"}, class: "button" %>
</div>
</div>
</div>
show.html.haml
1
2
3
4
5
6
7
8
.wrapper_with_padding
#doc_show
%h1= @doc.title
%p= simple_format(@doc.content)

.buttons
= link_to "Fix Doc", edit_doc_path(@doc), class: "button"
= link_to "Delete It", doc_path(@doc), method: :delete, data: {confirm: "Are you sure?"}, class: "button"

參考資料:
haml官網教材
haml refrence

[前端筆記] html/CSS觀念記錄

上完code school的javascript課後,回頭再複習前端的基礎-html和CSS
對一個網頁來說,html控制文章架構、CSS則決定樣式
以下記錄一些觀念:

HTML

1. ul是指unordered list,顯示bullet項目
2. ol是指ordered list,顯示編號項目
3. 在head中加入CSS的檔案,需使用<link style type="text/css" rel="stylesheet" href="css file"> 
    4. 續上,link和img都是empty tag。它們不用加上closing tag 

The box model

html的文字採用box model,包含4個項目:

  1. content area內文
2. padding 內文四個方向的留白
3. border 內文四個方向的框線
4. margin 是border外的留白區域

圖示如下:
box-model

當我們需要調整段落間的距離或文字間留白,可調整margin或padding;如果要加框線,則使用border

CSS Selector

如果我們要微調某部分的html style,可使用html class和CSS selector
以下例子是將ul內的項目左邊padding 15pixel:

1
2
3
<ul class="nav">
...
</ul>
1
2
3
.nav {
padding-left: 15px
}

要注意的是CSS的selector與其它樣式的順序會影響結果,以下這個例子會把nav class設定的padding-left覆寫:

1
2
3
4
5
6
.nav {
padding-left: 15px
}
ul {
padding-left: 10px
}

[Javascript筆記] Object介紹

Javascript的object”類似” C++或Java的class (說類似是因為它多了動態語言的特性,以及存取方式)

object擁有property和method(也可視為property)

例如一台車,它有馬力、重量以及啟動、煞車功能。 我們可以用一個object表示:

car.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
var car = {			// object使用{}宣告
horsePower: 200,
weight: 4000,
start: function() {
alert("Start engine!");
},

break: function() {
alert("Break!");
}
};

console.log(car.horsePower); // 印出200
console.log(car["weight"]; // 也可以使用[]存取property

Object也可以動態增加property:

car.js
1
car.brand = "Toyota";

刪除property:

car.js
1
delete car.brand;

Object也可以包含其它的object

vehicle.js
1
2
3
4
var vehicle = {
Car: {horsePower: 300, weight: 2000},
Boat: {weight: 400000, height: 300000}
};

增加一method可動態新增object

vehicle.js
1
2
3
4
5
6
7
8
9
var vehicle = {
Car: {horsePower: 300, weight: 2000},
Boat: {horsePower: 400000, weight: 300000},
addVehicle: function(name, horsePower, weight) {
this[name] = {horsePower: horsePower, weight: weight}; // 使用this表示vehicle本體
}
};

vehicle.addVehicle("Plane", 600000, 8900000);

假如要計算vehicle內object的個數,需要用for-in loop:

vehicle.js
1
2
3
for (key in vehicle) {		// key會存取vehicle內每個property
console.log(key); // 印出Car, Boat, addVehicle, Plane
}

參考資料:
object介紹

[Ruby筆記] .each v.s. for

練習Learn Ruby the Hardway時,習題32講到迴圈和陣列。 其中存取陣列時有二種不同的方式:.each和for

這二種方式看起來差不多,但對於程式碼執行結果會讓人有”驚喜”的感覺

二者的差異在於scope,對C/C++或Java等靜態語言來說,在{}內或function內的變數只在local有效

而javascript或ruby對變數的scope則略有不同

如以下的程式碼,for不會建立新的scope;而each會建立新的scope

1
2
3
4
5
6
7
8
9
10
11
12
13
count = [1, 2, 3, 4, 5]
for number in count
z = number
end

puts z # 5
puts number # 5

count.each do |y|
a = y
end
puts a # undefined local variable
puts y # undefined local variable

我們在使用each時得小心這個特性,以免造成莫名其妙的bug。

參考資料:
stack overflow討論