12/08/2018, 17:02

Tìm hiểu về ứng dụng Rack và Middleware

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 ...

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.

Bạn có biết rằng Rails là một ứng dụng 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 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ụng
  • QUERY_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ụng
  • rack.url_scheme: http hoặc https?
  • 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 ứng puts, 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ức info, 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:

  1. Status
  2. Header
  3. 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.

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

Để 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!

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í

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ự.

0