Sinatra เป็นเฟรมเวิร์กเว็บสำหรับภาษา Ruby ที่มีชื่อเสียงด้านความเรียบง่ายและความยืดหยุ่น
มักถูกเปรียบเทียบเป็น "น้องชาย" ของ Rails แต่ Sinatra มีอัตลักษณ์เฉพาะตัวที่ทำให้เหมาะสำหรับโปรเจกต์ขนาดเล็กถึงกลาง API และ microservices
มาเจาะลึกการทำงานภายในของ Sinatra กัน:
- เกิดอะไรขึ้นเมื่อคุณ require Sinatra เข้าโปรเจกต์?
- กลไกการจับคู่เส้นทาง (route matching) ทำงานอย่างไร?
- คำขอ (request) และการตอบสนอง (response) ถูกประมวลผลอย่างไร?
คำถามเยอะมาก แต่เวลาไม่มากพอ...
ไม่เป็นไร! ผมได้ทำงานยากให้แล้วและรวบรวมบทความนี้ไว้เพื่อให้คุณเรียนรู้ได้เร็วขึ้น!
การเริ่มต้นทำงานของ Sinatra (Initialization)
ทุกอย่างเริ่มต้นจากไฟล์เดียว: sinatra.rb
ไฟล์นี้ทำเพียงแค่ require main.rb ซึ่งดูไม่ค่อยน่าตื่นเต้น...
แต่มันเริ่มน่าสนใจขึ้น!
ภายใน main.rb คุณจะพบการ require base.rb รวมถึงโค้ดสำหรับการแยกวิเคราะห์ตัวเลือก (port, environment, quiet mode, เป็นต้น)
Sinatra ใช้ optparse จากไลบรารีมาตรฐานของ Ruby
อีกอย่างที่น่าสนใจคือบล็อก at_exit:
at_exit { Application.run! if $!.nil? && Application.run? } นี่คือโค้ดที่จะรันเมื่อโปรแกรมจบการทำงาน
เกิดอะไรขึ้นคือ Ruby จะอ่านโค้ดทั้งหมดของคุณ และเนื่องจากไม่มีลูป sleeps หรืออย่างอื่น โปรแกรมจะจบการทำงานตามปกติ
...แต่ก่อนจะจบ บล็อก 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::Delegator เป็นโมดูลที่กำหนดเมธอด DSL ของ Sinatra เช่น get, post และ set
นั่นคือเหตุผลที่คุณเขียนได้แบบนี้:
get '/' do puts "Hello World!" end
Sinatra ขยายออบเจกต์ main ทั่วไปด้วยโมดูลนี้
การประมวลผลคำขอและการตอบสนอง (Request & Response Processing)
ตอนนี้เรามีเซิร์ฟเวอร์ที่ทำงานพร้อมรับการเชื่อมต่อใหม่
แต่เมื่อมีการเชื่อมต่อใหม่เข้ามา จะเกิดอะไรขึ้น?
Sinatra คล้ายกับ Rails และเฟรมเวิร์ก Ruby อื่นๆ ใช้ Rack จัดการส่วนระดับล่างทั้งหมด
Rack คาดหวังให้มีเมธอด call พร้อมใช้งานบนแอปพลิเคชันของคุณ นั่นคือออบเจกต์ที่คุณส่งให้ Rack ตอนเริ่มต้น
ในกรณีของ Sinatra ออบเจกต์นี้คือคลาส Sinatra::Base
นี่คือเมธอดนั้น:
# Rack call interface. 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 # เวอร์ชันที่ปรับเปลี่ยนของเมธอด call ของ Sinatra (เพื่อความชัดเจน) ดูเหมือนเราต้องสำรวจเมธอด 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 ขั้นตอน:
- ตรวจสอบไฟล์คงที่ (static files) ก่อน ไฟล์เช่น CSS, JS, รูปภาพ การตั้งค่านี้เปิดอยู่โดยค่าเริ่มต้นถ้ามีโฟลเดอร์ชื่อ "public"
- รัน before filter
- จับคู่เส้นทาง (route matching)
- รัน after filter
ตอนนี้เราสามารถขุดลึกแต่ละขั้นตอนได้
การบริการไฟล์คงที่ (Serving Static Files)
เมธอด 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 header "Cache Control"
บรรทัดสุดท้ายเรียก send_file ซึ่งทำงานตรงตามชื่อเลย 🙂
Before Filter
Before filter ให้คุณรันโค้ดก่อนที่จะพยายามหาเส้นทางที่ตรงกัน
นี่คือวิธีเพิ่ม filter:
# กำหนด before filter. # รันก่อนทุกคำขอภายในบริบทเดียวกับ route handlers # และอาจเข้าถึง/แก้ไข request และ response @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 เห็นได้ว่า filters เป็นแค่แฮชที่มี 2 คีย์ สำหรับแต่ละประเภทของ filter
แต่ compile! คืออะไร?
เมธอดนี้คืนค่าเป็นอาร์เรย์ 3 องค์ประกอบ: รูปแบบ (pattern), อาร์เรย์ของเงื่อนไข (conditions), และ wrapper
เมธอดเดียวกันนี้ใช้สร้างเส้นทาง (routes) เมื่อคุณใช้บล็อก 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 # เมธอดถูกตัดต่อเพื่อความชัดเจน จากนี้เราเรียนรู้ได้ว่า filter ของ Sinatra ทำงานและพฤติกรรมเหมือนกับ routes เลย
การจับคู่เส้นทาง (Route Matching)
ขั้นตอนต่อไปในวงจรการประมวลผลคำขอคือการจับคู่เส้นทาง:
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 เป็น regular expression
ถ้าเส้นทางตรงทั้ง path และเงื่อนไข route_eval จะถูกเรียก ซึ่งจะประเมินบล็อก (เนื้อหาของ get / post route) และจบกระบวนการจับคู่เส้นทาง
# รันบล็อกเส้นทางและโยน :halt พร้อมผลลัพธ์ def route_eval throw :halt, yield end
นี่ใช้กลไก catch / throw ที่แปลกใหม่สำหรับการควบคุมโฟลว์
ผมไม่แนะนำให้ใช้เพราะอาจทำให้การติดตามโฟลว์โค้ดสับสน แต่น่าสนใจที่ได้เห็นตัวอย่างจริงของฟีเจอร์นี้
การสร้างการตอบสนอง (Response Building)
ขั้นตอนสุดท้ายของวงจรคำขอคือการเตรียมการตอบสนอง
การตอบสนองไปไหน?
เมธอด invoke รวบรวมการตอบสนองแบบนี้:
res = catch(:halt) { yield } ผลลัพธ์นี้ถูกกำหนดให้กับ response body ผ่านเมธอด body:
body(res)
ถ้าดูย้อนกลับที่เมธอด call จะพบบรรทัดนี้:
@response.finish
นี่เรียกเมธอด finish บน @response ซึ่งเป็นออบเจกต์ Rack::Response
กล่าวคือ นี่จะกระตุ้นให้ส่งการตอบสนองไปยังไคลเอนท์จริงๆ
พิเศษ: เมธอด set ทำงานอย่างไร
เมธอด set เป็นส่วนหนึ่งของ DSL (Domain-Specific Language) ของ Sinatra ให้คุณตั้งค่าตัวเลือกการกำหนดค่าได้ทุกที่ในแอป Sinatra
ตัวอย่าง:
set :public_folder, '/var/www'
ทุกครั้งที่ใช้ set Sinatra จะสร้าง 3 เมธอด (ผ่าน metaprogramming):
define_singleton("#{option}=", setter) if setter define_singleton(option, getter) if getter define_singleton("#{option}?", "!!#{option}") unless method_defined? "#{option}?" 3 เมธอดคือ (ใช้ public_folder เป็นตัวอย่าง):
- public_folder
- public_folder=
- public_folder?
เมธอดนี้ยังเรียก setter method (public_folder=) ถ้ามันมีอยู่แล้ว:
if respond_to?("#{option}=") && !ignore_setter return __send__("#{option}=", value) end จำไว้ว่า metaprogramming ไม่ได้ฟรี ผมแนะนำให้ใช้แค่ options hash คุณไม่ต้องการเมธอดหรูเหล่านี้
สรุป
คุณเรียนรู้ว่า Sinatra เริ่มต้นทำงานอย่างไร จัดการคำขออย่างไร และขั้นตอนต่างๆ จนกว่าจะสร้างการตอบสนองได้ ข้อมูลนี้จะช่วยให้คุณเรียนรู้ทริค Ruby บางอย่าง และเข้าใจ Sinatra ได้ลึกขึ้น!
อย่าลืม แชร์โพสต์นี้ ให้เพื่อนพัฒนา Ruby คนอื่นๆ เพื่อให้พวกเขาเรียนรู้จากมันได้ too 🙂