Computer >> Hướng Dẫn Máy Tính >  >> Lập Trình >> Ruby

Deep Dive: Hiểu về Cơ Chế Bên Trong của Sinatra

Khởi tạo Sinatra

Mọi thứ bắt đầu từ một tệp: sinatra.rb.

Tệp này chỉ yêu cầu main.rb, không có gì đặc biệt phải không?

Bây giờ thì thú vị hơn!

Bên trong main.rb bạn sẽ thấy yêu cầu base.rb và cũng tìm thấy mã phân tích tùy chọn (cổng, môi trường, chế độ im lặng, v.v.).

Sinatra sử dụng optparse, từ thư viện chuẩn của Ruby.

Điều gì khác bạn có thể tìm thấy ở đây?

Hãy nhìn vào khối at_exit này:

at_exit { Application.run! if $!.nil? && Application.run? }

Đây là đoạn mã sẽ chạy khi chương trình kết thúc.

Điều xảy ra là toàn bộ mã của bạn sẽ được Ruby đọc và vì bạn không có vòng lặp, sleep hoặc bất cứ thứ gì như vậy, chương trình sẽ kết thúc tự nhiên.

... nhưng ngay trước khi nó kết thúc, khối at_exit sẽ được kích hoạt!

Sau đó Sinatra接管 và khởi động máy chủ web để có thể xử lý các yêu cầu.

Đây là mã thực hiện việc đó:

begin start_server(handler, server_settings, handler_name, &block) rescue Errno::EADDRINUSE $stderr.puts "== Someone is already performing on port #{port}!" raise end # Phần của phương thức `run!` trong base.rb

Ồ và một điều quan trọng khác xảy ra ở đây:

extend Sinatra::Delegator

Sinatra::Delegator là một module định nghĩa các phương thức DSL của Sinatra như get, post và set.

Đó là lý do bạn có thể làm điều này:

get '/' do puts "Hello World!" end

Sinatra mở rộng đối tượng main toàn cục với module này.

Xử lý Yêu cầu & Phản hồi

Ở thời điểm này, chúng ta đã có một máy chủ đang chạy sẵn sàng chấp nhận kết nối mới.

Nhưng điều gì xảy ra khi một kết nối mới được nhận?

Vâng, Sinatra, giống như Rails và các framework web Ruby khác, sử dụng Rack để xử lý tất cả các thứ cấp thấp hơn.

Rack mong đợi một phương thức call có sẵn trên ứng dụng của bạn. Đó chỉ là một đối tượng mà bạn đưa cho Rack khi khởi tạo nó.

Trong trường hợp của Sinatra, đối tượng này là lớp Sinatra::Base.

Đây là phương thức đó:

# Giao diện gọi Rack. def call!(env) @env = env @request = Request.new(env) @response = Response.new invoke { dispatch! } invoke { error_block!(response.status) } unless @env['sinatra.error'] @response.finish end # Phiên bản sửa đổi của phương thức call của Sinatra (để rõ ràng)

Có vẻ như chúng ta cần điều tra phương thức dispatch! tiếp theo để thấy cách một yêu cầu được xử lý.

Đây là phương thức đó:

def dispatch! invoke do static! if settings.static? && (request.get? || request.head?) filter! :before route! end rescue ::Exception => boom invoke { handle_exception!(boom) } ensure filter! :after unless env['sinatra.static_file'] end # Đã chỉnh sửa để chỉ giữ lại các phần quan trọng

Yêu cầu được chia thành 4 bước:

  1. Tệp tĩnh được kiểm tra trước. Đây là các tệp như css, js và hình ảnh. Cài đặt này được bật theo mặc định nếu tồn tại thư mục có tên "public"
  2. Bộ lọc before được chạy
  3. Khớp tuyến đường
  4. Bộ lọc after được chạy

Bây giờ chúng ta có thể đào sâu vào từng bước để xem điều gì xảy ra chi tiết hơn.

Phục vụ Tệp Tĩnh

Phương thức static! khá đơn giản:

def static!(options = {}) return if (public_dir = settings.public_folder).nil? path = File.expand_path("#{public_dir}#{URI_INSTANCE.unescape(request.path_info)}" ) return unless File.file?(path) cache_control(*settings.static_cache_control) if settings.static_cache_control? send_file(path, options) end

Mã này kiểm tra xem tệp được yêu cầu có tồn tại không, sau đó nó đặt tiêu đề HTTP "Cache Control".

Trên dòng cuối cùng, nó gọi send_file và nó làm đúng như tên gọi của nó 🙂

Bộ lọc Before

Bộ lọc before cho phép bạn chạy mã trước khi cố gắng tìm tuyến đường khớp.

Cách thêm bộ lọc:

# Định nghĩa bộ lọc before. # Chạy trước tất cả các yêu cầu trong cùng ngữ cảnh với trình xử lý tuyến đường # và có thể truy cập/sửa đổi yêu cầu và phản hồi. @filters = {:before => [], :after => []} def before(path = /.*/, **options, &block) add_filter(:before, path, options, &block) end def after(path = /.*/, **options, &block) add_filter(:after, path, options, &block) end def add_filter(type, path = /.*/, **options, &block) filters[type] << compile!(type, path, block, options) end

Như bạn thấy, filters chỉ là một hash với hai khóa, một cho mỗi loại bộ lọc.

Nhưng compile! là gì?

Phương thức này trả về một mảng với 3 phần tử: một mẫu, một mảng các điều kiện và một wrapper.

Cùng phương thức này được sử dụng để tạo tuyến đường (khi bạn sử dụng khối get hoặc post):

def get(path, opts = {}, &block) route('GET', path, opts, &block) end def route(verb, path, options = {}, &block) signature = compile!(verb, path, block, options) (@routes[verb] ||= []) << signature signature end # Các phương thức đã được chỉnh sửa để rõ ràng

Từ đây, chúng ta có thể học được rằng bộ lọc của Sinatra hoạt động và làm việc giống hệt như tuyến đường.

Khớp Tuyến Đường

Bước tiếp theo trong chu trình xử lý yêu cầu là khớp tuyến đường:

def route!(base = settings, pass_block = nil) routes = base.routes[@request.request_method] routes.each do |pattern, conditions, block| process_route(pattern, conditions) route_eval end route_missing end # Phương thức đã chỉnh sửa

Mã này lặp qua mọi tuyến đường khớp với phương thức yêu cầu (get, post, v.v.).

Khớp tuyến đường xảy ra bên trong phương thức process_route:

def process_route(pattern, keys, conditions, block = nil, values = []) route = @request.path_info route = '/' if route.empty? and not settings.empty_path_info? return unless match = pattern.match(route) end

Ở đây pattern là một biểu thức chính quy.

Nếu một tuyến đường khớp cả đường dẫn và điều kiện thì route_eval sẽ được gọi, đánh giá khối (phần thân của tuyến đường get / post của bạn) và kết thúc quá trình khớp tuyến đường.

# Chạy khối tuyến đường và ném :halt với kết quả. def route_eval throw :halt, yield end

Điều này sử dụng cơ chế catch / throw bất thường để kiểm soát luồng.

Tôi khuyên không nên sử dụng nó vì nó có thể rất khó theo dõi luồng mã, nhưng thú vị khi thấy một ví dụ thực tế của tính năng này đang được sử dụng.

Xây dựng Phản hồi

Bước cuối cùng của chu trình yêu cầu là chuẩn bị phản hồi.

Vậy phản hồi đi đâu?

Phương thức invoke thu thập phản hồi như sau:

res = catch(:halt) { yield }

Kết quả này được gán cho phần thân phản hồi bằng phương thức body:

body(res)

Bây giờ nếu chúng ta nhìn lại nơi chúng ta bắt đầu, phương thức call, chúng ta sẽ tìm thấy dòng mã này:

@response.finish

Điều này gọi phương thức finish trên @response, là một đối tượng Rack::Response.

Nói cách khác, điều này thực sự kích hoạt việc gửi phản hồi đến máy khách.

Thêm: Cách Phương thức Set Hoạt Động

Phương thức set là một phần của DSL (Ngôn ngữ Đặc thù Miền) của Sinatra và cho phép bạn đặt tùy chọn cấu hình ở bất kỳ đâu trong ứng dụng Sinatra của bạn.

Ví dụ:

set :public_folder, '/var/www'

Mỗi khi bạn sử dụng set, Sinatra tạo ra 3 phương thức (thông qua metaprogramming):

define_singleton("#{option}=", setter) if setter define_singleton(option, getter) if getter define_singleton("#{option}?", "!!#{option}") unless method_defined? "#{option}?"

3 phương thức là (với public_folder làm ví dụ):

  • public_folder
  • public_folder=
  • public_folder?

Phương thức này cũng sẽ gọi phương thức setter (public_folder=) nếu nó đã tồn tại:

if respond_to?("#{option}=") && !ignore_setter return __send__("#{option}=", value) end

Hãy nhớ rằng metaprogramming không miễn phí, vì vậy tôi chỉ dính với một hash options. Bạn không cần những phương thức phức tạp đó.

Tóm tắt

Bạn đã học cách Sinatra được khởi tạo, cách nó xử lý yêu cầu và các bước khác nhau nó thực hiện cho đến khi phản hồi có thể được tạo ra. Điều này sẽ giúp bạn học được một vài thủ thuật Ruby và hiểu Sinatra tốt hơn!

Đừng quên chia sẻ bài viết này với các nhà phát triển Ruby khác để họ cũng có thể học hỏi từ nó 🙂