1 day ago

承接上篇文章,若對於 rails turoblinks 還不知道是什麼,可以參考此文:Rails Turbolinks & PJAX。這篇條列幾點在實作 rails turbolinks 時要注意的事項:

只有 <body> 的內容會被更動

Rails Turbolinks 目前版本只有 <body> 與 <title> 的內容會被更動,為什麼說「目前這個版本」?因為今年底前 turbolinks 第 3 版應就會隨著 rails 5 一起被 release,屆時將會有非常非常大的變動...可參考 turbolinks

不會有 progressbar 出現

目前 turbolinks 套用後的頁面切換是不會有一般瀏覽器的進度列,但在第 3 版後將可以手動開啟。

js/css 要放在 <head> 裡面

為了使 js/css 可以享受 turbolinks 的魔力,要把 js/css 放在 <head> 內,而非一般建議的放在頁面最下方

可以加上 async:true

即使放在 <head> 內也能使用 async:true 使網頁讀取不要因 js/css 而緩慢

<%= javascript_include_tag "application", async: true %>

解決 page:ready 問題

平常用 turbolinks 需要注意 js 的 $(document).ready(...) 是無法正常使用,而這個 gem 將解決這個問題,不用特別實作 page:change 的 event,這 gem 為 jquery.turbolinks.js

BTW, 此 gem 依舊有 $(document).on("ready", function(){...}) 的問題

tracking code 要放在 <body>

一些像是 google analytics, facebook pixel 等的 tracking code 要放在 <body> 內才能正常在頁面切換時被追蹤到。BTW, 新版將有「data-turbolinks-eval」可以做 js 要不要被執行的控制。

小結:好期待 turbolinks 3!

若想多了解,可看 upcase: https://upcase.com/videos/turbolinks

 
11 days ago

ruby 提供非常多的函式,但同一種需求有不同寫法,往往我們都沒注意到效能細節,只知道要把功能寫出來。最近 github 上便有人針對類似功能的函式作些效能比較,在此針對特別或是速度差很多的地方作些個人筆記。原出處:https://github.com/JuanitoFatas/fast-ruby

註:函式名稱會有標記哪種寫法較快,是 ruby 2.2.0 版本

多個變數的 assignment

def fast
  _a, _b, _c, _d, _e, _f, _g, _h = 1, 2, 3, 4, 5, 6, 7, 8
  nil
end

def slow
  _a = 1
  _b = 2
  _c = 3
  _d = 4
  _e = 5
  _f = 6
  _g = 7
  _h = 8
  nil
end

Parallel Assignment 比 Sequential Assignment 快上 1.22 倍

Array#bsearch & Array#find

data = [*0..100_000_000]

Benchmark.ips do |x|
  x.report('find')    { data.find    { |number| number > 77_777_777 } }
  x.report('bsearch') { data.bsearch { |number| number > 77_777_777 } }
  x.compare!
end

find 與 bsearch 主要是找到 array 裡面第一個符合條件的元素,在大筆數據上,bsearch 竟然比 find 快上 3137489.63 倍!

Array#length & Array#size & Array#count

ARRAY = [*1..100]

Benchmark.ips do |x|
  x.report("Array#length") { ARRAY.length }
  x.report("Array#size") { ARRAY.size }
  x.report("Array#count") { ARRAY.count }
  x.compare!
end
  • length 單純回傳 array 裡面有幾個 element
  • size 是 length 的別名(一樣的事情,只是不同名字)
  • count 會實際去計算裡面有幾個 element length = size 比 count 快上 1.24 倍(length 比 size 快上 1.01 倍,大概就是別名轉換的時間吧!)

Array#shuffle.first & Array#sample

sample 跟 shuffle.first 一樣都是做「取得隨機一個陣列中的元素」,但 sample 比 shuffle.first 快上 18.82 倍!(而且只在 array 只有 100 個元素下)

Array[] & Array#first & Array#last

Define: ARRAY = [*1..100]

  • ARRAY[0] 比 ARRAY.first 快上 1.15 倍
  • ARRAY[-1] 比 ARRAY.last 快上 1.12 倍

Enumerable#each_with_index & while

ARRAY = [*1..100]

def slow
  ARRAY.each_with_index do |number, index|
    number + index
  end
end

def fast
  index = 0
  while index < ARRAY.size
    ARRAY[index] + index
    index += 1
  end
end

Benchmark.ips do |x|
  x.report('each_with_index') { slow  }
  x.report('While Loop')      { fast  }
  x.compare!
end

這結果蠻令人吃驚,單純用 while 去做 index 比 each_with_index 快上 1.88 倍。

Enumerable#map & Array#flatten & Enumerable#flat_map

ARRAY = (1..100).to_a

def slow_flatten_1
  ARRAY.map { |e| [e, e] }.flatten(1)
end

def slow_flatten
  ARRAY.map { |e| [e, e] }.flatten
end

def fast
  ARRAY.flat_map { |e| [e, e] }
end

flat_map 寫法快上 1.6x 倍!

Hash#merge & Hash#merge!

ENUM = (1..100)

def slow
  ENUM.inject({}) do |h, e|
    h.merge(e => e)
  end
end

def fast
  ENUM.inject({}) do |h, e|
    h.merge!(e => e)
  end
end

原來 merge! (有加驚嘆號)比 merge 快上整整 24 倍!(兩者差異在,沒加驚嘆號會是回傳一個新的 Array,有加驚嘆號是直接套用在呼叫的 Array 上)

cover? & include?

BEGIN_OF_JULY = Date.new(2015, 7, 1)
END_OF_JULY = Date.new(2015, 7, 31)
DAY_IN_JULY = Date.new(2015, 7, 15)

def fast
  (BEGIN_OF_JULY..END_OF_JULY).cover? DAY_IN_JULY
end

def slow
  (BEGIN_OF_JULY..END_OF_JULY).include? DAY_IN_JULY
end

range 內找是否有這個 element 的情況下,cover? 比 include? 快上 23.35 倍!

 
15 days ago

rails 目前整個 cookie/session 機制搭載的是 activerecord-session_store 這套 gem,設定檔是在 config/initializers/session_store.rb 內。

以目前開發中的專案設定為例:

Rails.application.config.session_store :cookie_store, key: Rails.application.secrets.secret_key_base

對於 cookie/session 的機制而言,這像是個 key-value store,但以我們丟進去的 key 作為加密(不然網站早就都被 hack 光光 lol)

順道一提,rails 有提供一個產生 key 的指令:rake secret。

那說了這麼多,以想做的需求:cookie/session cross all subdomains,目前這樣的設定是沒辦法的。因為預設都是 by domain,例如 www.facebook.com 與 m.facebook.com 是有各自擁有的 cookie / session。

在 rails 中,只需在後面加入 domain: :all 即可。

Rails.application.config.session_store :cookie_store, key: Rails.application.secrets.secret_key_base, domain: :all
 
23 days ago

instance_eval 與 class_eval 從英文語意上來看感覺很簡單,一個是 instance level,另個是 class level,實際瞭解後才發現沒這麼單純,instance 也非平常所理解的 instance。

先看看以下的範例:

String.instance_eval do
  def from_instance_eval
    self
  end
end

String.class_eval do
  def from_class_eval
    self
  end
end

p String.from_instance_eval      #String

p "string".from_class_eval       #"string"


begin
  String.from_class_eval
rescue Exception  => e
  p e                            # => #<NoMethodError: undefined method `from_class_eval' for String:Class>

end

begin
  "string".from_instance_eval
rescue Exception  => e
  p e                            # => #<NoMethodError: undefined method `from_instance_eval' for "string":String>

end

from gist

看起來好像是 class method 要用 instance_eval 來定義,instance method 要用 class_eval 來做?再讓我們深入探討一下~

instance_eval

"string2".instance_eval { p self }   # => "string2"

begin
  "string2".class_eval { p self }
rescue Exception  => e
  p e                               # => #<NoMethodError: undefined method `class_eval' for "string2":String>

end

from gist

到此我們可以發現,instance_eval 用來定義於任何 object 上,即使是 class。(這邊要回想到與切記,在 ruby 的世界裡,任何東西也包含 Class,都是物件 object)

class_eval

而 class_eval 也很好理解,以下兩個東西是相等的:

  1. class_eval 寫法

    class Thing
    end
    Thing.class_eval { def far; end }
    
  2. 原始 class 定義寫法

    class Thing
    def far
    end
    end
    

小記

實務上,這可以拿來用作擴充 rails gem 所定義的 class。例如 globalize 這個 gem,會針對 model 多開一個 translation 的 class。

像是在 Post 使用 Globalize

class Post < ActiveRecord::Base
  translates :title, :text
end

那 Globalize 的 gem 就會幫我們多定義一個 class 用作多語系版本的用途,為「Post::Translation」
如果想要在這個 class 中多增加方法呢?在 config/initializers 內多一個檔案定義 module_eval 即可

Post::Translation.module_eval do
  def test
    puts "hello world"
  end
end

module_eval 與 class_eval 沒有太大差異,只是定義的 scope 不同而已。

 
about 1 month ago

臉書 og image 其實可以設定很多個,讓使用者在分享網站連結時可以有圖片的選擇:

<meta content="https://example.com/1.jpg" property="og:image" />
<meta content="https://example.com/2.jpg" property="og:image" />
<meta content="https://example.com/3.jpg" property="og:image" />
<meta content="https://example.com/4.jpg" property="og:image" />
<meta content="https://example.com/5.jpg" property="og:image" />

裡面有些規則在,判定標準的「順序」整理如下:

  1. 高解析度的 priority 較高!
  2. html meta 列的順序由下至上,如範例就會是 5.jpg -> 4 -> 3 -> 2 -> 1
  3. 不管列再多,最多只會出現5張給使用者選

若需要測試網頁 og meta 是否正確設定,可以用此連結測試:https://developers.facebook.com/tools/debug/og/object/

 
about 1 month ago

開始前簡單介紹一下什麼是 Eager Loading 什麼是 N+1 Query,Eager Loading 出現主要是為解決 N+1 Query。N+1 Query 像是這樣:

# controller

@posts = Post.publish.take(10)
# view
<table>
  <th>標題</th>
  <th>簡介</th>
  <th>作者</th>
  <% @posts.each do |post| %>
  <tr>
    <td><%= post.title %></td>
    <td><%= post.summary %></td>
    <td><%= post.author.name %></td>
  </tr>
  <% end %>
</table>
Read on →
 
about 2 months ago

前陣子在接觸 rails turbolinks 時想說 turbolinks 怎這麼 suck,光是 GA、Facebook Pixel 等 tracking script 無法正常運作就讓人十分頭痛(當時直接怒拔...),但在最近因為看了這篇文章 瞬間懂了。

Turbolinks 是在 Ruby on Rails 4.0 被默認的一個 gem,當時(2013年)很多人在分享時都搭上一句標題「Turbolinks for Rails (like pjax)」。那什麼是 pjax?turbolinks?更詳細解說可看上述提及的文章,這邊就針對一些所獲的重點摘錄做分享。
<!--more-->

講什麼是 pjax 跟 turbolinks 之前要先知道在 HTML5 的 History Interface及裡面的 pushState。這是文章內的解說

pushState允許Javascript在會話歷史中隨意儲存數據,並且綁定一個標題和可選的URL。back()和forward()方法允許我們瀏覽已存儲的會話歷史數據。通過這個方法,我們可以使用pushState保存當前頁的瀏覽歷史,並且動態的在不同狀態下,後退和前進,而不需要重新載入整個頁面。

PJAX & Turbolinks

簡單來說,這兩個東西的理念都是:當使用者點擊一個連結時,不需要重新載入整個頁面(平常使用網站會有突然閃爍、白一下的感覺),而是直接載入並替換需要更新的網站內容。

那有什麼不同?

1. 更新的範圍

PJAX 可以指定要更新哪個網頁元素,like this

$.pjax({url:'/authors',container:'#main'})

但 Turbolinks 是直接替換整個網頁的 <title> 和 <body>,而且預設所有網站內的超鏈結都屬於 turbolinks 範疇。所以不用特別指定哪些連結需要而去標記他們,Turbolinks 會自動處理(好像很夢幻,但若不懂會覺得網站很多雷...)。

只需要:

# Gemfile
gem "turbolinks"

# app/assets/javascripts/application.js
//= require turbolinks

2. 伺服器回應的內容

PJAX 也只需要伺服器回應元素的內容就好,像是

if request.headers['X-PJAX']
  render:layout
end

而 Turbolinks 不需要而外在 controller 做設定,照舊即可(有原生的 gem support 真好!)

Rails Turbolinks 注意事項

當使用 document.ready 或是 $(function(){}) 時,這些事件只會在 DOM 完成載入時被觸發,但不會在 Turbolinks 更改網頁內容時觸發,所以可利用 page:change 這個事件

$(function(){
  initPage();
});
$(window).bind('page:change', function(){
  initPage();
});

另外若有像是 GA 或是 Pixel 等的 tracking script 記得也需要加入 page:change 的事件處理,或是將這些 script 擺在 <body> 內。

小結

雖然 PJAX 看起來可以讓客戶端要處理的數據量較小(伺服器只回應需要的部份),但在開發與設定上也更加的麻煩,讓開發者需要額外設定的工作也變更多,更違反了 Rails 快速開發的哲學。既然用 Rails 了,就好好體會其中的設計,使用原生內建的 turbolinks 吧!

BTW, 會體會這麼深是因為有個專案在 themeforest 買了個版,裡面大量使用 pjax,整個頁面切換很 smooth 很夢幻(所以才挑它...),但在整理 assets 時發現 pjax theme 根本 rails 天敵,建議大家若有買版(例如 themeforest)的習慣,要留意裡面是否有用 pjax,不然光要整理裡面的 js 就很暈...

Read on →
 
about 2 months ago

參考源於此:http://blog.honeybadger.io/ruby-exception-vs-standarderror-whats-the-difference/

「Never rescue Exception in Ruby」
或許你可曾聽過這句話,但若不知道 Exception 與 Standard Error 的差別還真是讓人摸不著頭緒。

通常我們會在 Ruby 這樣 rescue exceptions

begin
  do_something()
rescue => e
  puts e # e is an exception object containing info about the error.
end

但在看到這張表後馬上明白為什麼不能這樣寫

Exception
 NoMemoryError
 ScriptError
   LoadError
   NotImplementedError
   SyntaxError
 SignalException
   Interrupt
 StandardError
   ArgumentError
   IOError
     EOFError
   IndexError
   LocalJumpError
   NameError
     NoMethodError
   RangeError
     FloatDomainError
   RegexpError
   RuntimeError
   SecurityError
   SystemCallError
   SystemStackError
   ThreadError
   TypeError
   ZeroDivisionError
 SystemExit
 fatal

可以看到 StandardError 只是 Exception 的一個子類別而已,所以當我們若 rescue 整個 exception 類別,會發現「ScriptError::SyntaxError」、「NoMemoryError」這種最常 typo 的錯誤就都跑進去 rescue 裡面了,開發時都不會發現,大概要功能上線後發現怎都沒資料才會注意到吧(笑

所以應該這樣寫:

begin
  do_something()
rescue StandardError => e
  # Only your app's exceptions are swallowed. Things like SyntaxErrror are left alone. 
end
 
3 months ago

Arrays: map & each_with_index

在跑陣列(array) for-each 每一個 interation 可以帶上相對應的 index,利用 each_with_index 這個方法:

['a', 'b', 'c'].each_with_index do { |item, index|
  puts "#{index}-#{item}"
}

當然,我們也可以利用 index 對應到的 element 組成一個新的 object:

['a', 'b', 'c'].each_with_index.map { |item, index|
  { :letter => item,
    :position => index }
}

View: cycle is no even odd

在網頁設計上,常為了讓表格內容更容易被閱讀,讓表格每行的顏色交錯,像是:
{}bootstrap-table
正常寫法:

<% an_array.each_with_index do |element, index| %>
  <tr class="<%= index.odd? ? 'odd' : 'even' %>">
    <td>...</td>
  </tr>
<% end %>

在 Rails 有提供一個很棒的 helper 函式 cycle:

<% an_array.each do |item| %>
  <tr class="<%= cycle('odd','even') %>">
<td>...</td>
  </tr>
<% end %>

更多 cycle 用法可參考文件

try

這是 Ruby 語法中最讓我驚艷不已的一個...省掉超級多行。原先為避免對 nil 呼叫產生錯誤,會這樣寫:

if @person.present? && @person.a_property == 'foo'
  ..
end

<% if @an_array.present? %>
  <% @an_array.each do |item| %>
    ...
  <% end %>
<% end %>

學會 try 之後可以這樣寫:

if @person.try(:a_property) == 'foo'
   ...
end

<% @an_array.try(:each) do |item| %>
  ...
<% end %>
 
6 months ago

Sidekiq 在 heroku 的 deployment 可以參照官方的這篇文章:https://github.com/mperham/sidekiq/wiki/Deployment#heroku

大概整理以下要點與步驟:

  • Sidekiq 版本 3.0 以前會自動設定 Redis-to-Go 的相關 Redis 伺服器位置。3.0 後就要自己設定嚕,目前最新版本是 2015/2/6 的 v3.3.2。
  • 新增 heroku add-on: https://addons.heroku.com/
  • 因上點之描述,手動設定 heroku 環境變數,指定 redis server address
    heroku config:set REDIS_PROVIDER=REDISTOGO_URL # Redis to Go add-on
    heroku config:set REDIS_PROVIDER=REGISCLOUD_URL # Redis Cloud add-on
    
  • 重新啟動 heroku 去套用新的環境變數: heroku restart