2022.09.14
Better Rails: 3 Filters giúp Controller của bạn sang xịn mịn hơn rất nhiều
Các bạn khi bắt đầu làm việc với Rails hầu hết đều làm việc với 7 actions chuẩn khi tạo một Controller mới: index, new, show, create, edit, update, destroy. Sau một thời gian làm việc, các bạn sẽ để ý thấy một điều rằng, các bạn thường sẽ muốn dùng đi dùng lại một đoạn tính năng tương tự nhau ở các actions hoặc thậm chí là các controllers. Trong bài viết này chúng ta sẽ cùng đi chung với nhau trên con đường giải quyết vấn đề này với một tính năng rất hay: Filters.
Bonus: Trong bài viết, chúng ta sẽ dùng một chút metaprogramming để tạo ra đoạn code giúp bỏ đi rất nhiều đoạn code lặp đi lặp lại nhé.
Trong bài viết chúng ta sẽ đi qua các phần sau:
- Tìm hiểu về Filters trong Rails,
- Áp dụng Filters để share code giữa các action trong cùng một Controller
- Sử dụng metaprogramming để share code giữa các controller
Bắt đầu thôi. Let’s go.
3 loại filters trong Rails
Đầu tiên chúng ta sẽ nói về tính năng Filters mà Rails cung cấp. Rails cung cấp cho chúng ta 3 loại filters:
before_filter
: chạy trước khi request được gửi đến action trong controllerafter_filter
: chạy sau khi action xử lý requestaround_filter
: bọc xung quanh action, chủ yếu sử dụng cho xử lý ngoại lệ (exception)
Cùng tìm hiểu kỹ hơn nhé.
before_filter
Đây là Filter được sử dụng phổ biến nhất. Có 2 cách để gọi một before_filter
.
Cách thứ nhất là sử dụng một anonymous block:
class ArticlesController < ApplicationController before_filter do @article = Article.find(params[:id]) if params[:id] end #...
Đoạn code bên trên sẽ được thực thi trước tất cả request đến bất kì action nào bên trong controller. Vậy nếu như chỉ muốn áp dụng đoạn code đó cho action new
và edit
thôi thì làm thế nào? Một chút nữa chúng ta sẽ nói đến cách để scope phạm vi của filter.
Cách viết thứ hai sau đây là cách mà mình ưa thích hơn vì sự gọn gàng của nó: sử dụng tên của method.
class ArticlesController < ApplicationController before_filter :load_article # Actions... private def load_article @article = Article.find(params[:id]) if params[:id] end end
Các bạn có thể để ý thấy mình để method load_article
là private method. Bởi vì filter này chỉ được sử dụng bên trong controller, và sẽ không được truy cập bởi router để map vào một đường dẫn nào, vì vậy nên tốt hơn hết, chúng ta sẽ để nó là private method.
after_filter
Sau khi tìm hiểu về before_filter
bên trên, chắc hẳn các bạn cũng đã đoán ra after_filter
sử dụng như thế nào rồi. Nó hoạt động với cơ chế tương tự như before_filter, chỉ khác là được thực thi sau khi action của controller được thực thi. Bạn có thể sử dụng nó để lọc hoặc mask các trường thông tin nhạy cảm trước khi trả kết quả về cho client chẳng hạn.
around_filter
Đây là filter hiếm khi được sử dụng và cũng hơi khó hiểu một chút xíu. Cách dùng của nó như sau:
around_filter :wrap_actions def wrap_actions begin # wrap action code will be here yield # after yeild, action code will be run rescue render text: "It broke!" # if there is any exception throw by action code, reach here end end
Như mình đã comment trong đoạn code trên, khi nào yield
được gọi, code bên trong action của controller sẽ được thực thi. Trong quá trình thực thi của action, nếu có exception xảy ra, exception sẽ được bắt ở đoạn code rescue
của around_filter
bên trên. Đoạn code trên là một trong những ứng dụng của around_filter: bắt exception.
Scope phạm vi của filter: only và except
Nếu để mặc định, các đoạn code Filter ở trên sẽ được thực thi trước tất cả request đến bất kì action nào bên trong controller. Vậy nếu như chỉ muốn áp dụng đoạn code đó cho action new
và edit
thôi thì làm thế nào? Sau đây mình sẽ nói về cách scope phạm vi của Filter tới action mà các bạn mong muốn thôi: sử dụng only
và except
.
Cả 3 filter bên trên đều nhận option :only
và :except
:only
: danh sách các actions mà filter đó sẽ chạy cùng:except
: danh sách các actions mà filter đó sẽ không chạy cùng
Ví dụ, ở đoạn code ví dụ cho before_filter
bên trên, các bạn có thể bỏ điều kiện check if params[:id]
nếu như bạn scope filter này tới các action sẽ luôn có params[:id]
, ví dụ như :show, :edit, :update, :destroy
. Cách làm như sau:
class ArticlesController < ApplicationController before_filter :load_article, only: [:show, :edit, :update, :destroy] # Actions... private def load_article @article = Article.find(params[:id]) end end
Hoặc các bạn cũng có thể sử dụng :except
như sau:
class ArticlesController < ApplicationController before_filter :load_article, except: [:index, :new, :create] #...
Như vậy là chúng ta đã đi qua 3 loại filter được sử dụng trong Rails và cũng đã tìm hiểu làm sao để config các actions mà chúng ta muốn chạy filter bên trong controller. Sau đây sẽ tới phần thú vị nhất, share các filter này giữa các controller luôn. Bật mí, chúng ta sẽ sử dụng một chút metaprogramming.
Sharing Filters
Thông thường, filter sẽ được sử dụng để share code giữa các action bên trong một controller cụ thể. Tuy nhiên, chúng ta còn có thể share chúng giữa các controller, sử dụng OOP (lập trình hướng đối tượng).
Share filter giữa các controller thông qua ApplicationController
Cách thông thường nhất để tái sử dụng filter giữa các controller đó là di chuyển đoạn code filter này ra ApplicationController
. Bởi vì tất cả các controller khác của chúng ta kế thừa từ ApplicationController
, các controller này sẽ có quyền truy cập đến các filter này.
class ApplicationController < ActionController::Base protect_from_forgery private def load_article @article = Article.find(params[:id]) end end class ArticlesController < ApplicationController before_filter :load_article, only: [:show, :edit, :update, :destroy] # Actions... end
Nhưng các bạn có thấy điều gì bất thường ở trong đoạn code trên hay không? Đúng rồi, ngoài controller ArticlesController
ra, các controller khác đâu cần biến @article
làm gì. Vậy có cách nào để:
ArticlesController
thì tạo ra biến@article
PostsController
thì tạo ra biến@post
OrdersController
thì tạo ra biến@order
- …
hay không?
Sử dụng một chút metaprogramming, chúng ta có thể làm được. Chúng ta sẽ sửa method load_article
lại thành find_resource
. Trong method này, sử dụng params[:controller]
để suy ra model tương ứng, kiểu như sau:
ArticlesController
-> Model:Article
-> biến:@article
PostsController
-> Model:Post
-> biến:@post
OrdersController
-> Model:Order
-> biến:@order
class ApplicationController < ActionController::Base protect_from_forgery private def find_resource class_name = params[:controller].singularize klass = class_name.camelize.constantize self.instance_variable_set "@" + class_name, klass.find(params[:id]) end end class ArticlesController < ApplicationController before_filter :find_resource, only: [:show, :edit, :update, :destroy] # Actions... end
Như vậy qua bài viết này, mình cùng với các bạn đã đi qua các kỹ thuật áp dụng Filter trong Rails để khiến code trở nên ngắn gọn và clean hơn. Hi vọng bài viết giúp ích phần nào đó cho các bạn trên con đường sử dụng framework Ruby on Rails trong công việc của mình. Các bạn có chia sẻ nào thêm có thể để dưới phần bình luận nhé.
References: