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

สร้าง Rails Application รองรับหลาย Subdomain: คู่มือครบวงจร

วันนี้เราจะเรียนรู้สร้าง Rails app รองรับหลาย subdomain สมมติเรามีเว็บไซต์เกม funkygames.co และต้องการรองรับ subdomain หลายตัว เช่น app.funkygames.co, api.funkygames.co, และ dev.funkygames.co ด้วย Rails application เพียงตัวเดียว เราต้องการให้ authentication ทำงานถูกต้องสำหรับทุก subdomain และไม่มี routes ซ้ำ

เราจะใช้ routing constructs อันทรงพลังของ Rails เพื่อรองรับหลาย subdomain ในแอปพลิเคชัน เราจะตั้งค่า subdomain ใน local และเขียน test สำหรับหลาย subdomain

ข้อกำหนดเบื้องต้น

สำหรับบทความนี้ สมมติว่าคุณตั้งค่า DNS records สำหรับ subdomain ทั้งหมดให้ชี้ไปที่ Rails app แล้ว เราจะจัดการเฉพาะฝั่ง Rails

จัดการหลาย Subdomain

Rails ใช้ไฟล์ routes.rb จัดการ request เข้าและ map ไปยัง controller actions ใน app แบบ trivial ทุก mapping ใน routes.rb map route ไป controller action เช่น:

get '/games/:id', to: 'games#show'

ด้วยวิธีนี้ ทุก endpoints ใน routes.rb ใช้ได้กับทุก subdomain ดังนั้น app.funkygames.co/games/1 และ api.funkygames.co/games/1 จะถูกจัดการโดย route เดียวกัน แต่เราอยากให้ request จาก subdomain app เท่านั้นที่ถูกจัดการโดย route นี้ Subdomain api ใช้สำหรับ API routes เท่านั้น เราจะเพิ่ม rules ให้ routes ทำงานก็ต่อเมื่อ rule เฉพาะเจาะจงสำหรับ request นั้นถูกต้อง

Rails routing มี helper method constraints ระบุ rules เพิ่มเติมสำหรับ route

get '/games/:id', to: 'games#show', constraints: { subdomain: 'app' }

นี่จะ ensure ว่า request จาก app.funkygames.co/games/1 ถูกจัดการโดย GamesController show action Request จาก subdomain อื่นนอกจาก app จะไม่ถูกจัดการโดย route นี้

แต่จะทำให้ต้อง define constraints ซ้ำๆ สำหรับทุก route

get '/games/:id', to: 'games#show', constraints: { subdomain: 'app' }
get '/games/list', to: 'games#list', constraints: { subdomain: 'app' }
post '/games/start', to: 'games#start', constraints: { subdomain: 'app' }

เราสามารถใช้ block form ของ constraints helper เพื่อ define หลาย routes สำหรับ subdomain เดียว

constraints subdomain: 'app' do
  get '/games/:id', to: 'games#show'
  get '/games/list', to: 'games#list'
  post '/games/start', to: 'games#start'
end

เพื่อ define routes สำหรับหลาย subdomain ให้เพิ่มหลาย constraints blocks ใน routes.rb

constraints subdomain: 'app' do ... end
constraints subdomain: 'api' do ... end
constraints subdomain: 'dev' do ... end

ภายใต้ฝาฝน (Under the Hood)

Rails routing มี request constraints และ segment constraints Segment constraints เพิ่ม rules บน request path ส่วน request constraints เพิ่ม conditions บน incoming request Hash key ใน request constraint ต้องเป็น method บน object Request ที่คืนค่า string และ value ต้องเป็นค่าที่คาดหวัง

constraints subdomain: 'app' do ... end

ในกรณีนี้ เราใช้ method subdomain บน object Request และ match กับ string เช่น app, api หรือ dev

รายละเอียดเพิ่มเติม ดู Rails routing guide

จัดการ Multi-level Subdomains

สมมติเราใช้ app.staging.funkygames.co สำหรับ staging environment ถ้ามี setup ข้างต้น จะสังเกตว่า request ที่ควรไป subdomain app ส่งคืน 404 ถ้า debug จะพบ constraint สำหรับ subdomain ล้มเหลว

request.subdomain #=> "app.staging"

เราคาดหวัง subdomain จะคืน app แต่มันคืน app.staging อยากแก้โดยไม่เพิ่ม environment-specific code! การ parse subdomain ของ request จัดการโดย option config.action_dispatch.tld_length ค่าเริ่มต้นคือ 1 รองรับ subdomain ระดับเดียว เรามี two-level subdomain จึงต้องตั้ง config.action_dispatch.tld_length เป็น 2

# config/application.rb
config.action_dispatch.tld_length = Integer(ENV['TLD_LENGTH'] || 1)

ตั้งค่าผ่าน environment variable เพื่อใช้โค้ดเดียวกันได้ทั้ง staging และ production ตอนนี้ routing setup จะทำงานกับ app.staging.funkygames.co ได้

การจัดการ Session

ตอน routes จัดการ request จากหลาย subdomain แล้ว ต้องดูแล authentication สำหรับทุก subdomain ทำได้ 2 วิธี — อนุญาตให้ user session เดียวใช้ร่วมกันทั้ง subdomain หรือแยก session ต่อ subdomain

Authentication ในกระเป๋า

Rails ใช้ cookies เก็บ user session key โดยค่าเริ่มต้น User login แล้ว session info จะเก็บใน session store ที่เลือก และ session key เก็บเป็น cookie ใน browser ครั้งต่อไป user เข้าเว็บ cookie เดียวกันส่งจาก browser ไป server server ตัดสินใจว่า user login หรือไม่ จาก session ที่มีสำหรับ session cookie นั้น

Default config สำหรับ session ใน Rails app:

Rails.application.config.session_store :cookie_store, key: "_funkygames_session"

Key _funkygames_session ใช้เป็นชื่อ session cookie และ value คือ session id

Cookies Primer

โดยค่าเริ่มต้น cookies ถูก set โดย browser บน domain ของ request ถ้าเราเข้า app จาก app.funkygames.co session cookie จะถูก set ต่อ app.funkygames.co แต่ละ subdomain จะ set session cookies ของตัวเอง ดังนั้น user session จะไม่ถูก share ระหว่าง subdomain โดยค่าเริ่มต้น

Share Session ระหว่าง Subdomain ต่างๆ

ถ้าอยาก share user session ระหว่าง subdomain ต้อง set session cookie บน domain funkygames.co เอง เพื่อให้ subdomain ทุกตัวเข้าถึงได้ ทำได้โดยส่ง option domain ไป session store settings

Rails.application.config.session_store :cookie_store, key: "_funkygames_session", domain: :all

ส่ง domain เป็น :all บอก Rails ให้ set session cookie บน top-level domain เช่น funkygames.co แทน request host ที่อาจรวม subdomain ด้วย ทำให้ session share ระหว่าง subdomain ได้

เราสามารถส่ง list of domains ไป option domains ในรูปแบบ array เพื่อรองรับหลาย domain ได้

มีอีก option หนึ่งต้อง config ให้ cookies set ถูกต้องสำหรับทุก subdomain คือ tld_length option เมื่อใช้ domain: :all option นี้ระบุวิธี parse domain เพื่อ interpret TLD ในกรณี app.funkygames.co ควรตั้ง tld_length เป็น 2 เพื่อให้ Rails interpret TLD เป็น funkygames.co ตอน set cookies ดังนั้น final session store config สำหรับหลาย subdomain:

Rails.application.config.session_store :cookie_store, key: "_funkygames_session", domain: :all, tld_length: 2

Option tld_length จาก session store ต่างจาก config.action_dispatch.tld_length ที่กล่าวไปก่อนหน้านี้

เขียน Test สำหรับหลาย Subdomain

เพราะ routes เป็น subdomain-specific request specs หรือ integration tests จะได้ 404 ถ้า test request ไม่มี subdomain ที่ถูกต้อง Rails integration tests มี helper host! ตั้ง subdomain ให้ถูกต้องสำหรับทุก request ใน test file

# Configuring subdomain in Rails integration tests
setup do
  host! 'dev.example.com'
end

# Configuring subdomain in RSpec request specs
before do
  host! 'dev.example.com'
end

หลังจากนี้ request จะถูก route ถูกต้องไป controller actions ตาม subdomain routing ใน routes.rb

domain ไม่สำคัญ แค่ subdomain ที่ถูกต้องตามโค้ดที่ test

ตั้งค่าหลาย Subdomain ใน Local สำหรับ Development

มีหลายวิธีตั้งค่า subdomain ใน local วิธีง่ายที่สุดคือ edit ไฟล์ /etc/hosts

127.0.0.1 dev.funkygames.local
127.0.0.1 app.funkygames.local
127.0.0.1 api.funkygames.local

ทำให้ subdomain setup ทำงานใน local environment เราสามารถใช้ tools อย่าง pow จัดการ subdomain ใน local ได้

ข้อควรระวังกับ Constraints-based Subdomain Routing

แม้ constraints-based subdomain routing ทำงานในกรณีส่วนใหญ่ แต่อาจเป็นปวดหัวในบางสถานการณ์

จัดการกับ External APIs

ตอนทำงานกับ third-party APIs และสร้าง integrations local development TLDs เช่น .local หรือ .dev ไม่อนุญาต ต้องใช้ tools อย่าง ngrok Subdomain-based routing ไม่ทำงานในกรณีเหล่านี้ ต้อง whitelist routes บางอย่างให้เข้าถึงได้ผ่าน ngrok ด้วย

Routes นอก Subdomain Constraints

Routes บางอย่างไม่สามารถวางใน subdomain constraints ได้ ตัวอย่างทั่วไปคือ endpoints healthcheck หรือ ping ถ้าใช้ load balancer หน้า Rails app load balancer ต้องเช็คเป็นระยะว่า app up หรือไม่ healthcheck endpoint ในกรณีนี้ไม่สามารถอยู่ใต้ subdomain constraints เพราะ load balancer อาจไม่รู้ request host

ขาด root Route

Rails มี root route พิเศษ คือ default route ของแอป ถ้าไม่มี route ไหน match request root route จะถูกใช้ ตอนเรามี routes ทั้งหมดใต้ subdomain ใด subdomain หนึ่ง อาจไม่มี root route เลย Gems บางตัวอาจ depend บนการมี root route ต้องเพิ่ม checks และ balances ตามเหมาะสม

สรุป

ในบทความนี้ เราตั้งค่า Rails app กับหลาย subdomain ด้วย config เล็กน้อย เรายังดูวิธีตั้งค่า subdomain ใน local และ environment ต่างๆ พร้อม tips สำหรับเขียน test ที่มีประสิทธิภาพสำหรับหลาย subdomain ด้วย plumbing ที่ Rails ให้ ทำให้ setup และ test Rails app กับหลาย subdomain ง่ายขึ้น

P.S. ถ้าอยากอ่าน Ruby Magic posts ทันทีที่เผยแพร่ สมัครรับข่าวสาร Ruby Magic newsletter และไม่พลาดบทความเด็ด!