Як працює Sinatra

3 хв. читання

Як працює зіставлення маршруту? Як обробляються запити та відповіді? Так багато питань, але так мало часу ...

Без проблем! Ця стаття відповідає на поставленні питання!


Ініціалізація Sinatra

Все починається з одного файлу: sinatra.rb. Все, що робить цей файл, це вимагає наявності main.rb. Не дуже захопливо, так?

Але ось де все стає цікавіше. Всередині main.rb ми знайдемо потребу в base.rb і також побачимо код для аналізу параметрів (порт, середовище тощо). Sinatra використовує optparse зі стандартної бібліотеки Ruby.

Інша важлива річ, що відбувається тут – блок at_exit:

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

Ця частина коду буде виконуватись після закінчення програми. Відбувається наступне: Весь ваш код буде прочитаний Ruby, і оскільки у вас немає циклів, або чогось іншого, то програма закінчиться природним шляхом, але незабаром перед її завершенням буде ініційований блок at_exit.

Коли це станеться, Sinatra візьме керування на себе і запустить веб-сервер, щоб він міг обробляти запити:

begin
  start_server(handler, server_settings, handler_name, &block)
rescue Errno::EADDRINUSE
  $stderr.puts "== Someone is already performing on port #{port}!"
  raise
end
 
# Частина методу `run!` base.rb

О, і тут відбувається ще одна важлива річ:

extend Sinatra::Delegator

Цей фрагмент визначає методи Sinatra DSL (Domain-Specific Language), такі як get, post та set.

Ось чому ви можете зробити так:

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

Обробка запитів та відповідей

Гаразд, тепер ми маємо налаштований сервер, готовий приймати нові з'єднання.

Але що відбувається, коли надходить нове з'єднання? Sinatra, як і Rails та інші веб-фреймворки Ruby, використовує Rack gem для обробки всіх низкорівневих речей. Rack очікує, що в вашому застосунку буде доступний метод call. Це об'єкт, який ви надаєте Rack при його ініціалізації.

У випадку з Sinatra цей об'єкт – клас Sinatra::Base.

Ось тут можна побачити цей метод:

 
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

Схоже, ми повинні розібратися в методі dispatch!, щоб показати, як буде оброблятися запит.

Ось тут можна побачити реалізацію цього метода:

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

Запит можна розбити на 4 кроки:

  1. Спочатку перевіряються статичні файли. Це такі файли, як css, js та зображення. Цей параметр ввімкнено за умовчанням, якщо існує каталог з назвою «public»
  2. Дії перед запуском фільтра
  3. Зіставлення маршруту
  4. Запуск фільтра.

Тепер ми можемо розібратися в кожному кроці, щоб детальніше побачити, що відбувається.

Обслуговування статичних файлів

Метод static!досить простий:

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

Цей код перевіряє наявність запитуваного файлу, після чого встановлюється заголовок HTTP «Cache Control». В останньому рядку викликається метод send_file, все що він робить – вказує ім'я. 🙂

Перед фільтром

Перед фільтром ви можете запустити код, перш ніж намагатись знайти відповідний маршрут.

Ось так додається фільтр:

@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

Як ви можете бачити, фільтри – це хеш з двома ключами, по одному для кожного типу фільтра. Але що таке compile!? Цей метод повертає масив з 3-х елементів: шаблон, масив умов і обгортка.

Той самий метод використовується для створення маршрутів (коли ви використовуєте блоки get або 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

З цього можна дізнатись, що фільтри Sinatra працюють так само, як і маршрути.

Зіставлення маршруту

Наступним кроком в циклі обробки запиту є зіставлення маршрутів:

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

Цей фрагмент коду переглядає кожен маршрут, який відповідає методу запиту (get, post і т. д.).

Зіставлення маршруту відбувається всередині методу 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

Де pattern є регулярним виразом.

Якщо маршрут відповідає як шляху, так і умовам, то буде викликаний метод route_eval, який оцінює блок (тіло вашого маршруту get / post) і завершує процес зіставлення маршруту.

def route_eval
  throw :halt, yield
end

Цей код використовує незвичний механізм catch / throw для контролю потоку.

Я не раджу цього робити, оскільки можна заплутатись стеживши за потоком, але цікаво побачити реальний приклад використання цієї функції.

Формування відповіді

Останнім кроком циклу запиту є підготовка відповіді.

Метод invoke збирає відповідь наступним чином:

res = catch(:halt) { yield }

Цей результат присвоюється тілу відповіді, використовуючи метод body:

body(res)

Тепер, якщо ми подивимося туди, де ми викликали метод call, ми знайдемо такий фрагмент коду:

@response.finish

Ця дія викликає метод finish в @response, який є об'єктом Rack::Response.

Іншими словами, це фактично призведе до того, що відповідь буде надіслана клієнту.

Бонус: Як працює метод Set

Метод set є частиною DSL Sinatra і дозволяє вам встановлювати параметри конфігурації в будь-якому місці вашого додатка Sinatra.

Приклад:

set :public_folder, '/var/www'

Кожен раз, коли ви використовуєте метод set, Sinatra створює 3 методи (за допомогою метапрограмування):

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

Три створених методи будуть виглядати наступним чином (розглянемо на прикладі методу public_folder):

  • public_folder
  • public_folder=
  • public_folder?

Цей метод також викличе метод setter (public_folder=) якщо він вже існує:

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

Пам'ятайте, що метапрограмування має свої мінуси, тому я просто дотримувався хеш-опцій. Вам не потрібні ці шалені методи.

Помітили помилку? Повідомте автору, для цього достатньо виділити текст з помилкою та натиснути Ctrl+Enter
Codeguida 5.6K
Приєднався: 8 місяців тому
Коментарі (0)

    Ще немає коментарів

Щоб залишити коментар необхідно авторизуватися.

Вхід / Реєстрація