蟑螂窩

使用 Babosa 配合 Friendly_id 解決中文網址問題

| Comments

FriendlyId 是用來讓 ActiveRecord 產生 Slug 的 Gem,一般 Rails App 通常是用資料庫的 id,以 SQL 資料庫來說就會是一個遞增的整數 http://example.com/users/1,這樣的網址沒有意義,會讓競爭對手知道你有多少 Record,而且要寫爬蟲也非常簡單,一直遞增數字就可以把整個網站爬完了。

為了解決這個問題,通常會產生 Slug 來當做 record 的識別,一般的用法是這樣:

user.rb
1
2
3
4
class User < ActiveRecord::Base
  extend FriendlyId
  friendly_id :name, use: :slugged
end

如果 slug 是唯一的,就可以用 http://example.com/users/roach-king 來讀取到唯一的 record,而不會有醜醜的網址:

1
2
3
4
5
6
user = User.create! name: "Roach King"
user.to_param #=> "roach-king"

# In UserController#show

user.friendly.find(parmas[:id])

可是這個 Gem 的設計會使用 ActiveSupport 的 parameterize,把非 a-z,0-9,- 的字元全部變成 -,於是中文字就會被吃掉了:

friendly_id/lib/friendly_id/slugged.rblink
1
2
3
def normalize_friendly_id(value)
  value.to_s.parameterize
end

為了解決這個問題,可以用另外一個 Gem babosa 來配合,他可以把 UTF-8 字元處理好,而不是都消滅:

1
"蟑 & 螂  窩".to_slug.normalize.to_s #=> "蟑螂窩"

跟 FriendlyId 配合只要把 normalize_friendly_id override 就可以了:

user.rb
1
2
3
4
5
6
7
8
9
class User < ActiveRecord::Base
  extend FriendlyId
  friendly_id :name, use: :slugged

  def normalize_friendly_id(input)
    input.to_s.to_slug.normalize.to_s
  end

end

Bundle Install Parallel

| Comments

Bundler 在 v1.4 以後新增了 parallel install 的功能,可以增快 bundle install 的速度。

根據 https://github.com/bundler/bundler/pull/2481 這個 pull requst 來看,在 gem 的數量很多的時候,速度是相差非常大的:

1
2
bundle --path sequential  183.03s user 45.13s system 38% cpu 9:55.48 total
bundle --path parallel -j4  234.85s user 50.14s system 77% cpu 6:05.52 total

事先準備

首先要確定自己的 bundler 版本是不是大於 1.4:

1
bundle --version

如果不是的話請先升級 bundler:

1
gem install bundler -v ">=1.5.1"

確認自己 cpu 有幾個核心(以 osx 為範例):

1
sysctl -n hw.ncpu

我是用 macbook air,是四核,下面都以 4 核為範例。

使用 parallel install

升級 bundler 後就可以使用 parallel 了,只要在後面加上 -j4,4 代表 4 個 jobs:

1
bundle install -j4

也可以 global 更改預設的 jobs 數:

1
bundle config --global jobs 4

這樣就不用每次都打 -j4

使用在 capistrano 上

如果在 capistrano 的 deploy.rb 是使用 require "bundler/capistrano",只要在 deploy.rb 裡面加上:

1
set :bundle_flags,    "--deployment --quiet -j4"

這樣在跑 cap deploy 的時候, bundle install 就會加上 -j4 了。

Devise Create User Hook

| Comments

今天做案子時,需要在 Devise user create 前後做一些事情,但是又不想污染 User model,以往的方法就是寫一個 Users::RegistrationsController 去繼承 Devise::RegistrationsController,然後把整個 method copy 過來一次:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
class Users::RegistrationsController < Devise::RegistrationsController
  def create
    # 在這裡加入 before hook

    build_resource(sign_up_params)

    if resource.save
      # 在這裡加入 after hook

      if resource.active_for_authentication?
        set_flash_message :notice, :signed_up if is_navigational_format?
        sign_up(resource_name, resource)
        respond_with resource, :location => after_sign_up_path_for(resource)
      else
        set_flash_message :notice, :"signed_up_but_#{resource.inactive_message}" if is_navigational_format?
        expire_session_data_after_sign_in!
        respond_with resource, :location => after_inactive_sign_up_path_for(resource)
      end
    else
      clean_up_passwords resource
      respond_with resource
    end
  end
end

但是這樣的寫法非常髒,而且如果 devise 有什麼修改,這邊有可能會壞掉。

今天在翻 Devise registrations_controller 的時候發現,在 3.2.1 版以後,create action 裡面多了一行:

1
yield resource if block_given?

於是,如果想要在 create user 的前後做一些事情,只需要:

1
2
3
4
5
6
7
8
9
class Users::RegistrationsController < Devise::RegistrationsController
  def create
    # 在這裡加入 before hook

    super do |resource|
      # 在這裡加入 after hook
    end
  end
end

仔細看會發現 update、destroy 也有這一行,所以也可以如法炮製用在 update、destroy user 上。

神祕的 Rails CRUD 陷阱

| Comments

最近做專案的時候,要把一個由 Form 送出去的 DELETE,變成用 Ajax 送出去,發生了很危險而且意料之外的事情。

情境

先來重現一下一開始的情境:

一開始先有一個 comment 的 Controller,裡面有一個 Destroy 的 Action,在使用者送 DELETE 到 /posts/1/comments/1時,會刪除 ID 為 1 的 Comment,接著 redirect 到 /posts/1

comments_controller.rb
1
2
3
4
5
6
7
class CommentsController < ApplicationController
  def destroy
    @comment = @post.comments.find(params[:id])
    @comment.destroy
    redirect_to post_url(@post)
  end
end

接著使用 jQuery ujs,送出 DELETE Form 到 post_comment_path(1, 1):

destroy_comment.html.erb
1
<%= link_to("Delete comment", post_comment_path(1, 1), :method => :delete) %>

狀況

這個時候覺得每次刪除 comment,都會重新刷新畫面,太擾人了,想改用 Ajax 來完成,於是我們把 HTML 多了一個, :remote => true

destroy_comment.html.erb
1
<%= link_to("Delete comment", post_comment_path(1, 1), :method => :delete, :remote => true) %>

Controller 則是維持不變。

這樣會發生什麼事情呢?如果你也覺得只會刪除 comment 那就掉入陷阱了。

實際情況

按下 Delete comment 會發生什麼事情呢?首先會刪除 comment,這應該沒有什麼問題。那接下來遇到的 redirect 會發生什麼事情呢?

如果送過來的不是 ajax,他會直接 redirect 到 /post/1 也就是 post_controller 的 show action,很理所當然的 show 出 post 內容。

但如果是 Ajax 呢?他一樣會 redirect 到 /post/1,但是卻會用 DELETE,所以是丟到 post_controller 的 destroy action,於是你的 post 就會被刪除了XDDD

為什麼會這樣呢?我也不知道XDDD,改天有空研究再另外 PO 一篇。

解決方法

其實只要依照不同的要求,回應不同的處理方法就好:

comments_controller.rb
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
class CommentsController < ApplicationController
  def destroy
    @comment = @post.comments.find(params[:id])
    @comment.destroy

    respond_to do |format|
      format.html do
        redirect_to post_url(@post)
      end
      format.js do
        head :no_content
      end
    end
  end
end

但是如果非常有自信得只改前端,肯定會掉入陷阱。第一次掉進去的時候,還真的嚇到我了,還好是在開發環境,沒刪到什麼重要資料,學了 Rails 半年多,到現在還是能給我驚奇 >////<

jQuery-ujs Link_to Issue

| Comments

在 Rails 中,常常會使用 remote => true 的方式,使連結變成 Ajax 的方式送出去:

1
link_to "Delete Comment", comment_path(@comment), :remote => true, :method => :delete

這樣的好處是不用另外寫 Ajax ,就可以輕鬆得送出 Ajax。

如果不指定 remote => true 只有指定 method,在點擊連結以後 jquery-ujs 會幫你產生一個表單,這個表單會放在 <body> 裡面,而且馬上送出去,如此,可以讓連結不只有 GET ,更可以做到 RESTful的效果。

1
link_to "Delete Comment", comment_path(@comment), :method => :delete

但是這樣的方法卻會產生一些 issue。