Rack، الأساس الخفي لكل تطبيق ويب رائع بلغة Ruby
يُستخدم Rack في كل إطار عمل تقريبًا يعمل على Ruby، ومع ذلك لا يفهم كثير من مطوري Rails ما هو أو كيف يعمل. وفهم Rack يجعل هذه الأطر أقل رهبة، وهو أسهل مما تتوقعون حين تلقون نظرة عليه.

واجهة Rack ومبادئها بسيطة جدًا في الواقع، كل ما في الأمر أننا لا نحتاج عادة إلى التفكير فيها عند العمل مع Rails أو Sinatra أو Hanami أو غيرها من الأطر المبنية فوق Rack.
إذن ما الذي يفعله Rack فعلًا؟
إنه واجهة بين خادم الويب وكود تطبيقكم! يستمع Rack إلى خادم الويب عبر handler، ثم يستدعي تطبيقكم بوسيط واحد هو Rack Environment. وللالتزام بهذه الواجهة، يجب أن يُرجع تطبيقكم مصفوفة من 3 عناصر: الحالة (status) والـ headers والمحتوى (body). يجب أن تكون الحالة عددًا صحيحًا يطابق رمز حالة HTTP صالحًا. ويجب أن تكون الـ headers عبارة عن hash من أزواج المفاتيح والقيم. وأخيرًا يجب أن يكون الـ body كائنًا يستجيب لـ each، وعادة ما يكون مصفوفة من السلاسل النصية.
مرحبًا يا عالم؟
لنبنِ أول تطبيق Rack. يأتي Rack مزودًا بـ handlers جاهزة لعدة خوادم ويب، وسنستخدم أحدها.
# hello_world.rb
require 'rack'
class HelloWorld
def call(env)
[200, {"Content-Type" => "text/plain"}, ["Hello, World!"]]
end
end
Rack::Handler::WEBrick.run HelloWorld.new
شغّلوه الآن وزوروا localhost:8080
ruby hello_world.rb
لنحوّل تطبيق Hello, World! إلى تطبيق Rack حقيقي
التطبيق أعلاه يُستدعى عبر الملف التنفيذي ruby، لكن Rack يوفر ملفًا تنفيذيًا اسمه rackup لتشغيل تطبيقات Rack. يتوقع هذا الملف وجود ملف في جذر تطبيقكم اسمه config.ru (اختصار r[ack]u[p])، ويُستخدم لإعداد سلسلة الـ middleware في Rack.
require 'thin'
class HelloWorld
def call(env)
[200, {"Content-Type" => "text/plain"}, ["Hello, World!\n"]]
end
end
class GoodbyeWorld
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
body << "Goodbye, World!\n"
[status, headers, body]
end
end
use GoodbyeWorld
run HelloWorld.new
حوّلنا الآن سكربت Rack إلى إعداد تطبيق Rack، وأجرينا أيضًا بعض التغييرات. استدعينا خادم الويب thin الذي يبدّل الـ handler تلقائيًا. وأضفنا أول middleware إلى السلسلة، وهو GoodbyeWorld. وأخيرًا صار بإمكاننا استخدام methods لغة Rack الخاصة (DSL)، وهي use وrun. فما الذي يفعله هذا الـ middleware؟ في هذه الحالة نشغّل التطبيق أولًا، ثم نضيف عبارة "Goodbye, World!" إلى محتوى الاستجابة. ويمكننا أن نفعل بالـ middleware أشياء أخرى أكثر واقعية. لنضف أداة بسيطة لقياس الأداء (profiler).
require 'thin'
class HelloWorld
def call(env)
[200, {"Content-Type" => "text/plain"}, ["Hello, World!\n"]]
end
end
class GoodbyeWorld
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
body << "Goodbye, World!\n"
[status, headers, body]
end
end
class Benchmarker
def initialize(app)
@app = app
end
def call(env)
start_time = current_time
status, headers, body = @app.call(env)
end_time = current_time
body << "Took #{end_time - start_time} seconds to complete"
[status, headers, body]
end
def current_time
Time.now.to_f * 1000
end
end
use Benchmarker
use GoodbyeWorld
run HelloWorld.new
تعديل الـ Environment
حتى الآن عرضنا فقط كيف نغلّف التطبيق، فلنغيّر ذلك بحيث نعدّل الـ environment فقط. الأمر بسيط: بدلًا من استدعاء التطبيق ثم تعديل الاستجابة، نعدّل env أولًا ثم نستدعي التطبيق.
require 'thin'
class HelloWorld
def call(env)
if env['flip'] == 'heads'
[200, {"Content-Type" => "text/plain"}, ["Hello, World!\n"]]
else
[401, {"Content-Type" => "text/plain"}, ["Naughty naughty\n"]]
end
end
end
class CoinFlip
def initialize(app)
@app = app
end
def call(env)
env['flip'] = %w[heads tails].sample
@app.call(env)
end
end
use CoinFlip
run HelloWorld.new
إيقاف التنفيذ
إذا أرجع أي middleware استجابة، فلن تُعالَج بقية السلسلة. وتستفيد من ذلك بعض أدوات middleware في Rack مثل rack-attack، التي تمنع المهاجمين من إرسال طلبات خبيثة إلى تطبيقنا. لنعدّل المنطق بحيث تفوز طلباتنا عشوائيًا.
require 'thin'
class HelloWorld
def call(env)
[200, {"Content-Type" => "text/plain"}, ["Hello, World!\n"]]
end
end
class CoinFlip
def initialize(app)
@app = app
end
def call(env)
if %w[heads tails].sample == 'heads'
@app.call(env)
else
[200, {"Content-Type" => "text/plain"}, ["Winner winner chicken dinner!\n"]]
end
end
end
class GoodbyeWorld
def initialize(app)
@app = app
end
def call(env)
status, headers, body = @app.call(env)
body << "Goodbye, World!\n"
[status, headers, body]
end
end
use CoinFlip
use GoodbyeWorld
run HelloWorld.new


