Tìm hiểu về ứng dụng Rack và Middleware
Bài đăng này đã không được cập nhật trong 6 năm
Nhiều web developer làm việc ở mức độ trừu tượng cao nhất khi chúng ta lập trình. Và đôi khi thật dễ dàng chấp nhận mọi thứ. Đặc biệt là khi chúng ta đang sử dụng Rails.
Bạn đã bao giờ đào vào internals để xem chu kì request/response hoạt động trong Rails như thế nào? Gần đây tôi mới nhận ra rằng tôi hầu như không biết gì về các ứng dụng Rack hoặc middlewares làm việc - vì vậy tôi đã dành chút thời gian để tìm hiểu. Dưới đây là những phát hiện của tôi.
Rack là gì?
Bạn có biết rằng Rails là một ứng dụng có thể tích hợp Rack? Sinatra cũng vậy. Rack là gì? Rack là một gói Ruby cung cấp một giao diện dễ sử dụng giữa các máy chủ web và framework web. Có thể nhanh chóng xây dựng các ứng dụng web đơn giản chỉ sử dụng Rack.
Để bắt đầu, bạn cần một đối tượng đáp ứng với call
method, lấy thông tin từ mảng băm các biến môi trường và trả về một mảng với HTTP response code
, header
và response body
. Một khi bạn đã viết code server, tất cả những gì bạn phải làm là khởi động nó bằng một máy chủ Ruby như Rack::Handler::WEBrick
, hoặc đặt nó vào một file config.ru
và chạy nó từ dòng lệnh bằng rackup config.ru
.
Rack hoạt động thế nào?
Rack thực sự là một cách để một nhà phát triển tạo ra một ứng dụng máy chủ trong khi vẫn tránh được mã boilerplate để hỗ trợ các máy chủ web Ruby khác nhau. Nếu bạn đã viết một số code đáp ứng được các đặc tả Rack, bạn có thể tải nó lên trong một máy chủ Ruby như WEBrick
, Mongrel
hoặc Thin
- và bạn sẽ sẵn sàng chấp nhận các requests và response.
Có một số phương pháp bạn nên biết về điều đó được cung cấp cho bạn. Bạn có thể gọi trực tiếp từ file config.ru
của bạn.
run
Lấy một ứng dụng - đối tượng response call
- như một đối số. Đoạn code sau đây từ trang web Rack thể hiện rõ cách thức này:
run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] }
map
Lấy một chuỗi xác định đường dẫn được xử lý và một khối có chứa code Rack app sẽ được chạy khi nhận được resquest với đường dẫn đó.
map '/posts' do
run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['first_post', 'second_post', 'third_post']] }
end
use
Điều này nói với Rack để sử dụng một số middleware.
The Environment Hash
Rack các đối tượng máy chủ lấy một environment hash. Những gì có trong hash đó? Đây là một số trong những phần thú vị hơn:
REQUEST_METHOD
: Các verbs HTTP của request. Điều này là bắt buộc.PATH_INFO
: Đường dẫn URL yêu cầu, liên quan đến gốc của ứng dụngQUERY_STRING
: Bất cứ thứ gì tiếp theo?
trong chuỗi URL yêu cầu.SERVER_NAME
vàSERVER_PORT
: Địa chỉ và cổng của máy chủrack.version
: Phiên bản rack sử dụngrack.url_scheme
:http
hoặchttps
?rack.input
: Đối tượng IO giống như chứa dữ liệu HTTP POST thô.rack.errors
: Một đối tượng đáp ứngputs
,write
, vàflush
.rack.session
: Kho giá trị quan trọng để lưu trữ dữ liệu request session.rack.logger
: Một đối tượng có thể đăng nhập các giao diện. Cần thực hiện phương thứcinfo
,debug
,warn
,error
, vàfatal
.
Rất nhiều các framework được xây dựng trên Rack env hash trong một đối tượng Rack::Request
. Đối tượng này cung cấp rất nhiều phương pháp tiện lợi. Ví dụ, request_method
, query_string
, session
và logger
trả lại các giá trị từ các phím mô tả ở trên. Nó cũng cho phép bạn kiểm tra những thứ như params
, HTTP scheme
, hoặc nếu bạn đang sử dụng ssl
Để có một danh sách đầy đủ các method, bạn có thể xem qua nguồn
The Response
Khi đối tượng máy chủ Rack của bạn trả về một response, nó phải chứa ba phần:
- Status
- Header
- Body
Giống như request, có một đối tượng Rack::Response
cung cấp cho bạn những phương pháp tiện lợi như write
, set_cookie
, và finish
. Ngoài ra, bạn có thể trả lại một mảng có chứa ba thành phần.
Status
Một HTTP status như 200 hoặc 404
Header
Cái gì đó đáp ứng cho mỗi cặp key-value. Các key phải là strings và tuân theo đặc tả token RFC7230. Đây là nơi bạn có thể đặt Content-Type
và Content-Length
nếu nó phù hợp với response của bạn.
Body
Body là dữ liệu mà máy chủ gửi lại cho người request. Nó phải đáp ứng với mỗi giá trị string.
Tất cả đã được Racked Up!
Bây giờ chúng ta đã tạo một ứng dụng Rack, làm cách nào chúng ta có thể tùy chỉnh nó để làm cho nó hữu ích? Bước đầu tiên là xem xét thêm một số middleware.
Middleware là gì?
Một trong những điều làm cho Rack trở nên tuyệt vời là cách dễ dàng để thêm các middleware components giữa các máy chủ web và ứng dụng để tùy chỉnh request/response của bạn.
Nhưng một middleware components là gì?
Một middleware components nằm giữa máy client và máy chủ, xử lý các inbound requests và outbound responses. Hiện có hàng tấn các thành phần trung gian có sẵn cho Rack mà loại bỏ sự phỏng đoán khỏi những vấn đề như cho phép lưu trữ, xác thực, và bẫy spam
Sử dụng Middleware trong ứng dụng Rack
Để thêm middleware vào ứng dụng Rack, tất cả những gì bạn phải làm là cho Rack sử dụng nó. Bạn có thể sử dụng nhiều middlewaren và nó sẽ thay đổi request hoặc response trước khi chuyển nó sang thành phần tiếp theo. Loạt các thành phần này được gọi là middleware stack.
Warden
Chúng ta sẽ xem xét cách thêm Warden vào một dự án. Warden phải đến sau một số loại session middleware trong stack, vì vậy chúng ta cũng sẽ sử dụng Rack::Session::Cookie
.
Trước tiên, thêm nó vào Gemfile với gem "warden" và cài đặt nó với bundle install
.
Bây giờ thêm vào tập tin config.ru:
require "warden"
use Rack::Session::Cookie, secret: "MY_SECRET"
failure_app = Proc.new { |env| ['401', {'Content-Type' => 'text/html'}, ["UNAUTHORIZED"]] }
use Warden::Manager do |manager|
manager.default_strategies :password, :basic
manager.failure_app = failure_app
end
run Proc.new { |env| ['200', {'Content-Type' => 'text/html'}, ['get rack\'d']] }
Cuối cùng, chạy máy chủ với rackup
. Nó sẽ tìm thấy config.ru
và khởi động trên cổng 9292.
Lưu ý rằng có thêm nhiều thiết lập liên quan đến việc Warden thực sự xác thực với ứng dụng của bạn. Đây chỉ là một ví dụ về làm thế nào để đưa nó nạp vào stack middleware. Để xem một ví dụ mạnh mẽ hơn về việc tích hợp Warden, hãy kiểm tra ý nghĩa này.
Có một cách khác để định nghĩa stack middleware. Thay vì gọi trực tiếp use
trong config.ru
, bạn có thể sử dụng Rack::Builder
để quấn một số middleware và app (s) vào một ứng dụng lớn.
Ví dụ:
failure_app = Proc.new { |env| ['401', {'Content-Type' => 'text/html'}, ["UNAUTHORIZED"]] }
app = Rack::Builder.new do
use Rack::Session::Cookie, secret: "MY_SECRET"
use Warden::Manager do |manager|
manager.default_strategies :password, :basic
manager.failure_app = failure_app
end
end
run app
Rack Basic Auth
Một middleware hữu ích là Rack::Auth::Basic
, có thể được sử dụng để bảo vệ bất kỳ ứng dụng Rack nào với xác thực HTTP cơ bản. Nó thực sự nhẹ và rất hữu ích cho việc bảo vệ các bit nhỏ của một ứng dụng. Ví dụ, Ryan Bates sử dụng nó để bảo vệ một máy chủ Resque trong một ứng dụng Rails trong tập Railscasts này.
Đây là cách thiết lập nó:
use Rack::Auth::Basic, "Restricted Area" do |username, password|
[username, password] == ['admin', 'abc123']
end
Thật đơn giản!
Sử dụng Middleware trong Rails
Vậy cái gì? Rack khá mát mẻ. Và chúng ta biết rằng Rails được xây dựng trên đó. Nhưng chỉ vì chúng tôi hiểu nó là gì, nó không làm cho nó thực sự hữu ích trong việc làm việc với một ứng dụng sản xuất.
Sử dụng Rack trong Rails như thế nào
Đã bao giờ bạn nhận thấy rằng có một file config.ru
trong thư mục gốc của mỗi dự án Rails tạo ra? Bạn đã bao giờ nhìn vào bên trong không? Đây là những gì nó chứa:
# This file is used by Rack-based servers to start the application.
require ::File.expand_path('../config/environment', __FILE__)
run Rails.application
Điều này khá đơn giản. Nó chỉ tải tập tin config/environment
, sau đó khởi động lên Rails.application
.
Hãy xem trong config/environment
, chúng ta có thể thấy rằng nó được định nghĩa trong config/application.rb
. config/environment
chỉ là gọi initialize!
trên đó.
Vì vậy, những gì trong config/application.rb
? Nếu chúng ta xem xét, chúng ta thấy rằng nó tải trong các gói gems từ config/boot.rb
, đòi hỏi rails/all
, tải lên môi trường (test, development, production, vv), và định nghĩa một namespaced version của ứng dụng.
Có vẻ như thế này:
module MyApplication
class Application < Rails::Application
...
end
end
Điều này phải có nghĩa là Rails::Application
là một ứng dụng Rack. Chắc chắn, nếu chúng ta kiểm tra mã nguồn, nó đáp ứng cuộc call!
.
Nhưng middleware là nó sử dụng là gì? Nếu chúng ta xem xét kỹ lưỡng, chúng ta sẽ thấy nó là autoloading rails/application/default_middleware_stack
- và kiểm tra xem nó ra sao, có vẻ như nó được định nghĩa trong ActionDispatch
.
Trường hợp ActionDispatch
đến từ đâu? ActionPack
Action Dispatch
ActionPack
là framework của Rails để xử lý các reqeust và response trên web. ActionPack là nơi chứa khá nhiều thứ mà bạn tìm thấy trong Rails, chẳng hạn như routes, các abstract controllers và view rendering.
Phần thích hợp nhất của ActionPack cho cuộc thảo luận ở đây là Action Dispatch. Nó cung cấp một số middleware components mà tương tác với ssl, cookies, debugging, các files tĩnh, và nhiều hơn nữa.
Nếu bạn xem xét từng thành phần của phần mềm trung gian ActionDispatch, bạn sẽ nhận thấy tất cả chúng đều tuân theo đặc tả Rack: Tất cả đều đáp ứng call
, lấy một ứng dụng và trả lại status
, header
và body
. Nhiều trong số đó cũng sử dụng các đối tượng Rack::Request
và Rack::Response
.
Đọc qua các code trong các thành phần này đã có rất nhiều bí ẩn ra khỏi những gì đang xảy ra đằng sau hậu trường khi thực hiện các request đến một ứng dụng Rails. Khi tôi nhận ra rằng đó chỉ là một bó các đối tượng Ruby theo đặc tả của Rack - vượt qua request và response với nhau - nó làm cho toàn bộ phần của Rails này ít bí ẩn hơn.
Bây giờ chúng ta hiểu một chút về những gì đang xảy ra dưới mui xe, chúng ta hãy xem làm thế nào để thực sự bao gồm một số middleware tùy chỉnh trong một ứng dụng Rails.
Thêm một custom middleware
Hãy tưởng tượng bạn đang lưu trữ một ứng dụng trên Engine Yard. Bạn có một API Rails chạy trên một máy chủ, và một ứng dụng JavaScript phía máy client đang chạy trên một máy chủ khác. API có một url của https://api.example.com, và ứng dụng phía client ở https://app.example.com.
Bạn sắp gặp sự cố khá nhanh: Bạn không thể truy cập tài nguyên tại api.example.com
từ ứng dụng JavaScript, do chính sách nguồn gốc giống nhau. Như bạn có thể biết, giải pháp cho vấn đề này là để cho phép chia sẻ nguồn gốc chéo nguồn (CORS). Có nhiều cách để cho phép CORS trên máy chủ của bạn-nhưng một trong những cách đơn giản nhất là sử dụng middleware Rack::Cors
Bắt đầu bằng cách yêu cầu nó trong Gemfile:
gem "rack-cors", require: "rack/cors"
Như với nhiều thứ, Rails cung cấp một cách rất dễ dàng để có được load middleware. Mặc dù chúng ta chắc chắn có thể thêm nó vào một khối Rack::Builder
trong config.ru
- như chúng ta đã làm ở trên - cách thức Rails là đặt nó vào trong config/application.rb
, sử dụng cú pháp sau:
module MyApp
class Application < Rails::Application
config.middleware.insert_before 0, "Rack::Cors" do
allow do
origins '*'
resource '*',
:headers => :any,
:expose => ['X-User-Authentication-Token', 'X-User-Id'],
:methods => [:get, :post, :options, :patch, :delete]
end
end
end
end
Lưu ý rằng chúng ta đang sử dụng insert_before ở đây để đảm bảo rằng Rack::Cors
xuất hiện trước midldeware còn lại trong ngăn xếp của ActionPack (và bất kỳ midldeware nào khác mà bạn có thể đang sử dụng).
Bạn cần khởi động lại server để nó có thể hoạt động! Ứng dụng phía máy client có thể truy cập api.example.com
mà không gặp lỗi JavaScript chính sách gốc.
Chạy dự án Ruby tiếp theo của bạn trên Engine Yard Bắt đầu dùng thử miễn phí
Tổng kết
Trong bài đăng này, chúng ta đã xem xét sâu vào Rack và, bằng cách mở rộng, chu kì request/response đối với một số framework web của Ruby, bao gồm cả Ruby on Rails.
Tôi hy vọng rằng sự hiểu biết những gì đang xảy ra khi một request truy cập máy chủ và ứng dụng gửi lại một response giúp làm cho mọi thứ rõ ràng. Tôi không biết về bạn, nhưng khi mọi việc trở nên sai lầm, khó có thể khắc phục được khi có phép thuật hơn là khi tôi hiểu điều gì đang xảy ra. Trong trường hợp đó, tôi có thể nói, "Ồ, nó chỉ là một phản ứng Rack" và xuống để sửa lỗi. Và nếu tôi đã thực hiện công việc của tôi, đọc bài viết này sẽ cho phép bạn làm điều tương tự.
All rights reserved