Як працює зіставлення маршруту? Як обробляються запити та відповіді? Так багато питань, але так мало часу ...
Без проблем! Ця стаття відповідає на поставленні питання!
Ініціалізація 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 кроки:
- Спочатку перевіряються статичні файли. Це такі файли, як css, js та зображення. Цей параметр ввімкнено за умовчанням, якщо існує каталог з назвою «public»
- Дії перед запуском фільтра
- Зіставлення маршруту
- Запуск фільтра.
Тепер ми можемо розібратися в кожному кроці, щоб детальніше побачити, що відбувається.
Обслуговування статичних файлів
Метод 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
Пам'ятайте, що метапрограмування має свої мінуси, тому я просто дотримувався хеш-опцій. Вам не потрібні ці шалені методи.
Ще немає коментарів