Friendly-URLs in Rails
Bài đăng này đã không được cập nhật trong 5 năm
Theo mặc định, các ứng dụng Rails xây dựng URL dựa trên primary key – the id
column từ cơ sở dữ liệu. Hãy tưởng tượng chúng ta có một model Person và associated controller. Chúng ta có person record là Bob Martin
có id
number 6
. URL cho trang hiển thị của anh ấy sẽ là:
1 /people/6
Nhưng, vì mục đích thẩm mỹ hoặc SEO, chúng ta muốn tên của Bob trong URL. tại vị trí số 6
, được gọi là "slug". Hãy xem xét một vài cách để làm slugs tốt hơn
1. Simple Approach
Cách tiếp cận đơn giản nhất là ghi đè to_param
method trong model Person
. Bất cứ khi nào chúng ta gọi một route helper như thế này:
person_path(@person)
Rails sẽ gọi to_param
để chuyển đổi đối tượng thành một slug cho URL. Nếu mô hình của bạn không xác định to_param
method thì Rails sẽ sử dụng quá trình thực hiện trong ActiveRecord::Base
đó, nó sẽ trả về id
.
Để to_param
method thành công, điều quan trọng là tất cả các liên kết đều sử dụng ActiveRecord
object thay vì gọi id
. Đừng làm điều này :
person_path(@person.id) # Bad!
Thay vào đó:
person_path(@person)
1.1. Slug Generation with to_param
Trong model, chúng ta có thể ghi đè to_param
để bao gồm một phiên bản parameterized của Person's name:
class Person < ActiveRecord::Base
def to_param
[id, name.parameterize].join("-")
end
end
Các parameterize method từ ActiveSupport
sẽ biến bất kỳ chuỗi thành ký tự hợp lệ cho một URL.
Đối với người dùng của chúng ta Bob Martin
có id
là 6
,to_param
sẽ tạo ra một slug 6-bob_martin
.
Đường dẫn đầy đủ sẽ là:
/people/6-bob-martin
1.2. Object Lookup
Chúng ta cần thay đổi gì để tìm thấy Person
mà chúng ta cần? Nothing!
Khi chúng ta gọi Person.find(x)
, tham số x
được chuyển đổi thành một số nguyên để thực hiện tra cứu SQL. Hãy kiểm tra cách to_i
xử lý các chuỗi có kết hợp các chữ cái và số:
"1".to_i
# => 1
"1-with-words".to_i
# => 1
"1-2345".to_i
# => 1
"6-bob-martin".to_i
# => 6
Vì chúng ta triển khai to_param
luôn có id
phía trước và theo sau là dấu gạch nối, nên nó sẽ luôn luôn tìm kiếm dựa trên chỉ id
và loại bỏ phần còn lại của slug.
1.3. Benefits / Limitations
Chúng ta đã thêm nội dung vào slug sẽ cải thiện SEO và làm cho URL của chúng ta dễ đọc hơn.
Một hạn chế là người dùng không thể thao tác trực tiếp trên URL. Ví dụ chúng ta có url 6-bob-martin
nhưng bạn sẽ không thể đoán được url có id
là 7
ví dụ như 7-russ-olsen
.
Một hạn chế khác là id
vẫn nằm trong URL. Nếu id
là thứ bạn muốn làm xáo trộn, việc tạo slug đơn giản bằng cách ghi đè to_param
không giúp ích được gì.
2. Using a Non-ID Field
Đôi khi bạn muốn thoát khỏi id
và sử dụng một thuộc tính khác trong cơ sở dữ liệu for lookups. Hãy tưởng tượng chúng ta có một Tag
object có một name
column..
2.1. Link Generation
Chúng ta có thể ghi đè lại to_param
để tạo liên kết:
class Tag < ActiveRecord::Base
validates_uniqueness_of :name
def to_param
name
end
end
Bây giờ khi chúng ta gọi tag_path(@tag)
ta sẽ nhận được một path như thế này /tags/ruby
.
2.2. Object Lookup
Việc tìm kiếm là khó khăn hơn, mặc dù. Khi có một request đến cho /tags/ruby
các ruby
sẽ được lưu trữ trong params[:id]
của router.
Một controller điển hình sẽ gọi Tag.find(params[:id])
, về cơ bản Tag.find("ruby")
sẽ thất bại.
Option 1: Query Name from Controller
Thay vào đó, chúng ta có thể sửa đổi controller để sử dụng Tag.find_by_name(params[:id])
. Nó sẽ làm việc , nhưng nó là thiết kế bad object-oriented. Chúng ta đang phá vỡ sự đóng gói của Tag
class.
Các nguyên tắc DRY nói rằng một mảnh kiến thức nên có một đại diện duy nhất trong một hệ thống. Trong quá trình triển khai thẻ này, ý tưởng "Thẻ có thể được tìm thấy theo tên của nó" hiện đã được thể hiện trong to_param
của model và tra cứu controller. Đó là một vấn đề về bảo trì lớn.
Option 2: Custom Finder
Trong model của chúng ta, ta có thể xác định một công cụ tìm tùy chỉnh:
class Tag < ActiveRecord::Base
validates_uniqueness_of :name
def to_param
name
end
def self.find_by_param(input)
find_by_name(input)
end
end
Sau đó trong controller gọi Tag.find_by_param(params[:id])
. Lớp trừu tượng này có nghĩa là chỉ có model biết chính xác cách một Tag
được chuyển đổi thành và từ một tham số. Việc đóng gói được khôi phục.
Nhưng chúng ta phải nhớ sử dụng Tag.find_by_param
thay vì Tag.find
ở mọi nơi. Đặc biệt nếu bạn đang sử dụng ID friendly trên một hệ thống hiện có, đây có thể là một nỗ lực đáng kể.
Option 3: Overriding Find
Thay vì triển khai công cụ tìm tùy chỉnh, chúng ta có thể ghi đè find
method:
class Tag < ActiveRecord::Base
#...
def self.find(input)
find_by_name(input)
end
end
Nó sẽ hoạt động khi bạn truyền vào một slug, nhưng sẽ bị hỏng khi một ID được truyền vào. Làm thế nào chúng ta có thể xử lý cả hai? Cách đầu tiên là thực hiện một số chuyển đổi:
class Tag < ActiveRecord::Base
#...
def self.find(input)
if input.is_a?(Integer)
super
else
find_by_name(input)
end
end
end
Điều đó sẽ hoạt động, nhưng việc kiểm tra type rất trái với ethos của Ruby. Việc luôn phải viết is_a?
khiến bạn tự hỏi "Có cách nào tốt hơn không?"
Và có một cách tốt hơn, dựa trên hai điều sau:
- Database cung cấp cho các
id
là1
cho bản ghi đầu tiên - Ruby chuyển đổi chuỗi bắt đầu bằng một chữ cái thành
0
class Tag < ActiveRecord::Base
#...
def self.find(input)
if input.to_i != 0
super
else
find_by_name(input)
end
end
end
Hoặc, ngưng tụ với một ternary:
class Tag < ActiveRecord::Base
#...
def self.find(input)
input.to_i == 0 ? find_by_name(input) : super
end
end
Mục tiêu của chúng ta đã đạt được, nhưng chúng ta đã đưa ra một lỗi có thể xảy ra: nếu một tên bắt đầu bằng một chữ số thì nó sẽ trông giống như một ID. Hãy thêm một xác nhận rằng tên không thể bắt đầu bằng một chữ số:
class Tag < ActiveRecord::Base
#...
validates_format_of :name, without: /^\d/
def self.find(input)
input.to_i == 0 ? find_by_name(input) : super
end
end
Bây giờ mọi thứ có lẽ đã làm việc tuyệt vời!
3. Using the FriendlyID Gem
Tham khảo tại: https://github.com/norman/friendly_id
Chúc các bạn thành công!!
All rights reserved