Computer >> บทช่วยสอนคอมพิวเตอร์ >  >> การเขียนโปรแกรม >> Ruby

ลึกลับ Sinatra: เข้าใจการทำงานภายในของเฟรมเวิร์ก Ruby

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 ขั้นตอน:

  1. ตรวจสอบไฟล์คงที่ (static files) ก่อน ไฟล์เช่น CSS, JS, รูปภาพ การตั้งค่านี้เปิดอยู่โดยค่าเริ่มต้นถ้ามีโฟลเดอร์ชื่อ "public"
  2. รัน before filter
  3. จับคู่เส้นทาง (route matching)
  4. รัน 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 🙂