從 Tealeaf 課程學習模組化 - Sluggify Module

因為 Post 與 Category 都的網址都需要 Sluggify 以便 SEO 的進行。所以我們把 Sluggify 模組化,讓同樣的程式碼只要寫一次就好。

1. 建立module Sluggable,並引入之

在lib資料夾中建立一個名為sluggable.rb的檔案。加入extend ActiveSupport::Concern,這個技巧會讓模組間的耦合變得更加簡單。而一個class載入Sluggable時,會先做完include區塊中寫下的事情。

1
2
3
4
5
6
7
module Sluggable
extend ActiveSupport:Concern

include do

end
end

打開config/application.rb加入路徑config.autoload_paths << Rails.root.join('lib')

還有另一個方法把rb檔initializers中,放在這個資料夾裡面代表app打開初始化時就會先跑過一遍。

2. 跟sluggify有關的方法通通搬過來

接著我們要把原本model(post.rb,category.rb)跟sluggify有關的方法搬過來。

  1. after_validation :generate_slug!放在include區塊。
  2. 其他方法貼進model中。

這樣會出現幾個問題,接下來的步驟會解決他們並且解釋之。

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
30
31
32
33
34
35
36
37
module Sluggable
extend ActiveSupport::Concern

# executes this code when included only once
included do
after_validation :generate_slug!
end

def generate_slug
the_slug = to_slug(title)
post = Post.find_by slug: the_slug
count = 2
while post && post != self
the_slug = append_suffix(the_slug, count)
post = Post.find_by slug: the_slug
count += 1
end
self.slug = str.downcase
end

def append_suffix(_the_slug, count)
if str.split('-').last.to_i != 0
return str.split('-').slice(0...-1).join('-') + '-' + count.to_s
else
return str + '-' + count.to_s
end
end

def to_slug(name)
str = name.strip
str.gsub! /\s*[^A-Za-z0-9]\s*/, '-' # 將符號轉成"-"

str.gsub! /-+/, '-' # 將多個"-"轉成一個"-"

str
end
end

3. class_attribute特性新增屬性到model上

因為post與category所要轉換成網址的欄位一個是title、一個是name。所以我們必須想個方法讓module至換掉原本設定the_slug的這一行:

1
2
3
4
5
6
7
def generate_slug
the_slug = to_slug(title)
.
.
.

end

class_attribute這個ruby語言獨有的特性可以幫助我們解決這個問題,簡單的說class_attribute可以讓屬性繼承給子class使用。所以我們先在剛剛建立的模組sluggable.rb中加入

1
2
3
4
include do
before_save :generates_slug!
class_attibute :slug_column
end

接著在post.rb中加入

這樣一來就可以使用post.slug_column這個新的變數。

4. 新增類別方法sluggable_cloumn讓model能夠初始化slug_column

1
2
3
4
5
6
7
8
9
10
module Sluggable
.
.
.
module ClassMethods
def sluggable_column(col_name)
self.slug_column = col_name
end
end
end

在Post中呼叫剛剛建立的sluggable_column方法,把title設成slug_column。

1
2
3
4
5
6
7
8
9
class Post
.
.

sluggable_column :title

.
.
end

5. 置換欄位

1
2
3
4
5
6
def generate_slug
the_slug = to_slug(title)
.
.
.
end

置換成

1
2
3
4
5
6
def generate_slug
the_slug = to_slug(self.send(self.class.slug_column.to_sym))
.
.
.
end

這句是什麼意思呢?以Post為例來解析一下它的意思

  1. self.class就是Post,所以變成了self.send(Post.slug_column.to_sym)

  2. Post.slug_column我們在post中設定成slug_column: title所以變成了self.send("title".to_sym)

  3. title字串轉成symbol,變成self.send(:title)

  4. self我們可以想像成一個新增的post物件post=Post.new,置換後post.send(:title)

  5. post.send(:title)就等同於post.(:title)就等同於post.title,我們成功的呼叫了post.title屬性!

6. 有了the_slug之後我們就可以來置換post與Post

  1. Post用self.class來取代
  2. post用obj來取代 => obj = self.class.find_by slug: the_slug
1
2
3
4
5
6
7
8
9
10
11
12
13
def generate_slug!
the_slug = to_slug(self.send(self.class.slug_column.to_sym))
I obj = self.class.find_by slug: the_slug
count = 2
while obj && obj != self
the_slug = append_suffix(the_slug, count)
obj = self.class.find_by slug: the_slug
count += 1
end
self.slug = the_slug.downcase
# self.slug = self.title.sub(" ","-").downcase # prefer the following
# self.slug = self.title.parameterize # rails way without gem
end

7. 完成品

lib/slugabble.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
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
module Sluggable
extend ActiveSupport::Concern

# executes this code when included only once
included do
after_validation :generate_slug!
class_attribute :slug_column
end

def generate_slug!
the_slug = to_slug(self.send(self.class.slug_column.to_sym))
obj = self.class.find_by slug: the_slug
count = 2
while obj && obj != self
the_slug = append_suffix(the_slug, count)
obj = self.class.find_by slug: the_slug
count += 1
end
self.slug = the_slug.downcase
# self.slug = self.title.sub(" ","-").downcase # prefer the following
# self.slug = self.title.parameterize # rails way without gem
end

def append_suffix(str, count)
if str.split('-').last.to_i != 0
return str.split('-').slice(0...-1).join('-') + '-' + count.to_s

else
return str + '-' + count.to_s
end
end

def to_slug(name)
str = name.strip
str.gsub! /\s*[^A-Za-z0-9]\s*/, '-'
str.gsub! /-+/, '-'
str.downcase
end

def to_param
self.slug
end
module ClassMethods
def sluggable_column(col_name)
self.slug_column = col_name
end
end
end

Post.rb

1
2
3
4
5
6
7
8
class Post < ActiveRecord::Base
include Sluggable

has_many :post_categories
has_many :categories, through: :post_categories
has_many :comments
sluggable_column :title
end

評論