因為 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有關的方法搬過來。
把after_validation :generate_slug!
放在include區塊。
其他方法貼進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 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為例來解析一下它的意思
self.class
就是Post,所以變成了self.send(Post.slug_column.to_sym)
Post.slug_column
我們在post中設定成slug_column: title
所以變成了self.send("title".to_sym)
title字串轉成symbol,變成self.send(:title)
self我們可以想像成一個新增的post物件post=Post.new
,置換後post.send(:title)
post.send(:title)
就等同於post.(:title)就等同於post.title
,我們成功的呼叫了post.title
屬性!
6. 有了the_slug之後我們就可以來置換post與Post
Post用self.class來取代
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 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 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 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